diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..043e1ac --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python Picnic API", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", + "features": { + //"ghcr.io/devcontainers-extra/features/poetry:2": {} + }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "charliermarsh.ruff", + "-ms-python.vscode-pylance" + ] + } + }, + "postAttachCommand": "python -m pip install --upgrade pip && python -m pip install uv==0.6.6 && python -m uv sync" + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 32c8384..0000000 --- a/.flake8 +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -extend-ignore = E203, E266, E501 -# line length is intentionally set to 80 here because black uses Bugbear -# See https://github.com/psf/black/blob/master/docs/the_black_code_style.md#line-length for more details -max-line-length = 80 -max-complexity = 18 -select = B,C,E,F,W,T4,B9 -# We need to configure the mypy.ini because the flake8-mypy's default -# options don't properly override it, so if we don't specify it we get -# half of the config from mypy.ini and half from flake8-mypy. -mypy_config = mypy.ini \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7edeb1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,24 @@ +name: Bug report +description: Create a report to help us improve +title: 'Enter a summary of the issue' +labels: "bug" +body: +- type: textarea + attributes: + label: Describe the bug + validations: + required: true +- type: textarea + attributes: + label: Steps to reproduce the issue + validations: + required: true +- type: textarea + attributes: + label: What did you expect to happen instead? + validations: + required: true +- type: input + attributes: + label: Python Version + placeholder: X.YY diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6cf904b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Report a bug with the Picnic Integration + url: https://github.com/home-assistant/core/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22integration%3A%20picnic%22 + about: Please report issues with Picnic in the Home Assistant core repository unless a developer told you otherwise. + - name: I have a question or need support + url: https://www.home-assistant.io/help + about: We use GitHub for tracking bugs, check the Home Assistant website for resources on getting help. + - name: Feature Request + url: https://github.com/orgs/home-assistant/discussions/new?category=integration-enhancements&integration_name=picnic + about: Please use the Home Assistant Discussions for making feature requests. + - name: I'm unsure where to go + url: https://www.home-assistant.io/join-chat + about: If you are unsure where to go, then joining our chat is recommended; Just ask! diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7a47c16 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..ecd6d7f --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,17 @@ +changelog: + categories: + - title: ⚠️ Breaking Changes + labels: + - breaking + - title: 🎉 New Features + labels: + - enhancement + - title: 🛠 Bugfixes + labels: + - bug + - title: 👒 Dependencies + labels: + - dependencies + - title: Others + labels: + - "*" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2f28ab8..37d8d8d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,37 +2,37 @@ name: CI on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8] - poetry-version: [1.1.4] + python-version: [3.11,3.12] + uv-version: [0.6.6] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - name: Install poetry ${{ matrix.poetry-version }} + - name: Install uv ${{ matrix.uv-version }} run: | python -m ensurepip python -m pip install --upgrade pip - python -m pip install poetry==${{ matrix.poetry-version }} + python -m pip install uv==${{ matrix.uv-version }} - name: Install dependencies shell: bash - run: python -m poetry install + run: uv sync - name: Test with pytest env: @@ -40,11 +40,16 @@ jobs: PASSWORD: ${{ secrets.PICNIC_PASSWORD }} COUNTRY_CODE: ${{ secrets.PICNIC_COUNTRY_CODE }} run: | - python -m poetry run python -m pytest -v tests + uv run pytest tests/ --cov --cov-report=xml - - name: Lint with flake8 + - name: Lint with ruff run: | - python -m poetry run python -m flake8 . + uv run ruff check --output-format=github --ignore FIX + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/it.yaml b/.github/workflows/it.yaml new file mode 100644 index 0000000..fb9c46a --- /dev/null +++ b/.github/workflows/it.yaml @@ -0,0 +1,38 @@ +name: Regular Integration Tests + +on: + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '0 0 */7 * *' + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.12 + uses: actions/setup-python@v6 + with: + python-version: 3.12 + + - name: Install uv 0.6.6 + run: | + python -m ensurepip + python -m pip install --upgrade pip + python -m pip install uv==0.6.6 + + - name: Install dependencies + shell: bash + run: uv sync + + - name: Test with pytest + env: + USERNAME: ${{ secrets.PICNIC_USERNAME }} + PASSWORD: ${{ secrets.PICNIC_PASSWORD }} + COUNTRY_CODE: ${{ secrets.PICNIC_COUNTRY_CODE }} + run: | + uv run pytest -v integration_tests + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..38c3140 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Release to PyPi +on: [workflow_dispatch] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: 3.11 + + - name: Install uv 0.6.6 + run: | + python -m ensurepip + python -m pip install --upgrade pip + python -m pip install uv==0.6.6 + + - name: Install dependencies + shell: bash + run: uv sync + + - name: Build package + shell: bash + run: uv build + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + name: release-dists + path: dist/ + pypi-publish: + runs-on: ubuntu-latest + environment: pypi + needs: + - build + permissions: + id-token: write + + steps: + - name: Retrieve release distributions + uses: actions/download-artifact@v5 + with: + name: release-dists + path: dist/ + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@v1.13.0 diff --git a/.gitignore b/.gitignore index 64535bc..0803cce 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,5 @@ dmypy.json # Visual Studio Code .vscode +.direnv +.ruff_cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..f48bb89 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Python-Picnic-API + +**This library is undergoing rapid changes as is the Picnic API itself. It is mainly intended for use within Home Assistant, but there are integration tests running regularly checking for failures in features not used by the Home Assistant integration.** + +**If you want to know why interacting with Picnic is getting harder than ever, check out their blogpost about architectural changes: https://blog.picnic.nl/adding-write-functionality-to-pages-with-self-service-apis-d09aa7dbc9c0** + +Fork of the Unofficial Python wrapper for the [Picnic](https://picnic.app) API. While not all API methods have been implemented yet, you'll find most of what you need to build a working application are available. + +This library is not affiliated with Picnic and retrieves data from the endpoints of the mobile application. **Use at your own risk.** + +## Credits + +A big thanks to @MikeBrink for building the first versions of this library. + +@maartenpaul and @thijmen-j continously provided fixes that were then merged into this fork. + +## Getting started + +The easiest way to install is directly from pip: + +```bash +$ pip install python-picnic-api2 +``` + +Then create a new instance of `PicnicAPI` and login using your credentials: + +```python +from python_picnic_api2 import PicnicAPI + +picnic = PicnicAPI(username='username', password='password', country_code="NL") +``` + +The country_code parameter defaults to `NL`, but you have to change it if you live in a different country than the Netherlands (ISO 3166-1 Alpha-2). This obviously only works for countries that picnic services. + +## Searching for an article + +```python +picnic.search('coffee') +``` + +```python +[{'items': [{'id': 's1019822', 'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'decorators': [], 'display_price': 1799, 'image_id': 'aecbf7d3b018025ec78daf5a1099b6842a860a2e3faeceec777c13d708ce442c', 'max_count': 99, 'unit_quantity': '1kg', 'sole_article_id': None}, ... ]}] +``` + +## Get article by ID + +```python +picnic.get_article("s1019822") +``` +```python +{'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'id': 's1019822'} +``` + +## Get article by GTIN (EAN) +```python +picnic.get_article_by_gtin("8000070025400") +``` +```python +{'name': 'Lavazza Caffè Crema e Aroma Bohnen', 'id': 's1019822'} +``` + +## Check cart + +```python +picnic.get_cart() +``` + +```python +{'type': 'ORDER', 'id': 'shopping_cart', 'items': [{'type': 'ORDER_LINE', 'id': '1470', 'items': [{'type': 'ORDER_ARTICLE', 'id': 's1019822', 'name': 'Lavazza Caffè Crema e Aroma Bohnen',... +``` + +## Manipulating your cart +All of these methods will return the shopping cart. + +```python +# Add product with ID "s1019822" 2x +picnic.add_product("s1019822", 2) + +# Remove product with ID "s1019822" 1x +picnic.remove_product("s1019822") + +# Clear your cart +picnic.clear_cart() +``` + +## See upcoming deliveries + +```python +picnic.get_current_deliveries() +``` + +```python +[{'delivery_id': 'XXYYZZ', 'creation_time': '2025-04-28T08:08:41.666+02:00', 'slot': {'slot_id': 'XXYYZZ', 'hub_id': '... +``` + +## See available delivery slots + +```python +picnic.get_delivery_slots() +``` + +```python +{'delivery_slots': [{'slot_id': 'XXYYZZ', 'hub_id': 'YYY', 'fc_id': 'FCX', 'window_start': '2025-04-29T17:15:00.000+02:00', 'window_end': '2025-04-29T19:15:00.000+02:00'... +``` diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..ec08cc9 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,3 @@ +ignore: + - "tests" + - "integration_tests" \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9611591 --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1757194111, + "narHash": "sha256-4I5rftBv5fUuLkGJ5TZ+LWBz8c+tZ/ZEkUJ/uB0QCOM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "64028f19ae7ae87174d168286073ec0dc2b61395", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b08be5d --- /dev/null +++ b/flake.nix @@ -0,0 +1,19 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let pkgs = nixpkgs.legacyPackages.${system}; + in { + devShell = pkgs.mkShell { buildInputs = with pkgs; [ + python313Packages.requests + python313Packages.python-lsp-ruff + python313Packages.ruff + python313Packages.typing-extensions + ]; + }; + }); +} diff --git a/integration_tests/test_client.py b/integration_tests/test_client.py index 7ea13a2..b432d26 100644 --- a/integration_tests/test_client.py +++ b/integration_tests/test_client.py @@ -1,6 +1,10 @@ -from python_picnic_api import PicnicAPI -from dotenv import load_dotenv import os +import time + +import pytest +from dotenv import load_dotenv + +from python_picnic_api2 import PicnicAPI load_dotenv() @@ -11,6 +15,12 @@ picnic = PicnicAPI(username, password, country_code=country_code) +@pytest.fixture(autouse=True) +def slow_down_tests(): + yield + time.sleep(2) + + def _get_amount(cart: dict, product_id: str): items = cart["items"][0]["items"] product = next((item for item in items if item["id"] == product_id), None) @@ -20,72 +30,75 @@ def _get_amount(cart: dict, product_id: str): def test_get_user(): response = picnic.get_user() assert isinstance(response, dict) - assert "contact_email" in response.keys() + assert "contact_email" in response assert response["contact_email"] == username def test_search(): - response = picnic.search("koffie") + response = picnic.search("kaffee") assert isinstance(response, list) assert isinstance(response[0], dict) - assert "id" in response[0].keys() - assert response[0]["id"] == "koffie" + assert "items" in response[0] + assert isinstance(response[0]["items"], list) + assert "id" in response[0]["items"][0] def test_get_article(): - response = picnic.get_article("s1001546") + response = picnic.get_article("s1018620") assert isinstance(response, dict) - assert "id" in response.keys() - assert response["id"] == "s1001546" - assert response["name"] == "Douwe Egberts aroma rood filterkoffie" + assert "id" in response + assert response["id"] == "s1018620" + assert response["name"] == "Gut&Günstig H-Milch 3,5%" def test_get_article_with_category_name(): - response = picnic.get_article("s1001546", add_category_name=True) + response = picnic.get_article("s1018620", add_category=True) assert isinstance(response, dict) - assert "id" in response.keys() - assert response["id"] == "s1001546" - assert response["name"] == "Douwe Egberts aroma rood filterkoffie" - assert "category_name" in response.keys() - assert response["category_name"] == "Koffie & thee" - + assert "category" in response + assert response["category"]["name"] == "H-Milch" + -def test_get_lists(): - response_1 = picnic.get_lists() - response_2 = picnic.get_lists("21725") - assert isinstance(response_1, list) - assert isinstance(response_2, list) +def test_get_article_by_gtin(): + response = picnic.get_article_by_gtin("4311501044209") + assert response["id"] == "s1018620" + assert response["name"] == "Gut&Günstig H-Milch 3,5%" + + +def test_get_article_by_gtin_unknown(): + response = picnic.get_article_by_gtin("4311501040000") + assert response is None def test_get_cart(): response = picnic.get_cart() assert isinstance(response, dict) - assert "id" in response.keys() + assert "id" in response assert response["id"] == "shopping_cart" def test_add_product(): # need a clear cart for reproducibility picnic.clear_cart() - response = picnic.add_product("10407428", count=2) + response = picnic.add_product("s1018620", count=2) assert isinstance(response, dict) - assert "items" in response.keys() - assert any(item["id"] == "10407428" for item in response["items"][0]["items"]) - assert _get_amount(response, "10407428") == 2 + assert "items" in response + assert any( + item["id"] == "s1018620" for item in response["items"][0]["items"]) + assert _get_amount(response, "s1018620") == 2 def test_remove_product(): # need a clear cart for reproducibility picnic.clear_cart() - # add two coffee to the cart so we can remove 1 - picnic.add_product("10407428", count=2) + # add two milk to the cart so we can remove 1 + picnic.add_product("s1018620", count=2) - response = picnic.remove_product("10407428", count=1) - amount = _get_amount(response, "10407428") + response = picnic.remove_product("s1018620", count=1) + amount = _get_amount(response, "s1018620") assert isinstance(response, dict) - assert "items" in response.keys() + assert "items" in response assert amount == 1 @@ -93,44 +106,38 @@ def test_clear_cart(): # need a clear cart for reproducibility picnic.clear_cart() # add two coffee to the cart so we can clear it - picnic.add_product("10407428", count=2) + picnic.add_product("s1018620", count=2) response = picnic.clear_cart() assert isinstance(response, dict) - assert "items" in response.keys() + assert "items" in response assert len(response["items"]) == 0 def test_get_delivery_slots(): response = picnic.get_delivery_slots() assert isinstance(response, dict) - assert "delivery_slots" in response.keys() + assert "delivery_slots" in response assert isinstance(response["delivery_slots"], list) def test_get_deliveries(): - response_1 = picnic.get_deliveries() - response_2 = picnic.get_deliveries(summary=True) - - assert isinstance(response_1, list) - assert isinstance(response_1[0], dict) - assert response_1[0]["type"] == "DELIVERY" - - assert isinstance(response_2, list) - assert isinstance(response_2[0], dict) + response = picnic.get_deliveries() - assert response_1 != response_2 + assert isinstance(response, list) + assert isinstance(response[0], dict) + assert response[0]["status"] == "COMPLETED" def test_get_delivery(): # get a id to test against response = picnic.get_deliveries() - deliveryId = response[0]["id"] + deliveryId = response[0]["delivery_id"] response = picnic.get_delivery(deliveryId) assert isinstance(response, dict) - assert response["type"] == "DELIVERY" + assert response["status"] == "COMPLETED" assert response["id"] == deliveryId @@ -151,4 +158,4 @@ def test_print_categories(capsys): assert isinstance(captured.out, str) -# TO DO: add test for re-logging +# TODO: add test for re-logging diff --git a/integration_tests/test_helper.py b/integration_tests/test_helper.py index 5c4f9ab..261575d 100644 --- a/integration_tests/test_helper.py +++ b/integration_tests/test_helper.py @@ -1,6 +1,8 @@ -from python_picnic_api.helper import get_image, get_recipe_image import requests +from python_picnic_api2.helper import get_image, get_recipe_image + + def test_get_image(): id = "8560e1f1c2d2811dfefbbb2342ef0d95250533f2131416aca459bde35d73e901" size = "tile-medium" diff --git a/integration_tests/test_session.py b/integration_tests/test_session.py index e962221..78039ad 100644 --- a/integration_tests/test_session.py +++ b/integration_tests/test_session.py @@ -1,9 +1,11 @@ -from python_picnic_api.session import PicnicAPISession, PicnicAuthError -from python_picnic_api.helper import _url_generator -from requests import Session -from dotenv import load_dotenv import os +from dotenv import load_dotenv +from requests import Session + +from python_picnic_api2.client import PicnicAPI +from python_picnic_api2.session import PicnicAPISession, PicnicAuthError + load_dotenv() username = os.getenv("USERNAME") @@ -19,20 +21,20 @@ def test_init(): def test_login(): - base_url = _url_generator(DEFAULT_URL, country_code, DEFAULT_API_VERSION) - - session = PicnicAPISession() - session.login(username, password, base_url) - assert "x-picnic-auth" in session.headers.keys() + client = PicnicAPI( + username=username, password=password, country_code=country_code + ) + assert "x-picnic-auth" in client.session.headers def test_login_auth_error(): - base_url = _url_generator(DEFAULT_URL, country_code, DEFAULT_API_VERSION) - try: - session = PicnicAPISession() - session.login('username', 'password', base_url) + PicnicAPI( + username="doesnotexistblue@me.com", + password="PasSWorD12345!", + country_code=country_code, + ) except PicnicAuthError: assert True else: - assert False + raise AssertionError() diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index b39fdc9..0000000 --- a/poetry.lock +++ /dev/null @@ -1,498 +0,0 @@ -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "20.3.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] - -[[package]] -name = "black" -version = "19.10b0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" -pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" -typed-ast = ">=1.4.0" - -[package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] - -[[package]] -name = "certifi" -version = "2020.12.5" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "chardet" -version = "3.0.4" -description = "Universal encoding detector for Python 2 and 3" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "click" -version = "7.1.2" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "flake8" -version = "3.8.4" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.6.0a1,<2.7.0" -pyflakes = ">=2.2.0,<2.3.0" - -[[package]] -name = "idna" -version = "2.10" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "importlib-metadata" -version = "3.1.1" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "more-itertools" -version = "8.6.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "packaging" -version = "20.7" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pyparsing = ">=2.0.2" - -[[package]] -name = "pathspec" -version = "0.8.1" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pluggy" -version = "0.13.1" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -name = "py" -version = "1.9.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pycodestyle" -version = "2.6.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyflakes" -version = "2.2.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyparsing" -version = "2.4.7" -description = "Python parsing module" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "pytest" -version = "5.4.3" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = ">=4.0.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" - -[package.extras] -checkqa-mypy = ["mypy (==v0.761)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] - -[[package]] -name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "regex" -version = "2020.11.13" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "requests" -version = "2.25.0" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" -idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "typed-ast" -version = "1.4.1" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "urllib3" -version = "1.26.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "zipp" -version = "3.4.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.6" -content-hash = "251e54bd29f3d67e6130d2c7f5a3334fae5e17e785518398350ad6344fc42e0e" - -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, -] -black = [ - {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, -] -certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, -] -chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, -] -click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -flake8 = [ - {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, - {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, -] -idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, -] -importlib-metadata = [ - {file = "importlib_metadata-3.1.1-py3-none-any.whl", hash = "sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013"}, - {file = "importlib_metadata-3.1.1.tar.gz", hash = "sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -more-itertools = [ - {file = "more-itertools-8.6.0.tar.gz", hash = "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf"}, - {file = "more_itertools-8.6.0-py3-none-any.whl", hash = "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330"}, -] -packaging = [ - {file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"}, - {file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"}, -] -pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, -] -pycodestyle = [ - {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, - {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, -] -pyflakes = [ - {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, - {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, -] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] -pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, -] -python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, -] -regex = [ - {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, - {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, - {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, - {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, - {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, - {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, - {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, - {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, - {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, - {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, - {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, - {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, - {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, -] -requests = [ - {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, - {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, - {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, - {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, -] -urllib3 = [ - {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, - {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -zipp = [ - {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, - {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, -] diff --git a/pyproject.toml b/pyproject.toml index 2fdaf2c..f04658d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,54 @@ -[tool.poetry] -name = "python-picnic-api" -version = "1.1.0" +[project] +name = "python-picnic-api2" +version = "1.3.2" description = "" -license = "Apache-2.0" -authors = ["Mike Brink "] readme = "README.rst" -repository = "https://github.com/MikeBrink/python-picnic-api" +license = {text = "Apache-2.0"} +maintainers = [ + { name = "CodeSalat", email = "pypi@codesalat.dev"} +] +authors = [ + { name = "Mike Brink", email = "mjh.brink@icloud.com"}, + { name = "CodeSalat", email = "pypi@codesalat.dev"} +] +urls = {homepage = "https://github.com/codesalatdev/python-picnic-api", repository = "https://github.com/codesalatdev/python-picnic-api"} +requires-python = ">=3.11" +dependencies = [ + "requests>=2.24.0", + "typing_extensions>=4.12.2" +] -[tool.poetry.dependencies] -python = "^3.6" -requests = "^2.24.0" +[tool.ruff] +line-length = 88 +indent-width = 4 +target-version = "py311" -[tool.poetry.dev-dependencies] -pytest = "^5.2" -flake8 = "^3.8.3" -black = {version = "^19.10b0", allow-prereleases = true} -python-dotenv = "^0.15.0" +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", + # flake8-fixme + "FIX" +] + +[dependency-groups] +dev = [ + "pytest>=8.3.5", + "pytest-cov>=6.0.0", + "python-dotenv>=1.0.1", + "ruff>=0.9.10", +] [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/python_picnic_api/client.py b/python_picnic_api/client.py deleted file mode 100644 index 83ab1ec..0000000 --- a/python_picnic_api/client.py +++ /dev/null @@ -1,162 +0,0 @@ -from hashlib import md5 - -from .helper import _tree_generator, _url_generator, _get_category_name -from .session import PicnicAPISession, PicnicAuthError - -DEFAULT_URL = "https://storefront-prod.{}.picnicinternational.com/api/{}" -DEFAULT_COUNTRY_CODE = "NL" -DEFAULT_API_VERSION = "15" - - -class PicnicAPI: - def __init__( - self, username: str = None, password: str = None, - country_code: str = DEFAULT_COUNTRY_CODE, auth_token: str = None - ): - self._country_code = country_code - self._base_url = _url_generator( - DEFAULT_URL, self._country_code, DEFAULT_API_VERSION - ) - - self.session = PicnicAPISession(auth_token=auth_token) - - # Login if not authenticated - if not self.session.authenticated and username and password: - self.login(username, password) - - self.high_level_categories = None - - def initialize_high_level_categories(self): - """Initialize high-level categories once to avoid multiple requests.""" - if not self.high_level_categories: - self.high_level_categories = self.get_categories(depth=1) - - def _get(self, path: str, add_picnic_headers=False): - url = self._base_url + path - - # Make the request, add special picnic headers if needed - headers = { - "x-picnic-agent": "30100;1.15.183-14941;", - "x-picnic-did": "00DE6414C744E7CB" - } if add_picnic_headers else None - response = self.session.get(url, headers=headers).json() - - if self._contains_auth_error(response): - raise PicnicAuthError("Picnic authentication error") - - return response - - def _post(self, path: str, data=None): - url = self._base_url + path - response = self.session.post(url, json=data).json() - - if self._contains_auth_error(response): - raise PicnicAuthError(f"Picnic authentication error: {response['error'].get('message')}") - - return response - - @staticmethod - def _contains_auth_error(response): - if not isinstance(response, dict): - return False - - error_code = response.setdefault("error", {}).get("code") - return error_code == "AUTH_ERROR" or error_code == "AUTH_INVALID_CRED" - - def login(self, username: str, password: str): - path = "/user/login" - secret = md5(password.encode("utf-8")).hexdigest() - data = {"key": username, "secret": secret, "client_id": 30100} - - return self._post(path, data) - - def logged_in(self): - return self.session.authenticated - - def get_user(self): - return self._get("/user") - - def search(self, term: str): - path = "/search?search_term=" + term - return self._get(path) - - def get_lists(self, list_id: str = None): - if list_id: - path = "/lists/" + list_id - else: - path = "/lists" - return self._get(path) - - def get_sublist(self, list_id: str, sublist_id: str) -> list: - """Get sublist. - - Args: - list_id (str): ID of list, corresponding to requested sublist. - sublist_id (str): ID of sublist. - - Returns: - list: Sublist result. - """ - return self._get(f"/lists/{list_id}?sublist={sublist_id}") - - def get_cart(self): - return self._get("/cart") - - def get_article(self, article_id: str, add_category_name=False): - path = "/articles/" + article_id - article = self._get(path) - if add_category_name and "category_link" in article: - self.initialize_high_level_categories() - article.update( - category_name=_get_category_name(article['category_link'], self.high_level_categories) - ) - return article - - def get_article_category(self, article_id: str): - path = "/articles/" + article_id + "/category" - return self._get(path) - - def add_product(self, product_id: str, count: int = 1): - data = {"product_id": product_id, "count": count} - return self._post("/cart/add_product", data) - - def remove_product(self, product_id: str, count: int = 1): - data = {"product_id": product_id, "count": count} - return self._post("/cart/remove_product", data) - - def clear_cart(self): - return self._post("/cart/clear") - - def get_delivery_slots(self): - return self._get("/cart/delivery_slots") - - def get_delivery(self, delivery_id: str): - path = "/deliveries/" + delivery_id - return self._get(path) - - def get_delivery_scenario(self, delivery_id: str): - path = "/deliveries/" + delivery_id + "/scenario" - return self._get(path, add_picnic_headers=True) - - def get_delivery_position(self, delivery_id: str): - path = "/deliveries/" + delivery_id + "/position" - return self._get(path, add_picnic_headers=True) - - def get_deliveries(self, summary: bool = False, data=None): - data = [] if data is None else data - if summary: - return self._post("/deliveries/summary", data=data) - return self._post("/deliveries", data=data) - - def get_current_deliveries(self): - return self.get_deliveries(data=["CURRENT"]) - - def get_categories(self, depth: int = 0): - return self._get(f"/my_store?depth={depth}")["catalog"] - - def print_categories(self, depth: int = 0): - tree = "\n".join(_tree_generator(self.get_categories(depth=depth))) - print(tree) - - -__all__ = ["PicnicAPI"] diff --git a/python_picnic_api/helper.py b/python_picnic_api/helper.py deleted file mode 100644 index b28fb1e..0000000 --- a/python_picnic_api/helper.py +++ /dev/null @@ -1,78 +0,0 @@ -import re - -# prefix components: -space = " " -branch = "│ " -# pointers: -tee = "├── " -last = "└── " - -IMAGE_SIZES = ["small", "medium", "regular", "large", "extra-large"] -IMAGE_BASE_URL = "https://storefront-prod.nl.picnicinternational.com/static/images" - -def _tree_generator(response: list, prefix: str = ""): - """A recursive tree generator, - will yield a visual tree structure line by line - with each line prefixed by the same characters - """ - # response each get pointers that are ├── with a final └── : - pointers = [tee] * (len(response) - 1) + [last] - for pointer, item in zip(pointers, response): - if "name" in item: # print the item - pre = "" - if "unit_quantity" in item.keys(): - pre = f"{item['unit_quantity']} " - after = "" - if "display_price" in item.keys(): - after = f" €{int(item['display_price'])/100.0:.2f}" - - yield prefix + pointer + pre + item["name"] + after - if "items" in item: # extend the prefix and recurse: - extension = branch if pointer == tee else space - # i.e. space because last, └── , above so no more | - yield from _tree_generator(item["items"], prefix=prefix + extension) - - -def _url_generator(url: str, country_code: str, api_version: str): - return url.format(country_code.lower(), api_version) - - -def _get_category_id_from_link(category_link: str) -> str: - pattern = r'categories/(\d+)' - first_number = re.search(pattern, category_link) - if first_number: - result = str(first_number.group(1)) - return result - else: - return None - - -def _get_category_name(category_link: str, categories: list) -> str: - category_id = _get_category_id_from_link(category_link) - if category_id: - category = next((item for item in categories if item["id"] == category_id), None) - if category: - return category["name"] - else: - return None - else: - return None - -def get_recipe_image(id: str, size="regular"): - sizes = IMAGE_SIZES + ["1250x1250"] - assert size in sizes, "size must be one of: " + ", ".join(sizes) - return f"{IMAGE_BASE_URL}/recipes/{id}/{size}.png" - - -def get_image(id: str, size="regular", suffix="webp"): - assert "tile" in size if suffix == "webp" else True, ( - "webp format only supports tile sizes" - ) - assert suffix in ["webp", "png"], "suffix must be webp or png" - sizes = IMAGE_SIZES + [f"tile-{size}" for size in IMAGE_SIZES] - - assert size in sizes, ( - "size must be one of: " + ", ".join(sizes) - ) - return f"{IMAGE_BASE_URL}/{id}/{size}.{suffix}" - diff --git a/python_picnic_api/__init__.py b/src/python_picnic_api2/__init__.py similarity index 100% rename from python_picnic_api/__init__.py rename to src/python_picnic_api2/__init__.py diff --git a/src/python_picnic_api2/client.py b/src/python_picnic_api2/client.py new file mode 100644 index 0000000..ea578a0 --- /dev/null +++ b/src/python_picnic_api2/client.py @@ -0,0 +1,229 @@ +import re +from hashlib import md5 +from urllib.parse import quote + +import typing_extensions + +from .helper import ( + _extract_search_results, + _tree_generator, + _url_generator, + find_nodes_by_content, +) +from .session import PicnicAPISession, PicnicAuthError + +DEFAULT_URL = "https://storefront-prod.{}.picnicinternational.com/api/{}" +GLOBAL_GATEWAY_URL = "https://gateway-prod.global.picnicinternational.com" +DEFAULT_COUNTRY_CODE = "NL" +DEFAULT_API_VERSION = "15" +_HEADERS = { + "x-picnic-agent": "30100;1.206.1-#15408", + "x-picnic-did": "598F770380CA54B6", +} + + +class PicnicAPI: + def __init__( + self, + username: str = None, + password: str = None, + country_code: str = DEFAULT_COUNTRY_CODE, + auth_token: str = None, + ): + self._country_code = country_code + self._base_url = _url_generator( + DEFAULT_URL, self._country_code, DEFAULT_API_VERSION + ) + + self.session = PicnicAPISession(auth_token=auth_token) + + # Login if not authenticated + if not self.session.authenticated and username and password: + self.login(username, password) + + self.high_level_categories = None + + def initialize_high_level_categories(self): + """Initialize high-level categories once to avoid multiple requests.""" + if not self.high_level_categories: + self.high_level_categories = self.get_categories(depth=1) + + def _get(self, path: str, add_picnic_headers=False): + url = self._base_url + path + + # Make the request, add special picnic headers if needed + headers = _HEADERS if add_picnic_headers else None + response = self.session.get(url, headers=headers).json() + + if self._contains_auth_error(response): + raise PicnicAuthError("Picnic authentication error") + + return response + + def _post(self, path: str, data=None, base_url_override=None): + url = (base_url_override if base_url_override else self._base_url) + path + response = self.session.post(url, json=data).json() + + if self._contains_auth_error(response): + raise PicnicAuthError( + f"Picnic authentication error: \ + {response['error'].get('message')}" + ) + + return response + + @staticmethod + def _contains_auth_error(response): + if not isinstance(response, dict): + return False + + error_code = response.setdefault("error", {}).get("code") + return error_code == "AUTH_ERROR" or error_code == "AUTH_INVALID_CRED" + + def login(self, username: str, password: str): + path = "/user/login" + secret = md5(password.encode("utf-8")).hexdigest() + data = {"key": username, "secret": secret, "client_id": 30100} + + return self._post(path, data) + + def logged_in(self): + return self.session.authenticated + + def get_user(self): + return self._get("/user") + + def search(self, term: str): + path = f"/pages/search-page-results?search_term={quote(term)}" + raw_results = self._get(path, add_picnic_headers=True) + return _extract_search_results(raw_results) + + def get_cart(self): + return self._get("/cart") + + def get_article(self, article_id: str, add_category=False): + path = f"/pages/product-details-page-root?id={article_id}" + \ + "&show_category_action=true" + data = self._get(path, add_picnic_headers=True) + article_details = [] + + root_container = find_nodes_by_content( + data, {"id": "product-details-page-root-main-container"}, max_nodes=1) + if len(root_container) == 0: + return None + + article_details = root_container[0]["pml"]["component"]["children"] + + if len(article_details) == 0: + return None + + article = {} + if add_category: + cat_node = find_nodes_by_content( + data, {"id": "category-button"}, max_nodes=1) + if len(cat_node) == 0: + raise KeyError( + f"Could not extract category from article with id {article_id}") + category_regex = re.compile( + "app\\.picnic:\\/\\/categories\\/(\\d+)\\/l2\\/(\\d+)\\/l3\\/(\\d+)") + cat_ids = category_regex.match( + cat_node[0]["pml"]["component"]["onPress"]["target"]).groups() + article["category"] = self.get_category_by_ids( + int(cat_ids[1]), int(cat_ids[2])) + + color_regex = re.compile(r"#\(#\d{6}\)") + producer = re.sub(color_regex, "", str( + article_details[1].get("markdown", ""))) + article_name = re.sub(color_regex, "", str( + article_details[0]["markdown"])) + + article["name"] = f"{producer} {article_name}" + article["id"] = article_id + + return article + + def get_article_category(self, article_id: str): + path = "/articles/" + article_id + "/category" + return self._get(path) + + def add_product(self, product_id: str, count: int = 1): + data = {"product_id": product_id, "count": count} + return self._post("/cart/add_product", data) + + def remove_product(self, product_id: str, count: int = 1): + data = {"product_id": product_id, "count": count} + return self._post("/cart/remove_product", data) + + def clear_cart(self): + return self._post("/cart/clear") + + def get_delivery_slots(self): + return self._get("/cart/delivery_slots") + + def get_delivery(self, delivery_id: str): + path = "/deliveries/" + delivery_id + return self._get(path) + + def get_delivery_scenario(self, delivery_id: str): + path = "/deliveries/" + delivery_id + "/scenario" + return self._get(path, add_picnic_headers=True) + + def get_delivery_position(self, delivery_id: str): + path = "/deliveries/" + delivery_id + "/position" + return self._get(path, add_picnic_headers=True) + + @typing_extensions.deprecated( + """The option to show unsummarized deliveries was removed by picnic. + The optional parameter 'summary' will be removed in the future and default + to True. + You can ignore this warning if you do not pass the 'summary' argument to + this function.""" + ) + def get_deliveries(self, summary: bool = True, data: list = None): + data = [] if data is None else data + if not summary: + raise NotImplementedError() + return self._post("/deliveries/summary", data=data) + + def get_current_deliveries(self): + return self.get_deliveries(data=["CURRENT"]) + + def get_categories(self, depth: int = 0): + return self._get(f"/my_store?depth={depth}")["catalog"] + + def get_category_by_ids(self, l2_id: int, l3_id: int): + path = "/pages/L2-category-page-root" + \ + f"?category_id={l2_id}&l3_category_id={l3_id}" + data = self._get(path, add_picnic_headers=True) + nodes = find_nodes_by_content( + data, {"id": f"vertical-article-tiles-sub-header-{l3_id}"}, max_nodes=1) + if len(nodes) == 0: + raise KeyError("Could not find category with specified IDs") + return {"l2_id": l2_id, "l3_id": l3_id, + "name": nodes[0]["pml"]["component"]["accessibilityLabel"]} + + def print_categories(self, depth: int = 0): + tree = "\n".join(_tree_generator(self.get_categories(depth=depth))) + print(tree) + + def get_article_by_gtin(self, etan: str, maxRedirects: int = 5): + # Finds the article ID for a gtin/ean (barcode). + + url = "https://picnic.app/" + self._country_code.lower() + "/qr/gtin/" + etan + while maxRedirects > 0: + if url == "http://picnic.app/nl/link/store/storefront": + # gtin unknown + return None + r = self.session.get(url, headers=_HEADERS, allow_redirects=False) + maxRedirects -= 1 + if ";id=" in r.url: + # found the article id + return self.get_article(r.url.split(";id=", 1)[1]) + if "Location" not in r.headers: + # article id not found but also no futher redirect + return None + url = r.headers["Location"] + return None + + +__all__ = ["PicnicAPI"] diff --git a/src/python_picnic_api2/helper.py b/src/python_picnic_api2/helper.py new file mode 100644 index 0000000..8a96518 --- /dev/null +++ b/src/python_picnic_api2/helper.py @@ -0,0 +1,150 @@ +import json +import logging +import re + +# prefix components: +space = " " +branch = "│ " +# pointers: +tee = "├── " +last = "└── " + +LOGGER = logging.getLogger(__name__) + +IMAGE_SIZES = ["small", "medium", "regular", "large", "extra-large"] +IMAGE_BASE_URL = "https://storefront-prod.nl.picnicinternational.com/static/images" + +SOLE_ARTICLE_ID_PATTERN = re.compile(r'"sole_article_id":"(\w+)"') + + +def _tree_generator(response: list, prefix: str = ""): + """A recursive tree generator, + will yield a visual tree structure line by line + with each line prefixed by the same characters + """ + # response each get pointers that are ├── with a final └── : + pointers = [tee] * (len(response) - 1) + [last] + for pointer, item in zip(pointers, response, strict=False): + if "name" in item: # print the item + pre = "" + if "unit_quantity" in item: + pre = f"{item['unit_quantity']} " + after = "" + if "display_price" in item: + after = f" €{int(item['display_price']) / 100.0:.2f}" + + yield prefix + pointer + pre + item["name"] + after + if "items" in item: # extend the prefix and recurse: + extension = branch if pointer == tee else space + # i.e. space because last, └── , above so no more | + yield from _tree_generator(item["items"], prefix=prefix + extension) + + +def _url_generator(url: str, country_code: str, api_version: str): + return url.format(country_code.lower(), api_version) + + +def _get_category_id_from_link(category_link: str) -> str | None: + pattern = r"categories/(\d+)" + first_number = re.search(pattern, category_link) + if first_number: + result = str(first_number.group(1)) + return result + else: + return None + + +def _get_category_name(category_link: str, categories: list) -> str | None: + category_id = _get_category_id_from_link(category_link) + if category_id: + category = next( + (item for item in categories if item["id"] == category_id), None + ) + if category: + return category["name"] + else: + return None + else: + return None + + +def get_recipe_image(id: str, size="regular"): + sizes = IMAGE_SIZES + ["1250x1250"] + assert size in sizes, "size must be one of: " + ", ".join(sizes) + return f"{IMAGE_BASE_URL}/recipes/{id}/{size}.png" + + +def get_image(id: str, size="regular", suffix="webp"): + assert "tile" in size if suffix == "webp" else True, ( + "webp format only supports tile sizes" + ) + assert suffix in ["webp", "png"], "suffix must be webp or png" + sizes = IMAGE_SIZES + [f"tile-{size}" for size in IMAGE_SIZES] + + assert size in sizes, "size must be one of: " + ", ".join(sizes) + return f"{IMAGE_BASE_URL}/{id}/{size}.{suffix}" + + +def find_nodes_by_content(node, filter, max_nodes: int = 10): + nodes = [] + + if len(nodes) >= 10: + return nodes + + def is_dict_included(node_dict, filter_dict): + for k, v in filter_dict.items(): + if k not in node_dict: + return False + if isinstance(v, dict) and isinstance(node_dict[k], dict): + if not is_dict_included(node_dict[k], v): + return False + elif node_dict[k] != v and v is not None: + return False + return True + + if is_dict_included(node, filter): + nodes.append(node) + + if isinstance(node, dict): + for _, v in node.items(): + if isinstance(v, dict): + nodes.extend(find_nodes_by_content(v, filter, max_nodes)) + continue + if isinstance(v, list): + for item in v: + if isinstance(v, dict | list): + nodes.extend(find_nodes_by_content( + item, filter, max_nodes)) + continue + + return nodes + + +def _extract_search_results(raw_results, max_items: int = 10): + """Extract search results from the nested dictionary structure returned by + Picnic search. Number of max items can be defined to reduce excessive nested + search""" + + LOGGER.debug(f"Extracting search results from {raw_results}") + + body = raw_results.get("body", {}) + nodes = find_nodes_by_content(body.get("child", {}), { + "type": "SELLING_UNIT_TILE", "sellingUnit": {}}) + + search_results = [] + for node in nodes: + selling_unit = node["sellingUnit"] + sole_article_ids = SOLE_ARTICLE_ID_PATTERN.findall( + json.dumps(node)) + sole_article_id = sole_article_ids[0] if sole_article_ids else None + result_entry = { + **selling_unit, + "sole_article_id": sole_article_id, + } + LOGGER.debug(f"Found article {result_entry}") + search_results.append(result_entry) + + LOGGER.debug( + f"Found {len(search_results)}/{max_items} products after extraction") + + return [{"items": search_results}] diff --git a/python_picnic_api/session.py b/src/python_picnic_api2/session.py similarity index 83% rename from python_picnic_api/session.py rename to src/python_picnic_api2/session.py index c399140..5a8debc 100644 --- a/python_picnic_api/session.py +++ b/src/python_picnic_api2/session.py @@ -14,15 +14,16 @@ def __init__(self, auth_token: str = None): self.headers.update( { - "User-Agent": "okhttp/3.9.0", + "User-Agent": "okhttp/4.9.0", "Content-Type": "application/json; charset=UTF-8", - self.AUTH_HEADER: self._auth_token + self.AUTH_HEADER: self._auth_token, } ) @property def authenticated(self): - """Returns whether the user is authenticated by checking if the authentication token is set.""" + """Returns whether the user is authenticated by checking if the + authentication token is set.""" return bool(self._auth_token) @property @@ -38,14 +39,14 @@ def _update_auth_token(self, auth_token): def get(self, url, **kwargs) -> Response: """Do a GET request and update the auth token if set.""" - response = super(PicnicAPISession, self).get(url, **kwargs) + response = super().get(url, **kwargs) self._update_auth_token(response.headers.get(self.AUTH_HEADER)) return response def post(self, url, data=None, json=None, **kwargs) -> Response: """Do a POST request and update the auth token if set.""" - response = super(PicnicAPISession, self).post(url, data, json, **kwargs) + response = super().post(url, data, json, **kwargs) self._update_auth_token(response.headers.get(self.AUTH_HEADER)) return response diff --git a/tests/test_client.py b/tests/test_client.py index 359082c..f07dbd6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,13 +1,15 @@ import unittest from unittest.mock import patch -from python_picnic_api import PicnicAPI -from python_picnic_api.client import DEFAULT_URL -from python_picnic_api.session import PicnicAuthError +import pytest + +from python_picnic_api2 import PicnicAPI +from python_picnic_api2.client import DEFAULT_URL +from python_picnic_api2.session import PicnicAuthError PICNIC_HEADERS = { - "x-picnic-agent": "30100;1.15.77-10293", - "x-picnic-did": "3C417201548B2E3B", + "x-picnic-agent": "30100;1.206.1-#15408", + "x-picnic-did": "598F770380CA54B6", } @@ -21,7 +23,8 @@ def json(self): return self.json_data def setUp(self) -> None: - self.session_patcher = patch("python_picnic_api.client.PicnicAPISession") + self.session_patcher = patch( + "python_picnic_api2.client.PicnicAPISession") self.session_mock = self.session_patcher.start() self.client = PicnicAPI(username="test@test.nl", password="test") self.expected_base_url = DEFAULT_URL.format("nl", "15") @@ -31,29 +34,37 @@ def tearDown(self) -> None: def test_login_credentials(self): self.session_mock().authenticated = False - PicnicAPI(username='test@test.nl', password='test') + PicnicAPI(username="test@test.nl", password="test") self.session_mock().post.assert_called_with( - self.expected_base_url + '/user/login', - json={'key': 'test@test.nl', 'secret': '098f6bcd4621d373cade4e832627b4f6', "client_id": 1} + self.expected_base_url + "/user/login", + json={ + "key": "test@test.nl", + "secret": "098f6bcd4621d373cade4e832627b4f6", + "client_id": 30100, + }, ) def test_login_auth_token(self): self.session_mock().authenticated = True - PicnicAPI(username='test@test.nl', password='test', auth_token='a3fwo7f3h78kf3was7h8f3ahf3ah78f3') + PicnicAPI( + username="test@test.nl", + password="test", + auth_token="a3fwo7f3h78kf3was7h8f3ahf3ah78f3", + ) self.session_mock().login.assert_not_called() def test_login_failed(self): response = { "error": { "code": "AUTH_INVALID_CRED", - "message": "Invalid credentials." + "message": "Invalid credentials.", } } self.session_mock().post.return_value = self.MockResponse(response, 200) client = PicnicAPI() with self.assertRaises(PicnicAuthError): - client.login('test-user', 'test-password') + client.login("test-user", "test-password") def test_get_user(self): response = { @@ -83,25 +94,126 @@ def test_get_user(self): def test_search(self): self.client.search("test-product") self.session_mock().get.assert_called_with( - self.expected_base_url + "/search?search_term=test-product", headers=None + self.expected_base_url + + "/pages/search-page-results?search_term=test-product", + headers=PICNIC_HEADERS, + ) + + def test_search_encoding(self): + self.client.search("Gut&Günstig H-Milch") + self.session_mock().get.assert_called_with( + self.expected_base_url + + "/pages/search-page-results?search_term=Gut%26G%C3%BCnstig%20H-Milch", + headers=PICNIC_HEADERS, + ) + + def test_get_article(self): + self.session_mock().get.return_value = self.MockResponse( + {"body": {"child": {"child": {"children": [{ + "id": "product-details-page-root-main-container", + "pml": { + "component": { + "children": [ + { + "markdown": "#(#333333)Goede start halvarine#(#333333)", + }, + { + "markdown": "Blue Band", + }, + + ] + } + } + }]}}}}, + 200 ) - def test_get_lists(self): - self.client.get_lists() + article = self.client.get_article("p3f2qa") self.session_mock().get.assert_called_with( - self.expected_base_url + "/lists", headers=None + "https://storefront-prod.nl.picnicinternational.com/api/15/pages/product-details-page-root?id=p3f2qa&show_category_action=true", + headers=PICNIC_HEADERS, + ) + + self.assertEqual( + article, {'name': 'Blue Band Goede start halvarine', 'id': 'p3f2qa'}) + + def test_get_article_with_category(self): + self.session_mock().get.return_value = self.MockResponse( + {"body": {"child": {"child": {"children": [{ + "id": "product-details-page-root-main-container", + "pml": { + "component": { + "children": [ + { + "markdown": "#(#333333)Goede start halvarine#(#333333)", + }, + { + "markdown": "Blue Band", + }, + + ] + } + } + }, + { + "id": "category-button", + "pml": {"component": {"onPress": {"target": "app.picnic://categories/1000/l2/2000/l3/3000"}}} + }]}}}}, + 200 + ) + + category_patch = patch( + "python_picnic_api2.client.PicnicAPI.get_category_by_ids") + category_patch.start().return_value = { + "l2_id": 2000, "l3_id": 3000, "name": "Test"} + + article = self.client.get_article("p3f2qa", True) + + category_patch.stop() + self.session_mock().get.assert_called_with( + "https://storefront-prod.nl.picnicinternational.com/api/15/pages/product-details-page-root?id=p3f2qa&show_category_action=true", + headers=PICNIC_HEADERS, ) - def test_get_sublist(self): - self.client.get_sublist(list_id="promotion", sublist_id="12345") + self.assertEqual( + article, {'name': 'Blue Band Goede start halvarine', 'id': 'p3f2qa', + "category": {"l2_id": 2000, "l3_id": 3000, "name": "Test"}}) + + def test_get_article_with_unsupported_structure(self): + self.session_mock().get.return_value = self.MockResponse( + {"body": {"child": {"child": {"children": [{ + "id": "unsupported-root-container", + "pml": { + "component": { + "children": [ + { + "markdown": "#(#333333)Goede start halvarine#(#333333)", + }, + { + "markdown": "Blue Band", + }, + + ] + } + } + }]}}}}, + 200 + ) + + article = self.client.get_article("p3f2qa") self.session_mock().get.assert_called_with( - self.expected_base_url + "/lists/promotion?sublist=12345", headers=None + "https://storefront-prod.nl.picnicinternational.com/api/15/pages/product-details-page-root?id=p3f2qa&show_category_action=true", + headers=PICNIC_HEADERS, ) - def test_get_list_by_id(self): - self.client.get_lists("abc") + assert article is None + + def test_get_article_by_gtin(self): + self.client.get_article_by_gtin("123456789") self.session_mock().get.assert_called_with( - self.expected_base_url + "/lists/abc", headers=None + "https://picnic.app/nl/qr/gtin/123456789", + headers=PICNIC_HEADERS, + allow_redirects=False, ) def test_get_cart(self): @@ -163,22 +275,20 @@ def test_get_delivery_position(self): headers=PICNIC_HEADERS, ) - def test_get_deliveries(self): - self.client.get_deliveries() - self.session_mock().post.assert_called_with( - self.expected_base_url + "/deliveries", json=[] - ) - def test_get_deliveries_summary(self): - self.client.get_deliveries(summary=True) + self.client.get_deliveries() self.session_mock().post.assert_called_with( self.expected_base_url + "/deliveries/summary", json=[] ) + def test_get_deliveries(self): + with pytest.raises(NotImplementedError): + self.client.get_deliveries(summary=False) + def test_get_current_deliveries(self): self.client.get_current_deliveries() self.session_mock().post.assert_called_with( - self.expected_base_url + "/deliveries", json=["CURRENT"] + self.expected_base_url + "/deliveries/summary", json=["CURRENT"] ) def test_get_categories(self): @@ -200,9 +310,34 @@ def test_get_categories(self): ) self.assertDictEqual( - categories[0], {"type": "CATEGORY", "id": "purchases", "name": "Besteld"} + categories[0], + {"type": "CATEGORY", "id": "purchases", "name": "Besteld"}, + ) + + def test_get_category_by_ids(self): + self.session_mock().get.return_value = self.MockResponse( + {"children": [ + { + "id": "vertical-article-tiles-sub-header-22193", + "pml": { + "component": { + "accessibilityLabel": "Halvarine" + } + } + } + ]}, + 200 ) + category = self.client.get_category_by_ids(1000, 22193) + self.session_mock().get.assert_called_with( + f"{self.expected_base_url}/pages/L2-category-page-root" + + "?category_id=1000&l3_category_id=22193", headers=PICNIC_HEADERS + ) + + self.assertDictEqual( + category, {"name": "Halvarine", "l2_id": 1000, "l3_id": 22193}) + def test_get_auth_exception(self): self.session_mock().get.return_value = self.MockResponse( {"error": {"code": "AUTH_ERROR"}}, 400 diff --git a/tests/test_session.py b/tests/test_session.py index e81128a..63d57ac 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -3,7 +3,7 @@ from requests import Session -from python_picnic_api.session import PicnicAPISession +from python_picnic_api2.session import PicnicAPISession class TestSession(unittest.TestCase): @@ -14,27 +14,33 @@ def __init__(self, headers): @patch.object(Session, "post") def test_update_auth_token(self, post_mock): """Test that the initial auth-token is saved.""" - post_mock.return_value = self.MockResponse({ - "x-picnic-auth": "3p9fqahw3uehfaw9fh8aw3ufaw389fpawhuo3fa" - }) + post_mock.return_value = self.MockResponse( + {"x-picnic-auth": "3p9fqahw3uehfaw9fh8aw3ufaw389fpawhuo3fa"} + ) picnic_session = PicnicAPISession() - picnic_session.post("https://picnic.app/user/login", json={"test": "data"}) - self.assertDictEqual(dict(picnic_session.headers), { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "User-Agent": "okhttp/3.9.0", - "Content-Type": "application/json; charset=UTF-8", - "x-picnic-auth": "3p9fqahw3uehfaw9fh8aw3ufaw389fpawhuo3fa" - }) + picnic_session.post( + "https://picnic.app/user/login", json={"test": "data"} + ) + self.assertDictEqual( + dict(picnic_session.headers), + { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "okhttp/4.9.0", + "Content-Type": "application/json; charset=UTF-8", + "x-picnic-auth": "3p9fqahw3uehfaw9fh8aw3ufaw389fpawhuo3fa", + }, + ) @patch.object(Session, "post") def test_update_auth_token_refresh(self, post_mock): - """Test that the auth-token is updated if a new one is given in the response headers.""" - post_mock.return_value = self.MockResponse({ - "x-picnic-auth": "renewed-auth-token" - }) + """Test that the auth-token is updated if a new one is given in the + response headers.""" + post_mock.return_value = self.MockResponse( + {"x-picnic-auth": "renewed-auth-token"} + ) picnic_session = PicnicAPISession(auth_token="initial-auth-token") self.assertEqual(picnic_session.auth_token, "initial-auth-token") @@ -42,20 +48,28 @@ def test_update_auth_token_refresh(self, post_mock): picnic_session.post("https://picnic.app", json={"test": "data"}) self.assertEqual(picnic_session.auth_token, "renewed-auth-token") - self.assertDictEqual(dict(picnic_session.headers), { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "User-Agent": "okhttp/3.9.0", - "Content-Type": "application/json; charset=UTF-8", - "x-picnic-auth": "renewed-auth-token" - }) + self.assertDictEqual( + dict(picnic_session.headers), + { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "okhttp/4.9.0", + "Content-Type": "application/json; charset=UTF-8", + "x-picnic-auth": "renewed-auth-token", + }, + ) def test_authenticated_with_auth_token(self): picnic_session = PicnicAPISession(auth_token=None) self.assertFalse(picnic_session.authenticated) self.assertIsNone(picnic_session.headers[picnic_session.AUTH_HEADER]) - picnic_session = PicnicAPISession(auth_token="3p9aw8fhzsefaw29f38h7p3fwuefah37f8kwg3i") + picnic_session = PicnicAPISession( + auth_token="3p9aw8fhzsefaw29f38h7p3fwuefah37f8kwg3i" + ) self.assertTrue(picnic_session.authenticated) - self.assertEqual(picnic_session.headers[picnic_session.AUTH_HEADER], "3p9aw8fhzsefaw29f38h7p3fwuefah37f8kwg3i") + self.assertEqual( + picnic_session.headers[picnic_session.AUTH_HEADER], + "3p9aw8fhzsefaw29f38h7p3fwuefah37f8kwg3i", + ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..86a5ab8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,324 @@ +version = 1 +revision = 1 +requires-python = ">=3.11" + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, + { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, + { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, + { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, + { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, + { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, + { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, + { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, + { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, + { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, + { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, + { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, + { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, + { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, + { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, + { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, + { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, + { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, + { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, + { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, + { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, + { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, + { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, + { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, + { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, + { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, + { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, + { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, + { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, + { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, + { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, + { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "python-picnic-api2" +version = "1.3.2" +source = { editable = "." } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "python-dotenv" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "requests", specifier = ">=2.24.0" }, + { name = "typing-extensions", specifier = ">=4.12.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "ruff", specifier = ">=0.9.10" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "ruff" +version = "0.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 }, + { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 }, + { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 }, + { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 }, + { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 }, + { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 }, + { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 }, + { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 }, + { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 }, + { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 }, + { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 }, + { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 }, + { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 }, + { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 }, + { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 }, + { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 }, + { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +]