diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 107bf9e7..5eb7e8e8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,6 @@ name: CI -on: [push, pull_request] +on: [ push, pull_request ] jobs: @@ -8,32 +8,34 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@v2 with: - python-version: '3.11' - cache: 'pip' - cache-dependency-path: 'requirements/*.txt' - - name: Install dev dependencies - run: python -m pip install -r requirements/dev.txt - - name: Run linting - run: python -m tox -e lint + enable-cache: true + cache-dependency-glob: "uv.lock" + - name: Set up Python + run: uv python install 3.12 + - name: Install the project + run: uv sync --all-extras --dev + - name: Lint + run: uv run ruff check --select I test: strategy: matrix: - python: ['3.8', '3.9', '3.10', '3.11'] - os: [ubuntu-latest, windows-latest, macos-latest] + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + os: [ ubuntu-latest, windows-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@v2 with: - python-version: ${{ matrix.python }} - cache: 'pip' - cache-dependency-path: 'requirements/*.txt' - - name: Install dev dependencies - run: python -m pip install -r requirements/dev.txt + enable-cache: true + cache-dependency-glob: "uv.lock" + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + - name: Install the project + run: uv sync --all-extras --dev - name: Run tests - run: python -m tox -e py # Run tox using the version of Python in `PATH` + run: uv run pytest tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75ad2ef1..72f95e9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,27 +1,24 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-toml - - repo: https://github.com/pycqa/isort - rev: 5.13.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.8 hooks: - - id: isort - args: [ "--profile", "black", "--filter-files" ] - - repo: https://github.com/psf/black - rev: 24.4.2 - hooks: - - id: black - language_version: python3.11 + # Run the linter. + - id: ruff + # Run the formatter. + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.11.2 hooks: - id: mypy - files: ^(cloudevents/) - exclude: ^(cloudevents/tests/) + files: ^(src/cloudevents/|tests/) + exclude: ^(src/cloudevents/v1/) types: [ python ] - args: [ ] - additional_dependencies: - - "pydantic~=2.7" + args: [ + "--config-file=pyproject.toml", + ] diff --git a/README.md b/README.md index abcf5cbf..6682cbcc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Python SDK for [CloudEvents](https://github.com/cloudevents/spec) +# Python SDK v2 for [CloudEvents](https://github.com/cloudevents/spec) [![PyPI version](https://badge.fury.io/py/cloudevents.svg)](https://badge.fury.io/py/cloudevents) @@ -10,12 +10,11 @@ will) break with every update. This SDK current supports the following versions of CloudEvents: - v1.0 -- v0.3 ## Python SDK -Package **cloudevents** provides primitives to work with CloudEvents specification: -https://github.com/cloudevents/spec. +Package [**cloudevents**](src/cloudevents) provides primitives to work with +[CloudEvents specification](https://github.com/cloudevents/spec). ### Installing @@ -33,15 +32,15 @@ Below we will provide samples on how to send cloudevents using the popular ### Binary HTTP CloudEvent ```python -from cloudevents.http import CloudEvent -from cloudevents.conversion import to_binary +from cloudevents_v1.http import CloudEvent +from cloudevents_v1.conversion import to_binary import requests # Create a CloudEvent # - The CloudEvent "id" is generated if omitted. "specversion" defaults to "1.0". attributes = { - "type": "com.example.sampletype1", - "source": "https://example.com/event-producer", + "type": "com.example.sampletype1", + "source": "https://example.com/event-producer", } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) @@ -56,15 +55,15 @@ requests.post("", data=body, headers=headers) ### Structured HTTP CloudEvent ```python -from cloudevents.conversion import to_structured -from cloudevents.http import CloudEvent +from cloudevents_v1.conversion import to_structured +from cloudevents_v1.http import CloudEvent import requests # Create a CloudEvent # - The CloudEvent "id" is generated if omitted. "specversion" defaults to "1.0". attributes = { - "type": "com.example.sampletype2", - "source": "https://example.com/event-producer", + "type": "com.example.sampletype2", + "source": "https://example.com/event-producer", } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) @@ -87,7 +86,7 @@ The code below shows how to consume a cloudevent using the popular python web fr ```python from flask import Flask, request -from cloudevents.http import from_http +from cloudevents_v1.http import from_http app = Flask(__name__) @@ -95,20 +94,20 @@ app = Flask(__name__) # create an endpoint at http://localhost:/3000/ @app.route("/", methods=["POST"]) def home(): - # create a CloudEvent - event = from_http(request.headers, request.get_data()) + # create a CloudEvent + event = from_http(request.headers, request.get_data()) - # you can access cloudevent fields as seen below - print( - f"Found {event['id']} from {event['source']} with type " - f"{event['type']} and specversion {event['specversion']}" - ) + # you can access cloudevent fields as seen below + print( + f"Found {event['id']} from {event['source']} with type " + f"{event['type']} and specversion {event['specversion']}" + ) - return "", 204 + return "", 204 if __name__ == "__main__": - app.run(port=3000) + app.run(port=3000) ``` You can find a complete example of turning a CloudEvent into a HTTP request @@ -162,18 +161,13 @@ with one of the project's SDKs, please send an email to ## Maintenance -We use [black][black] and [isort][isort] for autoformatting. We set up a [tox][tox] -environment to reformat the codebase. - -e.g. - -```bash -pip install tox -tox -e reformat -``` +We use [uv][uv] for dependency and package management, [ruff][ruff] and [isort][isort] +for autoformatting and [pre-commit][pre-commit] to automate those with commit +hooks. For information on releasing version bumps see [RELEASING.md](RELEASING.md) -[black]: https://black.readthedocs.io/ +[uv]: https://docs.astral.sh/uv/ +[ruff]: https://docs.astral.sh/ruff [isort]: https://pycqa.github.io/isort/ -[tox]: https://tox.wiki/ +[pre-commit]: https://pre-commit.com diff --git a/RELEASING.md b/RELEASING.md index f6ca05b1..9240a9fe 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -4,7 +4,7 @@ This repository is configured to automatically publish the corresponding [PyPI package](https://pypi.org/project/cloudevents/) and GitHub Tag via GitHub Actions. To release a new CloudEvents SDK, contributors should bump `__version__` in -[cloudevents](cloudevents/__init__.py) to reflect the new release version. On merge, the action +[cloudevents](cloudevents_v1/__init__.py) to reflect the new release version. On merge, the action will automatically build and release to PyPI using [this PyPI GitHub Action](https://github.com/pypa/gh-action-pypi-publish). This action gets called on all pushes to main (such as a version branch being merged @@ -12,7 +12,7 @@ into main), but only releases a new version when the version number has changed. this action assumes pushes to main are version updates. Consequently, [pypi-release.yml](.github/workflows/pypi-release.yml) will fail if you attempt to push to main without updating `__version__` in -[cloudevents](cloudevents/__init__.py) so don't forget to do so. +[cloudevents](cloudevents_v1/__init__.py) so don't forget to do so. After a version update is merged, the script [pypi_packaging.py](pypi_packaging.py) will create a GitHub tag for the new cloudevents version using `__version__`. diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index d8fb9cc0..00000000 --- a/mypy.ini +++ /dev/null @@ -1,16 +0,0 @@ -[mypy] -plugins = pydantic.mypy -python_version = 3.8 - -pretty = True -show_error_context = True -follow_imports_for_stubs = True -# subset of mypy --strict -# https://mypy.readthedocs.io/en/stable/config_file.html -check_untyped_defs = True -disallow_incomplete_defs = True -warn_return_any = True -strict_equality = True - -[mypy-deprecation.*] -ignore_missing_imports = True diff --git a/pypi_packaging.py b/pypi_packaging.py deleted file mode 100644 index c81986d5..00000000 --- a/pypi_packaging.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2018-Present The CloudEvents Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - -import pkg_resources - -from setup import pypi_config - - -def createTag(): - from git import Repo - - # Get local pypi cloudevents version - published_pypi_version = pkg_resources.get_distribution( - pypi_config["package_name"] - ).version - - # Ensure pypi and local package versions match - if pypi_config["version_target"] == published_pypi_version: - # Create local git tag - repo = Repo(os.getcwd()) - repo.create_tag(pypi_config["version_target"]) - - # Push git tag to remote main - origin = repo.remote() - origin.push(pypi_config["version_target"]) - - else: - # PyPI publish likely failed - print( - f"Expected {pypi_config['package_name']}=={pypi_config['version_target']} " - f"but found {pypi_config['package_name']}=={published_pypi_version}" - ) - exit(1) - - -if __name__ == "__main__": - createTag() diff --git a/pyproject.toml b/pyproject.toml index 8727d44f..5c152bc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,143 @@ -[tool.black] +[project] +name = "cloudevents" +dynamic = ["version"] +description = "CloudEvents Python SDK" +authors = [ + { name = "The Cloud Events Contributors", email = "cncfcloudevents@gmail.com" } +] +readme = "README.md" +requires-python = ">= 3.9" +license = "Apache-2.0" +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Development Status :: 5 - Production/Stable", + "Operating System :: OS Independent", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Typing :: Typed", +] +keywords = [ + "CloudEvents", + "Eventing", + "Serverless", +] +dependencies = [ + "ruff>=0.6.8", +] + +[project.urls] +"Source code" = "https://github.com/cloudevents/sdk-python" +"Documentation" = "https://cloudevents.io" +"Home page" = "https://cloudevents.io" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv] +dev-dependencies = [ + "pytest>=8.3.3", + "mypy>=1.11.2", + "isort>=5.13.2", + "flake8>=7.1.1", + "pep8-naming>=0.14.1", + "flake8-print>=5.0.0", + "pre-commit>=3.8.0", + "pytest-cov>=5.0.0", +] + +[tool.uv.pip] +universal = true +generate-hashes = true + +[tool.hatch.version] +path = "src/cloudevents/__init__.py" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel.force-include] +"CHANGELOG.md" = "CHANGELOG.md" +"MAINTAINERS.md" = "MAINTAINERS.md" +"README.md" = "README.md" + +[tool.hatch.build.targets.sdist] +packages = ["src/cloudevents"] + +[tool.hatch.build.targets.sdist.force-include] +"CHANGELOG.md" = "CHANGELOG.md" +"MAINTAINERS.md" = "MAINTAINERS.md" + +[tool.ruff] line-length = 88 -include = '\.pyi?$' -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist -)/ -''' - -[tool.isort] -profile = "black" +target-version = "py39" + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "site-packages", + "venv", +] + +[tool.ruff.lint] +ignore = ["E731"] +extend-ignore = ["E203"] +select = ["I"] + + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] + +[tool.mypy] +python_version = 3.9 + +ignore_missing_imports = true +namespace_packages = true +explicit_package_bases = true +scripts_are_modules = true +pretty = true +show_error_context = true +follow_imports_for_stubs = true +warn_redundant_casts = true +warn_unused_ignores = true +# subset of mypy --strict +# https://mypy.readthedocs.io/en/stable/config_file.html +check_untyped_defs = true +disallow_incomplete_defs = true +warn_return_any = true +strict_equality = true +disallow_untyped_defs = true +exclude = [ + "src/cloudevents/v1", +] diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 63872949..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,7 +0,0 @@ -black -isort -flake8 -pep8-naming -flake8-print -tox -pre-commit diff --git a/requirements/publish.txt b/requirements/publish.txt deleted file mode 100644 index a296666f..00000000 --- a/requirements/publish.txt +++ /dev/null @@ -1,2 +0,0 @@ -GitPython -cloudevents diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index 3e32e4a8..00000000 --- a/requirements/test.txt +++ /dev/null @@ -1,13 +0,0 @@ -flake8 -pep8-naming -flake8-print -pytest -pytest-cov -# web app tests -sanic -sanic-testing -aiohttp -Pillow -requests -flask -pydantic>=2.0.0,<3.0 diff --git a/samples/http-image-cloudevents/client.py b/samples/http-image-cloudevents/client.py index 021c1f56..a61303f1 100644 --- a/samples/http-image-cloudevents/client.py +++ b/samples/http-image-cloudevents/client.py @@ -15,9 +15,8 @@ import sys import requests - -from cloudevents.conversion import to_binary, to_structured -from cloudevents.http import CloudEvent +from cloudevents_v1.conversion import to_binary, to_structured +from cloudevents_v1.http import CloudEvent resp = requests.get( "https://raw.githubusercontent.com/cncf/artwork/master/projects/cloudevents/horizontal/color/cloudevents-horizontal-color.png" # noqa diff --git a/samples/http-image-cloudevents/image_sample_server.py b/samples/http-image-cloudevents/image_sample_server.py index da303025..ead3e596 100644 --- a/samples/http-image-cloudevents/image_sample_server.py +++ b/samples/http-image-cloudevents/image_sample_server.py @@ -14,11 +14,10 @@ import io +from cloudevents_v1.http import from_http from flask import Flask, request from PIL import Image -from cloudevents.http import from_http - app = Flask(__name__) diff --git a/samples/http-image-cloudevents/image_sample_test.py b/samples/http-image-cloudevents/image_sample_test.py index 5fe6ec9d..33895c69 100644 --- a/samples/http-image-cloudevents/image_sample_test.py +++ b/samples/http-image-cloudevents/image_sample_test.py @@ -18,12 +18,11 @@ import pytest from client import image_bytes +from cloudevents_v1.conversion import to_binary, to_structured +from cloudevents_v1.http import CloudEvent, from_http from image_sample_server import app from PIL import Image -from cloudevents.conversion import to_binary, to_structured -from cloudevents.http import CloudEvent, from_http - image_fileobj = io.BytesIO(image_bytes) image_expected_shape = (1880, 363) diff --git a/samples/http-json-cloudevents/client.py b/samples/http-json-cloudevents/client.py index 5ecc3793..f68f27b3 100644 --- a/samples/http-json-cloudevents/client.py +++ b/samples/http-json-cloudevents/client.py @@ -15,9 +15,8 @@ import sys import requests - -from cloudevents.conversion import to_binary, to_structured -from cloudevents.http import CloudEvent +from cloudevents_v1.conversion import to_binary, to_structured +from cloudevents_v1.http import CloudEvent def send_binary_cloud_event(url): diff --git a/samples/http-json-cloudevents/json_sample_server.py b/samples/http-json-cloudevents/json_sample_server.py index c3a399ee..9fa1a6d1 100644 --- a/samples/http-json-cloudevents/json_sample_server.py +++ b/samples/http-json-cloudevents/json_sample_server.py @@ -12,10 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from cloudevents_v1.http import from_http from flask import Flask, request -from cloudevents.http import from_http - app = Flask(__name__) diff --git a/samples/http-json-cloudevents/json_sample_test.py b/samples/http-json-cloudevents/json_sample_test.py index 1d92874d..612aade0 100644 --- a/samples/http-json-cloudevents/json_sample_test.py +++ b/samples/http-json-cloudevents/json_sample_test.py @@ -13,11 +13,10 @@ # under the License. import pytest +from cloudevents_v1.conversion import to_binary, to_structured +from cloudevents_v1.http import CloudEvent from json_sample_server import app -from cloudevents.conversion import to_binary, to_structured -from cloudevents.http import CloudEvent - @pytest.fixture def client(): diff --git a/setup.py b/setup.py deleted file mode 100644 index a4e4befc..00000000 --- a/setup.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2018-Present The CloudEvents Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import codecs -import os -import pathlib - -from setuptools import find_packages, setup - - -def read(rel_path): - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, rel_path), "r") as fp: - return fp.read() - - -def get_version(rel_path): - for line in read(rel_path).splitlines(): - if line.startswith("__version__"): - delim = '"' if '"' in line else "'" - return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") - - -# FORMAT: 1.x.x -pypi_config = { - "version_target": get_version("cloudevents/__init__.py"), - "package_name": "cloudevents", -} - -here = pathlib.Path(__file__).parent.resolve() -long_description = (here / "README.md").read_text(encoding="utf-8") - -if __name__ == "__main__": - setup( - name=pypi_config["package_name"], - summary="CloudEvents Python SDK", - long_description_content_type="text/markdown", - long_description=long_description, - description="CloudEvents Python SDK", - url="https://github.com/cloudevents/sdk-python", - author="The Cloud Events Contributors", - author_email="cncfcloudevents@gmail.com", - home_page="https://cloudevents.io", - classifiers=[ - "Intended Audience :: Information Technology", - "Intended Audience :: System Administrators", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Development Status :: 5 - Production/Stable", - "Operating System :: OS Independent", - "Natural Language :: English", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Typing :: Typed", - ], - keywords="CloudEvents Eventing Serverless", - license="https://www.apache.org/licenses/LICENSE-2.0", - license_file="LICENSE", - packages=find_packages(exclude=["cloudevents.tests"]), - include_package_data=True, - version=pypi_config["version_target"], - install_requires=["deprecation>=2.0,<3.0"], - extras_require={"pydantic": "pydantic>=1.0.0,<3.0"}, - zip_safe=True, - ) diff --git a/src/cloudevents/__init__.py b/src/cloudevents/__init__.py new file mode 100644 index 00000000..9ef80432 --- /dev/null +++ b/src/cloudevents/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +__version__ = "2.0.0-alpha1" diff --git a/src/cloudevents/core/__init__.py b/src/cloudevents/core/__init__.py new file mode 100644 index 00000000..e01d2a11 --- /dev/null +++ b/src/cloudevents/core/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""This package contains the core functionality of the CloudEvents spec.""" diff --git a/src/cloudevents/core/v1/__init__.py b/src/cloudevents/core/v1/__init__.py new file mode 100644 index 00000000..896dfe12 --- /dev/null +++ b/src/cloudevents/core/v1/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +CloudEvent implementation for v1.0 +""" diff --git a/src/cloudevents/core/v1/event.py b/src/cloudevents/core/v1/event.py new file mode 100644 index 00000000..043670b5 --- /dev/null +++ b/src/cloudevents/core/v1/event.py @@ -0,0 +1,324 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re +from collections import defaultdict +from datetime import datetime +from typing import Any, Final, Optional + +from cloudevents.core.v1.exceptions import ( + BaseCloudEventException, + CloudEventValidationError, + CustomExtensionAttributeError, + InvalidAttributeTypeError, + InvalidAttributeValueError, + MissingRequiredAttributeError, +) + +REQUIRED_ATTRIBUTES: Final[list[str]] = ["id", "source", "type", "specversion"] +OPTIONAL_ATTRIBUTES: Final[list[str]] = [ + "datacontenttype", + "dataschema", + "subject", + "time", +] + + +class CloudEvent: + """ + The CloudEvent Python wrapper contract exposing generically-available + properties and APIs. + + Implementations might handle fields and have other APIs exposed but are + obliged to follow this contract. + """ + + def __init__(self, attributes: dict[str, Any], data: Optional[dict] = None) -> None: + """ + Create a new CloudEvent instance. + + :param attributes: The attributes of the CloudEvent instance. + :param data: The payload of the CloudEvent instance. + + :raises ValueError: If any of the required attributes are missing or have invalid values. + :raises TypeError: If any of the attributes have invalid types. + """ + self._validate_attribute(attributes=attributes) + self._attributes: dict[str, Any] = attributes + self._data: Optional[dict] = data + + @staticmethod + def _validate_attribute(attributes: dict[str, Any]) -> None: + """ + Validates the attributes of the CloudEvent as per the CloudEvents specification. + + See https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#required-attributes + """ + errors: dict[str, list[BaseCloudEventException]] = defaultdict(list) + errors.update(CloudEvent._validate_required_attributes(attributes=attributes)) + errors.update(CloudEvent._validate_optional_attributes(attributes=attributes)) + errors.update(CloudEvent._validate_extension_attributes(attributes=attributes)) + if errors: + raise CloudEventValidationError(errors=errors) + + @staticmethod + def _validate_required_attributes( + attributes: dict[str, Any], + ) -> dict[str, list[BaseCloudEventException]]: + """ + Validates the types of the required attributes. + + :param attributes: The attributes of the CloudEvent instance. + :return: A dictionary of validation error messages. + """ + errors = defaultdict(list) + + if "id" not in attributes: + errors["id"].append(MissingRequiredAttributeError(attribute_name="id")) + if attributes.get("id") is None: + errors["id"].append( + InvalidAttributeValueError( + attribute_name="id", msg="Attribute 'id' must not be None" + ) + ) + if not isinstance(attributes.get("id"), str): + errors["id"].append( + InvalidAttributeTypeError(attribute_name="id", expected_type=str) + ) + + if "source" not in attributes: + errors["source"].append( + MissingRequiredAttributeError(attribute_name="source") + ) + if not isinstance(attributes.get("source"), str): + errors["source"].append( + InvalidAttributeTypeError(attribute_name="source", expected_type=str) + ) + + if "type" not in attributes: + errors["type"].append(MissingRequiredAttributeError(attribute_name="type")) + if not isinstance(attributes.get("type"), str): + errors["type"].append( + InvalidAttributeTypeError(attribute_name="type", expected_type=str) + ) + + if "specversion" not in attributes: + errors["specversion"].append( + MissingRequiredAttributeError(attribute_name="specversion") + ) + if not isinstance(attributes.get("specversion"), str): + errors["specversion"].append( + InvalidAttributeTypeError( + attribute_name="specversion", expected_type=str + ) + ) + if attributes.get("specversion") != "1.0": + errors["specversion"].append( + InvalidAttributeValueError( + attribute_name="specversion", + msg="Attribute 'specversion' must be '1.0'", + ) + ) + return errors + + @staticmethod + def _validate_optional_attributes( + attributes: dict[str, Any], + ) -> dict[str, list[BaseCloudEventException]]: + """ + Validates the types and values of the optional attributes. + + :param attributes: The attributes of the CloudEvent instance. + :return: A dictionary of validation error messages. + """ + errors = defaultdict(list) + + if "time" in attributes: + if not isinstance(attributes["time"], datetime): + errors["time"].append( + InvalidAttributeTypeError( + attribute_name="time", expected_type=datetime + ) + ) + if hasattr(attributes["time"], "tzinfo") and not attributes["time"].tzinfo: + errors["time"].append( + InvalidAttributeValueError( + attribute_name="time", + msg="Attribute 'time' must be timezone aware", + ) + ) + if "subject" in attributes: + if not isinstance(attributes["subject"], str): + errors["subject"].append( + InvalidAttributeTypeError( + attribute_name="subject", expected_type=str + ) + ) + if not attributes["subject"]: + errors["subject"].append( + InvalidAttributeValueError( + attribute_name="subject", + msg="Attribute 'subject' must not be empty", + ) + ) + if "datacontenttype" in attributes: + if not isinstance(attributes["datacontenttype"], str): + errors["datacontenttype"].append( + InvalidAttributeTypeError( + attribute_name="datacontenttype", expected_type=str + ) + ) + if not attributes["datacontenttype"]: + errors["datacontenttype"].append( + InvalidAttributeValueError( + attribute_name="datacontenttype", + msg="Attribute 'datacontenttype' must not be empty", + ) + ) + if "dataschema" in attributes: + if not isinstance(attributes["dataschema"], str): + errors["dataschema"].append( + InvalidAttributeTypeError( + attribute_name="dataschema", expected_type=str + ) + ) + if not attributes["dataschema"]: + errors["dataschema"].append( + InvalidAttributeValueError( + attribute_name="dataschema", + msg="Attribute 'dataschema' must not be empty", + ) + ) + return errors + + @staticmethod + def _validate_extension_attributes( + attributes: dict[str, Any], + ) -> dict[str, list[BaseCloudEventException]]: + """ + Validates the extension attributes. + + :param attributes: The attributes of the CloudEvent instance. + :return: A dictionary of validation error messages. + """ + errors = defaultdict(list) + extension_attributes = [ + key + for key in attributes.keys() + if key not in REQUIRED_ATTRIBUTES and key not in OPTIONAL_ATTRIBUTES + ] + for extension_attribute in extension_attributes: + if extension_attribute == "data": + errors[extension_attribute].append( + CustomExtensionAttributeError( + attribute_name=extension_attribute, + msg="Extension attribute 'data' is reserved and must not be used", + ) + ) + if not (1 <= len(extension_attribute) <= 20): + errors[extension_attribute].append( + CustomExtensionAttributeError( + attribute_name=extension_attribute, + msg=f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long", + ) + ) + if not re.match(r"^[a-z0-9]+$", extension_attribute): + errors[extension_attribute].append( + CustomExtensionAttributeError( + attribute_name=extension_attribute, + msg=f"Extension attribute '{extension_attribute}' should only contain lowercase letters and numbers", + ) + ) + return errors + + def get_id(self) -> str: + """ + Retrieve the ID of the event. + + :return: The ID of the event. + """ + return self._attributes["id"] # type: ignore + + def get_source(self) -> str: + """ + Retrieve the source of the event. + + :return: The source of the event. + """ + return self._attributes["source"] # type: ignore + + def get_type(self) -> str: + """ + Retrieve the type of the event. + + :return: The type of the event. + """ + return self._attributes["type"] # type: ignore + + def get_specversion(self) -> str: + """ + Retrieve the specversion of the event. + + :return: The specversion of the event. + """ + return self._attributes["specversion"] # type: ignore + + def get_datacontenttype(self) -> Optional[str]: + """ + Retrieve the datacontenttype of the event. + + :return: The datacontenttype of the event. + """ + return self._attributes.get("datacontenttype") + + def get_dataschema(self) -> Optional[str]: + """ + Retrieve the dataschema of the event. + + :return: The dataschema of the event. + """ + return self._attributes.get("dataschema") + + def get_subject(self) -> Optional[str]: + """ + Retrieve the subject of the event. + + :return: The subject of the event. + """ + return self._attributes.get("subject") + + def get_time(self) -> Optional[datetime]: + """ + Retrieve the time of the event. + + :return: The time of the event. + """ + return self._attributes.get("time") + + def get_extension(self, extension_name: str) -> Any: + """ + Retrieve an extension attribute of the event. + + :param extension_name: The name of the extension attribute. + :return: The value of the extension attribute. + """ + return self._attributes.get(extension_name) + + def get_data(self) -> Optional[dict]: + """ + Retrieve data of the event. + + :return: The data of the event. + """ + return self._data diff --git a/src/cloudevents/core/v1/exceptions.py b/src/cloudevents/core/v1/exceptions.py new file mode 100644 index 00000000..ba6b63ae --- /dev/null +++ b/src/cloudevents/core/v1/exceptions.py @@ -0,0 +1,76 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +class BaseCloudEventException(Exception): + """A CloudEvent generic exception.""" + + +class CloudEventValidationError(BaseCloudEventException): + """ + Holds validation errors aggregated during a CloudEvent creation. + """ + + def __init__(self, errors: dict[str, list[BaseCloudEventException]]) -> None: + """ + :param errors: The errors gathered during the CloudEvent creation where key + is the name of the attribute and value is a list of errors related to that attribute. + """ + super().__init__("Failed to create CloudEvent due to the validation errors:") + self.errors: dict[str, list[BaseCloudEventException]] = errors + + def __str__(self) -> str: + error_messages: list[str] = [ + f"{key}: {', '.join(str(e) for e in value)}" + for key, value in self.errors.items() + ] + return f"{super().__str__()}: {', '.join(error_messages)}" + + +class MissingRequiredAttributeError(BaseCloudEventException, ValueError): + """ + Raised for attributes that are required to be present by the specification. + """ + + def __init__(self, attribute_name: str) -> None: + self.attribute_name: str = attribute_name + super().__init__(f"Missing required attribute: '{attribute_name}'") + + +class CustomExtensionAttributeError(BaseCloudEventException, ValueError): + """ + Raised when a custom extension attribute violates naming conventions. + """ + + def __init__(self, attribute_name: str, msg: str) -> None: + self.attribute_name: str = attribute_name + super().__init__(msg) + + +class InvalidAttributeTypeError(BaseCloudEventException, TypeError): + """ + Raised when an attribute has an unsupported type. + """ + + def __init__(self, attribute_name: str, expected_type: type) -> None: + self.attribute_name: str = attribute_name + super().__init__(f"Attribute '{attribute_name}' must be a {expected_type}") + + +class InvalidAttributeValueError(BaseCloudEventException, ValueError): + """ + Raised when an attribute has an invalid value. + """ + + def __init__(self, attribute_name: str, msg: str) -> None: + self.attribute_name: str = attribute_name + super().__init__(msg) diff --git a/cloudevents/py.typed b/src/cloudevents/py.typed similarity index 100% rename from cloudevents/py.typed rename to src/cloudevents/py.typed diff --git a/cloudevents/__init__.py b/src/cloudevents/v1/__init__.py similarity index 100% rename from cloudevents/__init__.py rename to src/cloudevents/v1/__init__.py diff --git a/cloudevents/abstract/__init__.py b/src/cloudevents/v1/abstract/__init__.py similarity index 90% rename from cloudevents/abstract/__init__.py rename to src/cloudevents/v1/abstract/__init__.py index 4000c8a7..13ebec5b 100644 --- a/cloudevents/abstract/__init__.py +++ b/src/cloudevents/v1/abstract/__init__.py @@ -12,6 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.abstract.event import AnyCloudEvent, CloudEvent +from cloudevents_v1.abstract.event import AnyCloudEvent, CloudEvent __all__ = ["AnyCloudEvent", "CloudEvent"] diff --git a/cloudevents/abstract/event.py b/src/cloudevents/v1/abstract/event.py similarity index 100% rename from cloudevents/abstract/event.py rename to src/cloudevents/v1/abstract/event.py diff --git a/cloudevents/conversion.py b/src/cloudevents/v1/conversion.py similarity index 97% rename from cloudevents/conversion.py rename to src/cloudevents/v1/conversion.py index c73e3ed0..3d9899a2 100644 --- a/cloudevents/conversion.py +++ b/src/cloudevents/v1/conversion.py @@ -16,11 +16,11 @@ import json import typing -from cloudevents import exceptions as cloud_exceptions -from cloudevents.abstract import AnyCloudEvent -from cloudevents.sdk import converters, marshaller, types -from cloudevents.sdk.converters import is_binary -from cloudevents.sdk.event import v1, v03 +from cloudevents_v1 import exceptions as cloud_exceptions +from cloudevents_v1.abstract import AnyCloudEvent +from cloudevents_v1.sdk import converters, marshaller, types +from cloudevents_v1.sdk.converters import is_binary +from cloudevents_v1.sdk.event import v03, v1 def _best_effort_serialize_to_json( # type: ignore[no-untyped-def] diff --git a/cloudevents/exceptions.py b/src/cloudevents/v1/exceptions.py similarity index 100% rename from cloudevents/exceptions.py rename to src/cloudevents/v1/exceptions.py diff --git a/cloudevents/http/__init__.py b/src/cloudevents/v1/http/__init__.py similarity index 73% rename from cloudevents/http/__init__.py rename to src/cloudevents/v1/http/__init__.py index 6e75636e..86f6030a 100644 --- a/cloudevents/http/__init__.py +++ b/src/cloudevents/v1/http/__init__.py @@ -13,16 +13,16 @@ # under the License. -from cloudevents.http.conversion import from_dict, from_http, from_json -from cloudevents.http.event import CloudEvent -from cloudevents.http.event_type import is_binary, is_structured # deprecated -from cloudevents.http.http_methods import ( # deprecated +from cloudevents_v1.http.conversion import from_dict, from_http, from_json +from cloudevents_v1.http.event import CloudEvent +from cloudevents_v1.http.event_type import is_binary, is_structured # deprecated +from cloudevents_v1.http.http_methods import ( # deprecated to_binary, to_binary_http, to_structured, to_structured_http, ) -from cloudevents.http.json_methods import to_json # deprecated +from cloudevents_v1.http.json_methods import to_json # deprecated __all__ = [ "to_binary", diff --git a/cloudevents/http/conversion.py b/src/cloudevents/v1/http/conversion.py similarity index 88% rename from cloudevents/http/conversion.py rename to src/cloudevents/v1/http/conversion.py index a7da926b..050eb25c 100644 --- a/cloudevents/http/conversion.py +++ b/src/cloudevents/v1/http/conversion.py @@ -14,11 +14,11 @@ import typing -from cloudevents.conversion import from_dict as _abstract_from_dict -from cloudevents.conversion import from_http as _abstract_from_http -from cloudevents.conversion import from_json as _abstract_from_json -from cloudevents.http.event import CloudEvent -from cloudevents.sdk import types +from cloudevents_v1.conversion import from_dict as _abstract_from_dict +from cloudevents_v1.conversion import from_http as _abstract_from_http +from cloudevents_v1.conversion import from_json as _abstract_from_json +from cloudevents_v1.http.event import CloudEvent +from cloudevents_v1.sdk import types def from_json( diff --git a/cloudevents/http/event.py b/src/cloudevents/v1/http/event.py similarity index 96% rename from cloudevents/http/event.py rename to src/cloudevents/v1/http/event.py index c7a066d6..69f38110 100644 --- a/cloudevents/http/event.py +++ b/src/cloudevents/v1/http/event.py @@ -16,9 +16,9 @@ import typing import uuid -import cloudevents.exceptions as cloud_exceptions -from cloudevents import abstract -from cloudevents.sdk.event import v1, v03 +import cloudevents_v1.exceptions as cloud_exceptions +from cloudevents_v1 import abstract +from cloudevents_v1.sdk.event import v03, v1 _required_by_version = { "1.0": v1.Event._ce_required_fields, diff --git a/cloudevents/http/event_type.py b/src/cloudevents/v1/http/event_type.py similarity index 88% rename from cloudevents/http/event_type.py rename to src/cloudevents/v1/http/event_type.py index 52259e1e..38fceffb 100644 --- a/cloudevents/http/event_type.py +++ b/src/cloudevents/v1/http/event_type.py @@ -13,11 +13,10 @@ # under the License. import typing +from cloudevents_v1.sdk.converters import is_binary as _moved_is_binary +from cloudevents_v1.sdk.converters import is_structured as _moved_is_structured from deprecation import deprecated -from cloudevents.sdk.converters import is_binary as _moved_is_binary -from cloudevents.sdk.converters import is_structured as _moved_is_structured - # THIS MODULE IS DEPRECATED, YOU SHOULD NOT ADD NEW FUNCTIONALLY HERE diff --git a/cloudevents/http/http_methods.py b/src/cloudevents/v1/http/http_methods.py similarity index 86% rename from cloudevents/http/http_methods.py rename to src/cloudevents/v1/http/http_methods.py index 091c51b5..fe5cd42b 100644 --- a/cloudevents/http/http_methods.py +++ b/src/cloudevents/v1/http/http_methods.py @@ -14,15 +14,14 @@ import typing +from cloudevents_v1.abstract import AnyCloudEvent +from cloudevents_v1.conversion import to_binary as _moved_to_binary +from cloudevents_v1.conversion import to_structured as _moved_to_structured +from cloudevents_v1.http.conversion import from_http as _moved_from_http +from cloudevents_v1.http.event import CloudEvent +from cloudevents_v1.sdk import types from deprecation import deprecated -from cloudevents.abstract import AnyCloudEvent -from cloudevents.conversion import to_binary as _moved_to_binary -from cloudevents.conversion import to_structured as _moved_to_structured -from cloudevents.http.conversion import from_http as _moved_from_http -from cloudevents.http.event import CloudEvent -from cloudevents.sdk import types - # THIS MODULE IS DEPRECATED, YOU SHOULD NOT ADD NEW FUNCTIONALLY HERE diff --git a/cloudevents/http/json_methods.py b/src/cloudevents/v1/http/json_methods.py similarity index 83% rename from cloudevents/http/json_methods.py rename to src/cloudevents/v1/http/json_methods.py index 58e322c7..28a9873f 100644 --- a/cloudevents/http/json_methods.py +++ b/src/cloudevents/v1/http/json_methods.py @@ -14,14 +14,13 @@ import typing +from cloudevents_v1.abstract import AnyCloudEvent +from cloudevents_v1.conversion import to_json as _moved_to_json +from cloudevents_v1.http import CloudEvent +from cloudevents_v1.http.conversion import from_json as _moved_from_json +from cloudevents_v1.sdk import types from deprecation import deprecated -from cloudevents.abstract import AnyCloudEvent -from cloudevents.conversion import to_json as _moved_to_json -from cloudevents.http import CloudEvent -from cloudevents.http.conversion import from_json as _moved_from_json -from cloudevents.sdk import types - # THIS MODULE IS DEPRECATED, YOU SHOULD NOT ADD NEW FUNCTIONALLY HERE diff --git a/cloudevents/http/util.py b/src/cloudevents/v1/http/util.py similarity index 96% rename from cloudevents/http/util.py rename to src/cloudevents/v1/http/util.py index f44395e6..8158fb66 100644 --- a/cloudevents/http/util.py +++ b/src/cloudevents/v1/http/util.py @@ -13,11 +13,10 @@ # under the License. import typing -from deprecation import deprecated - -from cloudevents.conversion import ( +from cloudevents_v1.conversion import ( _best_effort_serialize_to_json as _moved_default_marshaller, ) +from deprecation import deprecated # THIS MODULE IS DEPRECATED, YOU SHOULD NOT ADD NEW FUNCTIONALLY HERE diff --git a/cloudevents/kafka/__init__.py b/src/cloudevents/v1/kafka/__init__.py similarity index 94% rename from cloudevents/kafka/__init__.py rename to src/cloudevents/v1/kafka/__init__.py index fbe1dfb0..b5648c30 100644 --- a/cloudevents/kafka/__init__.py +++ b/src/cloudevents/v1/kafka/__init__.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.kafka.conversion import ( +from cloudevents_v1.kafka.conversion import ( KafkaMessage, KeyMapper, from_binary, diff --git a/cloudevents/kafka/conversion.py b/src/cloudevents/v1/kafka/conversion.py similarity index 97% rename from cloudevents/kafka/conversion.py rename to src/cloudevents/v1/kafka/conversion.py index 97c355f2..e7f30733 100644 --- a/cloudevents/kafka/conversion.py +++ b/src/cloudevents/v1/kafka/conversion.py @@ -15,11 +15,11 @@ import json import typing -from cloudevents import exceptions as cloud_exceptions -from cloudevents import http -from cloudevents.abstract import AnyCloudEvent -from cloudevents.kafka.exceptions import KeyMapperError -from cloudevents.sdk import types +from cloudevents_v1 import exceptions as cloud_exceptions +from cloudevents_v1 import http +from cloudevents_v1.abstract import AnyCloudEvent +from cloudevents_v1.kafka.exceptions import KeyMapperError +from cloudevents_v1.sdk import types DEFAULT_MARSHALLER: types.MarshallerType = json.dumps DEFAULT_UNMARSHALLER: types.MarshallerType = json.loads diff --git a/cloudevents/kafka/exceptions.py b/src/cloudevents/v1/kafka/exceptions.py similarity index 92% rename from cloudevents/kafka/exceptions.py rename to src/cloudevents/v1/kafka/exceptions.py index 6459f0a2..352e9d24 100644 --- a/cloudevents/kafka/exceptions.py +++ b/src/cloudevents/v1/kafka/exceptions.py @@ -11,7 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -from cloudevents import exceptions as cloud_exceptions +from cloudevents_v1 import exceptions as cloud_exceptions class KeyMapperError(cloud_exceptions.GenericException): diff --git a/src/cloudevents/v1/py.typed b/src/cloudevents/v1/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/cloudevents/pydantic/__init__.py b/src/cloudevents/v1/pydantic/__init__.py similarity index 81% rename from cloudevents/pydantic/__init__.py rename to src/cloudevents/v1/pydantic/__init__.py index f8556ca1..4d22fe41 100644 --- a/cloudevents/pydantic/__init__.py +++ b/src/cloudevents/v1/pydantic/__init__.py @@ -14,24 +14,29 @@ from typing import TYPE_CHECKING -from cloudevents.exceptions import PydanticFeatureNotInstalled +from cloudevents_v1.exceptions import PydanticFeatureNotInstalled try: if TYPE_CHECKING: - from cloudevents.pydantic.v2 import CloudEvent, from_dict, from_http, from_json + from cloudevents_v1.pydantic.v2 import ( + CloudEvent, + from_dict, + from_http, + from_json, + ) else: from pydantic import VERSION as PYDANTIC_VERSION pydantic_major_version = PYDANTIC_VERSION.split(".")[0] if pydantic_major_version == "1": - from cloudevents.pydantic.v1 import ( + from cloudevents_v1.pydantic.v1 import ( CloudEvent, from_dict, from_http, from_json, ) else: - from cloudevents.pydantic.v2 import ( + from cloudevents_v1.pydantic.v2 import ( CloudEvent, from_dict, from_http, diff --git a/cloudevents/pydantic/fields_docs.py b/src/cloudevents/v1/pydantic/fields_docs.py similarity index 99% rename from cloudevents/pydantic/fields_docs.py rename to src/cloudevents/v1/pydantic/fields_docs.py index 00ed0bd3..0abeaf15 100644 --- a/cloudevents/pydantic/fields_docs.py +++ b/src/cloudevents/v1/pydantic/fields_docs.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.event import attribute +from cloudevents_v1.sdk.event import attribute FIELD_DESCRIPTIONS = { "data": { diff --git a/cloudevents/pydantic/v2/__init__.py b/src/cloudevents/v1/pydantic/v1/__init__.py similarity index 83% rename from cloudevents/pydantic/v2/__init__.py rename to src/cloudevents/v1/pydantic/v1/__init__.py index 55d2a7fd..3b0e435c 100644 --- a/cloudevents/pydantic/v2/__init__.py +++ b/src/cloudevents/v1/pydantic/v1/__init__.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.pydantic.v2.conversion import from_dict, from_http, from_json -from cloudevents.pydantic.v2.event import CloudEvent +from cloudevents_v1.pydantic.v1.conversion import from_dict, from_http, from_json +from cloudevents_v1.pydantic.v1.event import CloudEvent __all__ = ["CloudEvent", "from_json", "from_dict", "from_http"] diff --git a/cloudevents/pydantic/v1/conversion.py b/src/cloudevents/v1/pydantic/v1/conversion.py similarity index 88% rename from cloudevents/pydantic/v1/conversion.py rename to src/cloudevents/v1/pydantic/v1/conversion.py index dcf0b7db..efd7a7f4 100644 --- a/cloudevents/pydantic/v1/conversion.py +++ b/src/cloudevents/v1/pydantic/v1/conversion.py @@ -13,11 +13,11 @@ # under the License. import typing -from cloudevents.conversion import from_dict as _abstract_from_dict -from cloudevents.conversion import from_http as _abstract_from_http -from cloudevents.conversion import from_json as _abstract_from_json -from cloudevents.pydantic.v1.event import CloudEvent -from cloudevents.sdk import types +from cloudevents_v1.conversion import from_dict as _abstract_from_dict +from cloudevents_v1.conversion import from_http as _abstract_from_http +from cloudevents_v1.conversion import from_json as _abstract_from_json +from cloudevents_v1.pydantic.v1.event import CloudEvent +from cloudevents_v1.sdk import types def from_http( diff --git a/cloudevents/pydantic/v1/event.py b/src/cloudevents/v1/pydantic/v1/event.py similarity index 96% rename from cloudevents/pydantic/v1/event.py rename to src/cloudevents/v1/pydantic/v1/event.py index d18736a4..999828b3 100644 --- a/cloudevents/pydantic/v1/event.py +++ b/src/cloudevents/v1/pydantic/v1/event.py @@ -15,8 +15,8 @@ import json import typing -from cloudevents.exceptions import PydanticFeatureNotInstalled -from cloudevents.pydantic.fields_docs import FIELD_DESCRIPTIONS +from cloudevents_v1.exceptions import PydanticFeatureNotInstalled +from cloudevents_v1.pydantic.fields_docs import FIELD_DESCRIPTIONS try: from pydantic import VERSION as PYDANTIC_VERSION @@ -32,9 +32,9 @@ "Install it using pip install cloudevents[pydantic]" ) -from cloudevents import abstract, conversion, http -from cloudevents.exceptions import IncompatibleArgumentsError -from cloudevents.sdk.event import attribute +from cloudevents_v1 import abstract, conversion, http +from cloudevents_v1.exceptions import IncompatibleArgumentsError +from cloudevents_v1.sdk.event import attribute def _ce_json_dumps( # type: ignore[no-untyped-def] @@ -71,7 +71,9 @@ def _ce_json_dumps( # type: ignore[no-untyped-def] def _ce_json_loads( # type: ignore[no-untyped-def] - data: typing.AnyStr, *args, **kwargs # noqa + data: typing.AnyStr, + *args, + **kwargs, # noqa ) -> typing.Dict[typing.Any, typing.Any]: """Performs Pydantic-specific deserialization of the event. diff --git a/cloudevents/pydantic/v1/__init__.py b/src/cloudevents/v1/pydantic/v2/__init__.py similarity index 83% rename from cloudevents/pydantic/v1/__init__.py rename to src/cloudevents/v1/pydantic/v2/__init__.py index e17151a4..0bda7d88 100644 --- a/cloudevents/pydantic/v1/__init__.py +++ b/src/cloudevents/v1/pydantic/v2/__init__.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.pydantic.v1.conversion import from_dict, from_http, from_json -from cloudevents.pydantic.v1.event import CloudEvent +from cloudevents_v1.pydantic.v2.conversion import from_dict, from_http, from_json +from cloudevents_v1.pydantic.v2.event import CloudEvent __all__ = ["CloudEvent", "from_json", "from_dict", "from_http"] diff --git a/cloudevents/pydantic/v2/conversion.py b/src/cloudevents/v1/pydantic/v2/conversion.py similarity index 88% rename from cloudevents/pydantic/v2/conversion.py rename to src/cloudevents/v1/pydantic/v2/conversion.py index 65108544..a164091b 100644 --- a/cloudevents/pydantic/v2/conversion.py +++ b/src/cloudevents/v1/pydantic/v2/conversion.py @@ -14,11 +14,11 @@ import typing -from cloudevents.conversion import from_dict as _abstract_from_dict -from cloudevents.conversion import from_http as _abstract_from_http -from cloudevents.conversion import from_json as _abstract_from_json -from cloudevents.pydantic.v2.event import CloudEvent -from cloudevents.sdk import types +from cloudevents_v1.conversion import from_dict as _abstract_from_dict +from cloudevents_v1.conversion import from_http as _abstract_from_http +from cloudevents_v1.conversion import from_json as _abstract_from_json +from cloudevents_v1.pydantic.v2.event import CloudEvent +from cloudevents_v1.sdk import types def from_http( diff --git a/cloudevents/pydantic/v2/event.py b/src/cloudevents/v1/pydantic/v2/event.py similarity index 96% rename from cloudevents/pydantic/v2/event.py rename to src/cloudevents/v1/pydantic/v2/event.py index 643794c1..26c2fcb9 100644 --- a/cloudevents/pydantic/v2/event.py +++ b/src/cloudevents/v1/pydantic/v2/event.py @@ -17,11 +17,10 @@ import typing from typing import Any +from cloudevents_v1.exceptions import PydanticFeatureNotInstalled +from cloudevents_v1.pydantic.fields_docs import FIELD_DESCRIPTIONS from pydantic.deprecated import parse as _deprecated_parse -from cloudevents.exceptions import PydanticFeatureNotInstalled -from cloudevents.pydantic.fields_docs import FIELD_DESCRIPTIONS - try: from pydantic import BaseModel, ConfigDict, Field, model_serializer except ImportError: # pragma: no cover # hard to test @@ -30,9 +29,9 @@ "Install it using pip install cloudevents[pydantic]" ) -from cloudevents import abstract, conversion -from cloudevents.exceptions import IncompatibleArgumentsError -from cloudevents.sdk.event import attribute +from cloudevents_v1 import abstract, conversion +from cloudevents_v1.exceptions import IncompatibleArgumentsError +from cloudevents_v1.sdk.event import attribute class CloudEvent(abstract.CloudEvent, BaseModel): # type: ignore diff --git a/cloudevents/sdk/__init__.py b/src/cloudevents/v1/sdk/__init__.py similarity index 100% rename from cloudevents/sdk/__init__.py rename to src/cloudevents/v1/sdk/__init__.py diff --git a/cloudevents/sdk/converters/__init__.py b/src/cloudevents/v1/sdk/converters/__init__.py similarity index 82% rename from cloudevents/sdk/converters/__init__.py rename to src/cloudevents/v1/sdk/converters/__init__.py index cd8df680..c70f1464 100644 --- a/cloudevents/sdk/converters/__init__.py +++ b/src/cloudevents/v1/sdk/converters/__init__.py @@ -12,9 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.converters import binary, structured -from cloudevents.sdk.converters.binary import is_binary -from cloudevents.sdk.converters.structured import is_structured +from cloudevents_v1.sdk.converters import binary, structured +from cloudevents_v1.sdk.converters.binary import is_binary +from cloudevents_v1.sdk.converters.structured import is_structured TypeBinary: str = binary.BinaryHTTPCloudEventConverter.TYPE TypeStructured: str = structured.JSONHTTPCloudEventConverter.TYPE diff --git a/cloudevents/sdk/converters/base.py b/src/cloudevents/v1/sdk/converters/base.py similarity index 97% rename from cloudevents/sdk/converters/base.py rename to src/cloudevents/v1/sdk/converters/base.py index 43edf5d2..c0b0b3fb 100644 --- a/cloudevents/sdk/converters/base.py +++ b/src/cloudevents/v1/sdk/converters/base.py @@ -14,7 +14,7 @@ import typing -from cloudevents.sdk.event import base +from cloudevents_v1.sdk.event import base class Converter(object): diff --git a/cloudevents/sdk/converters/binary.py b/src/cloudevents/v1/sdk/converters/binary.py similarity index 90% rename from cloudevents/sdk/converters/binary.py rename to src/cloudevents/v1/sdk/converters/binary.py index c5fcbf54..a06d92ce 100644 --- a/cloudevents/sdk/converters/binary.py +++ b/src/cloudevents/v1/sdk/converters/binary.py @@ -14,11 +14,11 @@ import typing -from cloudevents.sdk import exceptions, types -from cloudevents.sdk.converters import base -from cloudevents.sdk.converters.util import has_binary_headers -from cloudevents.sdk.event import base as event_base -from cloudevents.sdk.event import v1, v03 +from cloudevents_v1.sdk import exceptions, types +from cloudevents_v1.sdk.converters import base +from cloudevents_v1.sdk.converters.util import has_binary_headers +from cloudevents_v1.sdk.event import base as event_base +from cloudevents_v1.sdk.event import v03, v1 class BinaryHTTPCloudEventConverter(base.Converter): diff --git a/cloudevents/sdk/converters/structured.py b/src/cloudevents/v1/sdk/converters/structured.py similarity index 92% rename from cloudevents/sdk/converters/structured.py rename to src/cloudevents/v1/sdk/converters/structured.py index 24eda895..b5e090ef 100644 --- a/cloudevents/sdk/converters/structured.py +++ b/src/cloudevents/v1/sdk/converters/structured.py @@ -14,10 +14,10 @@ import typing -from cloudevents.sdk import types -from cloudevents.sdk.converters import base -from cloudevents.sdk.converters.util import has_binary_headers -from cloudevents.sdk.event import base as event_base +from cloudevents_v1.sdk import types +from cloudevents_v1.sdk.converters import base +from cloudevents_v1.sdk.converters.util import has_binary_headers +from cloudevents_v1.sdk.event import base as event_base # TODO: Singleton? diff --git a/cloudevents/sdk/converters/util.py b/src/cloudevents/v1/sdk/converters/util.py similarity index 100% rename from cloudevents/sdk/converters/util.py rename to src/cloudevents/v1/sdk/converters/util.py diff --git a/cloudevents/sdk/event/__init__.py b/src/cloudevents/v1/sdk/event/__init__.py similarity index 100% rename from cloudevents/sdk/event/__init__.py rename to src/cloudevents/v1/sdk/event/__init__.py diff --git a/cloudevents/sdk/event/attribute.py b/src/cloudevents/v1/sdk/event/attribute.py similarity index 100% rename from cloudevents/sdk/event/attribute.py rename to src/cloudevents/v1/sdk/event/attribute.py diff --git a/cloudevents/sdk/event/base.py b/src/cloudevents/v1/sdk/event/base.py similarity index 99% rename from cloudevents/sdk/event/base.py rename to src/cloudevents/v1/sdk/event/base.py index 53e05d35..f207e7cb 100644 --- a/cloudevents/sdk/event/base.py +++ b/src/cloudevents/v1/sdk/event/base.py @@ -17,8 +17,8 @@ import typing from typing import Set -import cloudevents.exceptions as cloud_exceptions -from cloudevents.sdk import types +import cloudevents_v1.exceptions as cloud_exceptions +from cloudevents_v1.sdk import types # TODO(slinkydeveloper) is this really needed? diff --git a/cloudevents/sdk/event/opt.py b/src/cloudevents/v1/sdk/event/opt.py similarity index 100% rename from cloudevents/sdk/event/opt.py rename to src/cloudevents/v1/sdk/event/opt.py diff --git a/cloudevents/sdk/event/v03.py b/src/cloudevents/v1/sdk/event/v03.py similarity index 99% rename from cloudevents/sdk/event/v03.py rename to src/cloudevents/v1/sdk/event/v03.py index 6d69d2ab..d686b536 100644 --- a/cloudevents/sdk/event/v03.py +++ b/src/cloudevents/v1/sdk/event/v03.py @@ -13,7 +13,7 @@ # under the License. import typing -from cloudevents.sdk.event import base, opt +from cloudevents_v1.sdk.event import base, opt class Event(base.BaseEvent): diff --git a/cloudevents/sdk/event/v1.py b/src/cloudevents/v1/sdk/event/v1.py similarity index 98% rename from cloudevents/sdk/event/v1.py rename to src/cloudevents/v1/sdk/event/v1.py index 18d1f3af..dfa470d1 100644 --- a/cloudevents/sdk/event/v1.py +++ b/src/cloudevents/v1/sdk/event/v1.py @@ -13,7 +13,7 @@ # under the License. import typing -from cloudevents.sdk.event import base, opt +from cloudevents_v1.sdk.event import base, opt class Event(base.BaseEvent): diff --git a/cloudevents/sdk/exceptions.py b/src/cloudevents/v1/sdk/exceptions.py similarity index 100% rename from cloudevents/sdk/exceptions.py rename to src/cloudevents/v1/sdk/exceptions.py diff --git a/cloudevents/sdk/marshaller.py b/src/cloudevents/v1/sdk/marshaller.py similarity index 96% rename from cloudevents/sdk/marshaller.py rename to src/cloudevents/v1/sdk/marshaller.py index dfd18965..b650c26a 100644 --- a/cloudevents/sdk/marshaller.py +++ b/src/cloudevents/v1/sdk/marshaller.py @@ -15,9 +15,9 @@ import json import typing -from cloudevents.sdk import exceptions, types -from cloudevents.sdk.converters import base, binary, structured -from cloudevents.sdk.event import base as event_base +from cloudevents_v1.sdk import exceptions, types +from cloudevents_v1.sdk.converters import base, binary, structured +from cloudevents_v1.sdk.event import base as event_base class HTTPMarshaller(object): diff --git a/cloudevents/sdk/types.py b/src/cloudevents/v1/sdk/types.py similarity index 100% rename from cloudevents/sdk/types.py rename to src/cloudevents/v1/sdk/types.py diff --git a/cloudevents/tests/__init__.py b/src/cloudevents/v1/tests/__init__.py similarity index 100% rename from cloudevents/tests/__init__.py rename to src/cloudevents/v1/tests/__init__.py diff --git a/cloudevents/tests/data.py b/src/cloudevents/v1/tests/data.py similarity index 97% rename from cloudevents/tests/data.py rename to src/cloudevents/v1/tests/data.py index f5b0ea33..a7866c94 100644 --- a/cloudevents/tests/data.py +++ b/src/cloudevents/v1/tests/data.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.event import v1, v03 +from cloudevents_v1.sdk.event import v03, v1 content_type = "application/json" ce_type = "word.found.exclamation" diff --git a/cloudevents/tests/test_backwards_compatability.py b/src/cloudevents/v1/tests/test_backwards_compatability.py similarity index 62% rename from cloudevents/tests/test_backwards_compatability.py rename to src/cloudevents/v1/tests/test_backwards_compatability.py index 0a20f4cf..157b0dfe 100644 --- a/cloudevents/tests/test_backwards_compatability.py +++ b/src/cloudevents/v1/tests/test_backwards_compatability.py @@ -12,9 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. import pytest - -from cloudevents.conversion import _best_effort_serialize_to_json -from cloudevents.http import CloudEvent +from cloudevents_v1.conversion import _best_effort_serialize_to_json +from cloudevents_v1.http import CloudEvent @pytest.fixture() @@ -23,10 +22,10 @@ def dummy_event(): def test_json_methods(dummy_event): - from cloudevents.conversion import to_json - from cloudevents.http.conversion import from_json - from cloudevents.http.json_methods import from_json as deprecated_from_json - from cloudevents.http.json_methods import to_json as deprecated_to_json + from cloudevents_v1.conversion import to_json + from cloudevents_v1.http.conversion import from_json + from cloudevents_v1.http.json_methods import from_json as deprecated_from_json + from cloudevents_v1.http.json_methods import to_json as deprecated_to_json assert from_json(to_json(dummy_event)) == deprecated_from_json( deprecated_to_json(dummy_event) @@ -34,10 +33,12 @@ def test_json_methods(dummy_event): def test_http_methods(dummy_event): - from cloudevents.http import from_http, to_binary, to_structured - from cloudevents.http.http_methods import from_http as deprecated_from_http - from cloudevents.http.http_methods import to_binary as deprecated_to_binary - from cloudevents.http.http_methods import to_structured as deprecated_to_structured + from cloudevents_v1.http import from_http, to_binary, to_structured + from cloudevents_v1.http.http_methods import from_http as deprecated_from_http + from cloudevents_v1.http.http_methods import to_binary as deprecated_to_binary + from cloudevents_v1.http.http_methods import ( + to_structured as deprecated_to_structured, + ) assert from_http(*to_binary(dummy_event)) == deprecated_from_http( *deprecated_to_binary(dummy_event) @@ -48,17 +49,17 @@ def test_http_methods(dummy_event): def test_util(): - from cloudevents.http.util import default_marshaller # noqa + from cloudevents_v1.http.util import default_marshaller # noqa assert _best_effort_serialize_to_json(None) == default_marshaller(None) def test_event_type(): - from cloudevents.http.event_type import is_binary, is_structured # noqa + from cloudevents_v1.http.event_type import is_binary, is_structured # noqa def test_http_module_imports(): - from cloudevents.http import ( # noqa + from cloudevents_v1.http import ( # noqa CloudEvent, from_dict, from_http, diff --git a/cloudevents/tests/test_base_events.py b/src/cloudevents/v1/tests/test_base_events.py similarity index 92% rename from cloudevents/tests/test_base_events.py rename to src/cloudevents/v1/tests/test_base_events.py index 8eb83d44..04db6470 100644 --- a/cloudevents/tests/test_base_events.py +++ b/src/cloudevents/v1/tests/test_base_events.py @@ -12,10 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +import cloudevents_v1.exceptions as cloud_exceptions import pytest - -import cloudevents.exceptions as cloud_exceptions -from cloudevents.sdk.event import v1, v03 +from cloudevents_v1.sdk.event import v03, v1 @pytest.mark.parametrize("event_class", [v1.Event, v03.Event]) diff --git a/cloudevents/tests/test_converters.py b/src/cloudevents/v1/tests/test_converters.py similarity index 93% rename from cloudevents/tests/test_converters.py rename to src/cloudevents/v1/tests/test_converters.py index b91d6b39..f9940409 100644 --- a/cloudevents/tests/test_converters.py +++ b/src/cloudevents/v1/tests/test_converters.py @@ -13,9 +13,8 @@ # under the License. import pytest - -from cloudevents.sdk import exceptions -from cloudevents.sdk.converters import base, binary +from cloudevents_v1.sdk import exceptions +from cloudevents_v1.sdk.converters import base, binary def test_binary_converter_raise_unsupported(): diff --git a/cloudevents/tests/test_data_encaps_refs.py b/src/cloudevents/v1/tests/test_data_encaps_refs.py similarity index 96% rename from cloudevents/tests/test_data_encaps_refs.py rename to src/cloudevents/v1/tests/test_data_encaps_refs.py index 02405a93..cf923a9c 100644 --- a/cloudevents/tests/test_data_encaps_refs.py +++ b/src/cloudevents/v1/tests/test_data_encaps_refs.py @@ -16,10 +16,9 @@ from uuid import uuid4 import pytest - -from cloudevents.sdk import converters, marshaller -from cloudevents.sdk.event import v1, v03 -from cloudevents.tests import data +from cloudevents_v1.sdk import converters, marshaller +from cloudevents_v1.sdk.event import v03, v1 +from cloudevents_v1.tests import data @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) diff --git a/cloudevents/tests/test_deprecated_functions.py b/src/cloudevents/v1/tests/test_deprecated_functions.py similarity index 97% rename from cloudevents/tests/test_deprecated_functions.py rename to src/cloudevents/v1/tests/test_deprecated_functions.py index a99f6247..eec0f527 100644 --- a/cloudevents/tests/test_deprecated_functions.py +++ b/src/cloudevents/v1/tests/test_deprecated_functions.py @@ -13,8 +13,7 @@ # under the License. import pytest - -from cloudevents.http import ( +from cloudevents_v1.http import ( CloudEvent, to_binary, to_binary_http, diff --git a/cloudevents/tests/test_event_extensions.py b/src/cloudevents/v1/tests/test_event_extensions.py similarity index 97% rename from cloudevents/tests/test_event_extensions.py rename to src/cloudevents/v1/tests/test_event_extensions.py index eea8edfa..87424090 100644 --- a/cloudevents/tests/test_event_extensions.py +++ b/src/cloudevents/v1/tests/test_event_extensions.py @@ -15,8 +15,7 @@ import json import pytest - -from cloudevents.http import CloudEvent, from_http, to_binary, to_structured +from cloudevents_v1.http import CloudEvent, from_http, to_binary, to_structured test_data = json.dumps({"data-key": "val"}) test_attributes = { diff --git a/cloudevents/tests/test_event_from_request_converter.py b/src/cloudevents/v1/tests/test_event_from_request_converter.py similarity index 93% rename from cloudevents/tests/test_event_from_request_converter.py rename to src/cloudevents/v1/tests/test_event_from_request_converter.py index 901284bb..2f98a640 100644 --- a/cloudevents/tests/test_event_from_request_converter.py +++ b/src/cloudevents/v1/tests/test_event_from_request_converter.py @@ -15,11 +15,10 @@ import json import pytest - -from cloudevents.sdk import marshaller -from cloudevents.sdk.converters import binary, structured -from cloudevents.sdk.event import v1, v03 -from cloudevents.tests import data +from cloudevents_v1.sdk import marshaller +from cloudevents_v1.sdk.converters import binary, structured +from cloudevents_v1.sdk.event import v03, v1 +from cloudevents_v1.tests import data @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) diff --git a/cloudevents/tests/test_event_pipeline.py b/src/cloudevents/v1/tests/test_event_pipeline.py similarity index 94% rename from cloudevents/tests/test_event_pipeline.py rename to src/cloudevents/v1/tests/test_event_pipeline.py index efc79749..fdb547d5 100644 --- a/cloudevents/tests/test_event_pipeline.py +++ b/src/cloudevents/v1/tests/test_event_pipeline.py @@ -15,11 +15,10 @@ import json import pytest - -from cloudevents.sdk import converters, marshaller -from cloudevents.sdk.converters import structured -from cloudevents.sdk.event import v1, v03 -from cloudevents.tests import data +from cloudevents_v1.sdk import converters, marshaller +from cloudevents_v1.sdk.converters import structured +from cloudevents_v1.sdk.event import v03, v1 +from cloudevents_v1.tests import data @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) diff --git a/cloudevents/tests/test_event_to_request_converter.py b/src/cloudevents/v1/tests/test_event_to_request_converter.py similarity index 93% rename from cloudevents/tests/test_event_to_request_converter.py rename to src/cloudevents/v1/tests/test_event_to_request_converter.py index fd25be5a..c7fb7022 100644 --- a/cloudevents/tests/test_event_to_request_converter.py +++ b/src/cloudevents/v1/tests/test_event_to_request_converter.py @@ -15,10 +15,9 @@ import json import pytest - -from cloudevents.sdk import converters, marshaller -from cloudevents.sdk.event import v1, v03 -from cloudevents.tests import data +from cloudevents_v1.sdk import converters, marshaller +from cloudevents_v1.sdk.event import v03, v1 +from cloudevents_v1.tests import data @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) diff --git a/cloudevents/tests/test_http_cloudevent.py b/src/cloudevents/v1/tests/test_http_cloudevent.py similarity index 97% rename from cloudevents/tests/test_http_cloudevent.py rename to src/cloudevents/v1/tests/test_http_cloudevent.py index 6ad1537f..0c68c15c 100644 --- a/cloudevents/tests/test_http_cloudevent.py +++ b/src/cloudevents/v1/tests/test_http_cloudevent.py @@ -12,11 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import cloudevents_v1.exceptions as cloud_exceptions import pytest - -import cloudevents.exceptions as cloud_exceptions -from cloudevents.conversion import _json_or_string -from cloudevents.http import CloudEvent +from cloudevents_v1.conversion import _json_or_string +from cloudevents_v1.http import CloudEvent @pytest.fixture(params=["0.3", "1.0"]) diff --git a/cloudevents/tests/test_http_conversions.py b/src/cloudevents/v1/tests/test_http_conversions.py similarity index 96% rename from cloudevents/tests/test_http_conversions.py rename to src/cloudevents/v1/tests/test_http_conversions.py index 3b9c6717..db582a21 100644 --- a/cloudevents/tests/test_http_conversions.py +++ b/src/cloudevents/v1/tests/test_http_conversions.py @@ -17,10 +17,9 @@ import json import pytest - -from cloudevents.conversion import to_dict, to_json -from cloudevents.http import CloudEvent, from_dict, from_json -from cloudevents.sdk.event.attribute import SpecVersion +from cloudevents_v1.conversion import to_dict, to_json +from cloudevents_v1.http import CloudEvent, from_dict, from_json +from cloudevents_v1.sdk.event.attribute import SpecVersion test_data = json.dumps({"data-key": "val"}) test_attributes = { diff --git a/cloudevents/tests/test_http_events.py b/src/cloudevents/v1/tests/test_http_events.py similarity index 97% rename from cloudevents/tests/test_http_events.py rename to src/cloudevents/v1/tests/test_http_events.py index b21c3729..6956df2a 100644 --- a/cloudevents/tests/test_http_events.py +++ b/src/cloudevents/v1/tests/test_http_events.py @@ -17,17 +17,16 @@ import json import typing +import cloudevents_v1.exceptions as cloud_exceptions import pytest +from cloudevents_v1.http import CloudEvent, from_http, to_binary, to_structured +from cloudevents_v1.http.event_type import is_binary as deprecated_is_binary +from cloudevents_v1.http.event_type import is_structured as deprecated_is_structured +from cloudevents_v1.sdk import converters +from cloudevents_v1.sdk.converters.binary import is_binary +from cloudevents_v1.sdk.converters.structured import is_structured from sanic import Sanic, response -import cloudevents.exceptions as cloud_exceptions -from cloudevents.http import CloudEvent, from_http, to_binary, to_structured -from cloudevents.http.event_type import is_binary as deprecated_is_binary -from cloudevents.http.event_type import is_structured as deprecated_is_structured -from cloudevents.sdk import converters -from cloudevents.sdk.converters.binary import is_binary -from cloudevents.sdk.converters.structured import is_structured - invalid_test_headers = [ { "ce-source": "", diff --git a/cloudevents/tests/test_kafka_conversions.py b/src/cloudevents/v1/tests/test_kafka_conversions.py similarity index 98% rename from cloudevents/tests/test_kafka_conversions.py rename to src/cloudevents/v1/tests/test_kafka_conversions.py index 5580773a..661aebbc 100644 --- a/cloudevents/tests/test_kafka_conversions.py +++ b/src/cloudevents/v1/tests/test_kafka_conversions.py @@ -17,18 +17,17 @@ import json import pytest - -from cloudevents import exceptions as cloud_exceptions -from cloudevents.http import CloudEvent -from cloudevents.kafka.conversion import ( +from cloudevents_v1 import exceptions as cloud_exceptions +from cloudevents_v1.http import CloudEvent +from cloudevents_v1.kafka.conversion import ( KafkaMessage, from_binary, from_structured, to_binary, to_structured, ) -from cloudevents.kafka.exceptions import KeyMapperError -from cloudevents.sdk import types +from cloudevents_v1.kafka.exceptions import KeyMapperError +from cloudevents_v1.sdk import types def simple_serialize(data: dict) -> bytes: diff --git a/cloudevents/tests/test_marshaller.py b/src/cloudevents/v1/tests/test_marshaller.py similarity index 93% rename from cloudevents/tests/test_marshaller.py rename to src/cloudevents/v1/tests/test_marshaller.py index 90609891..d3ba81a7 100644 --- a/cloudevents/tests/test_marshaller.py +++ b/src/cloudevents/v1/tests/test_marshaller.py @@ -14,13 +14,12 @@ import json +import cloudevents_v1.exceptions as cloud_exceptions import pytest - -import cloudevents.exceptions as cloud_exceptions -from cloudevents.http import CloudEvent, from_http, to_binary, to_structured -from cloudevents.sdk import exceptions, marshaller -from cloudevents.sdk.converters import binary, structured -from cloudevents.sdk.event import v1 +from cloudevents_v1.http import CloudEvent, from_http, to_binary, to_structured +from cloudevents_v1.sdk import exceptions, marshaller +from cloudevents_v1.sdk.converters import binary, structured +from cloudevents_v1.sdk.event import v1 @pytest.fixture diff --git a/cloudevents/tests/test_options.py b/src/cloudevents/v1/tests/test_options.py similarity index 95% rename from cloudevents/tests/test_options.py rename to src/cloudevents/v1/tests/test_options.py index aba812b9..86b9ef4c 100644 --- a/cloudevents/tests/test_options.py +++ b/src/cloudevents/v1/tests/test_options.py @@ -13,8 +13,7 @@ # under the License. import pytest - -from cloudevents.sdk.event.opt import Option +from cloudevents_v1.sdk.event.opt import Option def test_set_raise_error(): diff --git a/cloudevents/tests/test_pydantic_cloudevent.py b/src/cloudevents/v1/tests/test_pydantic_cloudevent.py similarity index 97% rename from cloudevents/tests/test_pydantic_cloudevent.py rename to src/cloudevents/v1/tests/test_pydantic_cloudevent.py index 87ac5507..4a2762a5 100644 --- a/cloudevents/tests/test_pydantic_cloudevent.py +++ b/src/cloudevents/v1/tests/test_pydantic_cloudevent.py @@ -15,15 +15,14 @@ from json import loads import pytest +from cloudevents_v1.conversion import _json_or_string +from cloudevents_v1.exceptions import IncompatibleArgumentsError +from cloudevents_v1.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent +from cloudevents_v1.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent +from cloudevents_v1.sdk.event.attribute import SpecVersion from pydantic import ValidationError as PydanticV2ValidationError from pydantic.v1 import ValidationError as PydanticV1ValidationError -from cloudevents.conversion import _json_or_string -from cloudevents.exceptions import IncompatibleArgumentsError -from cloudevents.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent -from cloudevents.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent -from cloudevents.sdk.event.attribute import SpecVersion - _DUMMY_SOURCE = "dummy:source" _DUMMY_TYPE = "tests.cloudevents.override" _DUMMY_TIME = "2022-07-16T11:20:34.284130+00:00" diff --git a/cloudevents/tests/test_pydantic_conversions.py b/src/cloudevents/v1/tests/test_pydantic_conversions.py similarity index 90% rename from cloudevents/tests/test_pydantic_conversions.py rename to src/cloudevents/v1/tests/test_pydantic_conversions.py index 801b76bd..abf5cf6e 100644 --- a/cloudevents/tests/test_pydantic_conversions.py +++ b/src/cloudevents/v1/tests/test_pydantic_conversions.py @@ -17,18 +17,17 @@ import json import pytest +from cloudevents_v1.conversion import to_json +from cloudevents_v1.pydantic.v1.conversion import from_dict as pydantic_v1_from_dict +from cloudevents_v1.pydantic.v1.conversion import from_json as pydantic_v1_from_json +from cloudevents_v1.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent +from cloudevents_v1.pydantic.v2.conversion import from_dict as pydantic_v2_from_dict +from cloudevents_v1.pydantic.v2.conversion import from_json as pydantic_v2_from_json +from cloudevents_v1.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent +from cloudevents_v1.sdk.event.attribute import SpecVersion from pydantic import ValidationError as PydanticV2ValidationError from pydantic.v1 import ValidationError as PydanticV1ValidationError -from cloudevents.conversion import to_json -from cloudevents.pydantic.v1.conversion import from_dict as pydantic_v1_from_dict -from cloudevents.pydantic.v1.conversion import from_json as pydantic_v1_from_json -from cloudevents.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent -from cloudevents.pydantic.v2.conversion import from_dict as pydantic_v2_from_dict -from cloudevents.pydantic.v2.conversion import from_json as pydantic_v2_from_json -from cloudevents.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent -from cloudevents.sdk.event.attribute import SpecVersion - test_data = json.dumps({"data-key": "val"}) test_attributes = { "type": "com.example.string", diff --git a/cloudevents/tests/test_pydantic_events.py b/src/cloudevents/v1/tests/test_pydantic_events.py similarity index 96% rename from cloudevents/tests/test_pydantic_events.py rename to src/cloudevents/v1/tests/test_pydantic_events.py index 3e536f05..3b1921cf 100644 --- a/cloudevents/tests/test_pydantic_events.py +++ b/src/cloudevents/v1/tests/test_pydantic_events.py @@ -17,21 +17,20 @@ import json import typing +import cloudevents_v1.exceptions as cloud_exceptions import pytest +from cloudevents_v1.conversion import to_binary, to_structured +from cloudevents_v1.pydantic.v1.conversion import from_http as pydantic_v1_from_http +from cloudevents_v1.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent +from cloudevents_v1.pydantic.v2.conversion import from_http as pydantic_v2_from_http +from cloudevents_v1.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent +from cloudevents_v1.sdk import converters +from cloudevents_v1.sdk.converters.binary import is_binary +from cloudevents_v1.sdk.converters.structured import is_structured from pydantic import ValidationError as PydanticV2ValidationError from pydantic.v1 import ValidationError as PydanticV1ValidationError from sanic import Sanic, response -import cloudevents.exceptions as cloud_exceptions -from cloudevents.conversion import to_binary, to_structured -from cloudevents.pydantic.v1.conversion import from_http as pydantic_v1_from_http -from cloudevents.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent -from cloudevents.pydantic.v2.conversion import from_http as pydantic_v2_from_http -from cloudevents.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent -from cloudevents.sdk import converters -from cloudevents.sdk.converters.binary import is_binary -from cloudevents.sdk.converters.structured import is_structured - invalid_test_headers = [ { "ce-source": "", diff --git a/cloudevents/tests/test_v03_event.py b/src/cloudevents/v1/tests/test_v03_event.py similarity index 97% rename from cloudevents/tests/test_v03_event.py rename to src/cloudevents/v1/tests/test_v03_event.py index a4755318..a66ebe20 100644 --- a/cloudevents/tests/test_v03_event.py +++ b/src/cloudevents/v1/tests/test_v03_event.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.event import v03 +from cloudevents_v1.sdk.event import v03 def test_v03_time_property(): diff --git a/cloudevents/tests/test_v1_event.py b/src/cloudevents/v1/tests/test_v1_event.py similarity index 97% rename from cloudevents/tests/test_v1_event.py rename to src/cloudevents/v1/tests/test_v1_event.py index de900b0a..e72ecf47 100644 --- a/cloudevents/tests/test_v1_event.py +++ b/src/cloudevents/v1/tests/test_v1_event.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.event import v1 +from cloudevents_v1.sdk.event import v1 def test_v1_time_property(): diff --git a/cloudevents/tests/test_with_sanic.py b/src/cloudevents/v1/tests/test_with_sanic.py similarity index 93% rename from cloudevents/tests/test_with_sanic.py rename to src/cloudevents/v1/tests/test_with_sanic.py index 026f55b7..2f6d788e 100644 --- a/cloudevents/tests/test_with_sanic.py +++ b/src/cloudevents/v1/tests/test_with_sanic.py @@ -12,12 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. +from cloudevents_v1.sdk import converters, marshaller +from cloudevents_v1.sdk.event import v1 +from cloudevents_v1.tests import data as test_data from sanic import Sanic, response -from cloudevents.sdk import converters, marshaller -from cloudevents.sdk.event import v1 -from cloudevents.tests import data as test_data - m = marshaller.NewDefaultHTTPMarshaller() app = Sanic("test_with_sanic") diff --git a/tests/test_cloudevents/__init__.py b/tests/test_cloudevents/__init__.py new file mode 100644 index 00000000..8043675e --- /dev/null +++ b/tests/test_cloudevents/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/tests/test_cloudevents/test_cloudevents_version.py b/tests/test_cloudevents/test_cloudevents_version.py new file mode 100644 index 00000000..d895c5f5 --- /dev/null +++ b/tests/test_cloudevents/test_cloudevents_version.py @@ -0,0 +1,19 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cloudevents import __version__ + + +def test_cloudevents_version() -> None: + assert __version__ is not None diff --git a/tests/test_core/__init__.py b/tests/test_core/__init__.py new file mode 100644 index 00000000..8043675e --- /dev/null +++ b/tests/test_core/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/tests/test_core/test_v1/__init__.py b/tests/test_core/test_v1/__init__.py new file mode 100644 index 00000000..8043675e --- /dev/null +++ b/tests/test_core/test_v1/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/tests/test_core/test_v1/test_event.py b/tests/test_core/test_v1/test_event.py new file mode 100644 index 00000000..acd3fd2b --- /dev/null +++ b/tests/test_core/test_v1/test_event.py @@ -0,0 +1,333 @@ +# Copyright 2018-Present The CloudEvents Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from datetime import datetime, timezone +from typing import Any + +import pytest + +from cloudevents.core.v1.event import CloudEvent +from cloudevents.core.v1.exceptions import ( + CloudEventValidationError, + CustomExtensionAttributeError, + InvalidAttributeTypeError, + InvalidAttributeValueError, + MissingRequiredAttributeError, +) + + +def test_missing_required_attributes() -> None: + with pytest.raises(CloudEventValidationError) as e: + CloudEvent({}) + + expected_errors = { + "id": [ + str(MissingRequiredAttributeError("id")), + str(InvalidAttributeValueError("id", "Attribute 'id' must not be None")), + str(InvalidAttributeTypeError("id", str)), + ], + "source": [ + str(MissingRequiredAttributeError("source")), + str(InvalidAttributeTypeError("source", str)), + ], + "type": [ + str(MissingRequiredAttributeError("type")), + str(InvalidAttributeTypeError("type", str)), + ], + "specversion": [ + str(MissingRequiredAttributeError("specversion")), + str(InvalidAttributeTypeError("specversion", str)), + str( + InvalidAttributeValueError( + "specversion", "Attribute 'specversion' must be '1.0'" + ) + ), + ], + } + + actual_errors = { + key: [str(e) for e in value] for key, value in e.value.errors.items() + } + assert actual_errors == expected_errors + + +@pytest.mark.parametrize( + "time,expected_error", + [ + ( + "2023-10-25T17:09:19.736166Z", + {"time": [str(InvalidAttributeTypeError("time", datetime))]}, + ), + ( + datetime(2023, 10, 25, 17, 9, 19, 736166), + { + "time": [ + str( + InvalidAttributeValueError( + "time", "Attribute 'time' must be timezone aware" + ) + ) + ] + }, + ), + ( + 1, + {"time": [str(InvalidAttributeTypeError("time", datetime))]}, + ), + ], +) +def test_time_validation(time: Any, expected_error: dict) -> None: + with pytest.raises(CloudEventValidationError) as e: + CloudEvent( + { + "id": "1", + "source": "/", + "type": "test", + "specversion": "1.0", + "time": time, + } + ) + actual_errors = { + key: [str(e) for e in value] for key, value in e.value.errors.items() + } + assert actual_errors == expected_error + + +@pytest.mark.parametrize( + "subject,expected_error", + [ + ( + 1234, + {"subject": [str(InvalidAttributeTypeError("subject", str))]}, + ), + ( + "", + { + "subject": [ + str( + InvalidAttributeValueError( + "subject", "Attribute 'subject' must not be empty" + ) + ) + ] + }, + ), + ], +) +def test_subject_validation(subject: Any, expected_error: dict) -> None: + with pytest.raises(CloudEventValidationError) as e: + CloudEvent( + { + "id": "1", + "source": "/", + "type": "test", + "specversion": "1.0", + "subject": subject, + } + ) + + actual_errors = { + key: [str(e) for e in value] for key, value in e.value.errors.items() + } + assert actual_errors == expected_error + + +@pytest.mark.parametrize( + "datacontenttype,expected_error", + [ + ( + 1234, + { + "datacontenttype": [ + str(InvalidAttributeTypeError("datacontenttype", str)) + ] + }, + ), + ( + "", + { + "datacontenttype": [ + str( + InvalidAttributeValueError( + "datacontenttype", + "Attribute 'datacontenttype' must not be empty", + ) + ) + ] + }, + ), + ], +) +def test_datacontenttype_validation(datacontenttype: Any, expected_error: dict) -> None: + with pytest.raises(CloudEventValidationError) as e: + CloudEvent( + { + "id": "1", + "source": "/", + "type": "test", + "specversion": "1.0", + "datacontenttype": datacontenttype, + } + ) + + actual_errors = { + key: [str(e) for e in value] for key, value in e.value.errors.items() + } + assert actual_errors == expected_error + + +@pytest.mark.parametrize( + "dataschema,expected_error", + [ + ( + 1234, + {"dataschema": [str(InvalidAttributeTypeError("dataschema", str))]}, + ), + ( + "", + { + "dataschema": [ + str( + InvalidAttributeValueError( + "dataschema", "Attribute 'dataschema' must not be empty" + ) + ) + ] + }, + ), + ], +) +def test_dataschema_validation(dataschema: Any, expected_error: dict) -> None: + with pytest.raises(CloudEventValidationError) as e: + CloudEvent( + { + "id": "1", + "source": "/", + "type": "test", + "specversion": "1.0", + "dataschema": dataschema, + } + ) + + actual_errors = { + key: [str(e) for e in value] for key, value in e.value.errors.items() + } + assert actual_errors == expected_error + + +@pytest.mark.parametrize( + "extension_name,expected_error", + [ + ( + "", + { + "": [ + str( + CustomExtensionAttributeError( + "", + "Extension attribute '' should be between 1 and 20 characters long", + ) + ), + str( + CustomExtensionAttributeError( + "", + "Extension attribute '' should only contain lowercase letters and numbers", + ) + ), + ] + }, + ), + ( + "thisisaverylongextension", + { + "thisisaverylongextension": [ + str( + CustomExtensionAttributeError( + "thisisaverylongextension", + "Extension attribute 'thisisaverylongextension' should be between 1 and 20 characters long", + ) + ) + ] + }, + ), + ( + "data", + { + "data": [ + str( + CustomExtensionAttributeError( + "data", + "Extension attribute 'data' is reserved and must not be used", + ) + ) + ] + }, + ), + ], +) +def test_custom_extension(extension_name: str, expected_error: dict) -> None: + with pytest.raises(CloudEventValidationError) as e: + CloudEvent( + { + "id": "1", + "source": "/", + "type": "test", + "specversion": "1.0", + extension_name: "value", + } + ) + + actual_errors = { + key: [str(e) for e in value] for key, value in e.value.errors.items() + } + assert actual_errors == expected_error + + +def test_cloud_event_constructor() -> None: + id = "1" + source = "/source" + type = "com.test.type" + specversion = "1.0" + datacontenttype = "application/json" + dataschema = "http://example.com/schema" + subject = "test_subject" + time = datetime.now(tz=timezone.utc) + data = {"key": "value"} + customextension = "customExtension" + + event = CloudEvent( + attributes={ + "id": id, + "source": source, + "type": type, + "specversion": specversion, + "datacontenttype": datacontenttype, + "dataschema": dataschema, + "subject": subject, + "time": time, + "customextension": customextension, + }, + data=data, + ) + + assert event.get_id() == id + assert event.get_source() == source + assert event.get_type() == type + assert event.get_specversion() == specversion + assert event.get_datacontenttype() == datacontenttype + assert event.get_dataschema() == dataschema + assert event.get_subject() == subject + assert event.get_time() == time + assert event.get_extension("customextension") == customextension + assert event.get_data() == data diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..ae08d830 --- /dev/null +++ b/uv.lock @@ -0,0 +1,484 @@ +version = 1 +requires-python = ">=3.9" + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "cloudevents" +version = "2.0.0a1" +source = { editable = "." } +dependencies = [ + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "flake8" }, + { name = "flake8-print" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pep8-naming" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [{ name = "ruff", specifier = ">=0.6.8" }] + +[package.metadata.requires-dev] +dev = [ + { name = "flake8", specifier = ">=7.1.1" }, + { name = "flake8-print", specifier = ">=5.0.0" }, + { name = "isort", specifier = ">=5.13.2" }, + { name = "mypy", specifier = ">=1.11.2" }, + { name = "pep8-naming", specifier = ">=0.14.1" }, + { name = "pre-commit", specifier = ">=3.8.0" }, + { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "flake8" +version = "7.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/72/e8d66150c4fcace3c0a450466aa3480506ba2cae7b61e100a2613afc3907/flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", size = 48054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/42/65004373ac4617464f35ed15931b30d764f53cdd30cc78d5aea349c8c050/flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213", size = 57731 }, +] + +[[package]] +name = "flake8-print" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, + { name = "pycodestyle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/a6/770c5832a6b563e023def7d81925d1b9f3079ebc805e48be0a5ee206f716/flake8-print-5.0.0.tar.gz", hash = "sha256:76915a2a389cc1c0879636c219eb909c38501d3a43cc8dae542081c9ba48bdf9", size = 5166 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/2c/aa2ffda404b5d9c89dad8bcc4e0f4af673ab2de67e96997d13f04ad68b5b/flake8_print-5.0.0-py3-none-any.whl", hash = "sha256:84a1a6ea10d7056b804221ac5e62b1cee1aefc897ce16f2e5c42d3046068f5d8", size = 5687 }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "isort" +version = "5.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, +] + +[[package]] +name = "mypy" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401 }, + { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697 }, + { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508 }, + { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712 }, + { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319 }, + { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630 }, + { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973 }, + { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659 }, + { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010 }, + { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873 }, + { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335 }, + { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119 }, + { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856 }, + { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066 }, + { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000 }, + { url = "https://files.pythonhosted.org/packages/16/64/bb5ed751487e2bea0dfaa6f640a7e3bb88083648f522e766d5ef4a76f578/mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", size = 10937294 }, + { url = "https://files.pythonhosted.org/packages/a9/a3/67a0069abed93c3bf3b0bebb8857e2979a02828a4a3fd82f107f8f1143e8/mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", size = 10107707 }, + { url = "https://files.pythonhosted.org/packages/2f/4d/0379daf4258b454b1f9ed589a9dabd072c17f97496daea7b72fdacf7c248/mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d", size = 12498367 }, + { url = "https://files.pythonhosted.org/packages/3b/dc/3976a988c280b3571b8eb6928882dc4b723a403b21735a6d8ae6ed20e82b/mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", size = 13018014 }, + { url = "https://files.pythonhosted.org/packages/83/84/adffc7138fb970e7e2a167bd20b33bb78958370179853a4ebe9008139342/mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", size = 9568056 }, + { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pep8-naming" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flake8" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/8e/1de32e908d8b008bb9352bfe7749aedecb71e2793d36c7ee342716acd1ec/pep8-naming-0.14.1.tar.gz", hash = "sha256:1ef228ae80875557eb6c1549deafed4dabbf3261cfcafa12f773fe0db9be8a36", size = 16546 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/a2/450b71d1a87fcee50a7b994a53b1c68fc6a6b718df0eb035f2bffb2d3a4f/pep8_naming-0.14.1-py3-none-any.whl", hash = "sha256:63f514fc777d715f935faf185dedd679ab99526a7f2f503abb61587877f7b1c5", size = 8859 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643 }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284 }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + +[[package]] +name = "ruff" +version = "0.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/f9/4ce3e765a72ab8fe0f80f48508ea38b4196daab3da14d803c21349b2d367/ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18", size = 3084543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/07/42ee57e8b76ca585297a663a552b4f6d6a99372ca47fdc2276ef72cc0f2f/ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2", size = 10404327 }, + { url = "https://files.pythonhosted.org/packages/eb/51/d42571ff8156d65086acb72d39aa64cb24181db53b497d0ed6293f43f07a/ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c", size = 10018797 }, + { url = "https://files.pythonhosted.org/packages/c1/d7/fa5514a60b03976af972b67fe345deb0335dc96b9f9a9fa4df9890472427/ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5", size = 9691303 }, + { url = "https://files.pythonhosted.org/packages/d6/c4/d812a74976927e51d0782a47539069657ac78535779bfa4d061c4fc8d89d/ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f", size = 10719452 }, + { url = "https://files.pythonhosted.org/packages/ec/b6/aa700c4ae6db9b3ee660e23f3c7db596e2b16a3034b797704fba33ddbc96/ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb", size = 10161353 }, + { url = "https://files.pythonhosted.org/packages/ea/39/0b10075ffcd52ff3a581b9b69eac53579deb230aad300ce8f9d0b58e77bc/ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f", size = 10980630 }, + { url = "https://files.pythonhosted.org/packages/c1/af/9eb9efc98334f62652e2f9318f137b2667187851911fac3b395365a83708/ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0", size = 11768996 }, + { url = "https://files.pythonhosted.org/packages/e0/59/8b1369cf7878358952b1c0a1559b4d6b5c824c003d09b0db26d26c9d094f/ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87", size = 11317469 }, + { url = "https://files.pythonhosted.org/packages/b9/6d/e252e9b11bbca4114c386ee41ad559d0dac13246201d77ea1223c6fea17f/ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098", size = 12467185 }, + { url = "https://files.pythonhosted.org/packages/48/44/7caa223af7d4ea0f0b2bd34acca65a7694a58317714675a2478815ab3f45/ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0", size = 10887766 }, + { url = "https://files.pythonhosted.org/packages/81/ed/394aff3a785f171869158b9d5be61eec9ffb823c3ad5d2bdf2e5f13cb029/ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750", size = 10711609 }, + { url = "https://files.pythonhosted.org/packages/47/31/f31d04c842e54699eab7e3b864538fea26e6c94b71806cd10aa49f13e1c1/ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce", size = 10237621 }, + { url = "https://files.pythonhosted.org/packages/20/95/a764e84acf11d425f2f23b8b78b4fd715e9c20be4aac157c6414ca859a67/ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa", size = 10558329 }, + { url = "https://files.pythonhosted.org/packages/2a/76/d4e38846ac9f6dd62dce858a54583911361b5339dcf8f84419241efac93a/ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44", size = 10954102 }, + { url = "https://files.pythonhosted.org/packages/e7/36/f18c678da6c69f8d022480f3e8ddce6e4a52e07602c1d212056fbd234f8f/ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a", size = 8511090 }, + { url = "https://files.pythonhosted.org/packages/4c/c4/0ca7d8ffa358b109db7d7d045a1a076fd8e5d9cbeae022242d3c060931da/ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263", size = 9350079 }, + { url = "https://files.pythonhosted.org/packages/d9/bd/a8b0c64945a92eaeeb8d0283f27a726a776a1c9d12734d990c5fc7a1278c/ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", size = 8669595 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, +]