diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..f36c003 --- /dev/null +++ b/.flake8 @@ -0,0 +1,9 @@ +[flake8] +exclude = + __pycache__, + .git, + .venv, + venv, + docs +ignore = E203,W503 +max-line-length = 120 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 731b172..7340029 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,9 @@ -name: Build & Upload Python Package to PYPI production +name: Build & Upload Python Package to PyPI on: - release: - types: [ created ] + push: + tags: + - '*' branches: - main @@ -10,28 +11,42 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 + - name: Checkout 🛒 + uses: actions/checkout@v4 + + - name: Install uv 💜 + uses: astral-sh/setup-uv@v6 + + - name: Install and run ruff 🐶 + uses: astral-sh/ruff-action@v3 + + - name: Set up Python 🐍 + uses: actions/setup-python@v4 with: - python-version: '3.11' - - name: Install dependencies + python-version: "3.13" + + - name: Install dependencies 📦 run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install flake8 pytest - pip install -r requirements.txt - - name: Lint with flake8 + uv sync --all-groups --frozen + + - name: Lint with flake8 ❄️ run: | - # stop the build if there are Python syntax errors or undefined names - flake8 src/ --config=flake8.cfg - - name: Test with pytest + uv run flake8 + + - name: Test with pytest ✅ + run: | + uv run pytest tests + + - name: Version replacement based on tag ↔️ + if: github.ref_type == 'tag' run: | - pytest tests - - name: Build and publish + TAG_VERSION=${GITHUB_REF#refs/tags/} + echo "Tag version: $TAG_VERSION" + uv version $TAG_VERSION + + - name: Build and publish 🚀 env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }} run: | - python setup.py sdist bdist_wheel - twine upload dist/* \ No newline at end of file + uv build + uv publish diff --git a/.github/workflows/deploy_to_test.yml b/.github/workflows/deploy_to_test.yml index 0d86674..40989f6 100644 --- a/.github/workflows/deploy_to_test.yml +++ b/.github/workflows/deploy_to_test.yml @@ -1,4 +1,4 @@ -name: Deploy To Test PYPI +name: Build & Upload Python Package To Test PyPI on: workflow_dispatch @@ -6,29 +6,42 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 + - name: Checkout 🛒 + uses: actions/checkout@v4 + + - name: Install uv 💜 + uses: astral-sh/setup-uv@v6 + + - name: Install and run ruff 🐶 + uses: astral-sh/ruff-action@v3 + + - name: Set up Python 🐍 + uses: actions/setup-python@v4 with: - python-version: '3.11' - skip_existing: true - - name: Install dependencies + python-version: "3.13" + + - name: Install dependencies 📦 run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install flake8 pytest - pip install -r requirements.txt - - name: Lint with flake8 + uv sync --all-groups --frozen + + - name: Lint with flake8 ❄️ run: | - # stop the build if there are Python syntax errors or undefined names - flake8 src/ --config=flake8.cfg - - name: Test with pytest + uv run flake8 + + - name: Test with pytest ✅ + run: | + uv run pytest tests + + - name: Version replacement based on tag ↔️ + if: github.ref_type == 'tag' run: | - pytest tests - - name: Build and publish + TAG_VERSION=${GITHUB_REF#refs/tags/} + echo "Tag version: $TAG_VERSION" + uv version $TAG_VERSION + + - name: Build and publish 🚀 env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME_TEST }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD_TEST }} + UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN_TEST_PYPI }} run: | - python setup.py sdist bdist_wheel - twine upload --repository testpypi dist/* \ No newline at end of file + uv build + uv publish --index testpypi diff --git a/.github/workflows/push_dev.yml b/.github/workflows/push_dev.yml index f37eee5..f59f665 100644 --- a/.github/workflows/push_dev.yml +++ b/.github/workflows/push_dev.yml @@ -11,23 +11,38 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.14"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} + - name: Checkout 🛒 + uses: actions/checkout@v4 + + - name: Install uv 💜 + uses: astral-sh/setup-uv@v6 + + - name: Install and run ruff 🐶 + uses: astral-sh/ruff-action@v3 + + - name: Set up Python ${{ matrix.python-version }} 🐍 uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + + - name: Install dependencies 📦 run: | - python -m pip install --upgrade pip - pip install flake8 pytest - pip install -r requirements.txt - - name: Lint with flake8 + uv sync --all-groups --frozen + + - name: Lint with flake8 ❄️ + run: | + uv run flake8 + + - name: Test with pytest ✅ run: | - # stop the build if there are Python syntax errors or undefined names - flake8 src/ --config=flake8.cfg - - name: Test with pytest + uv run pytest tests + + - name: Version replacement based on tag ↔️ + if: github.ref_type == 'tag' run: | - pytest tests + TAG_VERSION=${GITHUB_REF#refs/tags/} + echo "Tag version: $TAG_VERSION" + uv version $TAG_VERSION diff --git a/.github/workflows/push_main.yml b/.github/workflows/push_main.yml index bb1fb6d..5ab17b0 100644 --- a/.github/workflows/push_main.yml +++ b/.github/workflows/push_main.yml @@ -9,19 +9,25 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python + - name: Checkout 🛒 + uses: actions/checkout@v4 + + - name: Install uv 💜 + uses: astral-sh/setup-uv@v6 + + - name: Set up Python 🐍 uses: actions/setup-python@v4 with: python-version: "3.13" - - name: Create html documentation + + - name: Create html documentation 📚 run: | - pip install --user pdoc3 - python setup.py install - pdoc --html -f -o .\docs keboola.component - mv .docs/keboola/component/* docs - rm -r .docs/keboola - - name: Commit docs + uv sync --all-groups --frozen + uv run pdoc --html -f -o ./docs keboola.component + mv ./docs/keboola/component/* docs + rm -r ./docs/keboola + + - name: Commit docs 📝 run: | git config --global user.name 'KCF' git config --global user.email 'kcf@users.noreply.github.com' diff --git a/LICENSE b/LICENSE index d16cdbe..db8be01 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) Keboola :(){:|:&};: s.r.o. +Copyright (c) 2025 Keboola Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/flake8.cfg b/flake8.cfg deleted file mode 100644 index b47810b..0000000 --- a/flake8.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -exclude = - .git, - __pycache__, - tests, - mapping.py -max-line-length = 119 - -# F812: list comprehension redefines ... -# H101: Use TODO(NAME) -# H202: assertRaises Exception too broad -# H233: Python 3.x incompatible use of print operator -# H301: one import per line -# H306: imports not in alphabetical order (time, os) -# H401: docstring should not start with a space -# H403: multi line docstrings should end on a new line -# H404: multi line docstring should start without a leading new line -# H405: multi line docstring summary not separated with an empty line -# H501: Do not use self.__dict__ for string formatting -ignore = F812,H101,H202,H233,H301,H306,H401,H403,H404,H405,H501 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..06052a0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[project] +name = "keboola.component" +version = "0.0.0" # replaced by the actual version based on the release tag in github actions +dependencies = [ + "pygelf", + "pytz<2021.0", + "deprecated", +] +requires-python = ">=3.8" + +authors = [ + { name = "Keboola KDS Team", email = "support@keboola.com" } +] +description = "General library for Python applications running in Keboola Connection environment" +readme = "README.md" +license = "MIT" +license-files = [ "LICENSE" ] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: Education", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Development Status :: 5 - Production/Stable", +] + +[project.urls] +Documentation = "https://keboola.github.io/python-component/interface.html" +Repository = "https://github.com/keboola/python-component" +"Component Template project" = "https://github.com/keboola/cookiecutter-python-component" + +[dependency-groups] +dev = [ + "flake8>=5.0.4", + "pytest>=8.3.5", + "ruff>=0.13.2", + "pdoc3", +] + +[[tool.uv.index]] +name = "testpypi" +url = "https://test.pypi.org/simple/" +publish-url = "https://test.pypi.org/legacy/" +explicit = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dfe2499..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pygelf -pytz<2021.0 -deprecated \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 6d22d2a..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - # remove header - header_lines = 3 - long_description = long_description.split("\n", header_lines)[header_lines] - -project_urls = { - "Documentation": "https://keboola.github.io/python-component/interface.html", - "Component Template project": "https://github.com/keboola/cookiecutter-python-component", -} - -setuptools.setup( - name="keboola.component", - version="1.6.13", - author="Keboola KDS Team", - project_urls=project_urls, - setup_requires=[ - "pytest-runner", - "flake8", - ], - tests_require=["pytest"], - install_requires=[ - "pygelf", - "pytz", - "deprecated", - ], - author_email="support@keboola.com", - description="General library for Python applications running in Keboola Connection environment", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/keboola/python-component", - package_dir={"": "src"}, - packages=["keboola.component"], - include_package_data=True, - zip_safe=False, - test_suite="tests", - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "Intended Audience :: Education", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Libraries :: Python Modules", - "Development Status :: 5 - Production/Stable", - ], - python_requires=">=3.8", -) diff --git a/tests/test_dao.py b/tests/test_dao.py index f33d82f..8673806 100644 --- a/tests/test_dao.py +++ b/tests/test_dao.py @@ -1,9 +1,21 @@ +import json import os import tempfile import unittest +from datetime import datetime + from keboola.component import dao -from keboola.component.dao import * +from keboola.component.dao import ( + BaseType, + ColumnDefinition, + DataType, + FileDefinition, + SupportedDataTypes, + TableDefinition, + TableMetadata, +) +from keboola.component.exceptions import UserException class TestTableMetadata(unittest.TestCase): @@ -49,7 +61,7 @@ def test_multi_column_datatypes_for_manifest_is_valid(self): { "key": "KBC.datatype.nullable", "value": False - } + } ], "col_2": [{ "key": "KBC.datatype.basetype", "value": "STRING" @@ -57,7 +69,7 @@ def test_multi_column_datatypes_for_manifest_is_valid(self): { "key": "KBC.datatype.nullable", "value": False - } + } ]} tmetadata = TableMetadata() @@ -73,7 +85,7 @@ def test_datatype_accepts_enum_for_manifest_valid(self): { "key": "KBC.datatype.nullable", "value": False - } + } ], "col_2": [{ "key": "KBC.datatype.basetype", "value": "STRING" @@ -81,7 +93,7 @@ def test_datatype_accepts_enum_for_manifest_valid(self): { "key": "KBC.datatype.nullable", "value": False - } + } ]} tmetadata = TableMetadata() @@ -91,7 +103,7 @@ def test_datatype_accepts_enum_for_manifest_valid(self): def test_invalid_datatype_fails(self): tmetadata = TableMetadata() - with self.assertRaises(ValueError) as ctx: + with self.assertRaises(ValueError): tmetadata.add_column_data_type('col', 'invalid type') def test_table_description_metadata_for_legacy_manifest_is_valid(self): @@ -104,7 +116,7 @@ def test_table_description_metadata_for_legacy_manifest_is_valid(self): { "key": "custom_key", "value": "custom_value" - } + } ] tmetadata.add_table_description("Description of table") tmetadata.add_table_metadata("custom_key", "custom_value") @@ -211,7 +223,7 @@ def test_out_old_to_new_has_headers_columns(self): self.assertEqual(manifest['has_header'], False) def test_out_pkey_and_no_columns_incompatible(self): - with self.assertRaises(UserException) as e: + with self.assertRaises(UserException): TableDefinition("testDef", "somepath", primary_key=['foo']) def test_out_legacy_to_new_compatible(self): @@ -237,8 +249,8 @@ def test_table_manifest_minimal(self): def test_table_manifest_missing_key(self): with self.assertRaises(UserException) as e: - table_def = TableDefinition("testDef", "somepath", is_sliced=False, - primary_key=['foo', 'bar']) + TableDefinition("testDef", "somepath", is_sliced=False, + primary_key=['foo', 'bar']) self.assertEqual(str(e.exception), "Primary key column foo not found in schema. Please specify all columns / schema") @@ -441,7 +453,7 @@ def test_unsupported_legacy_queue_properties_log(self): with self.assertLogs(level='WARNING') as log: td = TableDefinition("testDef", "somepath", write_always=True, stage='out') - manifest = td.get_manifest_dictionary(legacy_queue=True) + td.get_manifest_dictionary(legacy_queue=True) self.assertEqual(len(log.output), 1) self.assertEqual(len(log.records), 1) self.assertIn("WARNING:root:Running on legacy queue " @@ -818,7 +830,7 @@ def test_build_from_manifest_s3_staging(self): self.assertEqual(table_def.s3_staging.bucket, "test") self.assertEqual(table_def.s3_staging.credentials_access_key_id, "ASDF") self.assertEqual(table_def.s3_staging.credentials_secret_access_key, "1234") - self.assertEqual(table_def.s3_staging.credentials_session_token ,"abcd1234") + self.assertEqual(table_def.s3_staging.credentials_session_token, "abcd1234") self.assertEqual(table_def.s3_staging.is_sliced, True) self.assertEqual(table_def.s3_staging.key, "test/asdf/12345.csv.gzmanifest") self.assertEqual(table_def.s3_staging.region, "eu-central-1") @@ -831,7 +843,11 @@ def test_build_from_manifest_abs_staging(self): self.assertEqual(table_def.abs_staging.container, "exp-2-export-test-test") self.assertEqual(table_def.abs_staging.credentials_expiration, "2020-08-27T22:42:08+0200") - self.assertEqual(table_def.abs_staging.credentials_sas_connection_string, "BlobEndpoint=https://asdf.blob.core.windows.net;SharedAccessSignature=sv=2017-11-09&sr=c&st=2020-08-27T08:42:08Z&se=2020-08-27T20:42:08Z&sp=rl&sig=UJW4DPh%2Baaaaaaaaaa") + self.assertEqual( + table_def.abs_staging.credentials_sas_connection_string, + ("BlobEndpoint=https://asdf.blob.core.windows.net;SharedAccessSignature=" + "sv=2017-11-09&sr=c&st=2020-08-27T08:42:08Z&se=2020-08-27T20:42:08Z&sp=rl&sig=UJW4DPh%2Baaaaaaaaaa") + ) self.assertEqual(table_def.abs_staging.is_sliced, True) self.assertEqual(table_def.abs_staging.name, "12345.csv.gzmanifest") - self.assertEqual(table_def.abs_staging.region, "us-east-1") \ No newline at end of file + self.assertEqual(table_def.abs_staging.region, "us-east-1") diff --git a/tests/test_interface.py b/tests/test_interface.py index 355fed7..af0b629 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -1,1051 +1,1052 @@ -import json -import os -import unittest - -from keboola.component import CommonInterface, Configuration - - -class TestCommonInterface(unittest.TestCase): - - def setUp(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'data_examples', 'data1') - os.environ["KBC_DATADIR"] = path - os.environ['KBC_STACKID'] = 'connection.keboola.com' - os.environ['KBC_PROJECT_FEATURE_GATES'] = 'queuev2' - - def test_all_env_variables_initialized(self): - # set all variables - os.environ['KBC_RUNID'] = 'KBC_RUNID' - os.environ['KBC_PROJECTID'] = 'KBC_PROJECTID' - os.environ['KBC_STACKID'] = 'KBC_STACKID' - os.environ['KBC_CONFIGID'] = 'KBC_CONFIGID' - os.environ['KBC_COMPONENTID'] = 'KBC_COMPONENTID' - os.environ['KBC_PROJECTNAME'] = 'KBC_PROJECTNAME' - os.environ['KBC_TOKENID'] = 'KBC_TOKENID' - os.environ['KBC_TOKENDESC'] = 'KBC_TOKENDESC' - os.environ['KBC_TOKEN'] = 'KBC_TOKEN' - os.environ['KBC_URL'] = 'KBC_URL' - os.environ['KBC_LOGGER_ADDR'] = 'KBC_LOGGER_ADDR' - os.environ['KBC_LOGGER_PORT'] = 'KBC_LOGGER_PORT' - - ci = CommonInterface() - self.assertEqual(ci.environment_variables.data_dir, os.environ["KBC_DATADIR"]) - self.assertEqual(ci.environment_variables.run_id, 'KBC_RUNID') - self.assertEqual(ci.environment_variables.project_id, 'KBC_PROJECTID') - self.assertEqual(ci.environment_variables.stack_id, 'KBC_STACKID') - self.assertEqual(ci.environment_variables.config_id, 'KBC_CONFIGID') - self.assertEqual(ci.environment_variables.component_id, 'KBC_COMPONENTID') - self.assertEqual(ci.environment_variables.project_name, 'KBC_PROJECTNAME') - self.assertEqual(ci.environment_variables.token_id, 'KBC_TOKENID') - self.assertEqual(ci.environment_variables.token_desc, 'KBC_TOKENDESC') - self.assertEqual(ci.environment_variables.token, 'KBC_TOKEN') - self.assertEqual(ci.environment_variables.url, 'KBC_URL') - self.assertEqual(ci.environment_variables.logger_addr, 'KBC_LOGGER_ADDR') - self.assertEqual(ci.environment_variables.logger_port, 'KBC_LOGGER_PORT') - - def test_empty_required_params_pass(self): - c = CommonInterface - return True - # # set env - # interface = CommonInterface(mandatory_params=[]) - # ` - # # tests - # try: - # interface.validate_config() - # except Exception: # noeq - # self.fail("validateConfig() fails on empty Parameters!") - - def test_required_params_missing_fail(self): - return True - # set env - missing notbar - # hdlr = CommonInterface(mandatory_params=['fooBar', 'notbar']) - # - # with self.assertRaises(ValueError) as er: - # hdlr.validate_config(['fooBar', 'notbar']) - # - # self.assertEqual('Missing mandatory config parameters fields: [notbar] ', str(er.exception)) - - def test_unknown_config_tables_input_mapping_properties_pass(self): - """Unknown properties in storage.intpu.tables will be ignored when getting dataclass""" - - def test_missing_dir(self): - os.environ["KBC_DATADIR"] = "asdf" - with self.assertRaisesRegex( - ValueError, - "The data directory does not exist"): - CommonInterface() - - # ########## PROPERTIES - - def test_missing_config(self): - os.environ["KBC_DATADIR"] = os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'data_examples') - with self.assertRaisesRegex( - ValueError, - "Configuration file config.json not found"): - ci = CommonInterface() - c = ci.configuration - - def test_get_data_dir(self): - ci = CommonInterface() - self.assertEqual(os.getenv('KBC_DATADIR', ''), ci.data_folder_path) - - def test_get_tables_out_dir(self): - ci = CommonInterface() - tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'out', 'tables') - self.assertEqual(tables_out, ci.tables_out_path) - - def test_get_tables_in_dir(self): - ci = CommonInterface() - tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'in', 'files') - self.assertEqual(tables_out, ci.files_in_path) - - def test_get_files_out_dir(self): - ci = CommonInterface() - tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'out', 'files') - self.assertEqual(tables_out, ci.files_out_path) - - def test_get_files_in_dir(self): - ci = CommonInterface() - tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'in', 'tables') - self.assertEqual(tables_out, ci.tables_in_path) - - def test_legacy_queue(self): - os.environ['KBC_PROJECT_FEATURE_GATES'] = '' - ci = CommonInterface() - - # with no KBC_PROJECT_FEATURE_GATES env default to legacy queue - self.assertEqual(True, ci.is_legacy_queue) - - # otherwise check for queuev2 - os.environ['KBC_PROJECT_FEATURE_GATES'] = 'queuev2;someotherfeature' - ci = CommonInterface() - self.assertEqual(False, ci.is_legacy_queue) - - # If feature gates exists but doesn't contain queuev2 it's old queue - os.environ['KBC_PROJECT_FEATURE_GATES'] = 'feature1;someotherfeature' - ci = CommonInterface() - self.assertEqual(True, ci.is_legacy_queue) - - # when running locally default to queue v2 - os.environ['KBC_STACKID'] = '' - ci = CommonInterface() - self.assertEqual(False, ci.is_legacy_queue) - - def test_create_and_write_table_manifest_deprecated(self): - ci = CommonInterface() - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - columns=['foo', 'bar'], - destination='some-destination', - primary_key=['foo'], - incremental=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'} - ) - out_table.table_metadata.add_table_metadata('bar', 'kochba') - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - - # write - ci.write_tabledef_manifest(out_table) - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - { - 'destination': 'some-destination', - 'columns': ['foo', 'bar'], - 'primary_key': ['foo'], - 'incremental': True, - 'delimiter': ',', - 'enclosure': '"', - 'metadata': [{'key': 'bar', 'value': 'kochba'}], - 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, - 'delete_where_column': 'lilly', - 'delete_where_values': ['a', 'b'], - 'delete_where_operator': 'eq', - 'write_always': False - }, - config - ) - os.remove(manifest_filename) - - def test_create_and_write_table_manifest(self): - ci = CommonInterface() - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - columns=['foo', 'bar'], - destination='some-destination', - primary_key=['foo'], - incremental=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'}, - write_always=True, - description='some-description' - ) - out_table.table_metadata.add_table_metadata('bar', 'kochba') - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - - # write - ci.write_manifest(out_table, legacy_manifest=True) - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - { - 'destination': 'some-destination', - 'columns': ['foo', 'bar'], - 'primary_key': ['foo'], - 'incremental': True, - 'write_always': True, - 'delimiter': ',', - 'enclosure': '"', - 'metadata': [{'key': 'KBC.description', 'value': 'some-description'}, - {'key': 'bar', 'value': 'kochba'}], - 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, - 'delete_where_column': 'lilly', - 'delete_where_values': ['a', 'b'], - 'delete_where_operator': 'eq' - }, - config - ) - os.remove(manifest_filename) - - def test_create_and_write_table_manifest_old_queue(self): - # If feature gates exists but doesn't contain queuev2 it's old queue - os.environ['KBC_PROJECT_FEATURE_GATES'] = 'feature1;someotherfeature' - - ci = CommonInterface() - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - columns=['foo', 'bar'], - destination='some-destination', - primary_key=['foo'], - incremental=True, - # the write_always will then not be present in the manifest even if set - write_always=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'} - ) - out_table.table_metadata.add_table_metadata('bar', 'kochba') - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - - # write - ci.write_manifest(out_table, legacy_manifest=True) - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - { - 'destination': 'some-destination', - 'columns': ['foo', 'bar'], - 'primary_key': ['foo'], - 'incremental': True, - 'delimiter': ',', - 'enclosure': '"', - 'metadata': [{'key': 'bar', 'value': 'kochba'}], - 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, - 'delete_where_column': 'lilly', - 'delete_where_values': ['a', 'b'], - 'delete_where_operator': 'eq' - }, - config - ) - os.remove(manifest_filename) - - def test_legacy_manifest_without_columns_with_header(self): - # If feature gates exists but doesn't contain queuev2 it's old queue - os.environ['KBC_PROJECT_FEATURE_GATES'] = 'feature1;someotherfeature' - - ci = CommonInterface() - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - columns=['foo', 'bar'], - destination='some-destination', - primary_key=['foo'], - incremental=True, - # the write_always will then not be present in the manifest even if set - write_always=True, - has_header=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'} - ) - out_table.table_metadata.add_table_metadata('bar', 'kochba') - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - - # write - ci.write_manifest(out_table, legacy_manifest=True) - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - { - 'destination': 'some-destination', - 'primary_key': ['foo'], - 'incremental': True, - 'delimiter': ',', - 'enclosure': '"', - 'metadata': [{'key': 'bar', 'value': 'kochba'}], - 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, - 'delete_where_column': 'lilly', - 'delete_where_values': ['a', 'b'], - 'delete_where_operator': 'eq' - }, - config - ) - os.remove(manifest_filename) - - # #### DATA FOLDER MANIPULATION - def test_create_and_write_table_manifest_multi_deprecated(self): - ci = CommonInterface() - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - columns=['foo', 'bar'], - destination='some-destination', - primary_key=['foo'], - incremental=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'} - ) - out_table.table_metadata.add_table_metadata('bar', 'kochba') - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - - # write - ci.write_tabledef_manifests([out_table]) - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - { - 'destination': 'some-destination', - 'columns': ['foo', 'bar'], - 'primary_key': ['foo'], - 'incremental': True, - 'metadata': [{'key': 'bar', 'value': 'kochba'}], - 'delimiter': ',', - 'enclosure': '"', - 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, - 'delete_where_column': 'lilly', - 'delete_where_values': ['a', 'b'], - 'delete_where_operator': 'eq', - 'write_always': False - }, - config - ) - os.remove(manifest_filename) - - def test_create_and_write_table_manifest_multi(self): - ci = CommonInterface() - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - columns=['foo', 'bar'], - destination='some-destination', - primary_key=['foo'], - incremental=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'} - ) - out_table.table_metadata.add_table_metadata('bar', 'kochba') - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - - # write - ci.write_manifests([out_table], legacy_manifest=True) - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - { - 'destination': 'some-destination', - 'columns': ['foo', 'bar'], - 'primary_key': ['foo'], - 'incremental': True, - 'metadata': [{'key': 'bar', 'value': 'kochba'}], - 'delimiter': ',', - 'enclosure': '"', - 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, - 'delete_where_column': 'lilly', - 'delete_where_values': ['a', 'b'], - 'delete_where_operator': 'eq', - 'write_always': False - }, - config - ) - os.remove(manifest_filename) - - def test_create_and_write_table_manifest_new(self): - os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" - ci = CommonInterface() - del os.environ['KBC_DATA_TYPE_SUPPORT'] - - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - schema=['foo', 'bar'], - has_header=True, - destination='some-destination', - description='some-description', - primary_key=['foo'], - incremental=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'} - ) - out_table.table_metadata.add_table_metadata('bar', 'kochba') - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - - # write - ci.write_manifests([out_table]) - - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - {'delete_where_column': 'lilly', - 'delete_where_operator': 'eq', - 'delete_where_values': ['a', 'b'], - 'delimiter': ',', - 'destination': 'some-destination', - 'enclosure': '"', - 'has_header': True, - 'incremental': True, - 'manifest_type': 'out', - 'schema': [{'data_type': {'base': {'type': 'STRING'}}, - 'name': 'foo', - 'nullable': True, - 'primary_key': True}, - {'data_type': {'base': {'type': 'STRING'}}, - 'name': 'bar', - 'nullable': True}], - 'table_metadata': {'KBC.description': 'some-description', 'bar': 'kochba'}, - 'write_always': False}, - config - ) - os.remove(manifest_filename) - - def test_legacy_column_metadata_ignored_on_new_schema(self): - # TODO: this is not implemented on purpose - os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" - ci = CommonInterface() - # create table def - out_table = ci.create_out_table_definition('some-table.csv', - columns=['foo', 'bar'], - destination='some-destination', - primary_key=['foo'], - incremental=True, - delete_where={'column': 'lilly', - 'values': ['a', 'b'], - 'operator': 'eq'} - ) - # this will be ignored - out_table.table_metadata.add_table_metadata('bar', 'kochba') - # this will be ignored - out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') - # this will be ignored - out_table.table_metadata.add_column_data_type('bar', 'NUMERIC') - - # write - ci.write_manifest(out_table) - - del os.environ['KBC_DATA_TYPE_SUPPORT'] - manifest_filename = out_table.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - {'destination': 'some-destination', - 'incremental': True, - 'manifest_type': 'out', - 'write_always': False, - 'delimiter': ',', - 'enclosure': '"', - 'table_metadata': {'bar': 'kochba'}, - 'has_header': False, - 'delete_where_column': 'lilly', 'delete_where_values': ['a', 'b'], 'delete_where_operator': 'eq', - 'schema': [ - {'name': 'foo', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True, 'primary_key': True}, - {'name': 'bar', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}]}, - config - ) - - def test_get_input_tables_definition(self): - ci = CommonInterface() - - tables = ci.get_input_tables_definitions() - - self.assertEqual(6, len(tables)) - for table in tables: - if table.name == 'sample.csv': - self.assertEqual(table.columns, [ - "x", - "Sales", - "CompPrice", - "Income", - "Advertising", - "Population", - "Price", - "ShelveLoc", - "Age", - "Education", - "Urban", - "US", - "High" - ]) - self.assertEqual(table.rows_count, 400) - self.assertEqual(table.data_size_bytes, 81920) - elif table.name == 'fooBar': - self.assertEqual(table.id, 'in.c-main.test2') - self.assertEqual(table.full_path, os.path.join(ci.tables_in_path, 'fooBar')) - self.assertEqual(table.name, 'fooBar') - - def test_get_input_tables_definition_orphaned_manifest(self): - ci = CommonInterface() - - tables = ci.get_input_tables_definitions(orphaned_manifests=True) - - self.assertEqual(7, len(tables)) - for table in tables: - if table.name == 'sample.csv': - self.assertEqual(table.columns, [ - "x", - "Sales", - "CompPrice", - "Income", - "Advertising", - "Population", - "Price", - "ShelveLoc", - "Age", - "Education", - "Urban", - "US", - "High" - ]) - self.assertEqual(table.rows_count, 400) - self.assertEqual(table.data_size_bytes, 81920) - elif table.name == 'fooBar': - self.assertEqual(table.id, 'in.c-main.test2') - self.assertEqual(table.full_path, os.path.join(ci.tables_in_path, 'fooBar')) - self.assertEqual(table.name, 'fooBar') - - def test_state_file_initialized(self): - ci = CommonInterface() - state = ci.get_state_file() - self.assertEqual(state['test_state'], 1234) - - def test_state_file_created(self): - ci = CommonInterface() - # write - ci.write_state_file({"some_state": 1234}) - - # load - state_filename = os.path.join(ci.data_folder_path, 'out', 'state.json') - with open(state_filename) as state_file: - state = json.load(state_file) - - self.assertEqual( - {"some_state": 1234}, - state - ) - - # cleanup - os.remove(state_filename) - - def test_get_input_table_by_name_fails_on_nonexistent(self): - ci = CommonInterface() - with self.assertRaises(ValueError): - ci.get_input_table_definition_by_name('nonexistent.csv') - - def test_get_input_table_by_name_existing_passes(self): - ci = CommonInterface() - in_table = ci.get_input_table_definition_by_name('fooBar') - self.assertEqual(in_table.id, 'in.c-main.test2') - self.assertEqual(in_table.full_path, os.path.join(ci.tables_in_path, 'fooBar')) - self.assertEqual(in_table.name, 'fooBar') - - # Files - - def test_create_and_write_file_manifest_deprecated(self): - ci = CommonInterface() - # create table def - out_file = ci.create_out_file_definition('some-file.jpg', - is_permanent=True, - is_encrypted=True, - is_public=True, - tags=['foo', 'bar'], - notify=True - ) - - # write - ci.write_filedef_manifest(out_file) - manifest_filename = out_file.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - {'tags': ['foo', 'bar'], - 'is_public': True, - 'is_permanent': True, - 'is_encrypted': True, - 'notify': True}, - config - ) - os.remove(manifest_filename) - - def test_create_and_write_file_manifest(self): - ci = CommonInterface() - # create table def - out_file = ci.create_out_file_definition('some-file.jpg', - is_permanent=True, - is_encrypted=True, - is_public=True, - tags=['foo', 'bar'], - notify=True - ) - - # write - ci.write_manifest(out_file) - - manifest_filename = out_file.full_path + '.manifest' - with open(manifest_filename) as manifest_file: - config = json.load(manifest_file) - self.assertEqual( - {'tags': ['foo', 'bar'], - 'is_public': True, - 'is_permanent': True, - 'is_encrypted': True, - 'notify': True}, - config - ) - os.remove(manifest_filename) - - def test_get_input_files_definition_latest(self): - ci = CommonInterface() - - files = ci.get_input_files_definitions() - - self.assertEqual(len(files), 5) - for file in files: - if file.name == 'duty_calls.png': - self.assertEqual(file.id, '151971455') - - def test_get_input_files_definition_by_tag(self): - ci = CommonInterface() - - files = ci.get_input_files_definitions(tags=['dilbert']) - - self.assertEqual(len(files), 3) - for file in files: - if file.name == '21702.strip.print.gif': - self.assertEqual(file.tags, [ - "dilbert" - ]) - self.assertEqual(file.max_age_days, 180) - self.assertEqual(file.size_bytes, 4931) - - def test_get_input_files_definition_by_tag_w_system(self): - ci = CommonInterface(os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'data_examples', 'data_system_tags')) - - files = ci.get_input_files_definitions(tags=['dilbert']) - - self.assertEqual(len(files), 3) - for file in files: - if file.name == '21702.strip.print.gif': - self.assertEqual(file.tags, [ - "dilbert", - "componentId: 1234", - "configurationId: 12345", - "configurationRowId: 12345", - "runId: 22123", - "branchId: 312321" - ]) - self.assertEqual(file.max_age_days, 180) - self.assertEqual(file.size_bytes, 4931) - - def test_get_input_files_definition_tag_group_w_system(self): - ci = CommonInterface(os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'data_examples', 'data_system_tags')) - - files = ci.get_input_file_definitions_grouped_by_tag_group(only_latest_files=False) - - self.assertEqual(len(files), 2) - self.assertEqual(len(files["bar;foo"]), 3) - for file in files["bar;foo"]: - if file.name == 'compiler_complaint.png': - self.assertEqual(file.tags, [ - "foo", - "bar", - "componentId: 1234", - "configurationId: 12345", - "configurationRowId: 12345", - "runId: 22123", - "branchId: 312321" - ]) - - def test_get_input_files_definition_nofilter(self): - ci = CommonInterface() - - files = ci.get_input_files_definitions(only_latest_files=False) - - self.assertEqual(len(files), 6) - for file in files: - if file.name == 'duty_calls': - self.assertEqual(file.tags, [ - "xkcd" - ]) - self.assertEqual(file.max_age_days, 180) - self.assertEqual(file.size_bytes, 30027) - - def test_get_input_files_definition_no_manifest_passes(self): - ci = CommonInterface(os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'data_examples', 'data2')) - - files = ci.get_input_files_definitions(only_latest_files=True) - - self.assertEqual(len(files), 1) - for file in files: - self.assertEqual(file.max_age_days, 0) - self.assertEqual(file.size_bytes, 0) - self.assertEqual(file.created, None) - - def test_convert_old_to_new_manifest(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data4') - os.environ["KBC_DATADIR"] = path - - ci = CommonInterface() - tables = ci.get_input_tables_definitions() - - os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" - - new_manifest = tables[0].get_manifest_dictionary('out') - - self.assertEqual({ - 'write_always': False, - 'delimiter': ',', - 'enclosure': '"', - 'manifest_type': 'out', - 'has_header': True, - 'schema': [ - {'name': 'x', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True, 'metadata': {'foo': 'gogo'}}, - {'name': 'Sales', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'CompPrice', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Income', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Advertising', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Population', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Price', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'ShelveLoc', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Age', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Education', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Urban', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'US', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'High', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}] - }, new_manifest) - - del os.environ['KBC_DATA_TYPE_SUPPORT'] - - def test_convert_new_to_old_manifest_has_header_false(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data_new_manifest') - os.environ["KBC_DATADIR"] = path - - ci = CommonInterface() - tables = ci.get_input_tables_definitions() - - old_manifest = tables[0].get_manifest_dictionary('out', legacy_manifest=True) - - self.assertEqual({ - 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', - 'Education', 'Urban', 'US', 'High'], - 'delimiter': ',', - 'enclosure': '"', - 'incremental': False, - 'write_always': False - }, old_manifest) - - def test_convert_new_to_old_manifest_storage_param(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', - 'data_storage_parameter_data_types') - os.environ["KBC_DATADIR"] = path - os.environ['KBC_DATA_TYPE_SUPPORT'] = 'authoritative' - - ci = CommonInterface() - tables = ci.get_input_tables_definitions() - - ci.write_manifests([tables[0]]) - manifest_filename = tables[0].full_path + '.manifest' - with open(manifest_filename) as manifest_file: - old_manifest = json.load(manifest_file) - - self.assertEqual({ - 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', - 'Education', 'Urban', 'US', 'High'], - 'delimiter': ',', - 'enclosure': '"', - 'incremental': False, - 'write_always': False - }, old_manifest) - - def test_full_input_manifest(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data_full_input_manifest') - os.environ["KBC_DATADIR"] = path - - ci = CommonInterface() - tables = ci.get_input_tables_definitions() - - ci.write_manifests([tables[0]]) - manifest_filename = tables[0].full_path + '.manifest' - with open(manifest_filename) as manifest_file: - old_manifest = json.load(manifest_file) - - self.assertEqual({ - 'id': 'in.c-main.test', - 'uri': 'https://connection.keboola.com//v2/storage/tables/in.c-main.test', - 'name': 'sample.csv', - 'created': '2015-11-02T09:11:37+0100', - 'last_change_date': '2015-11-02T09:11:37+0100', - 'last_import_date': '2015-11-02T09:11:37+0100', - 'rows_count': 400, - 'data_size_bytes': 81920, - 'is_alias': False, - 'indexed_columns': ['x'], - 'primary_key': ['x'], - 'column_metadata': {'x': [{'key': 'foo', 'value': 'gogo'}]}, - 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', - 'Education', 'Urban', 'US', 'High'] - }, old_manifest) - - def test_full_input_manifest_dtypes_support(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data_full_input_manifest') - os.environ["KBC_DATADIR"] = path - os.environ['KBC_DATA_TYPE_SUPPORT'] = 'authoritative' - - ci = CommonInterface() - tables = ci.get_input_tables_definitions() - - ci.write_manifests([tables[0]]) - manifest_filename = tables[0].full_path + '.manifest' - with open(manifest_filename) as manifest_file: - old_manifest = json.load(manifest_file) - - self.maxDiff = None - - self.assertEqual({ - 'id': 'in.c-main.test', - 'uri': 'https://connection.keboola.com//v2/storage/tables/in.c-main.test', - 'name': 'sample.csv', - 'created': '2015-11-02T09:11:37+0100', - 'last_change_date': '2015-11-02T09:11:37+0100', - 'last_import_date': '2015-11-02T09:11:37+0100', - 'rows_count': 400, - 'data_size_bytes': 81920, - 'is_alias': False, - 'indexed_columns': ['x'], - 'primary_key': ['x'], - 'column_metadata': {'x': [{'key': 'foo', 'value': 'gogo'}]}, - 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', - 'Education', 'Urban', 'US', 'High'] - }, old_manifest) - - def test_separator_delimiter(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data5') - os.environ["KBC_DATADIR"] = path - - ci = CommonInterface() - tables = ci.get_input_tables_definitions() - - old_manifest = tables[0].get_manifest_dictionary('out', legacy_manifest=True) - - self.assertEqual({ - 'columns': [ - 'x', - 'Sales', - 'CompPrice', - 'Income', - 'Advertising', - 'Population', - 'Price', - 'ShelveLoc', - 'Age', - 'Education', - 'Urban', - 'US', - 'High' - ], - 'delimiter': '\t', - 'enclosure': "'", - 'incremental': True, - 'primary_key': [ - 'x' - ], - 'write_always': False, - 'delete_where_column': 'Advertising', - 'delete_where_values': ['Video', 'Search'], - 'delete_where_operator': 'eq', - 'destination': 'out.c-main.Leads' - }, old_manifest) - - def test_separator_delimiter_dtypes(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data5') - os.environ["KBC_DATADIR"] = path - - ci = CommonInterface() - tables = ci.get_input_tables_definitions() - - os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" - - new_manifest = tables[0].get_manifest_dictionary('out') - - self.assertEqual({ - 'write_always': False, - 'delimiter': '\t', - 'enclosure': '\'', - 'manifest_type': 'out', - 'has_header': False, - 'incremental': True, - 'schema': [ - {'name': 'x', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True, 'primary_key': True}, - {'name': 'Sales', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'CompPrice', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Income', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Advertising', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Population', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Price', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'ShelveLoc', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Age', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Education', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'Urban', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'US', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, - {'name': 'High', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}], - 'delete_where_column': 'Advertising', - 'delete_where_values': ['Video', 'Search'], - 'delete_where_operator': 'eq', - 'destination': 'out.c-main.Leads' - }, new_manifest) - - del os.environ['KBC_DATA_TYPE_SUPPORT'] - - -class TestConfiguration(unittest.TestCase): - - def setUp(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'data_examples', 'data1') - os.environ["KBC_DATADIR"] = path - - def test_missing_config(self): - with self.assertRaisesRegex( - ValueError, - "Configuration file config.json not found"): - Configuration('/non-existent/') - - def test_get_parameters(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - params = cfg.parameters - self.assertEqual({'fooBar': {'bar': 24, 'foo': 42}, 'baz': 'bazBar'}, - params) - self.assertEqual(params['fooBar']['foo'], 42) - self.assertEqual(params['fooBar']['bar'], 24) - - def test_get_action(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - - self.assertEqual(cfg.action, 'run') - - def test_get_action_empty_config(self): - cfg = Configuration(os.path.join(os.getenv('KBC_DATADIR', ''), '..', - 'data2')) - self.assertEqual(cfg.action, '') - - def test_get_input_mappings(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - tables = cfg.tables_input_mapping - - self.assertEqual(len(tables), 2) - for table in tables: - if table['destination'] == 'sample.csv': - self.assertEqual(table['source'], 'in.c-main.test') - else: - self.assertEqual('in.c-main.test2', table['source']) - - def test_get_input_mappings_with_column_types(self): - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'data_examples', 'data4') - cfg = Configuration(path) - tables = cfg.tables_input_mapping - coltypes = tables[0].column_types[0] - source = coltypes.source - self.assertEqual(source, "Sales") - column_type = coltypes.type - self.assertEqual(column_type, "VARCHAR") - destination = coltypes.destination - self.assertEqual(destination, "id") - length = coltypes.length - self.assertEqual(length, "255") - nullable = coltypes.nullable - self.assertEqual(nullable, False) - convert_empty_values_to_null = coltypes.convert_empty_values_to_null - self.assertEqual(convert_empty_values_to_null, False) - - def test_get_output_mapping(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - tables = cfg.tables_output_mapping - self.assertEqual(len(tables), 2) - self.assertEqual(tables[0]['source'], 'results.csv') - self.assertEqual(tables[1]['source'], 'results-new.csv') - - def test_empty_storage(self): - cfg = Configuration(os.path.join(os.getenv('KBC_DATADIR', ''), '..', - 'data2')) - self.assertEqual(cfg.tables_output_mapping, []) - self.assertEqual(cfg.files_output_mapping, []) - self.assertEqual(cfg.tables_input_mapping, []) - self.assertEqual(cfg.files_input_mapping, []) - self.assertEqual(cfg.parameters, {}) - - def test_empty_params(self): - cfg = Configuration(os.path.join(os.getenv('KBC_DATADIR', ''), '..', - 'data3')) - self.assertEqual([], cfg.tables_output_mapping) - self.assertEqual([], cfg.files_output_mapping) - self.assertEqual({}, cfg.parameters) - - def test_get_authorization(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - auth = cfg.oauth_credentials - # self.assertEqual(auth['id'], "123456") - self.assertEqual(auth["id"], "main") - - def test_get_oauthapi_data(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - self.assertDictEqual(cfg.oauth_credentials.data, {"mykey": "myval"}) - - def test_get_oauthapi_appsecret(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - self.assertEqual(cfg.oauth_credentials.appSecret, "myappsecret") - - def test_get_oauthapi_appkey(self): - cfg = Configuration(os.environ["KBC_DATADIR"]) - self.assertEqual(cfg.oauth_credentials.appKey, "myappkey") - - # def test_file_manifest(self): - # cfg = docker.Config() - # some_file = os.path.join(tempfile.mkdtemp('kbc-test') + 'someFile.txt') - # cfg.write_file_manifest(some_file, file_tags=['foo', 'bar'], - # is_public=True, is_permanent=False, - # notify=True) - # manifest_filename = some_file + '.manifest' - # with open(manifest_filename) as manifest_file: - # config = json.load(manifest_file) - # self.assertEqual( - # {'is_public': True, 'is_permanent': False, 'notify': True, - # 'tags': ['foo', 'bar']}, - # config - # ) - # os.remove(manifest_filename) - - -if __name__ == '__main__': - unittest.main() +import json +import os +import unittest + +from keboola.component import CommonInterface, Configuration + + +class TestCommonInterface(unittest.TestCase): + + def setUp(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data_examples', 'data1') + os.environ["KBC_DATADIR"] = path + os.environ['KBC_STACKID'] = 'connection.keboola.com' + os.environ['KBC_PROJECT_FEATURE_GATES'] = 'queuev2' + + def test_all_env_variables_initialized(self): + # set all variables + os.environ['KBC_RUNID'] = 'KBC_RUNID' + os.environ['KBC_PROJECTID'] = 'KBC_PROJECTID' + os.environ['KBC_STACKID'] = 'KBC_STACKID' + os.environ['KBC_CONFIGID'] = 'KBC_CONFIGID' + os.environ['KBC_COMPONENTID'] = 'KBC_COMPONENTID' + os.environ['KBC_PROJECTNAME'] = 'KBC_PROJECTNAME' + os.environ['KBC_TOKENID'] = 'KBC_TOKENID' + os.environ['KBC_TOKENDESC'] = 'KBC_TOKENDESC' + os.environ['KBC_TOKEN'] = 'KBC_TOKEN' + os.environ['KBC_URL'] = 'KBC_URL' + os.environ['KBC_LOGGER_ADDR'] = 'KBC_LOGGER_ADDR' + os.environ['KBC_LOGGER_PORT'] = 'KBC_LOGGER_PORT' + + ci = CommonInterface() + self.assertEqual(ci.environment_variables.data_dir, os.environ["KBC_DATADIR"]) + self.assertEqual(ci.environment_variables.run_id, 'KBC_RUNID') + self.assertEqual(ci.environment_variables.project_id, 'KBC_PROJECTID') + self.assertEqual(ci.environment_variables.stack_id, 'KBC_STACKID') + self.assertEqual(ci.environment_variables.config_id, 'KBC_CONFIGID') + self.assertEqual(ci.environment_variables.component_id, 'KBC_COMPONENTID') + self.assertEqual(ci.environment_variables.project_name, 'KBC_PROJECTNAME') + self.assertEqual(ci.environment_variables.token_id, 'KBC_TOKENID') + self.assertEqual(ci.environment_variables.token_desc, 'KBC_TOKENDESC') + self.assertEqual(ci.environment_variables.token, 'KBC_TOKEN') + self.assertEqual(ci.environment_variables.url, 'KBC_URL') + self.assertEqual(ci.environment_variables.logger_addr, 'KBC_LOGGER_ADDR') + self.assertEqual(ci.environment_variables.logger_port, 'KBC_LOGGER_PORT') + + def test_empty_required_params_pass(self): + return True + # # set env + # interface = CommonInterface(mandatory_params=[]) + # ` + # # tests + # try: + # interface.validate_config() + # except Exception: # noeq + # self.fail("validateConfig() fails on empty Parameters!") + + def test_required_params_missing_fail(self): + return True + # set env - missing notbar + # hdlr = CommonInterface(mandatory_params=['fooBar', 'notbar']) + # + # with self.assertRaises(ValueError) as er: + # hdlr.validate_config(['fooBar', 'notbar']) + # + # self.assertEqual('Missing mandatory config parameters fields: [notbar] ', str(er.exception)) + + def test_unknown_config_tables_input_mapping_properties_pass(self): + """Unknown properties in storage.intpu.tables will be ignored when getting dataclass""" + + def test_missing_dir(self): + os.environ["KBC_DATADIR"] = "asdf" + with self.assertRaisesRegex( + ValueError, + "The data directory does not exist"): + CommonInterface() + + # ########## PROPERTIES + + def test_missing_config(self): + os.environ["KBC_DATADIR"] = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data_examples') + with self.assertRaisesRegex( + ValueError, + "Configuration file config.json not found"): + ci = CommonInterface() + ci.configuration + + def test_get_data_dir(self): + ci = CommonInterface() + self.assertEqual(os.getenv('KBC_DATADIR', ''), ci.data_folder_path) + + def test_get_tables_out_dir(self): + ci = CommonInterface() + tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'out', 'tables') + self.assertEqual(tables_out, ci.tables_out_path) + + def test_get_tables_in_dir(self): + ci = CommonInterface() + tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'in', 'files') + self.assertEqual(tables_out, ci.files_in_path) + + def test_get_files_out_dir(self): + ci = CommonInterface() + tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'out', 'files') + self.assertEqual(tables_out, ci.files_out_path) + + def test_get_files_in_dir(self): + ci = CommonInterface() + tables_out = os.path.join(os.getenv('KBC_DATADIR', ''), 'in', 'tables') + self.assertEqual(tables_out, ci.tables_in_path) + + def test_legacy_queue(self): + os.environ['KBC_PROJECT_FEATURE_GATES'] = '' + ci = CommonInterface() + + # with no KBC_PROJECT_FEATURE_GATES env default to legacy queue + self.assertEqual(True, ci.is_legacy_queue) + + # otherwise check for queuev2 + os.environ['KBC_PROJECT_FEATURE_GATES'] = 'queuev2;someotherfeature' + ci = CommonInterface() + self.assertEqual(False, ci.is_legacy_queue) + + # If feature gates exists but doesn't contain queuev2 it's old queue + os.environ['KBC_PROJECT_FEATURE_GATES'] = 'feature1;someotherfeature' + ci = CommonInterface() + self.assertEqual(True, ci.is_legacy_queue) + + # when running locally default to queue v2 + os.environ['KBC_STACKID'] = '' + ci = CommonInterface() + self.assertEqual(False, ci.is_legacy_queue) + + def test_create_and_write_table_manifest_deprecated(self): + ci = CommonInterface() + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + columns=['foo', 'bar'], + destination='some-destination', + primary_key=['foo'], + incremental=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'} + ) + out_table.table_metadata.add_table_metadata('bar', 'kochba') + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + + # write + ci.write_tabledef_manifest(out_table) + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + { + 'destination': 'some-destination', + 'columns': ['foo', 'bar'], + 'primary_key': ['foo'], + 'incremental': True, + 'delimiter': ',', + 'enclosure': '"', + 'metadata': [{'key': 'bar', 'value': 'kochba'}], + 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, + 'delete_where_column': 'lilly', + 'delete_where_values': ['a', 'b'], + 'delete_where_operator': 'eq', + 'write_always': False + }, + config + ) + os.remove(manifest_filename) + + def test_create_and_write_table_manifest(self): + ci = CommonInterface() + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + columns=['foo', 'bar'], + destination='some-destination', + primary_key=['foo'], + incremental=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'}, + write_always=True, + description='some-description' + ) + out_table.table_metadata.add_table_metadata('bar', 'kochba') + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + + # write + ci.write_manifest(out_table, legacy_manifest=True) + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + { + 'destination': 'some-destination', + 'columns': ['foo', 'bar'], + 'primary_key': ['foo'], + 'incremental': True, + 'write_always': True, + 'delimiter': ',', + 'enclosure': '"', + 'metadata': [{'key': 'KBC.description', 'value': 'some-description'}, + {'key': 'bar', 'value': 'kochba'}], + 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, + 'delete_where_column': 'lilly', + 'delete_where_values': ['a', 'b'], + 'delete_where_operator': 'eq' + }, + config + ) + os.remove(manifest_filename) + + def test_create_and_write_table_manifest_old_queue(self): + # If feature gates exists but doesn't contain queuev2 it's old queue + os.environ['KBC_PROJECT_FEATURE_GATES'] = 'feature1;someotherfeature' + + ci = CommonInterface() + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + columns=['foo', 'bar'], + destination='some-destination', + primary_key=['foo'], + incremental=True, + # the write_always will then not be present + # in the manifest even if set + write_always=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'} + ) + out_table.table_metadata.add_table_metadata('bar', 'kochba') + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + + # write + ci.write_manifest(out_table, legacy_manifest=True) + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + { + 'destination': 'some-destination', + 'columns': ['foo', 'bar'], + 'primary_key': ['foo'], + 'incremental': True, + 'delimiter': ',', + 'enclosure': '"', + 'metadata': [{'key': 'bar', 'value': 'kochba'}], + 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, + 'delete_where_column': 'lilly', + 'delete_where_values': ['a', 'b'], + 'delete_where_operator': 'eq' + }, + config + ) + os.remove(manifest_filename) + + def test_legacy_manifest_without_columns_with_header(self): + # If feature gates exists but doesn't contain queuev2 it's old queue + os.environ['KBC_PROJECT_FEATURE_GATES'] = 'feature1;someotherfeature' + + ci = CommonInterface() + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + columns=['foo', 'bar'], + destination='some-destination', + primary_key=['foo'], + incremental=True, + # the write_always will then not be present + # in the manifest even if set + write_always=True, + has_header=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'} + ) + out_table.table_metadata.add_table_metadata('bar', 'kochba') + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + + # write + ci.write_manifest(out_table, legacy_manifest=True) + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + { + 'destination': 'some-destination', + 'primary_key': ['foo'], + 'incremental': True, + 'delimiter': ',', + 'enclosure': '"', + 'metadata': [{'key': 'bar', 'value': 'kochba'}], + 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, + 'delete_where_column': 'lilly', + 'delete_where_values': ['a', 'b'], + 'delete_where_operator': 'eq' + }, + config + ) + os.remove(manifest_filename) + + # #### DATA FOLDER MANIPULATION + def test_create_and_write_table_manifest_multi_deprecated(self): + ci = CommonInterface() + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + columns=['foo', 'bar'], + destination='some-destination', + primary_key=['foo'], + incremental=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'} + ) + out_table.table_metadata.add_table_metadata('bar', 'kochba') + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + + # write + ci.write_tabledef_manifests([out_table]) + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + { + 'destination': 'some-destination', + 'columns': ['foo', 'bar'], + 'primary_key': ['foo'], + 'incremental': True, + 'metadata': [{'key': 'bar', 'value': 'kochba'}], + 'delimiter': ',', + 'enclosure': '"', + 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, + 'delete_where_column': 'lilly', + 'delete_where_values': ['a', 'b'], + 'delete_where_operator': 'eq', + 'write_always': False + }, + config + ) + os.remove(manifest_filename) + + def test_create_and_write_table_manifest_multi(self): + ci = CommonInterface() + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + columns=['foo', 'bar'], + destination='some-destination', + primary_key=['foo'], + incremental=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'} + ) + out_table.table_metadata.add_table_metadata('bar', 'kochba') + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + + # write + ci.write_manifests([out_table], legacy_manifest=True) + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + { + 'destination': 'some-destination', + 'columns': ['foo', 'bar'], + 'primary_key': ['foo'], + 'incremental': True, + 'metadata': [{'key': 'bar', 'value': 'kochba'}], + 'delimiter': ',', + 'enclosure': '"', + 'column_metadata': {'bar': [{'key': 'foo', 'value': 'gogo'}]}, + 'delete_where_column': 'lilly', + 'delete_where_values': ['a', 'b'], + 'delete_where_operator': 'eq', + 'write_always': False + }, + config + ) + os.remove(manifest_filename) + + def test_create_and_write_table_manifest_new(self): + os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" + ci = CommonInterface() + del os.environ['KBC_DATA_TYPE_SUPPORT'] + + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + schema=['foo', 'bar'], + has_header=True, + destination='some-destination', + description='some-description', + primary_key=['foo'], + incremental=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'} + ) + out_table.table_metadata.add_table_metadata('bar', 'kochba') + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + + # write + ci.write_manifests([out_table]) + + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + {'delete_where_column': 'lilly', + 'delete_where_operator': 'eq', + 'delete_where_values': ['a', 'b'], + 'delimiter': ',', + 'destination': 'some-destination', + 'enclosure': '"', + 'has_header': True, + 'incremental': True, + 'manifest_type': 'out', + 'schema': [{'data_type': {'base': {'type': 'STRING'}}, + 'name': 'foo', + 'nullable': True, + 'primary_key': True}, + {'data_type': {'base': {'type': 'STRING'}}, + 'name': 'bar', + 'nullable': True}], + 'table_metadata': {'KBC.description': 'some-description', 'bar': 'kochba'}, + 'write_always': False}, + config + ) + os.remove(manifest_filename) + + def test_legacy_column_metadata_ignored_on_new_schema(self): + # TODO: this is not implemented on purpose + os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" + ci = CommonInterface() + # create table def + out_table = ci.create_out_table_definition('some-table.csv', + columns=['foo', 'bar'], + destination='some-destination', + primary_key=['foo'], + incremental=True, + delete_where={'column': 'lilly', + 'values': ['a', 'b'], + 'operator': 'eq'} + ) + # this will be ignored + out_table.table_metadata.add_table_metadata('bar', 'kochba') + # this will be ignored + out_table.table_metadata.add_column_metadata('bar', 'foo', 'gogo') + # this will be ignored + out_table.table_metadata.add_column_data_type('bar', 'NUMERIC') + + # write + ci.write_manifest(out_table) + + del os.environ['KBC_DATA_TYPE_SUPPORT'] + manifest_filename = out_table.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + {'destination': 'some-destination', + 'incremental': True, + 'manifest_type': 'out', + 'write_always': False, + 'delimiter': ',', + 'enclosure': '"', + 'table_metadata': {'bar': 'kochba'}, + 'has_header': False, + 'delete_where_column': 'lilly', 'delete_where_values': ['a', 'b'], 'delete_where_operator': 'eq', + 'schema': [ + {'name': 'foo', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True, 'primary_key': True}, + {'name': 'bar', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}]}, + config + ) + + def test_get_input_tables_definition(self): + ci = CommonInterface() + + tables = ci.get_input_tables_definitions() + + self.assertEqual(6, len(tables)) + for table in tables: + if table.name == 'sample.csv': + self.assertEqual(table.columns, [ + "x", + "Sales", + "CompPrice", + "Income", + "Advertising", + "Population", + "Price", + "ShelveLoc", + "Age", + "Education", + "Urban", + "US", + "High" + ]) + self.assertEqual(table.rows_count, 400) + self.assertEqual(table.data_size_bytes, 81920) + elif table.name == 'fooBar': + self.assertEqual(table.id, 'in.c-main.test2') + self.assertEqual(table.full_path, os.path.join(ci.tables_in_path, 'fooBar')) + self.assertEqual(table.name, 'fooBar') + + def test_get_input_tables_definition_orphaned_manifest(self): + ci = CommonInterface() + + tables = ci.get_input_tables_definitions(orphaned_manifests=True) + + self.assertEqual(7, len(tables)) + for table in tables: + if table.name == 'sample.csv': + self.assertEqual(table.columns, [ + "x", + "Sales", + "CompPrice", + "Income", + "Advertising", + "Population", + "Price", + "ShelveLoc", + "Age", + "Education", + "Urban", + "US", + "High" + ]) + self.assertEqual(table.rows_count, 400) + self.assertEqual(table.data_size_bytes, 81920) + elif table.name == 'fooBar': + self.assertEqual(table.id, 'in.c-main.test2') + self.assertEqual(table.full_path, os.path.join(ci.tables_in_path, 'fooBar')) + self.assertEqual(table.name, 'fooBar') + + def test_state_file_initialized(self): + ci = CommonInterface() + state = ci.get_state_file() + self.assertEqual(state['test_state'], 1234) + + def test_state_file_created(self): + ci = CommonInterface() + # write + ci.write_state_file({"some_state": 1234}) + + # load + state_filename = os.path.join(ci.data_folder_path, 'out', 'state.json') + with open(state_filename) as state_file: + state = json.load(state_file) + + self.assertEqual( + {"some_state": 1234}, + state + ) + + # cleanup + os.remove(state_filename) + + def test_get_input_table_by_name_fails_on_nonexistent(self): + ci = CommonInterface() + with self.assertRaises(ValueError): + ci.get_input_table_definition_by_name('nonexistent.csv') + + def test_get_input_table_by_name_existing_passes(self): + ci = CommonInterface() + in_table = ci.get_input_table_definition_by_name('fooBar') + self.assertEqual(in_table.id, 'in.c-main.test2') + self.assertEqual(in_table.full_path, os.path.join(ci.tables_in_path, 'fooBar')) + self.assertEqual(in_table.name, 'fooBar') + + # Files + + def test_create_and_write_file_manifest_deprecated(self): + ci = CommonInterface() + # create table def + out_file = ci.create_out_file_definition('some-file.jpg', + is_permanent=True, + is_encrypted=True, + is_public=True, + tags=['foo', 'bar'], + notify=True + ) + + # write + ci.write_filedef_manifest(out_file) + manifest_filename = out_file.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + {'tags': ['foo', 'bar'], + 'is_public': True, + 'is_permanent': True, + 'is_encrypted': True, + 'notify': True}, + config + ) + os.remove(manifest_filename) + + def test_create_and_write_file_manifest(self): + ci = CommonInterface() + # create table def + out_file = ci.create_out_file_definition('some-file.jpg', + is_permanent=True, + is_encrypted=True, + is_public=True, + tags=['foo', 'bar'], + notify=True + ) + + # write + ci.write_manifest(out_file) + + manifest_filename = out_file.full_path + '.manifest' + with open(manifest_filename) as manifest_file: + config = json.load(manifest_file) + self.assertEqual( + {'tags': ['foo', 'bar'], + 'is_public': True, + 'is_permanent': True, + 'is_encrypted': True, + 'notify': True}, + config + ) + os.remove(manifest_filename) + + def test_get_input_files_definition_latest(self): + ci = CommonInterface() + + files = ci.get_input_files_definitions() + + self.assertEqual(len(files), 5) + for file in files: + if file.name == 'duty_calls.png': + self.assertEqual(file.id, '151971455') + + def test_get_input_files_definition_by_tag(self): + ci = CommonInterface() + + files = ci.get_input_files_definitions(tags=['dilbert']) + + self.assertEqual(len(files), 3) + for file in files: + if file.name == '21702.strip.print.gif': + self.assertEqual(file.tags, [ + "dilbert" + ]) + self.assertEqual(file.max_age_days, 180) + self.assertEqual(file.size_bytes, 4931) + + def test_get_input_files_definition_by_tag_w_system(self): + ci = CommonInterface(os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data_examples', 'data_system_tags')) + + files = ci.get_input_files_definitions(tags=['dilbert']) + + self.assertEqual(len(files), 3) + for file in files: + if file.name == '21702.strip.print.gif': + self.assertEqual(file.tags, [ + "dilbert", + "componentId: 1234", + "configurationId: 12345", + "configurationRowId: 12345", + "runId: 22123", + "branchId: 312321" + ]) + self.assertEqual(file.max_age_days, 180) + self.assertEqual(file.size_bytes, 4931) + + def test_get_input_files_definition_tag_group_w_system(self): + ci = CommonInterface(os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data_examples', 'data_system_tags')) + + files = ci.get_input_file_definitions_grouped_by_tag_group(only_latest_files=False) + + self.assertEqual(len(files), 2) + self.assertEqual(len(files["bar;foo"]), 3) + for file in files["bar;foo"]: + if file.name == 'compiler_complaint.png': + self.assertEqual(file.tags, [ + "foo", + "bar", + "componentId: 1234", + "configurationId: 12345", + "configurationRowId: 12345", + "runId: 22123", + "branchId: 312321" + ]) + + def test_get_input_files_definition_nofilter(self): + ci = CommonInterface() + + files = ci.get_input_files_definitions(only_latest_files=False) + + self.assertEqual(len(files), 6) + for file in files: + if file.name == 'duty_calls': + self.assertEqual(file.tags, [ + "xkcd" + ]) + self.assertEqual(file.max_age_days, 180) + self.assertEqual(file.size_bytes, 30027) + + def test_get_input_files_definition_no_manifest_passes(self): + ci = CommonInterface(os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data_examples', 'data2')) + + files = ci.get_input_files_definitions(only_latest_files=True) + + self.assertEqual(len(files), 1) + for file in files: + self.assertEqual(file.max_age_days, 0) + self.assertEqual(file.size_bytes, 0) + self.assertEqual(file.created, None) + + def test_convert_old_to_new_manifest(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data4') + os.environ["KBC_DATADIR"] = path + + ci = CommonInterface() + tables = ci.get_input_tables_definitions() + + os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" + + new_manifest = tables[0].get_manifest_dictionary('out') + + self.assertEqual({ + 'write_always': False, + 'delimiter': ',', + 'enclosure': '"', + 'manifest_type': 'out', + 'has_header': True, + 'schema': [ + {'name': 'x', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True, 'metadata': {'foo': 'gogo'}}, + {'name': 'Sales', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'CompPrice', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Income', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Advertising', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Population', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Price', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'ShelveLoc', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Age', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Education', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Urban', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'US', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'High', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}] + }, new_manifest) + + del os.environ['KBC_DATA_TYPE_SUPPORT'] + + def test_convert_new_to_old_manifest_has_header_false(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data_new_manifest') + os.environ["KBC_DATADIR"] = path + + ci = CommonInterface() + tables = ci.get_input_tables_definitions() + + old_manifest = tables[0].get_manifest_dictionary('out', legacy_manifest=True) + + self.assertEqual({ + 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', + 'Education', 'Urban', 'US', 'High'], + 'delimiter': ',', + 'enclosure': '"', + 'incremental': False, + 'write_always': False + }, old_manifest) + + def test_convert_new_to_old_manifest_storage_param(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', + 'data_storage_parameter_data_types') + os.environ["KBC_DATADIR"] = path + os.environ['KBC_DATA_TYPE_SUPPORT'] = 'authoritative' + + ci = CommonInterface() + tables = ci.get_input_tables_definitions() + + ci.write_manifests([tables[0]]) + manifest_filename = tables[0].full_path + '.manifest' + with open(manifest_filename) as manifest_file: + old_manifest = json.load(manifest_file) + + self.assertEqual({ + 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', + 'Education', 'Urban', 'US', 'High'], + 'delimiter': ',', + 'enclosure': '"', + 'incremental': False, + 'write_always': False + }, old_manifest) + + def test_full_input_manifest(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data_full_input_manifest') + os.environ["KBC_DATADIR"] = path + + ci = CommonInterface() + tables = ci.get_input_tables_definitions() + + ci.write_manifests([tables[0]]) + manifest_filename = tables[0].full_path + '.manifest' + with open(manifest_filename) as manifest_file: + old_manifest = json.load(manifest_file) + + self.assertEqual({ + 'id': 'in.c-main.test', + 'uri': 'https://connection.keboola.com//v2/storage/tables/in.c-main.test', + 'name': 'sample.csv', + 'created': '2015-11-02T09:11:37+0100', + 'last_change_date': '2015-11-02T09:11:37+0100', + 'last_import_date': '2015-11-02T09:11:37+0100', + 'rows_count': 400, + 'data_size_bytes': 81920, + 'is_alias': False, + 'indexed_columns': ['x'], + 'primary_key': ['x'], + 'column_metadata': {'x': [{'key': 'foo', 'value': 'gogo'}]}, + 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', + 'Education', 'Urban', 'US', 'High'] + }, old_manifest) + + def test_full_input_manifest_dtypes_support(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data_full_input_manifest') + os.environ["KBC_DATADIR"] = path + os.environ['KBC_DATA_TYPE_SUPPORT'] = 'authoritative' + + ci = CommonInterface() + tables = ci.get_input_tables_definitions() + + ci.write_manifests([tables[0]]) + manifest_filename = tables[0].full_path + '.manifest' + with open(manifest_filename) as manifest_file: + old_manifest = json.load(manifest_file) + + self.maxDiff = None + + self.assertEqual({ + 'id': 'in.c-main.test', + 'uri': 'https://connection.keboola.com//v2/storage/tables/in.c-main.test', + 'name': 'sample.csv', + 'created': '2015-11-02T09:11:37+0100', + 'last_change_date': '2015-11-02T09:11:37+0100', + 'last_import_date': '2015-11-02T09:11:37+0100', + 'rows_count': 400, + 'data_size_bytes': 81920, + 'is_alias': False, + 'indexed_columns': ['x'], + 'primary_key': ['x'], + 'column_metadata': {'x': [{'key': 'foo', 'value': 'gogo'}]}, + 'columns': ['x', 'Sales', 'CompPrice', 'Income', 'Advertising', 'Population', 'Price', 'ShelveLoc', 'Age', + 'Education', 'Urban', 'US', 'High'] + }, old_manifest) + + def test_separator_delimiter(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data5') + os.environ["KBC_DATADIR"] = path + + ci = CommonInterface() + tables = ci.get_input_tables_definitions() + + old_manifest = tables[0].get_manifest_dictionary('out', legacy_manifest=True) + + self.assertEqual({ + 'columns': [ + 'x', + 'Sales', + 'CompPrice', + 'Income', + 'Advertising', + 'Population', + 'Price', + 'ShelveLoc', + 'Age', + 'Education', + 'Urban', + 'US', + 'High' + ], + 'delimiter': '\t', + 'enclosure': "'", + 'incremental': True, + 'primary_key': [ + 'x' + ], + 'write_always': False, + 'delete_where_column': 'Advertising', + 'delete_where_values': ['Video', 'Search'], + 'delete_where_operator': 'eq', + 'destination': 'out.c-main.Leads' + }, old_manifest) + + def test_separator_delimiter_dtypes(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data_examples', 'data5') + os.environ["KBC_DATADIR"] = path + + ci = CommonInterface() + tables = ci.get_input_tables_definitions() + + os.environ['KBC_DATA_TYPE_SUPPORT'] = "authoritative" + + new_manifest = tables[0].get_manifest_dictionary('out') + + self.assertEqual({ + 'write_always': False, + 'delimiter': '\t', + 'enclosure': '\'', + 'manifest_type': 'out', + 'has_header': False, + 'incremental': True, + 'schema': [ + {'name': 'x', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True, 'primary_key': True}, + {'name': 'Sales', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'CompPrice', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Income', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Advertising', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Population', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Price', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'ShelveLoc', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Age', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Education', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'Urban', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'US', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}, + {'name': 'High', 'data_type': {'base': {'type': 'STRING'}}, 'nullable': True}], + 'delete_where_column': 'Advertising', + 'delete_where_values': ['Video', 'Search'], + 'delete_where_operator': 'eq', + 'destination': 'out.c-main.Leads' + }, new_manifest) + + del os.environ['KBC_DATA_TYPE_SUPPORT'] + + +class TestConfiguration(unittest.TestCase): + + def setUp(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data_examples', 'data1') + os.environ["KBC_DATADIR"] = path + + def test_missing_config(self): + with self.assertRaisesRegex( + ValueError, + "Configuration file config.json not found"): + Configuration('/non-existent/') + + def test_get_parameters(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + params = cfg.parameters + self.assertEqual({'fooBar': {'bar': 24, 'foo': 42}, 'baz': 'bazBar'}, + params) + self.assertEqual(params['fooBar']['foo'], 42) + self.assertEqual(params['fooBar']['bar'], 24) + + def test_get_action(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + + self.assertEqual(cfg.action, 'run') + + def test_get_action_empty_config(self): + cfg = Configuration(os.path.join(os.getenv('KBC_DATADIR', ''), '..', + 'data2')) + self.assertEqual(cfg.action, '') + + def test_get_input_mappings(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + tables = cfg.tables_input_mapping + + self.assertEqual(len(tables), 2) + for table in tables: + if table['destination'] == 'sample.csv': + self.assertEqual(table['source'], 'in.c-main.test') + else: + self.assertEqual('in.c-main.test2', table['source']) + + def test_get_input_mappings_with_column_types(self): + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data_examples', 'data4') + cfg = Configuration(path) + tables = cfg.tables_input_mapping + coltypes = tables[0].column_types[0] + source = coltypes.source + self.assertEqual(source, "Sales") + column_type = coltypes.type + self.assertEqual(column_type, "VARCHAR") + destination = coltypes.destination + self.assertEqual(destination, "id") + length = coltypes.length + self.assertEqual(length, "255") + nullable = coltypes.nullable + self.assertEqual(nullable, False) + convert_empty_values_to_null = coltypes.convert_empty_values_to_null + self.assertEqual(convert_empty_values_to_null, False) + + def test_get_output_mapping(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + tables = cfg.tables_output_mapping + self.assertEqual(len(tables), 2) + self.assertEqual(tables[0]['source'], 'results.csv') + self.assertEqual(tables[1]['source'], 'results-new.csv') + + def test_empty_storage(self): + cfg = Configuration(os.path.join(os.getenv('KBC_DATADIR', ''), '..', + 'data2')) + self.assertEqual(cfg.tables_output_mapping, []) + self.assertEqual(cfg.files_output_mapping, []) + self.assertEqual(cfg.tables_input_mapping, []) + self.assertEqual(cfg.files_input_mapping, []) + self.assertEqual(cfg.parameters, {}) + + def test_empty_params(self): + cfg = Configuration(os.path.join(os.getenv('KBC_DATADIR', ''), '..', + 'data3')) + self.assertEqual([], cfg.tables_output_mapping) + self.assertEqual([], cfg.files_output_mapping) + self.assertEqual({}, cfg.parameters) + + def test_get_authorization(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + auth = cfg.oauth_credentials + # self.assertEqual(auth['id'], "123456") + self.assertEqual(auth["id"], "main") + + def test_get_oauthapi_data(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + self.assertDictEqual(cfg.oauth_credentials.data, {"mykey": "myval"}) + + def test_get_oauthapi_appsecret(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + self.assertEqual(cfg.oauth_credentials.appSecret, "myappsecret") + + def test_get_oauthapi_appkey(self): + cfg = Configuration(os.environ["KBC_DATADIR"]) + self.assertEqual(cfg.oauth_credentials.appKey, "myappkey") + + # def test_file_manifest(self): + # cfg = docker.Config() + # some_file = os.path.join(tempfile.mkdtemp('kbc-test') + 'someFile.txt') + # cfg.write_file_manifest(some_file, file_tags=['foo', 'bar'], + # is_public=True, is_permanent=False, + # notify=True) + # manifest_filename = some_file + '.manifest' + # with open(manifest_filename) as manifest_file: + # config = json.load(manifest_file) + # self.assertEqual( + # {'is_public': True, 'is_permanent': False, 'notify': True, + # 'tags': ['foo', 'bar']}, + # config + # ) + # os.remove(manifest_filename) + + +if __name__ == '__main__': + unittest.main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..81c5a3f --- /dev/null +++ b/uv.lock @@ -0,0 +1,888 @@ +version = 1 +revision = 3 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "flake8" +version = "5.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version < '3.8.1'" }, + { name = "pycodestyle", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "pyflakes", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", size = 145862, upload-time = "2022-08-03T23:21:27.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248", size = 61897, upload-time = "2022-08-03T23:21:25.027Z" }, +] + +[[package]] +name = "flake8" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.9'" }, + { name = "pycodestyle", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyflakes", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "keboola-component" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "deprecated" }, + { name = "pygelf" }, + { name = "pytz" }, +] + +[package.dev-dependencies] +dev = [ + { name = "flake8", version = "5.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "flake8", version = "7.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pdoc3", version = "0.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pdoc3", version = "0.11.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "deprecated" }, + { name = "pygelf" }, + { name = "pytz", specifier = "<2021.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "flake8", specifier = ">=5.0.4" }, + { name = "pdoc3" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "ruff", specifier = ">=0.13.2" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, +] + +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pdoc3" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mako", marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/4e/741d6dbd64934c4769c055dd6952e1c6d117a1a25236f633e68ea2d375c8/pdoc3-0.11.0.tar.gz", hash = "sha256:12f28c6ee045ca8ad6a624b86d1982c51de20e83c0a721cd7b0933f44ae0a655", size = 97667, upload-time = "2024-06-22T01:25:30.594Z" } + +[[package]] +name = "pdoc3" +version = "0.11.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mako", marker = "python_full_version >= '3.9'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f0/07d8b771b99c16a06741cd7b2639494a15357df819ecf899c33b87db6257/pdoc3-0.11.6.tar.gz", hash = "sha256:1ea5e84b87a754d191fb64bf5e517ca6c50d0d84a614c1efecf6b46d290ae387", size = 177107, upload-time = "2025-03-20T22:53:53.099Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/98/629f269c2bd91bdcac147aad5cf51ceb645c0196e23a41ee3c051125190f/pdoc3-0.11.6-py3-none-any.whl", hash = "sha256:8b72723767bd48d899812d2aec8375fc1c3476e179455db0b4575e6dccb44b93", size = 255188, upload-time = "2025-03-20T22:53:51.671Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127, upload-time = "2022-08-03T23:13:29.715Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493, upload-time = "2022-08-03T23:13:27.416Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pyflakes" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3", size = 66388, upload-time = "2022-07-30T17:29:05.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", size = 66116, upload-time = "2022-07-30T17:29:04.179Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygelf" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/91/ac1605bb40092ae41fbb833ee55447f72e19ce5459efa6bd3beecc67e971/pygelf-0.4.3.tar.gz", hash = "sha256:8ed972563be3c8f168483f01dbf522b6bc697959c97a3f4881324b3f79638911", size = 11017, upload-time = "2025-06-14T19:21:19.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/ee/ebac3de919431912e0be380fafd01059a091a489f6b5d7896c2a04548895/pygelf-0.4.3-py3-none-any.whl", hash = "sha256:0876c99a77f9f021834982c9808205b3239fabf5886788d701f31b495b65c8ae", size = 8750, upload-time = "2025-06-14T19:21:16.953Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", marker = "python_full_version == '3.9.*'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pygments", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytz" +version = "2020.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/44/404ec10dca553032900a65bcded8b8280cf7c64cc3b723324e2181bf93c9/pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5", size = 314194, upload-time = "2020-12-24T20:58:07.498Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/06/2c2d3034b4d6bf22f2a4ae546d16925898658a33b4400cfb7e2c1e2871a3/pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", size = 510773, upload-time = "2020-12-24T20:58:04.098Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" }, + { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" }, + { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f3/de535ccecede6960e28c7b722e5744846258111d6c9f071aa7578ea37ad3/wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4", size = 58035, upload-time = "2025-11-07T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/15/39d3ca5428a70032c2ec8b1f1c9d24c32e497e7ed81aed887a4998905fcc/wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45", size = 60383, upload-time = "2025-11-07T00:43:25.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/c2/dfd23754b7f7a4dce07e08f4309c4e10a40046a83e9ae1800f2e6b18d7c1/wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7", size = 58894, upload-time = "2025-11-07T00:43:27.074Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, + { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, + { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, + { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" }, + { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, + { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/26/ed6979672ebe0e33f6059fdc8182c4c536e575b6f03d349a542082ca03fb/wrapt-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:90897ea1cf0679763b62e79657958cd54eae5659f6360fc7d2ccc6f906342183", size = 77192, upload-time = "2025-11-07T00:45:04.493Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a5/fb0974e8d21ef17f75ffa365b395c04eefa23eb6e45548e94c781e93c306/wrapt-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50844efc8cdf63b2d90cd3d62d4947a28311e6266ce5235a219d21b195b4ec2c", size = 60475, upload-time = "2025-11-07T00:45:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7b/56bf38c8bd5e8a48749f1a13c743eddcbd7a616da342b4877f79ec3e7087/wrapt-2.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49989061a9977a8cbd6d20f2efa813f24bf657c6990a42967019ce779a878dbf", size = 61311, upload-time = "2025-11-07T00:45:06.822Z" }, + { url = "https://files.pythonhosted.org/packages/18/70/ba94af50f2145cb431163d74d405083beb16782818b20c956138e4f59299/wrapt-2.0.1-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:09c7476ab884b74dce081ad9bfd07fe5822d8600abade571cb1f66d5fc915af6", size = 118542, upload-time = "2025-11-07T00:45:08.324Z" }, + { url = "https://files.pythonhosted.org/packages/14/ac/537c8f9cec8a422cfed45b28665ea33344928fd67913e5ff98af0c11470c/wrapt-2.0.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1a8a09a004ef100e614beec82862d11fc17d601092c3599afd22b1f36e4137e", size = 120989, upload-time = "2025-11-07T00:45:09.928Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b8/463284d8a74e56c88f5f2fb9b572178a294e0beb945b8ee2a7ca43a1696d/wrapt-2.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:89a82053b193837bf93c0f8a57ded6e4b6d88033a499dadff5067e912c2a41e9", size = 118937, upload-time = "2025-11-07T00:45:11.157Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8e/08b8f9de6b3cfd269504b345d31679d283e50cc93cb0521a44475bb7311b/wrapt-2.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f26f8e2ca19564e2e1fdbb6a0e47f36e0efbab1acc31e15471fad88f828c75f6", size = 117150, upload-time = "2025-11-07T00:45:12.324Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/0eab878bb4d0eadbec2b75e399cfa6aa802e634587756d59419080aae1f5/wrapt-2.0.1-cp38-cp38-win32.whl", hash = "sha256:115cae4beed3542e37866469a8a1f2b9ec549b4463572b000611e9946b86e6f6", size = 57936, upload-time = "2025-11-07T00:45:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/fc964b370bf568312deda176682138ccbd41960285a7de49002183e2aa08/wrapt-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c4012a2bd37059d04f8209916aa771dfb564cccb86079072bdcd48a308b6a5c5", size = 60308, upload-time = "2025-11-07T00:45:13.573Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1f/5af0ae22368ec69067a577f9e07a0dd2619a1f63aabc2851263679942667/wrapt-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:68424221a2dc00d634b54f92441914929c5ffb1c30b3b837343978343a3512a3", size = 77478, upload-time = "2025-11-07T00:45:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b7/fd6b563aada859baabc55db6aa71b8afb4a3ceb8bc33d1053e4c7b5e0109/wrapt-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd1a18f5a797fe740cb3d7a0e853a8ce6461cc62023b630caec80171a6b8097", size = 60687, upload-time = "2025-11-07T00:45:17.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8c/9ededfff478af396bcd081076986904bdca336d9664d247094150c877dcb/wrapt-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb3a86e703868561c5cad155a15c36c716e1ab513b7065bd2ac8ed353c503333", size = 61563, upload-time = "2025-11-07T00:45:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/d795a1aa2b6ab20ca21157fe03cbfc6aa7e870a88ac3b4ea189e2f6c79f0/wrapt-2.0.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5dc1b852337c6792aa111ca8becff5bacf576bf4a0255b0f05eb749da6a1643e", size = 113395, upload-time = "2025-11-07T00:45:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/61/32/56cde2bbf95f2d5698a1850a765520aa86bc7ae0f95b8ec80b6f2e2049bb/wrapt-2.0.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c046781d422f0830de6329fa4b16796096f28a92c8aef3850674442cdcb87b7f", size = 115362, upload-time = "2025-11-07T00:45:22.809Z" }, + { url = "https://files.pythonhosted.org/packages/cf/53/8d3cc433847c219212c133a3e8305bd087b386ef44442ff39189e8fa62ac/wrapt-2.0.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f73f9f7a0ebd0db139253d27e5fc8d2866ceaeef19c30ab5d69dcbe35e1a6981", size = 111766, upload-time = "2025-11-07T00:45:20.294Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/14b50c2d0463c0dcef8f388cb1527ed7bbdf0972b9fd9976905f36c77ebf/wrapt-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b667189cf8efe008f55bbda321890bef628a67ab4147ebf90d182f2dadc78790", size = 114560, upload-time = "2025-11-07T00:45:24.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b8/4f731ff178f77ae55385586de9ff4b4261e872cf2ced4875e6c976fbcb8b/wrapt-2.0.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:a9a83618c4f0757557c077ef71d708ddd9847ed66b7cc63416632af70d3e2308", size = 110999, upload-time = "2025-11-07T00:45:25.596Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/5f1bb0f9ae9d12e19f1d71993d052082062603e83fe3e978377f918f054d/wrapt-2.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e9b121e9aeb15df416c2c960b8255a49d44b4038016ee17af03975992d03931", size = 113164, upload-time = "2025-11-07T00:45:26.8Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f6/f3a3c623d3065c7bf292ee0b73566236b562d5ed894891bd8e435762b618/wrapt-2.0.1-cp39-cp39-win32.whl", hash = "sha256:1f186e26ea0a55f809f232e92cc8556a0977e00183c3ebda039a807a42be1494", size = 58028, upload-time = "2025-11-07T00:45:30.943Z" }, + { url = "https://files.pythonhosted.org/packages/24/78/647c609dfa18063a7fcd5c23f762dd006be401cc9206314d29c9b0b12078/wrapt-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf4cb76f36be5de950ce13e22e7fdf462b35b04665a12b64f3ac5c1bbbcf3728", size = 60380, upload-time = "2025-11-07T00:45:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/07/90/0c14b241d18d80ddf4c847a5f52071e126e8a6a9e5a8a7952add8ef0d766/wrapt-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:d6cc985b9c8b235bd933990cdbf0f891f8e010b65a3911f7a55179cd7b0fc57b", size = 58895, upload-time = "2025-11-07T00:45:29.527Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]