diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index eae9983..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,11 +0,0 @@
-# https://editorconfig.org/
-
-root = true
-
-[*]
-# Config for all files
-
-insert_final_newline = true
-trim_trailing_whitespace = true
-end_of_line = lf
-charset = utf-8
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 4d80189..0000000
--- a/.flake8
+++ /dev/null
@@ -1,29 +0,0 @@
-# Copyright 2022 J.P. Morgan Chase & Co.
-#
-# 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.
-
-
-[flake8]
-# Flake 8 linting configuration, not supported in pyproject.toml
-
-ignore = E203,W503,E701
-max-line-length = 120
-exclude =
- .svn,
- CVS,
- .bzr,
- .hg,
- .git,
- __pycache__,
- .tox
- .venv/
- venv/
- .eggs/
- build/
diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml
index 9fd300b..5ab240d 100644
--- a/.github/workflows/release-package.yml
+++ b/.github/workflows/release-package.yml
@@ -9,32 +9,42 @@
# "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 workflow will upload a Python Package using Twine when a release is created
-# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Upload Python Package to PyPI
on:
- release:
- types:
- - published
+ push:
+ tags:
+ - 'v*'
+ branches:
+ - main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - name: Checkout
+ uses: actions/checkout@v4
+
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: '3.x'
+ python-version: '3.13'
+
+ - name: Install UV
+ uses: astral-sh/setup-uv@v7
+
- name: Install dependencies
run: |
- python -m pip install --upgrade pip
- python -m pip install build
+ make install
+
- name: Build package
- run: python -m build
+ run: |
+ make build
+
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index 24cf614..18eabc8 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -12,10 +12,12 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-name: Test Python package with Tox
+name: LocalStack py-avro-schema / Run Tests
on:
push:
+ branches:
+ - main
pull_request:
branches:
- main
@@ -23,23 +25,25 @@ on:
jobs:
build:
runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- python-version:
- - '3.11'
- - '3.12'
- - '3.13'
steps:
- uses: actions/checkout@v4
- - name: Set up Python ${{ matrix.python-version }}
+ - name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: ${{ matrix.python-version }}
+ python-version: "3.13"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+
- name: Install dependencies
run: |
- python -m pip install --upgrade pip
- python -m pip install tox tox-gh-actions
- - name: Run Tox commands
- run: python -m tox
+ make install-dev
+
+ - name: Linting
+ run: |
+ make lint
+
+ - name: Execute tests
+ run: |
+ make test
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
deleted file mode 100644
index a37b428..0000000
--- a/.pre-commit-config.yaml
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright 2022 J.P. Morgan Chase & Co.
-#
-# 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.
-
-# pre-commit Git hook script configuration
-#
-# See: https://pre-commit.com/
-
-repos:
- - repo: https://github.com/psf/black
- rev: 24.8.0
- hooks:
- - # Format code
- id: black
- - repo: https://github.com/PyCQA/flake8
- rev: 7.1.1
- hooks:
- - # Apply linting rules
- id: flake8
- - repo: https://github.com/econchick/interrogate
- rev: 1.7.0
- hooks:
- - # Enforce documentation
- id: interrogate
- exclude: ^tests
- - repo: https://github.com/PyCQA/isort
- rev: 5.13.2
- hooks:
- - # Sort imports
- id: isort
- - repo: https://github.com/Lucas-C/pre-commit-hooks
- rev: v1.5.5
- hooks:
- - # Insert an OSS license header to all relevant files
- id: insert-license
- types_or:
- - ini
- - python
- - toml
- - yaml
- args:
- - --license-filepath
- - LICENSE_HEADER.txt
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
deleted file mode 100644
index 36258ab..0000000
--- a/.readthedocs.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-# Read the Docs configuration file
-# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
-
-version: 2
-
-build:
- os: ubuntu-22.04
- tools:
- python: "3.10"
-
-sphinx:
- configuration: docs/conf.py
- fail_on_warning: true # https://docs.readthedocs.io/en/stable/tutorial/index.html#making-warnings-more-visible
-
-python:
- install:
- - method: pip
- path: .
- extra_requirements:
- - docs
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4c06e2a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,34 @@
+VENV_DIR ?= .venv
+VENV_RUN = . $(VENV_DIR)/bin/activate
+VENV_ACTIVATE = $(VENV_DIR)/bin/activate
+TEST_PATH ?= .
+TEST_EXEC ?= python -m
+PYTEST_LOGLEVEL ?= warning
+
+install:
+ uv venv --clear
+ uv sync --no-dev
+
+install-dev:
+ uv venv --clear
+ uv sync --group testing --group linting
+
+clean:
+ rm -rf $(VENV_DIR)
+
+clean-dist:
+ rm -rf dist/
+
+build:
+ uv build
+
+format:
+ ($(VENV_RUN); python -m ruff format; python -m ruff check --output-format=full --fix .)
+
+lint:
+ ($(VENV_RUN); python -m ruff check --output-format=full . && python -m ruff format --check .)
+
+test:
+ ($(VENV_RUN); $(TEST_EXEC) pytest --durations=10 --log-cli-level=$(PYTEST_LOGLEVEL) $(PYTEST_ARGS) $(TEST_PATH))
+
+.PHONY: clean install install-dev clean clean-dist build publish format lint test
diff --git a/README.md b/README.md
index 1dd350a..213509d 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,30 @@
+# localstack py-avro-schema
+
+This project is a fork of [py-avro-schema](https://github.com/jpmorganchase/py-avro-schema) (see the original README
+below).
+
+## Developing
+
+To setup a virtual environment under `.venv/`, first install `uv`, then run:
+
+```shell
+make install-dev
+```
+
+## Release a new version
+
+To release a new version you can create a tag with the following commands:
+
+```shell
+git tag v4.0.0
+git push origin v4.0.0
+```
+
+This will create a git tag and push it to the remote repository.
+This will trigger the build workflow and publish the package to PyPI.
+
+---
+
# py-avro-schema
Generate [Apache Avro][] schemas for Python types including standard library [data-classes][] and [Pydantic data models][].
diff --git a/docs/conf.py b/docs/conf.py
deleted file mode 100644
index 903d373..0000000
--- a/docs/conf.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2022 J.P. Morgan Chase & Co.
-#
-# 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.
-
-"""
-Configuration file for the Sphinx documentation builder.
-
-This file only contains a selection of the most common options. For a full
-list see the documentation:
-https://www.sphinx-doc.org/en/master/usage/configuration.html
-"""
-
-# -- Project information -----------------------------------------------------
-
-project = "py-avro-schema"
-copyright = "2022, J.P. Morgan Chase & Co."
-author = "J.P. Morgan Chase & Co."
-
-
-# -- General configuration ---------------------------------------------------
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = [
- "sphinx.ext.autodoc",
- "sphinx.ext.autosectionlabel",
- "sphinx.ext.intersphinx",
- "sphinx.ext.napoleon",
- "sphinx.ext.viewcode",
- "sphinx_rtd_theme",
-]
-
-html_theme = "sphinx_rtd_theme"
-
-add_module_names = False
-
-autosectionlabel_prefix_document = True
-
-intersphinx_mapping = {
- "python": ("https://docs.python.org/3/", None),
-}
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index 81af23a..0000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-..
- Copyright 2022 J.P. Morgan Chase & Co.
-
- 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.
-
-
-py-avro-schema
-==============
-
-Generate Apache Avro schemas for Python types including standard library data-classes and Pydantic data models.
-
-.. toctree::
- :maxdepth: 1
- :caption: Contents
-
- tutorial
- types
- modules
-
-
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs/modules.rst b/docs/modules.rst
deleted file mode 100644
index 6f1d51f..0000000
--- a/docs/modules.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-API reference documentation
-===========================
-
-.. toctree::
- :maxdepth: 1
-
- py_avro_schema
diff --git a/docs/py_avro_schema.rst b/docs/py_avro_schema.rst
deleted file mode 100644
index 7b9c5f3..0000000
--- a/docs/py_avro_schema.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-py\_avro\_schema
-================
-
-.. automodule:: py_avro_schema
- :members:
- :show-inheritance:
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
deleted file mode 100644
index 4c85860..0000000
--- a/docs/tutorial.rst
+++ /dev/null
@@ -1,120 +0,0 @@
-..
- Copyright 2022 J.P. Morgan Chase & Co.
-
- 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.
-
-
-Tutorial
-========
-
-This tutorial shows you how to use **py-avro-schema**, step by step.
-
-
-Define data types and structures
---------------------------------
-
-An example data structure could be defined like this in Python::
-
- # File shipping/models.py
-
- import dataclasses
-
-
- @dataclasses.dataclass
- class Ship:
- """A beautiful ship"""
-
- name: str
- year_launched: int
-
-This defines a single type ``Ship`` with 2 fields: ``name`` (some text) and ``year_launched`` (a number).
-
-The type hints are essential and used by **py-avro-schema** to generate the Avro schema!
-
-
-Generating the Avro schema
---------------------------
-
-To represent this as a data type, we run the following commands (here we use an interactive Python shell):
-
->>> import py_avro_schema as pas
->>> import shipping.models
->>> pas.generate(shipping.models.Ship)
-b'{"type":"record","name":"Ship","fields":[{"name":"name","type":"string"},{"name":"year_launched","type":"long"}],"namespace":"shipping","doc":"A beautiful ship"}'
-
-The output is the Avro schema as a (binary) JSON string.
-
-If we wanted to, we could format the JSON string a bit nicer:
-
->>> raw_json = pas.generate(Ship, options=pas.Option.JSON_INDENT_2)
->>> print(raw_json.decode())
-{
- "type": "record",
- "name": "Ship",
- "fields": [
- {
- "name": "name",
- "type": "string"
- },
- {
- "name": "year_launched",
- "type": "long"
- }
- ],
- "namespace": "shipping",
- "doc": "A beautiful ship"
-}
-
-This human-friendly representation is useful for debugging for example.
-
-
-Controlling the schema namespace
---------------------------------
-
-Avro named types such as a ``Record`` optionally define a "namespace" to qualify their name.
-
-
-Package name
-~~~~~~~~~~~~
-
-By default, **py-avro-schema** populates the namespace with the Python *package* name within which the Python type is defined.
-For example, if the type ``Ship`` is defined in module ``shipping.models``, the namespace will be ``shipping``.
-
-A good pattern is to define (or import-as) the types into a package's ``__init__.py`` module such that the types are importable using the Avro schema namespace exactly.
-For example::
-
- # File shipping/__init__.py
-
- from shipping.models import Ship
-
- __all__ = ["Ship"]
-
-This can be really useful for deserializing Avro data into Python objects.
-
-
-Module name
-~~~~~~~~~~~
-
-Alternatively, to use the full dotted module name (``shipping.models`` in the above example) instead of the top-level package name use the option :attr:`py_avro_schema.Option.AUTO_NAMESPACE_MODULE`.
-
-
-Manual
-~~~~~~
-
-A custom namespace can be specified like this:
-
->>> pas.generate(shipping.models.Ship, namespace="com.shipping.schemas")
-b'{"type":"record","name":"Ship","fields":[...],"namespace":"com.shipping.schemas", ...}'
-
-
-No namespace
-~~~~~~~~~~~~
-
-To *disable* automatic namespace population altogether, use this:
-
->>> pas.generate(Ship, options=pas.Option.NO_AUTO_NAMESPACE)
-b'{"type":"record","name":"Ship","fields":[...],"doc":"A beautiful ship"}'
diff --git a/docs/types.rst b/docs/types.rst
deleted file mode 100644
index 65b8787..0000000
--- a/docs/types.rst
+++ /dev/null
@@ -1,503 +0,0 @@
-..
- Copyright 2022 J.P. Morgan Chase & Co.
-
- 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.
-
-
-Supported data types
-====================
-
-.. seealso::
-
- Official Avro schema specification: https://avro.apache.org/docs/current/spec.html#schemas
-
-
-**py-avro-schema** supports the following Python types:
-
-
-Compound types/structures
--------------------------
-
-
-:func:`dataclasses.dataclass`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Supports Python classes decorated with :func:`dataclasses.dataclass`.
-
-Avro schema: ``record``
-
-The Avro ``record`` type is a named schema.
-**py-avro-schema** uses the Python class name as the schema name.
-
-Dataclass fields with types supported by **py-avro-schema** are output as expected, including population of default values.
-
-Example::
-
- # File shipping/models.py
-
- import dataclasses
- from typing import Optional
-
- @dataclasses.dataclass
- class Ship:
- """A beautiful ship"""
-
- name: str
- year_launched: Optional[int] = None
-
-Is output as:
-
-.. code-block:: json
-
- {
- "type": "record",
- "name": "Ship",
- "namespace": "shipping",
- "doc": "A beautiful ship",
- "fields": [
- {
- "name": "name",
- "type": "string"
- },
- {
- "name": "year_launched",
- "type": ["null", "long"],
- "default": null
- }
- ],
- }
-
-Field default values may improve Avro schema evolution and resolution.
-To validate that all dataclass fields are specified with a default value, use option :attr:`py_avro_schema.Option.DEFAULTS_MANDATORY`.
-
-The Avro record schema's ``doc`` field is populated from the Python class's docstring.
-To *disable* this, pass the option :attr:`py_avro_schema.Option.NO_DOC`.
-
-Recursive or repeated reference to the same Python dataclass is supported. After the first time the schema is output, any subsequent references are by name only.
-
-
-:class:`pydantic.BaseModel`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Supports Python classes inheriting from `pydantic.BaseModel `_.
-Requires Pydantic version 2 or greater. For Pydantic 1 support, use **py-avro-schema** version 2.
-
-.. (No intersphinx for Pydantic, unfortunately.)
-
-Avro schema: ``record``
-
-The Avro ``record`` type is a named schema.
-**py-avro-schema** uses the Python class name as the schema name.
-
-Pydantic model fields with types supported by **py-avro-schema** are output as expected, including population of default values and descriptions.
-
-Example::
-
- # File shipping/models.py
-
- import pydantic
- from typing import Optional
-
- class Ship(pydantic.BaseModel):
- """A beautiful ship"""
-
- name: str
- year_launched: Optional[int] = pydantic.Field(None, description="When we hit the water")
-
-Is output as:
-
-.. code-block:: json
-
- {
- "type": "record",
- "name": "Ship",
- "namespace": "shipping",
- "doc": "A beautiful ship",
- "fields": [
- {
- "name": "name",
- "type": "string"
- },
- {
- "name": "year_launched",
- "type": ["null", "long"],
- "default": null,
- "doc": "When we hit the water"
- }
- ],
- }
-
-Field default values may improve Avro schema evolution and resolution.
-To validate that all model fields are specified with a default value, use option :attr:`py_avro_schema.Option.DEFAULTS_MANDATORY`.
-
-The Avro record schema's ``doc`` attribute is populated from the Python class's docstring.
-For individual model fields, the ``doc`` attribute is taken from the Pydantic field's :attr:`description` attribute.
-To *disable* this, pass the option :attr:`py_avro_schema.Option.NO_DOC`.
-
-Recursive or repeated reference to the same Pydantic class is supported. After the first time the schema is output, any subsequent references are by name only.
-
-.. warning::
- When using a hierarchy of Pydantic model classes, recursive type references are supported in the *final class only* and not in any inherited/base class.
-
-
-Plain Python classes
-~~~~~~~~~~~~~~~~~~~~
-
-Supports Python classes with a :meth:`__init__` where all arguments have type hints and fully define all schema fields.
-
-Avro schema: ``record``
-
-The Avro ``record`` type is a named schema.
-**py-avro-schema** uses the Python class name as the schema name.
-
-Example::
-
- class Port:
- """A port you can sail to"""
-
- def __init__(self, name: str, country: str = "NLD"):
- self.name = name
- self.country = country.upper()
-
-Is output as:
-
-.. code-block:: json
-
- {
- "type": "record",
- "name": "Port",
- "namespace": "shipping",
- "doc": "A port you can sail to",
- "fields": [
- {
- "name": "name",
- "type": "string"
- },
- {
- "name": "country",
- "type": "string",
- "default": "NLD"
- }
- ]
- }
-
-
-:class:`typing.Union` and :class:`types.UnionType` (``X | Y``)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: JSON array of multiple Avro schemas
-
-Union members can be any other type supported by **py-avro-schema**.
-
-When defined as a class field with a **default** value, the union members may be re-ordered to ensure that the first member matches the type of the default value.
-
-
-Forward references
-~~~~~~~~~~~~~~~~~~
-
-Avro schema: any named schema
-
-**py-avro-schema** generally supports "forward" or recursive references, for example when a class attribute has the same
-type as a the class itself.
-
-Example::
-
- @dataclasses.dataclass
- class PyType:
- field_a: "PyType"
-
-Is output as:
-
-.. code-block:: json
-
- {
- "type": "record",
- "name": "PyType",
- "fields": [
- {
- "name": "field_a",
- "type": "PyType",
- },
- ],
- }
-
-.. warning::
- When using a hierarchy of **Pydantic** model classes, recursive type references are supported in the *final class only* and not in any inherited/base class.
-
-
-Collections
------------
-
-
-:class:`typing.Dict[str, typing.Any]`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. seealso::
-
- For a "normal" Avro ``map`` schema using fully typed Python dictionaries, see :ref:`types::class:`typing.mapping``.
-
-
-| Avro schema: ``bytes``
-| Avro logical type: ``json``
-
-Arbitrary Python dictionaries could be serialized as a ``bytes`` Avro schema by first serializing the data as JSON.
-**py-avro-schema** supports this "JSON-in-Avro" approach by adding the **custom** logical type ``json`` to a ``bytes`` schema.
-
-To support JSON serialization as *strings* instead of *bytes*, use :attr:`py_avro_schema.Option.LOGICAL_JSON_STRING`.
-
-
-:class:`typing.List[typing.Dict[str, typing.Any]]`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. seealso::
-
- For a "normal" Avro ``array`` schema using fully typed Python lists of dictionaries, see :ref:`types::class:`typing.sequence``.
-
-
-| Avro schema: ``bytes``
-| Avro logical type: ``json``
-
-Arbitrary lists of Python dictionaries could be serialized as a ``bytes`` Avro schema by first serializing the data as JSON.
-**py-avro-schema** supports this "JSON-in-Avro" approach by adding the **custom** logical type ``json`` to a ``bytes`` schema.
-
-To support JSON serialization as *strings* instead of *bytes*, use :attr:`py_avro_schema.Option.LOGICAL_JSON_STRING`.
-
-
-:class:`typing.Mapping`
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``map``
-
-This supports other "generic type" versions of :class:`collections.abc.Mapping`, including :class:`typing.Dict`.
-
-Avro ``map`` schemas support **string** keys only. Map values can be any other Python type supported by **py-avro-schema**.
-For example, ``Dict[str, int]`` is output as:
-
-.. code-block:: json
-
- {
- "type": "map",
- "values": "long"
- }
-
-
-:class:`typing.Sequence`
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``array``
-
-This supports other "generic type" versions of :class:`collections.abc.Sequence`, including :class:`typing.List`.
-
-Sequence values can be any Python type supported by **py-avro-schema**. For example, ``List[int]`` is output as:
-
-.. code-block:: json
-
- {
- "type": "array",
- "values": "long"
- }
-
-
-Simple types
-------------
-
-
-:class:`bool` (and subclasses)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``boolean``
-
-
-:class:`bytes` (and subclasses)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``bytes``
-
-
-:class:`datetime.date`
-~~~~~~~~~~~~~~~~~~~~~~
-
-| Avro schema: ``int``
-| Avro logical type: ``date``
-
-
-:class:`datetime.datetime`
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-| Avro schema: ``long``
-| Avro logical type: ``timestamp-micros``
-
-To output with millisecond precision instead (logical type ``timestamp-millis``), use :attr:`py_avro_schema.Option.MILLISECONDS`.
-
-
-:class:`datetime.time`
-~~~~~~~~~~~~~~~~~~~~~~
-
-| Avro schema: ``long``
-| Avro logical type: ``time-micros``
-
-To output with millisecond precision instead (logical type ``time-millis``), use :attr:`py_avro_schema.Option.MILLISECONDS`.
-In that case, the Avro schema is ``int``.
-
-
-:class:`datetime.timedelta`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-| Avro schema: ``fixed``
-| Avro logical type: ``duration``
-
-The Avro ``fixed`` type is a named schema.
-Here, **py-avro-schema** uses the name ``datetime.timedelta``.
-The full generated schema looks like this:
-
-.. code-block:: json
-
- {
- "type": "fixed",
- "name": "datetime.timedelta",
- "size": 12,
- "logicalType": "duration"
- }
-
-
-:class:`enum.Enum`
-~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``enum``
-
-The Avro ``enum`` type is a named schema.
-**py-avro-schema** uses the Python class name as the schema name.
-Avro enum symbols must be strings.
-
-Example::
-
- # File shipping/models.py
-
- import enum
-
- class ShipType(enum.Enum):
- SAILING_VESSEL = "SAILING_VESSEL"
- MOTOR_VESSEL = "MOTOR_VESSEL"
-
-Outputs as:
-
-.. code-block:: json
-
- {
- "type": "enum",
- "name": "ShipType",
- "namespace": "shipping",
- "symbols": ["SAILING_VESSEL", "MOTOR_VESSEL"],
- "default": "SAILING_VESSEL"
- }
-
-The default value is taken from the first defined enum symbol and is used to support writer/reader schema resolution.
-
-
-:class:`float` (and subclasses)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``double``
-
-To output as the 32-bit Avro schema ``float`` instead, use :attr:`py_avro_schema.Option.FLOAT_32`.
-
-
-:class:`int` (and subclasses)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``long``
-
-To output as the 32-bit Avro schema ``int`` instead, use :attr:`py_avro_schema.Option.INT_32`.
-
-
-:class:`NoneType`
-~~~~~~~~~~~~~~~~~
-
-Avro schema: ``null``
-
-This schema is typically used as a "unioned" type where the default value is ``None``.
-
-
-:class:`decimal.Decimal`
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-| Avro schema: ``bytes``
-| Avro logical type: ``decimal``
-
-The standard library's :class:`decimal.Decimal` should be annotated with additional metadata to define the decimal
-**precision** and **scale**.
-
-For example, a decimal field with precision 4 and scale 2 is defined like this::
-
- import decimal
- from typing import Annotated
- import py_avro_schema as pas
-
- construction_costs: Annotated[decimal.Decimal, pas.DecimalMeta(precision=4, scale=2)]
-
-Values can be assigned like normal, e.g. ``construction_costs = decimal.Decimal("12.34")``. The scale attribute can be
-omitted as per Avro specification in which case the scale equals to zero.
-
-The Avro schema for the above type is:
-
-.. code-block:: json
-
- {
- "type": "bytes",
- "logicalType": "decimal",
- "precision": 4,
- "scale": 2
- }
-
-
-:class:`str`
-~~~~~~~~~~~~
-
-Avro schema: ``string``
-
-
-:class:`str` subclasses ("named strings")
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: ``string``
-
-Python classes inheriting from :class:`str` are converted to Avro ``string`` schemas to support serialization of any arbitrary Python types "as a string value".
-
-Primarily to support *deserialization* of Avro data, a custom property ``namedString`` is added and populated as the schema's namespace followed by the class name.
-The custom property is used here since the Avro ``string`` schema is not a "named" schema.
-**py-avro-schema** schema uses the same namespace logic as with real named Avro schemas.
-
-Example::
-
- # file shipping/models.py
-
- class PortName(str):
- ...
-
-Outputs as:
-
-.. code-block:: json
-
- {
- "type": "string",
- "namedString": "shipping.PortName"
- }
-
-
-:class:`typing.Literal`
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Avro schema: schema corresponding to the type of the literal value, e.g. ``string``, ``long`` etc.
-
-Mixed types, e.g. ``Literal["", 42]`` are not supported.
-
-
-:class:`uuid.UUID`
-~~~~~~~~~~~~~~~~~~
-
-| Avro schema: ``string``
-| Avro logical type: ``uuid``
diff --git a/pyproject.toml b/pyproject.toml
index b160ff2..47d0e3e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,7 +48,7 @@ keywords = [
]
# Minimum supported Python version
-requires-python = ">=3.9"
+requires-python = ">=3.13"
# All runtime dependencies that must be packaged, pin major version only.
dependencies = [
"avro~=1.10",
@@ -67,13 +67,25 @@ dependencies = [
"Download" = "https://pypi.python.org/pypi/py-avro-schema"
"Source Code" = "https://github.com/localstack/py-avro-schema"
+[tool.ruff]
+target-version = "py313"
+line-length = 120
+exclude = [
+ ".venv",
+ "venv*",
+ "dist",
+ "build",
+ "target",
+ "*.egg-info",
+ ".git"
+]
-[project.optional-dependencies]
+[tool.ruff.lint]
+ignore = ["UP007", "UP045", "UP013", "B008", "B904"]
+select = ["B", "C", "E", "F", "I", "W", "T", "B9", "G", "UP"]
+
+[dependency-groups]
-docs = [
- "sphinx",
- "sphinx-rtd-theme",
-]
testing = [
"packaging", # A test case uses packaging.version.Version
"pydantic>=2",
@@ -81,12 +93,8 @@ testing = [
"pytest-cov",
]
linting = [
- "black",
- "flake8",
- "interrogate",
- "isort",
- "mypy",
"pre-commit",
+ "ruff>=0.6.9",
"pydantic>=2", # For mypy
]
@@ -104,25 +112,3 @@ build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
# Section required to populate package version from Git tag
-
-[tool.interrogate]
-
-fail-under = 100
-verbose = 2
-color = true
-omit-covered-files = true
-
-
-[tool.coverage.report]
-
-fail_under = 90
-
-
-[tool.black]
-
-line-length = 120
-
-
-[tool.isort]
-
-profile = "black"
diff --git a/src/py_avro_schema/__init__.py b/src/py_avro_schema/__init__.py
index be5a38e..7b08735 100644
--- a/src/py_avro_schema/__init__.py
+++ b/src/py_avro_schema/__init__.py
@@ -23,7 +23,6 @@
"""
import importlib.metadata
-from typing import Optional, Type
import memoization
import orjson
@@ -53,9 +52,9 @@
@memoization.cached
def generate(
- py_type: Type,
+ py_type: type,
*,
- namespace: Optional[str] = None,
+ namespace: str | None = None,
options: Option = Option(0),
) -> bytes:
"""
diff --git a/src/py_avro_schema/_alias.py b/src/py_avro_schema/_alias.py
index 6eb2d83..2ae9af8 100644
--- a/src/py_avro_schema/_alias.py
+++ b/src/py_avro_schema/_alias.py
@@ -7,7 +7,7 @@
import dataclasses
from collections import defaultdict
-from typing import Annotated, Type, get_args, get_origin
+from typing import Annotated, get_args, get_origin
FQN = str
"""Fully qualified name for a Python type"""
@@ -103,7 +103,7 @@ def get_aliases(fqn: str) -> list[str]:
return []
-def get_field_aliases_and_actual_type(py_type: Type) -> tuple[list[str] | None, Type]:
+def get_field_aliases_and_actual_type(py_type: type) -> tuple[list[str] | None, type]:
"""
Check if a type contains an alias metadata via `Alias` or `Aliases` as metadata.
It returns the eventual aliases and the type.
diff --git a/src/py_avro_schema/_schemas.py b/src/py_avro_schema/_schemas.py
index 853c0bc..1a6d03b 100644
--- a/src/py_avro_schema/_schemas.py
+++ b/src/py_avro_schema/_schemas.py
@@ -24,7 +24,6 @@
import enum
import inspect
import re
-import sys
import types
import uuid
from enum import StrEnum
@@ -32,14 +31,9 @@
TYPE_CHECKING,
Annotated,
Any,
- Dict,
Final,
ForwardRef,
- List,
Literal,
- Optional,
- Tuple,
- Type,
Union,
get_args,
get_origin,
@@ -60,17 +54,14 @@
import pydantic
import pydantic.fields
-if sys.version_info >= (3, 10):
- from typing import is_typeddict
-else:
- from typing_extensions import is_typeddict
+from typing import is_typeddict
JSONStr = str
-JSONObj = Dict[str, Any]
-JSONArray = List[Any]
+JSONObj = dict[str, Any]
+JSONArray = list[Any]
JSONType = Union[JSONStr, JSONObj, JSONArray]
-NamesType = List[str]
+NamesType = list[str]
RUNTIME_TYPE_KEY = "_runtime_type"
@@ -174,9 +165,9 @@ def _wrapper(_cls):
def schema(
- py_type: Type,
- namespace: Optional[str] = None,
- names: Optional[NamesType] = None,
+ py_type: type,
+ namespace: str | None = None,
+ names: NamesType | None = None,
options: Option = Option(0),
) -> JSONType:
"""
@@ -197,7 +188,7 @@ def schema(
return schema_data
-def _schema_obj(py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)) -> "Schema":
+def _schema_obj(py_type: type, namespace: str | None = None, options: Option = Option(0)) -> Schema:
"""
Dispatch to relevant schema classes
@@ -228,7 +219,7 @@ def validate_name(value: str) -> str:
class Schema(abc.ABC):
"""Schema base"""
- def __new__(cls, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
+ def __new__(cls, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""
Create an instance of this schema class if it handles py_type
@@ -241,7 +232,7 @@ def __new__(cls, py_type: Type, namespace: Optional[str] = None, options: Option
else:
return None
- def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""
A schema base
@@ -254,12 +245,12 @@ def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Opti
self._namespace = namespace # Namespace override
@property
- def namespace_override(self) -> Optional[str]:
+ def namespace_override(self) -> str | None:
"""Manually set namespace, if any"""
return self._namespace
@property
- def namespace(self) -> Optional[str]:
+ def namespace(self) -> str | None:
"""The namespace, taking into account auto-namespace options and any override"""
if self._namespace is None and Option.NO_AUTO_NAMESPACE not in self.options:
module = inspect.getmodule(self.py_type)
@@ -276,7 +267,7 @@ def data(self, names: NamesType) -> JSONType:
@classmethod
@abc.abstractmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
def make_default(self, py_default: Any) -> Any:
@@ -306,7 +297,7 @@ class PrimitiveSchema(Schema):
]
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return any(
_is_class(py_type, type_, include_subclasses=include_subclasses)
@@ -335,7 +326,7 @@ class StrSubclassSchema(Schema):
"""An Avro string schema for a Python subclass of str, with a custom property referencing the class' fullname"""
@classmethod
- def handles_type(cls, py_type: Type[str]) -> bool:
+ def handles_type(cls, py_type: type[str]) -> bool:
"""Whether this schema class can represent a given Python class"""
return (
inspect.isclass(py_type)
@@ -360,7 +351,7 @@ def data(self, names: NamesType) -> JSONObj:
class LiteralSchema(Schema):
"""An Avro schema of any type for a Python Literal type, e.g. ``Literal[""]``"""
- def __init__(self, py_type: Type[Any], namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type[Any], namespace: str | None = None, options: Option = Option(0)):
"""
An Avro schema of any type for a Python Literal type, e.g. ``Literal[""]``
@@ -380,7 +371,7 @@ def __init__(self, py_type: Type[Any], namespace: Optional[str] = None, options:
self.literal_value_schema = _schema_obj(literal_type, namespace=namespace, options=options)
@classmethod
- def handles_type(cls, py_type: Type[Any]) -> bool:
+ def handles_type(cls, py_type: type[Any]) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
return get_origin(py_type) is Literal
@@ -394,7 +385,7 @@ def data(self, names: NamesType) -> JSONType:
class FinalSchema(Schema):
"""An Avro schema for Python ``typing.Final``"""
- def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""An Avro schema for Python ``typing.Final``"""
super().__init__(py_type, namespace, options)
py_type = _type_from_annotated(py_type)
@@ -409,7 +400,7 @@ def data(self, names: NamesType) -> JSONType:
return self.real_schema.data(names=names)
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
return get_origin(py_type) is Final or py_type is Final
@@ -423,7 +414,7 @@ class TypeAsJSONSchema(Schema):
"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return is_logically_json(py_type)
@@ -445,7 +436,7 @@ class UUIDSchema(Schema):
"""An Avro string schema representing a Python UUID object"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return _is_class(py_type, uuid.UUID)
@@ -472,7 +463,7 @@ class DateSchema(Schema):
"""An Avro logical type date schema for a given Python date type"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return _is_class(py_type, datetime.date) and not _is_class(py_type, datetime.datetime)
@@ -490,7 +481,7 @@ class TimeSchema(Schema):
"""An Avro logical type time (microseconds precision) schema for a given Python time type"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return _is_class(py_type, datetime.time)
@@ -506,8 +497,8 @@ def data(self, names: NamesType) -> JSONObj:
def make_default(self, py_default: datetime.time) -> int:
"""Return an Avro schema compliant default value for a given Python value"""
# Force UTC as we're concerned only about time diffs
- dt1 = datetime.datetime(1, 1, 1, tzinfo=datetime.timezone.utc)
- dt2 = datetime.datetime.combine(datetime.datetime(1, 1, 1), py_default, tzinfo=datetime.timezone.utc)
+ dt1 = datetime.datetime(1, 1, 1, tzinfo=datetime.UTC)
+ dt2 = datetime.datetime.combine(datetime.datetime(1, 1, 1), py_default, tzinfo=datetime.UTC)
return int((dt2 - dt1).total_seconds() * 1e6)
@@ -516,7 +507,7 @@ class DateTimeSchema(Schema):
"""An Avro logical type timestamp (microseconds precision) schema for a given Python datetime type"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return _is_class(py_type, datetime.datetime)
@@ -529,7 +520,7 @@ def make_default(self, py_default: datetime.datetime) -> int:
"""Return an Avro schema compliant default value for a given Python value"""
if not py_default.tzinfo:
raise TypeError(f"Default {py_default!r} must be timezone-aware")
- return int((py_default - datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)).total_seconds() * 1e6)
+ return int((py_default - datetime.datetime.fromtimestamp(0, tz=datetime.UTC)).total_seconds() * 1e6)
@register_schema
@@ -537,7 +528,7 @@ class TimeDeltaSchema(Schema):
"""An Avro logical type duration schema for a given Python timedelta type"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return _is_class(py_type, datetime.timedelta)
@@ -563,7 +554,7 @@ class ForwardSchema(Schema):
"""A forward/circular reference which in Avro is just the schema name"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return isinstance(py_type, (str, ForwardRef))
@@ -589,7 +580,7 @@ class DecimalSchema(Schema):
"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
# Here we are greedy: we catch any decimal.Decimal. However, data() might fail if the annotation is not correct.
return (
@@ -598,7 +589,7 @@ def handles_type(cls, py_type: Type) -> bool:
)
@classmethod
- def _decimal_meta(cls, py_type: Type) -> py_avro_schema._typing.DecimalMeta:
+ def _decimal_meta(cls, py_type: type) -> py_avro_schema._typing.DecimalMeta:
"""Return a decimal precision and scale for type, if possible"""
origin = get_origin(py_type)
args = get_args(py_type)
@@ -621,7 +612,7 @@ def _decimal_meta(cls, py_type: Type) -> py_avro_schema._typing.DecimalMeta:
raise TypeError(f"{py_type} is not a decimal type")
@staticmethod
- def _validate_meta_tuple(tuple_: Tuple) -> bool:
+ def _validate_meta_tuple(tuple_: tuple) -> bool:
"""Checks whether a given tuple is a tuple of (precision, scale)"""
return len(tuple_) == 2 and all(isinstance(item, int) for item in tuple_)
@@ -672,7 +663,7 @@ class SequenceSchema(Schema):
"""An Avro array schema for a given Python sequence"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
origin = get_origin(py_type)
@@ -680,8 +671,8 @@ def handles_type(cls, py_type: Type) -> bool:
def __init__(
self,
- py_type: Type[collections.abc.MutableSequence],
- namespace: Optional[str] = None,
+ py_type: type[collections.abc.MutableSequence],
+ namespace: str | None = None,
options: Option = Option(0),
):
"""
@@ -743,7 +734,7 @@ class DictSchema(Schema):
"""An Avro map schema for a given Python mapping"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
origin = get_origin(py_type)
@@ -753,8 +744,8 @@ def handles_type(cls, py_type: Type) -> bool:
def __init__(
self,
- py_type: Type[collections.abc.MutableMapping],
- namespace: Optional[str] = None,
+ py_type: type[collections.abc.MutableMapping],
+ namespace: str | None = None,
options: Option = Option(0),
):
"""
@@ -767,7 +758,7 @@ def __init__(
super().__init__(py_type, namespace=namespace, options=options)
py_type = _type_from_annotated(py_type)
args = get_args(py_type)
- if args[0] != str and not issubclass(args[0], StrEnum):
+ if args[0] is not str and not issubclass(args[0], StrEnum):
raise TypeError(f"Cannot generate Avro mapping schema for Python dictionary {py_type} with non-string keys")
self.values_schema = _schema_obj(args[1], namespace=namespace, options=options)
@@ -784,7 +775,7 @@ class UnionSchema(Schema):
"""An Avro union schema for a given Python union type"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
origin = get_origin(py_type)
@@ -796,7 +787,12 @@ def handles_type(cls, py_type: Type) -> bool:
return origin == Union or origin == union_type
return origin == Union
- def __init__(self, py_type: Type[Union[Any]], namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(
+ self,
+ py_type: type[Any],
+ namespace: str | None = None,
+ options: Option = Option(0),
+ ):
"""
An Avro union schema for a given Python union type
@@ -858,7 +854,7 @@ def make_default(self, py_default: Any) -> JSONType:
class NamedSchema(Schema):
"""A named Avro schema base class"""
- def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""
A named Avro schema base class
@@ -910,11 +906,11 @@ class EnumSchema(NamedSchema):
"""An Avro enum schema for a Python enum with string values"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
return _is_class(py_type, enum.Enum)
- def __init__(self, py_type: Type[enum.Enum], namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type[enum.Enum], namespace: str | None = None, options: Option = Option(0)):
"""
An Avro enum schema for a Python enum with string values
@@ -978,7 +974,7 @@ def data_before_deduplication(self, names: NamesType) -> JSONObj:
class RecordSchema(NamedSchema):
"""An Avro record schema base class"""
- def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""
An Avro record schema base class
@@ -1013,9 +1009,9 @@ class RecordField:
def __init__(
self,
- py_type: Type,
+ py_type: type,
name: str,
- namespace: Optional[str],
+ namespace: str | None,
aliases: list[str] | None = None,
default: Any = dataclasses.MISSING,
docs: str = "",
@@ -1075,12 +1071,12 @@ class DataclassSchema(RecordSchema):
"""An Avro record schema for a given Python dataclass"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
return dataclasses.is_dataclass(py_type)
- def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""
An Avro record schema for a given Python dataclass
@@ -1123,12 +1119,17 @@ class PydanticSchema(RecordSchema):
"""An Avro record schema for a given Pydantic model class"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
return hasattr(py_type, "__pydantic_private__")
- def __init__(self, py_type: Type[pydantic.BaseModel], namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(
+ self,
+ py_type: type[pydantic.BaseModel],
+ namespace: str | None = None,
+ options: Option = Option(0),
+ ):
"""
An Avro record schema for a given Pydantic model class
@@ -1163,7 +1164,7 @@ def make_default(self, py_default: pydantic.BaseModel) -> JSONObj:
"""Return an Avro schema compliant default value for a given Python value"""
return {key: _schema_obj(self._annotation(key)).make_default(value) for key, value in py_default}
- def _annotation(self, field_name: str) -> Type:
+ def _annotation(self, field_name: str) -> type:
"""
Fetch the raw annotation for a given field name
@@ -1189,7 +1190,7 @@ def __init__(self, var: str = "foo"):
"""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema class can represent a given Python class"""
py_type = _type_from_annotated(py_type)
return (
@@ -1205,7 +1206,7 @@ def handles_type(cls, py_type: Type) -> bool:
and bool(get_type_hints(py_type))
)
- def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
+ def __init__(self, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""
An Avro record schema for a plain Python class with type hints
@@ -1232,7 +1233,7 @@ def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Opti
}
self.record_fields = [self._record_field(field) for field in self.py_fields]
- def _record_field(self, py_field: tuple[str, Type]) -> RecordField:
+ def _record_field(self, py_field: tuple[str, type]) -> RecordField:
"""Return an Avro record field object for a given Python instance attribute"""
aliases, actual_type = get_field_aliases_and_actual_type(py_field[1])
name = py_field[0]
@@ -1264,11 +1265,11 @@ class TypedDictSchema(RecordSchema):
"""An Avro record schema for Python TypedDicts. Uses `get_type_hints` for extract the fields."""
@classmethod
- def handles_type(cls, py_type: Type) -> bool:
+ def handles_type(cls, py_type: type) -> bool:
"""Whether this schema can represent a TypedDict"""
return is_typeddict(py_type)
- def __init__(self, py_type: Type, namespace: str | None = None, options: Option = Option(0)):
+ def __init__(self, py_type: type, namespace: str | None = None, options: Option = Option(0)):
"""
An Avro record schema for a given Python TypedDict
@@ -1279,10 +1280,10 @@ def __init__(self, py_type: Type, namespace: str | None = None, options: Option
super().__init__(py_type, namespace=namespace, options=options)
py_type = _type_from_annotated(py_type)
self.is_total = py_type.__dict__.get("__total__", True)
- self.py_fields: dict[str, Type] = get_type_hints(py_type, include_extras=True)
+ self.py_fields: dict[str, type] = get_type_hints(py_type, include_extras=True)
self.record_fields = [self._record_field(field) for field in self.py_fields.items()]
- def _record_field(self, py_field: tuple[str, Type]) -> RecordField:
+ def _record_field(self, py_field: tuple[str, type]) -> RecordField:
"""Return an Avro record field object for a given TypedDict field"""
aliases, actual_type = get_field_aliases_and_actual_type(py_field[1])
@@ -1303,7 +1304,7 @@ def _record_field(self, py_field: tuple[str, Type]) -> RecordField:
return field_obj
-def _doc_for_class(py_type: Type) -> str:
+def _doc_for_class(py_type: type) -> str:
"""Return the first line of the docstring for a given class, if any"""
doc = inspect.getdoc(py_type)
if doc:
@@ -1314,13 +1315,13 @@ def _doc_for_class(py_type: Type) -> str:
return ""
-def _is_dict_str_any(py_type: Type) -> bool:
+def _is_dict_str_any(py_type: type) -> bool:
"""Return whether a given type is ``Dict[str, Any]``"""
origin = get_origin(py_type)
return inspect.isclass(origin) and issubclass(origin, dict) and get_args(py_type) == (str, Any)
-def _is_list_dict_str_any(py_type: Type) -> bool:
+def _is_list_dict_str_any(py_type: type) -> bool:
"""Return whether a given type is ``List[Dict[str, Any]]``"""
origin = get_origin(py_type)
args = get_args(py_type)
@@ -1330,18 +1331,18 @@ def _is_list_dict_str_any(py_type: Type) -> bool:
return False
-def _is_list_any(py_type: Type) -> bool:
+def _is_list_any(py_type: type) -> bool:
"""Return whether a given type is ``List[Any]``"""
origin = get_origin(py_type)
return inspect.isclass(origin) and issubclass(origin, list) and get_args(py_type) == (Any,)
-def is_logically_json(py_type: Type) -> bool:
+def is_logically_json(py_type: type) -> bool:
"""Returns whether a given type is logically a JSON and can be serialized as such"""
return _is_list_any(py_type) or _is_list_dict_str_any(py_type) or _is_dict_str_any(py_type)
-def _is_class(py_type: Any, of_types: Union[Type, Tuple[Type, ...]], include_subclasses: bool = True) -> bool:
+def _is_class(py_type: Any, of_types: type | tuple[type, ...], include_subclasses: bool = True) -> bool:
"""Return whether the given type is a (sub) class of a type or types"""
py_type = _type_from_annotated(py_type)
if include_subclasses:
@@ -1353,7 +1354,7 @@ def _is_class(py_type: Any, of_types: Union[Type, Tuple[Type, ...]], include_sub
return py_type == of_types
-def _type_from_annotated(py_type: Type) -> Type:
+def _type_from_annotated(py_type: type) -> type:
"""
Return the "principal" type if the given type is annotated like this ``Annotated[{principal_type}, ...]``
diff --git a/src/py_avro_schema/_testing.py b/src/py_avro_schema/_testing.py
index 0259750..14271c3 100644
--- a/src/py_avro_schema/_testing.py
+++ b/src/py_avro_schema/_testing.py
@@ -13,9 +13,9 @@
"""
Test functions
"""
+
import dataclasses
import difflib
-from typing import Dict, Type, Union
import avro.schema # type: ignore
import orjson
@@ -23,7 +23,7 @@
import py_avro_schema._schemas
-def assert_schema(py_type: Type, expected_schema: Union[str, Dict[str, str]], **kwargs) -> None:
+def assert_schema(py_type: type, expected_schema: str | dict[str, str], **kwargs) -> None:
"""Test that the given Python type results in the correct Avro schema"""
if not kwargs.pop("do_auto_namespace", False):
kwargs["options"] = kwargs.get("options", py_avro_schema.Option(0)) | py_avro_schema.Option.NO_AUTO_NAMESPACE
@@ -33,19 +33,14 @@ def assert_schema(py_type: Type, expected_schema: Union[str, Dict[str, str]], **
expected_schema_json = orjson.dumps(expected_schema, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS).decode()
actual_schema_json = orjson.dumps(actual_schema, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS).decode()
if actual_schema != expected_schema:
- print("Expected schema:")
- print(expected_schema_json)
- print("Actual schema:")
- print(actual_schema_json)
- print("Differences:")
- for diff in difflib.unified_diff(
+ for _diff in difflib.unified_diff(
expected_schema_json.splitlines(),
actual_schema_json.splitlines(),
fromfile="expected",
tofile="actual",
n=5,
):
- print(diff)
+ pass
assert actual_schema == expected_schema
# Assert that we can parse the schema data as a valid Avro schema
diff --git a/src/py_avro_schema/_typing.py b/src/py_avro_schema/_typing.py
index f479b63..e3e4cde 100644
--- a/src/py_avro_schema/_typing.py
+++ b/src/py_avro_schema/_typing.py
@@ -13,10 +13,12 @@
"""
Additional type hint classes etc
"""
+
import dataclasses
import decimal
-from typing import _GenericAlias # type: ignore
-from typing import Optional, Tuple
+from typing import (
+ _GenericAlias, # type: ignore
+)
import typeguard
@@ -37,7 +39,7 @@ class DecimalMeta:
"""
precision: int
- scale: Optional[int] = None
+ scale: int | None = None
def __post_init__(self):
"""
@@ -70,7 +72,7 @@ class DecimalType:
"""
@typeguard.typechecked()
- def __class_getitem__(cls, params: Tuple[int, int]) -> _GenericAlias:
+ def __class_getitem__(cls, params: tuple[int, int]) -> _GenericAlias:
"""Class indexing/subscription using ``DecimalType[precision, scale]"""
precision, scale = params
if precision <= 0:
diff --git a/tests/test_avro_schema.py b/tests/test_avro_schema.py
index 5bffbcb..0ca88e3 100644
--- a/tests/test_avro_schema.py
+++ b/tests/test_avro_schema.py
@@ -55,7 +55,10 @@ class PyTypedDict(TypedDict):
value: str
json_data = pas.generate(PyTypedDict)
- assert json.loads(json_data)["aliases"] == ["test_avro_schema.OldDict", "test_avro_schema.VeryOldDict"]
+ assert json.loads(json_data)["aliases"] == [
+ "test_avro_schema.OldDict",
+ "test_avro_schema.VeryOldDict",
+ ]
register_type_alias(alias="test_avro_schema.SuperOldDict")(PyTypedDict)
pas.generate.cache_clear()
diff --git a/tests/test_dataclass.py b/tests/test_dataclass.py
index e5312a7..516a013 100644
--- a/tests/test_dataclass.py
+++ b/tests/test_dataclass.py
@@ -14,7 +14,7 @@
import decimal
import enum
import re
-from typing import Annotated, Dict, List, Optional, Tuple
+from typing import Annotated
import pytest
@@ -120,7 +120,7 @@ class PyType:
def test_optional_field_default():
@dataclasses.dataclass
class PyType:
- field_a: Optional[str] = None
+ field_a: str | None = None
expected = {
"type": "record",
@@ -142,7 +142,7 @@ class PyType:
def test_list_string_field():
@dataclasses.dataclass
class PyType:
- field_a: List[str]
+ field_a: list[str]
expected = {
"type": "record",
@@ -163,7 +163,7 @@ class PyType:
def test_list_string_field_default():
@dataclasses.dataclass
class PyType:
- field_a: List[str] = dataclasses.field(default_factory=list)
+ field_a: list[str] = dataclasses.field(default_factory=list)
expected = {
"type": "record",
@@ -185,7 +185,7 @@ class PyType:
def test_list_string_field_default_wrong_type():
@dataclasses.dataclass
class PyType:
- field_a: List[str] = dataclasses.field(default_factory=lambda: [1])
+ field_a: list[str] = dataclasses.field(default_factory=lambda: [1])
with pytest.raises(TypeError, match=r"type of default_value\[0\] must be str; got int instead"):
assert_schema(PyType, {})
@@ -229,7 +229,7 @@ class PyTypeChild:
@dataclasses.dataclass
class PyType:
- field_child: List[PyTypeChild]
+ field_child: list[PyTypeChild]
expected = {
"type": "record",
@@ -263,7 +263,7 @@ class PyTypeChild:
@dataclasses.dataclass
class PyType:
- field_child: List[PyTypeChild]
+ field_child: list[PyTypeChild]
expected = {
"type": "record",
@@ -299,7 +299,7 @@ class PyTypeChild:
@dataclasses.dataclass
class PyType:
- field_child: Dict[str, PyTypeChild]
+ field_child: dict[str, PyTypeChild]
expected = {
"type": "record",
@@ -335,7 +335,7 @@ class PyTypeChild:
@dataclasses.dataclass
class PyType:
- field_child: Optional[PyTypeChild]
+ field_child: PyTypeChild | None
expected = {
"type": "record",
@@ -656,7 +656,8 @@ class PyType:
field_a: Annotated[decimal.Decimal, pas.DecimalMeta(4, 2)] = decimal.Decimal("123.45")
with pytest.raises(
- ValueError, match="Default value 123.45 has precision 5 which is greater than the schema's precision 4"
+ ValueError,
+ match="Default value 123.45 has precision 5 which is greater than the schema's precision 4",
):
assert_schema(PyType, {})
@@ -666,7 +667,10 @@ def test_decimal_field_default_scale_too_big():
class PyType:
field_a: Annotated[decimal.Decimal, pas.DecimalMeta(4, 2)] = decimal.Decimal("1.234")
- with pytest.raises(ValueError, match="Default value 1.234 has scale 3 which is greater than the schema's scale 2"):
+ with pytest.raises(
+ ValueError,
+ match="Default value 1.234 has scale 3 which is greater than the schema's scale 2",
+ ):
assert_schema(PyType, {})
@@ -695,7 +699,7 @@ class PyType:
def test_time_field_default():
@dataclasses.dataclass
class PyType:
- field_a: datetime.time = datetime.time(12, 0, 0, tzinfo=datetime.timezone.utc)
+ field_a: datetime.time = datetime.time(12, 0, 0, tzinfo=datetime.UTC)
expected = {
"type": "record",
@@ -739,7 +743,7 @@ class PyType:
def test_datetime_field_default():
@dataclasses.dataclass
class PyType:
- field_a: datetime.datetime = datetime.datetime(1970, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
+ field_a: datetime.datetime = datetime.datetime(1970, 1, 1, 12, 0, 0, tzinfo=datetime.UTC)
expected = {
"type": "record",
@@ -764,7 +768,8 @@ class PyType:
field_a: datetime.datetime = datetime.datetime(1970, 1, 1, 12, 0, 0)
with pytest.raises(
- TypeError, match=re.escape("Default datetime.datetime(1970, 1, 1, 12, 0) must be timezone-aware")
+ TypeError,
+ match=re.escape("Default datetime.datetime(1970, 1, 1, 12, 0) must be timezone-aware"),
):
assert_schema(PyType, {})
@@ -832,13 +837,21 @@ class PyType:
def test_sequence_schema_defaults_with_items():
@dataclasses.dataclass
class PyType:
- field_a: List[str] = dataclasses.field(default_factory=lambda: ["foo", "bar"])
- field_b: Tuple[str, str] = dataclasses.field(default_factory=lambda: ("foo", "bar"))
+ field_a: list[str] = dataclasses.field(default_factory=lambda: ["foo", "bar"])
+ field_b: tuple[str, str] = dataclasses.field(default_factory=lambda: ("foo", "bar"))
expected = {
"fields": [
- {"default": ["foo", "bar"], "name": "field_a", "type": {"items": "string", "type": "array"}},
- {"default": ["foo", "bar"], "name": "field_b", "type": {"items": "string", "type": "array"}},
+ {
+ "default": ["foo", "bar"],
+ "name": "field_a",
+ "type": {"items": "string", "type": "array"},
+ },
+ {
+ "default": ["foo", "bar"],
+ "name": "field_b",
+ "type": {"items": "string", "type": "array"},
+ },
],
"name": "PyType",
"type": "record",
diff --git a/tests/test_logicals.py b/tests/test_logicals.py
index 67a10e0..848d259 100644
--- a/tests/test_logicals.py
+++ b/tests/test_logicals.py
@@ -13,7 +13,7 @@
import decimal
import re
import uuid
-from typing import Annotated, Any, Dict, List, Union
+from typing import Annotated, Any, Union
import pytest
@@ -250,7 +250,7 @@ def test_uuid_annotated():
def test_dict_json_logical_string_field():
- py_type = Dict[str, Any]
+ py_type = dict[str, Any]
expected = {
"type": "string",
"logicalType": "json",
@@ -260,7 +260,7 @@ def test_dict_json_logical_string_field():
def test_dict_json_logical_bytes_field():
- py_type = Dict[str, Any]
+ py_type = dict[str, Any]
expected = {
"type": "bytes",
"logicalType": "json",
@@ -269,7 +269,7 @@ def test_dict_json_logical_bytes_field():
def test_list_json_logical_string_field():
- py_type = List[Dict[str, Any]]
+ py_type = list[dict[str, Any]]
expected = {
"type": "string",
"logicalType": "json",
@@ -279,7 +279,7 @@ def test_list_json_logical_string_field():
def test_list_json_logical_bytes_field():
- py_type = List[Dict[str, Any]]
+ py_type = list[dict[str, Any]]
expected = {
"type": "bytes",
"logicalType": "json",
@@ -288,6 +288,6 @@ def test_list_json_logical_bytes_field():
def test_list_json_logical_list_any():
- py_type = List[Any]
+ py_type = list[Any]
expected = {"type": "bytes", "logicalType": "json"}
assert_schema(py_type, expected)
diff --git a/tests/test_plain_class.py b/tests/test_plain_class.py
index a90da2c..cb25e38 100644
--- a/tests/test_plain_class.py
+++ b/tests/test_plain_class.py
@@ -141,7 +141,11 @@ class Details:
class PyType:
details: Annotated[Details, Opaque]
- expected = {"fields": [{"name": "details", "type": "string"}], "name": "PyType", "type": "record"}
+ expected = {
+ "fields": [{"name": "details", "type": "string"}],
+ "name": "PyType",
+ "type": "record",
+ }
assert_schema(PyType, expected)
@@ -163,7 +167,6 @@ def test_type_aliases_future():
def test_typing_final():
-
class PyType:
var: Final[str]
field: Final[dict[str, int]]
@@ -173,7 +176,10 @@ def __init__(self):
self.field = {"John": 123}
expected = {
- "fields": [{"name": "var", "type": "string"}, {"name": "field", "type": {"type": "map", "values": "long"}}],
+ "fields": [
+ {"name": "var", "type": "string"},
+ {"name": "field", "type": {"type": "map", "values": "long"}},
+ ],
"name": "PyType",
"type": "record",
}
diff --git a/tests/test_primitives.py b/tests/test_primitives.py
index 822a287..1b939fc 100644
--- a/tests/test_primitives.py
+++ b/tests/test_primitives.py
@@ -12,16 +12,11 @@
import enum
import re
import sys
+from collections.abc import Mapping, MutableSequence, Sequence
from typing import (
Annotated,
- Dict,
- List,
Literal,
- Mapping,
- MutableSequence,
Optional,
- Sequence,
- Tuple,
Union,
)
@@ -169,7 +164,7 @@ def test_none_annotated():
def test_string_list():
- py_type = List[str]
+ py_type = list[str]
expected = {"type": "array", "items": "string"}
assert_schema(py_type, expected)
@@ -181,7 +176,7 @@ def test_string_set():
def test_string_list_annotated():
- py_type = Annotated[List[str], ...]
+ py_type = Annotated[list[str], ...]
expected = {"type": "array", "items": "string"}
assert_schema(py_type, expected)
@@ -193,7 +188,7 @@ def test_string_list_lower_list():
def test_int_list():
- py_type = List[int]
+ py_type = list[int]
expected = {"type": "array", "items": "long"}
assert_schema(py_type, expected)
@@ -205,7 +200,7 @@ def test_int_set():
def test_string_tuple():
- py_type = Tuple[str]
+ py_type = tuple[str]
expected = {"type": "array", "items": "string"}
assert_schema(py_type, expected)
@@ -239,7 +234,7 @@ def test_string_set_of_set():
def test_string_list_of_lists():
- py_type = List[List[str]]
+ py_type = list[list[str]]
expected = {
"type": "array",
"items": {
@@ -251,7 +246,7 @@ def test_string_list_of_lists():
def test_string_list_of_dicts():
- py_type = List[Dict[str, str]]
+ py_type = list[dict[str, str]]
expected = {
"type": "array",
"items": {
@@ -263,13 +258,13 @@ def test_string_list_of_dicts():
def test_string_dict():
- py_type = Dict[str, str]
+ py_type = dict[str, str]
expected = {"type": "map", "values": "string"}
assert_schema(py_type, expected)
def test_string_dict_annotated():
- py_type = Annotated[Dict[str, str], ...]
+ py_type = Annotated[dict[str, str], ...]
expected = {"type": "map", "values": "string"}
assert_schema(py_type, expected)
@@ -281,17 +276,17 @@ def test_string_dict_lower_dict():
def test_int_dict():
- py_type = Dict[str, int]
+ py_type = dict[str, int]
expected = {"type": "map", "values": "long"}
assert_schema(py_type, expected)
def test_string_dict_int_keys():
- py_type = Dict[int, str]
+ py_type = dict[int, str]
with pytest.raises(
TypeError,
match=re.escape(
- "Cannot generate Avro mapping schema for Python dictionary typing.Dict[int, str] with non-string keys"
+ "Cannot generate Avro mapping schema for Python dictionary dict[int, str] with non-string keys"
),
):
py_avro_schema._schemas.schema(py_type)
@@ -310,7 +305,7 @@ def test_string_mapping_annotated():
def test_string_dict_of_dicts():
- py_type = Dict[str, Dict[str, str]]
+ py_type = dict[str, dict[str, str]]
expected = {
"type": "map",
"values": {
@@ -338,7 +333,7 @@ def test_union_string_int():
def test_union_string_int_annotated():
- py_type = Annotated[Union[str, int], ...]
+ py_type = Annotated[str | int, ...]
expected = ["string", "long"]
assert_schema(py_type, expected)
@@ -371,7 +366,7 @@ def test_union_string_string_int_py310():
def test_union_of_union_string_int():
- py_type = Union[str, Union[str, int]]
+ py_type = Union[str, str | int]
expected = ["string", "long"]
assert_schema(py_type, expected)
@@ -404,7 +399,7 @@ def test_optional_str():
def test_optional_str_annotated():
- py_type = Annotated[Optional[str], ...]
+ py_type = Annotated[str | None, ...]
expected = ["string", "null"]
assert_schema(py_type, expected)
@@ -509,7 +504,8 @@ class PyType(enum.Enum):
GREEN = 1
with pytest.raises(
- TypeError, match="Avro enum schema members must be strings. uses {} values."
+ TypeError,
+ match="Avro enum schema members must be strings. uses {} values.",
):
assert_schema(PyType, {})
diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py
index a914b41..17f6924 100644
--- a/tests/test_pydantic.py
+++ b/tests/test_pydantic.py
@@ -11,7 +11,7 @@
import decimal
import uuid
-from typing import Annotated, List, Optional, Union
+from typing import Annotated
import pydantic
import pytest
@@ -83,7 +83,7 @@ class PyType(pydantic.BaseModel):
def test_optional_field_default():
class PyType(pydantic.BaseModel):
- field_a: Optional[str] = None
+ field_a: str | None = None
expected = {
"type": "record",
@@ -104,7 +104,7 @@ class PyType(pydantic.BaseModel):
def test_list_string_field():
class PyType(pydantic.BaseModel):
- field_a: List[str]
+ field_a: list[str]
expected = {
"type": "record",
@@ -124,7 +124,7 @@ class PyType(pydantic.BaseModel):
def test_list_string_field_default():
class PyType(pydantic.BaseModel):
- field_a: List[str] = [] # Pydantic allows mutable defaults like this
+ field_a: list[str] = [] # Pydantic allows mutable defaults like this
expected = {
"type": "record",
@@ -145,7 +145,7 @@ class PyType(pydantic.BaseModel):
def test_list_string_field_default_wrong_type():
class PyType(pydantic.BaseModel):
- field_a: List[str] = [1] # Pydantic allows mutable defaults like this
+ field_a: list[str] = [1] # Pydantic allows mutable defaults like this
with pytest.raises(TypeError, match=r"type of default_value\[0\] must be str; got int instead"):
assert_schema(PyType, {})
@@ -185,7 +185,7 @@ class PyTypeChild(pydantic.BaseModel):
field_a: str
class PyType(pydantic.BaseModel):
- field_child: List[PyTypeChild]
+ field_child: list[PyTypeChild]
expected = {
"type": "record",
@@ -476,7 +476,9 @@ class PyType(pydantic.BaseModel):
def test_annotated_decimal():
class PyType(pydantic.BaseModel):
field_a: Annotated[
- decimal.Decimal, pas.DecimalMeta(precision=3, scale=2), pydantic.BeforeValidator(lambda x: x)
+ decimal.Decimal,
+ pas.DecimalMeta(precision=3, scale=2),
+ pydantic.BeforeValidator(lambda x: x),
]
expected = {
@@ -518,7 +520,9 @@ class PyType(pydantic.BaseModel):
def test_annotated_decimal_in_base():
class Base(pydantic.BaseModel):
field_a: Annotated[
- decimal.Decimal, pas.DecimalMeta(precision=3, scale=2), pydantic.BeforeValidator(lambda x: x)
+ decimal.Decimal,
+ pas.DecimalMeta(precision=3, scale=2),
+ pydantic.BeforeValidator(lambda x: x),
]
class PyType(Base):
@@ -549,12 +553,16 @@ class PyType(Base):
def test_annotated_decimal_overridden():
class Base(pydantic.BaseModel):
field_a: Annotated[
- decimal.Decimal, pas.DecimalMeta(precision=3, scale=0), pydantic.BeforeValidator(lambda x: x)
+ decimal.Decimal,
+ pas.DecimalMeta(precision=3, scale=0),
+ pydantic.BeforeValidator(lambda x: x),
]
class PyType(Base):
field_a: Annotated[
- decimal.Decimal, pas.DecimalMeta(precision=3, scale=2), pydantic.BeforeValidator(lambda x: x)
+ decimal.Decimal,
+ pas.DecimalMeta(precision=3, scale=2),
+ pydantic.BeforeValidator(lambda x: x),
]
expected = {
@@ -615,7 +623,7 @@ class Nested(pydantic.BaseModel):
def test_nested_base_model_list_default():
class Default(pydantic.BaseModel):
- field_a: List[str] = pydantic.Field(..., default_factory=list)
+ field_a: list[str] = pydantic.Field(..., default_factory=list)
class PyType(pydantic.BaseModel):
default: Default = pydantic.Field(..., default_factory=Default)
@@ -649,13 +657,13 @@ class PyType(pydantic.BaseModel):
def test_nested_base_model_union_model_default():
class DefaultA(pydantic.BaseModel):
- field_a: List[str] = pydantic.Field(..., default_factory=list)
+ field_a: list[str] = pydantic.Field(..., default_factory=list)
class DefaultB(pydantic.BaseModel):
- field_b: Union[int, float] = 0.0
+ field_b: int | float = 0.0
class PyType(pydantic.BaseModel):
- default: Union[DefaultA, DefaultB] = pydantic.Field(..., default_factory=DefaultA)
+ default: DefaultA | DefaultB = pydantic.Field(..., default_factory=DefaultA)
expected = {
"fields": [
@@ -664,7 +672,13 @@ class PyType(pydantic.BaseModel):
"name": "default",
"type": [
{
- "fields": [{"default": [], "name": "field_a", "type": {"items": "string", "type": "array"}}],
+ "fields": [
+ {
+ "default": [],
+ "name": "field_a",
+ "type": {"items": "string", "type": "array"},
+ }
+ ],
"name": "DefaultA",
"type": "record",
},
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 0ddc24e..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright 2022 J.P. Morgan Chase & Co.
-#
-# 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.
-
-
-# Tox virtual environment manager for testing and quality assurance
-
-[tox]
-envlist =
- py311
- py312
- py313
- linting
- docs
-isolated_build = True
-# Developers may not have all Python versions
-skip_missing_interpreters = true
-
-[gh-actions]
-# Mapping from GitHub Actions Python versions to Tox environments
-python =
- 3.11: py311
- 3.12: py312
- 3.13: py313, linting, docs
-
-[testenv]
-# Base test environment
-
-extras =
- testing
-commands =
- python -m pytest --cov={envsitepackagesdir}{/}py_avro_schema
-
-[testenv:dev]
-# Scratch environment in .venv/
-
-envdir = {toxinidir}{/}.venv
-usedevelop = true
-extras =
- linting
- testing
-commands =
-
-[testenv:linting]
-# Linting tools environment
-
-extras =
- linting
-commands =
- python -m pip list
- python -m isort --diff --check .
- python -m black --diff --color --check .
- python -m interrogate src/
- python -m flake8 .
- python -m mypy --install-types --non-interactive --package=py_avro_schema
-
-[testenv:docs]
-# Sphinx documentation generation environment
-
-extras =
- docs
-commands =
- # Command exactly as executed on readthedocs.org. See https://readthedocs.org/projects/py-avro-schema/builds/.
- python -m sphinx -T -E -W --keep-going -b html -d {toxinidir}{/}docs{/}_build{/}doctrees -D language=en {toxinidir}{/}docs {toxinidir}{/}docs{/}_build{/}html
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..639e628
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,500 @@
+version = 1
+revision = 3
+requires-python = ">=3.13"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "avro"
+version = "1.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/60/00/af1eec633637e12d0945a97f05a429eed83ac45865af60cb453db4689d95/avro-1.12.1.tar.gz", hash = "sha256:c5b8dd2dd4c10816f0dc127cc29cfd43b5e405cf7e6840e89460a024bf3d098d", size = 91115, upload-time = "2025-10-15T21:05:33.859Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/2b/3d5f41d5b46246216fa62d1a0a6813c74ba8b0ef990644b6bd3cde82d042/avro-1.12.1-py2.py3-none-any.whl", hash = "sha256:970475dd6457924533966fe761be607c759d5a48390cc8fbed472f7c9a8868f2", size = 124250, upload-time = "2025-10-15T21:05:32.815Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" },
+ { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" },
+ { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" },
+ { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" },
+ { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" },
+ { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" },
+ { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" },
+ { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" },
+ { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" },
+ { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" },
+ { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" },
+ { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" },
+ { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" },
+ { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" },
+ { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" },
+ { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" },
+ { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" },
+ { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "localstack-py-avro-schema"
+source = { editable = "." }
+dependencies = [
+ { name = "avro" },
+ { name = "memoization" },
+ { name = "more-itertools" },
+ { name = "orjson" },
+ { name = "typeguard" },
+]
+
+[package.dev-dependencies]
+linting = [
+ { name = "pre-commit" },
+ { name = "pydantic" },
+ { name = "ruff" },
+]
+testing = [
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "avro", specifier = "~=1.10" },
+ { name = "memoization", specifier = "~=0.4" },
+ { name = "more-itertools", specifier = "~=10.0" },
+ { name = "orjson", specifier = "~=3.5" },
+ { name = "typeguard", specifier = "==2.13.3" },
+]
+
+[package.metadata.requires-dev]
+linting = [
+ { name = "pre-commit" },
+ { name = "pydantic", specifier = ">=2" },
+ { name = "ruff", specifier = ">=0.6.9" },
+]
+testing = [
+ { name = "packaging" },
+ { name = "pydantic", specifier = ">=2" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+]
+
+[[package]]
+name = "memoization"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/53/e948a943e16423a87ced16e34ea7583c300e161a4c3e85d47d77d83830bf/memoization-0.4.0.tar.gz", hash = "sha256:fde5e7cd060ef45b135e0310cfec17b2029dc472ccb5bbbbb42a503d4538a135", size = 41209, upload-time = "2021-08-01T18:48:53.002Z" }
+
+[[package]]
+name = "more-itertools"
+version = "10.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
+]
+
+[[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, upload-time = "2024-06-04T18:44:11.171Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" },
+ { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" },
+ { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" },
+ { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" },
+ { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" },
+ { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" },
+ { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.5.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/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage" },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" },
+ { url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" },
+ { url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" },
+ { url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" },
+ { url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" },
+ { url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" },
+ { url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" },
+ { url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" },
+]
+
+[[package]]
+name = "typeguard"
+version = "2.13.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/38/c61bfcf62a7b572b5e9363a802ff92559cb427ee963048e1442e3aef7490/typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4", size = 40604, upload-time = "2021-12-10T21:09:39.158Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/bb/d43e5c75054e53efce310e79d63df0ac3f25e34c926be5dffb7d283fb2a8/typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1", size = 17605, upload-time = "2021-12-10T21:09:37.844Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.35.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
+]