diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c76f3e..be893e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,7 @@ jobs: - '3.2' # LTS April 2024 - '4.2' # LTS April 2026 - '5.1' # December 2025 + - '5.2b1' exclude: - django-version: '4.2' postgres-version: '9.6' @@ -55,8 +56,16 @@ jobs: - postgres-version: '9.6' django-version: '5.1' + - postgres-version: '9.6' + django-version: '5.2b1' + + - postgres-version: '12' + django-version: '5.2b1' + - python-version: '3.9' django-version: '5.1' + - python-version: '3.9' + django-version: '5.2b1' - python-version: '3.13' django-version: '3.2' - python-version: '3.13' @@ -112,7 +121,11 @@ jobs: - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} - just test-lock Django~=${{ matrix.django-version }}.0 + if [ "${{ matrix.django-version }}" = "5.2b1" ]; then + just test-lock "Django==${{ matrix.django-version }}" + else + just test-lock "Django~=${{ matrix.django-version }}.0" + fi - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | diff --git a/README.md b/README.md index a1e9d76..25bd326 100644 --- a/README.md +++ b/README.md @@ -24,23 +24,23 @@ 🚨 **See [migration guide](https://django-enum.readthedocs.io/en/latest/changelog.html#migration-1-x-to-2-x) for notes on 1.x to 2.x migration.** 🚨 -Full and natural support for [enumerations](https://docs.python.org/3/library/enum.html#enum.Enum) as Django model fields. +Full and natural support for [PEP435](https://peps.python.org/pep-0435) [enumerations](https://docs.python.org/3/library/enum.html#enum.Enum) as [Django](https://www.djangoproject.com) model fields. -Many packages aim to ease usage of Python enumerations as model fields. Most were superseded when Django provided ``TextChoices`` and ``IntegerChoices`` types. The motivation for [django-enum](https://django-enum.readthedocs.io) was to: +Many packages aim to ease usage of Python enumerations as model fields. Most were superseded when Django provided [TextChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types) and [IntegerChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types) types. The motivation for [django-enum](https://pypi.org/project/enum-properties) was to: -* Work with any Python PEP 435 Enum including those that do not derive from Django's TextChoices and IntegerChoices. -* Coerce fields to instances of the Enum type by default. -* Allow strict adherence to Enum values to be disabled. +* Work with any [Enum](https://docs.python.org/3/library/enum.html#enum.Enum) including those that do not derive from Django's [TextChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types) and [IntegerChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types). +* Coerce fields to instances of the [Enum](https://docs.python.org/3/library/enum.html#enum.Enum) type by default. +* Allow strict adherence to [Enum](https://docs.python.org/3/library/enum.html#enum.Enum) values to be disabled. * Handle migrations appropriately. (See [migrations](https://django-enum.readthedocs.io/en/latest/usage.html#migrations)) -* Integrate as fully as possible with [Django's](https://www.djangoproject.com) existing level of enum support. -* Support [enum-properties](https://pypi.org/project/enum-properties) to enable richer enumeration types. (A less awkward alternative to dataclass enumerations with more features) +* Integrate as fully as possible with Django's [existing level of enum support](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types). +* Support [enum-properties](https://pypi.org/project/enum-properties) to enable richer enumeration types. (A less awkward alternative to [dataclass enumerations](https://docs.python.org/3/howto/enum.html#enum-dataclass-support) with more features) * Represent enum fields with the smallest possible column type. -* Support bit mask queries using standard Python Flag enumerations. +* Support [bit field](https://en.wikipedia.org/wiki/Bit_field) queries using standard Python Flag enumerations. * Be as simple and light-weight an extension to core [Django](https://www.djangoproject.com) as possible. -* Enforce enumeration value consistency at the database level using check constraints by default. +* Enforce enumeration value consistency at the database level using [check constraints](https://docs.djangoproject.com/en/stable/ref/models/constraints) by default. * (TODO) Support native database enumeration column types when available. -[django-enum](https://django-enum.readthedocs.io) works in concert with [Django's](https://www.djangoproject.com) built in ``TextChoices`` and ``IntegerChoices`` to provide a new model field type, ``EnumField``, that resolves the correct native [Django](https://www.djangoproject.com) field type for the given enumeration based on its value type and range. For example, ``IntegerChoices`` that contain values between 0 and 32767 become [PositiveSmallIntegerField](https://docs.djangoproject.com/en/stable/ref/models/fields/#positivesmallintegerfield). +[django-enum](https://pypi.org/project/enum-properties) provides a new model field type, [EnumField](https://django-enum.rtfd.io/en/stable/reference/fields.html#django_enum.fields.EnumField), that allows you to treat almost any [PEP435](https://peps.python.org/pep-0435) enumeration as a database column. [EnumField](https://django-enum.rtfd.io/en/stable/reference/fields.html#django_enum.fields.EnumField) resolves the correct native [Django](https://www.djangoproject.com) field type for the given enumeration based on its value type and range. For example, [IntegerChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types) that contain values between 0 and 32767 become [PositiveSmallIntegerField](https://docs.djangoproject.com/en/stable/ref/models/fields/#positivesmallintegerfield). ```python @@ -70,7 +70,7 @@ Many packages aim to ease usage of Python enumerations as model fields. Most wer int_enum = EnumField(IntEnum, default=IntEnum.ONE) ``` -``EnumField`` **is more than just an alias. The fields are now assignable and accessible as their enumeration type rather than by-value:** +[EnumField](https://django-enum.rtfd.io/en/stable/reference/fields.html#django_enum.fields.EnumField) **is more than just an alias. The fields are now assignable and accessible as their enumeration type rather than by-value:** ```python @@ -86,9 +86,9 @@ Many packages aim to ease usage of Python enumerations as model fields. Most wer assert instance.int_enum.value == 3 ``` -## Flag Support +## Flag Support (BitFields) -[Flag](https://docs.python.org/3/library/enum.html#enum.Flag) types are also seamlessly supported! This allows a database column to behave like a bit mask and is an alternative to multiple boolean columns. There are mostly positive performance implications for using a bit mask instead of booleans depending on the size of the bit mask and the types of queries you will run against it. For bit masks more than a few bits long the size reduction both speeds up queries and reduces the required storage space. See the documentation for [discussion and benchmarks](https://django-enum.readthedocs.io/en/latest/performance.html#flags). +[Flag](https://docs.python.org/3/library/enum.html#enum.Flag) types are also seamlessly supported! This allows a database column to behave like a bit field and is an alternative to having multiple boolean columns. There are positive performance implications for using a bit field instead of booleans proportional on the size of the bit field and the types of queries you will run against it. For bit fields more than a few bits long the size reduction both speeds up queries and reduces the required storage space. See the documentation for [discussion and benchmarks](https://django-enum.readthedocs.io/en/latest/performance.html#flags). ```python @@ -112,7 +112,7 @@ Many packages aim to ease usage of Python enumerations as model fields. Most wer ## Complex Enumerations -[django-enum](https://django-enum.readthedocs.io) supports enum types that do not derive from Django's ``IntegerChoices`` and ``TextChoices``. This allows us to use other libs like [enum-properties](https://pypi.org/project/enum-properties) which makes possible very rich enumeration fields: +[django-enum](https://pypi.org/project/django-enum) supports enum types that do not derive from Django's [IntegerChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types) and [TextChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types). This allows us to use other libs like [enum-properties](https://pypi.org/project/enum-properties) which makes possible very rich enumeration fields: ``?> pip install enum-properties`` @@ -176,7 +176,7 @@ Many packages aim to ease usage of Python enumerations as model fields. Most wer assert TextChoicesExample.objects.filter(color='FF0000').first() == instance ``` -While they should be unnecessary if you need to integrate with code that expects an interface fully compatible with Django's ``TextChoices`` and ``IntegerChoices`` django-enum provides ``TextChoices``, ``IntegerChoices``, ``FlagChoices`` and ``FloatChoices`` types that derive from enum-properties and Django's ``Choices``. So the above enumeration could also be written: +While they should be unnecessary if you need to integrate with code that expects an interface fully compatible with Django's [TextChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types) and [IntegerChoices](https://docs.djangoproject.com/en/stable/ref/models/fields/#field-choices-enum-types) [django-enum](https://pypi.org/project/django-enum) provides [TextChoices](https://django-enum.rtfd.io/en/stable/reference/choices.html#django_enum.choices.TextChoices), [IntegerChoices](https://django-enum.rtfd.io/en/stable/reference/choices.html#django_enum.choices.IntegerChoices), [FlagChoices](https://django-enum.rtfd.io/en/stable/reference/choices.html#django_enum.choices.FlagChoices) and [FloatChoices](https://django-enum.rtfd.io/en/stable/reference/choices.html#django_enum.choices.FloatChoices) types that derive from enum-properties and Django's ``Choices``. So the above enumeration could also be written: ```python @@ -204,23 +204,30 @@ While they should be unnecessary if you need to integrate with code that expects pip install django-enum ``` -``django-enum`` has several optional dependencies that are not pulled in by default. ``EnumFields`` work seamlessly with all Django apps that work with model fields with choices without any additional work. Optional integrations are provided with several popular libraries to extend this basic functionality. +[django-enum](https://pypi.org/project/django-enum) has several optional dependencies that are not installed by default. [EnumField](https://django-enum.rtfd.io/en/stable/reference/fields.html#django_enum.fields.EnumField) works seamlessly with all Django apps that work with model fields with choices without any additional work. Optional integrations are provided with several popular libraries to extend this basic functionality, these include: -Integrations are provided that leverage [enum-properties](https://pypi.org/project/enum-properties) to make enumerations do more work and to provide extended functionality for [django-filter](https://pypi.org/project/django-filter) and [djangorestframework](https://www.django-rest-framework.org). -```bash - pip install enum-properties - pip install django-filter - pip install djangorestframework -``` +* [enum-properties](https://pypi.org/project/enum-properties) + ```bash + > pip install "django-enum[properties]" + ``` +* [django-filter](https://pypi.org/project/django-filter) +* [djangorestframework](https://www.django-rest-framework.org) + -## Continuous Integration +## Database Support + +[![Postgres](https://img.shields.io/badge/Postgres-9.6%2B-blue)](https://www.postgresql.org/) +[![MySQL](https://img.shields.io/badge/MySQL-5.7%2B-blue)](https://www.mysql.com/) +[![MariaDB](https://img.shields.io/badge/MariaDB-10.2%2B-blue)](https://mariadb.org/) +[![SQLite](https://img.shields.io/badge/SQLite-3.8%2B-blue)](https://www.sqlite.org/) +[![Oracle](https://img.shields.io/badge/Oracle-18%2B-blue)](https://www.oracle.com/database/) -Like with Django, Postgres is the preferred database for support. The full test suite is run against all combinations of currently supported versions of Django, Python, and Postgres as well as psycopg3 and psycopg2. The other RDBMS supported by Django are also tested including SQLite, MySQL, MariaDB and Oracle. For these RDBMS (with the exception of Oracle), tests are run against the minimum and maximum supported version combinations to maximize coverage breadth. +Like with [Django](https://www.djangoproject.com), [PostgreSQL](https://www.postgresql.org) is the preferred database for support. The full test suite is run against all combinations of currently supported versions of [Django](https://www.djangoproject.com), [Python](https://www.python.org), and [PostgreSQL](https://www.postgresql.org) as well as [psycopg3](https://pypi.org/project/psycopg) and [psycopg2](https://pypi.org/project/psycopg2). The other RDBMS supported by [Django](https://www.djangoproject.com) are also tested including [SQLite](https://www.sqlite.org), [MySQL](https://www.mysql.com), [MariaDB](https://mariadb.org) and [Oracle](https://www.oracle.com/database). For these RDBMS (with the exception of [Oracle](https://www.oracle.com/database), tests are run against the minimum and maximum supported version combinations to maximize coverage breadth. **See the [latest test runs](https://github.com/bckohan/django-enum/actions/workflows/test.yml) for our current test matrix** -*For Oracle, only the latest version of the free database is tested against the minimum and maximum supported versions of Python, Django and the cx-Oracle driver.* +For [Oracle](https://www.oracle.com/database), only the latest version of the free database is tested against the minimum and maximum supported versions of Python, Django and the [cx-Oracle](https://pypi.org/project/cx-Oracle) driver. ## Further Reading diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index db2efe4..92e0f70 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -4,10 +4,15 @@ Change Log ========== -v2.2.0 (2025-03-07) +v2.2.0 (2025-03-XX) =================== +* Implemented `Test all example code in the docs `_ +* Implemented `Use intersphinx for doc references `_ +* Implemented `Support Django 5.2 `_ +* Implemented `Upgrade to enum-properties >=2.2 `_ * Implemented `Move form imports to locally scoped imports where needed in fields.py `_ +* Implemented `Reorganize documentation using diataxis `_ v2.1.0 (2025-02-24) =================== @@ -54,7 +59,8 @@ v2.0.0 (2024-09-09) Migration from 1.x -> 2.x ------------------------- -* Imports of enum-properties_ extended ``TextChoices`` and ``IntegerChoices`` have been changed: +* Imports of :doc:`enum-properties:index` extended ``TextChoices`` and ``IntegerChoices`` have been + changed: .. code-block:: python diff --git a/doc/source/conf.py b/doc/source/conf.py index acdc406..29b17e7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,5 @@ from datetime import datetime +import os import sys from pathlib import Path from sphinx.ext.autodoc import between @@ -6,6 +7,8 @@ sys.path.append(str(Path(__file__).parent.parent.parent)) import django_enum +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') + # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full @@ -25,11 +28,9 @@ # -- Project information ----------------------------------------------------- -project = 'django_enum' -copyright = f'2022-{datetime.now().year}, Brian Kohan' -author = 'Brian Kohan' - -# The full version, including alpha/beta/rc tags +project = django_enum.__title__ +copyright = django_enum.__copyright__ +author = django_enum.__author__ release = django_enum.__version__ @@ -39,6 +40,8 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + 'sphinxcontrib_django', + 'sphinx.ext.intersphinx', 'sphinx.ext.autodoc', 'sphinx.ext.todo' ] @@ -71,6 +74,17 @@ todo_include_todos = True +intersphinx_mapping = { + "django": ( + "https://docs.djangoproject.com/en/stable", + "https://docs.djangoproject.com/en/stable/_objects/", + ), + "enum-properties": ("https://enum-properties.readthedocs.io/en/stable", None), + "django-render-static": ("https://django-render-static.readthedocs.io/en/stable", None), + "django-filter": ("https://django-filter.readthedocs.io/en/stable", None), + "python": ('https://docs.python.org/3', None) +} + def setup(app): # Register a sphinx.ext.autodoc.between listener to ignore everything diff --git a/doc/source/eccentric.rst b/doc/source/eccentric.rst new file mode 100644 index 0000000..a0c7e1c --- /dev/null +++ b/doc/source/eccentric.rst @@ -0,0 +1,106 @@ +.. include:: refs.rst + +.. _eccentric: + +=============== +Eccentric Enums +=============== + +Python's :class:`enum.Enum` type is extremely lenient. Enumeration values may be any hashable type +and values of the same enumeration may be of different types. + +.. tip:: + + We define an eccentric enumeration to be any enumeration where the value type is not a simple + string or integer or where the enumeration values are not all of the same type. + +For use in databases it is recommended to use more strict enumeration types that only allow a single +value type of either string or integer. If additional properties need to be associated with +enumeration values, a library like :doc:`enum-properties:index` should be used to store them on the +enumeration value classes. + +However, the goal of django-enum_ is to provide as complete of a bridge as possible between Python +and the database so eccentric enumerations are supported with caveats. The following enumeration +value types are supported out of the box, and map to the obvious +:ref:`model field type `. + +* :class:`int` +* :class:`str` +* :class:`float` +* :class:`datetime.date` +* :class:`datetime.datetime` +* :class:`datetime.time` +* :class:`datetime.timedelta` +* :class:`decimal.Decimal` + +You should avoid eccentric enums if possible, but there may be some compelling reasons to use them. +For example, for unusual data types it may make sense in situations where the database will be used +in a non-Python context and the enumeration values need to retain their native meaning. Or you may +not have direct control over the enumeration you want to store. + +Mixed Value Enumerations +======================== + +Mixed value enumerations are supported. For example: + +.. literalinclude:: ../../tests/examples/models/mixed_value.py + :language: python + + +:class:`~django_enum.fields.EnumField` will determine the most appropriate database column type to +store the enumeration by trying each of the supported primitive types in order and selecting the +first one that is symmetrically coercible to and from each enumeration value. ``None`` values are +allowed and do not take part in the primitive type selection. In the above example, the database +column type would default to a string. + +.. note:: + + If none of the supported primitive types are symmetrically coercible + :class:`~django_enum.fields.EnumField` will not be able to determine an appropriate column + type and a :exc:`ValueError` will be raised. + +In these cases, or to override the primitive type selection made by +:class:`~django_enum.fields.EnumField`, pass the ``primitive`` parameter. It may be necessary to +extend one of the supported primitives to make it coercible. It may also be necessary +to override the :class:`enum.Enum` class's :meth:`~enum.Enum._missing_` method: + +.. literalinclude:: ../../tests/examples/mixed_value_example.py + :language: python + :lines: 4- + +In the above case since ``None`` is an enumeration value, :class:`~django_enum.fields.EnumField` +will automatically set null=True on the model field. + +The above yields:: + + obj.eccentric_str= + obj.eccentric_float= + obj.eccentric_str= + obj.eccentric_float= + obj.eccentric_str= + obj.eccentric_float= + obj.eccentric_str= + obj.eccentric_float= + obj.eccentric_str= + obj.eccentric_float= + +Custom Enum Value Types +======================= + +.. warning:: + There is almost certainly a better way to do what you might be trying to do by writing a custom + enumeration value - for example consider using :doc:`enum-properties:index` to make your + enumeration types more robust by pushing more of this functionality on the :class:`enum.Enum` + class itself. + +If you must use a custom value type, you can by specifying a symmetrically coercible primitive type. +For example Path is already symmetrically coercible to str so this works: + +.. literalinclude:: ../../tests/examples/models/path_value.py + :language: python + + +A fully custom value might look like the following contrived example: + +.. literalinclude:: ../../tests/examples/models/custom_value.py + :language: python diff --git a/doc/source/eccentric_enums.rst b/doc/source/eccentric_enums.rst deleted file mode 100644 index 428c4c1..0000000 --- a/doc/source/eccentric_enums.rst +++ /dev/null @@ -1,159 +0,0 @@ -.. include:: refs.rst - -.. _eccentric: - -====================== -Eccentric Enumerations -====================== - -Python's Enum_ type is extremely lenient. Enumeration values may be any -hashable type and values of the same enumeration may be of different types. - -For use in databases it is recommended to use more strict enumeration types -that only allow a single value type of either string or integer. If additional -properties need to be associated with enumeration values, a library like -enum-properties_ should be used to store them on the enumeration value classes. - -However, the goal of django-enum is to provide as complete a bridge as possible -between Python and the database so eccentric enumerations are supported with -caveats. The following enumeration value types are supported out of the box, -and map to the obvious Django model field type: - -* :class:`int` -* :class:`str` -* :class:`float` -* :class:`datetime.date` -* :class:`datetime.datetime` -* :class:`datetime.time` -* :class:`datetime.timedelta` -* :class:`decimal.Decimal` - -While it is mostly not advisable to use eccentric enumerations, there may be -some compelling reasons to do so. For example, it may make sense in -situations where the database will be used in a non-Python context and the -enumeration values need to retain their native meaning. - - -Mixed Value Enumerations -======================== - -Mixed value enumerations are supported. For example: - -.. code-block:: python - - from enum import Enum - - class EccentricEnum(Enum): - - NONE = None - VAL1 = 1 - VAL2 = '2.0' - VAL3 = 3.0 - VAL4 = Decimal('4.5') - - -:class:`~django_enum.fields.EnumField` will determine the most appropriate database column type -to store the enumeration by trying each of the supported primitive types in order and -selecting the first one that is symmetrically coercible to and from each -enumeration value. ``None`` values are allowed and do not take part in the -primitive type selection. In the above example, the database column type would -be a string. - -.. note:: - - If none of the supported primitive types are symmetrically coercible - :class:`~django_enum.fields.EnumField` will not be able to determine an appropriate column - type and a ``ValueError`` will be raised. - -In these cases, or to override the primitive type selection made by -:class:`~django_enum.fields.EnumField`, pass the ``primitive`` parameter. It may be necessary to -extend one of the supported primitives to make it coercible. It may also be necessary -to override the Enum_'s ``_missing_`` method: - -.. code-block:: python - - # eccentric will be a string - eccentric_str = EnumField(EccentricEnum) - - # primitive will be a float - eccentric_float = EnumField(EccentricEnum, primitive=float) - -In the above case since ``None`` is an enumeration value, :class:`~django_enum.fields.EnumField` -will automatically set null=True on the model field. - -Custom Enumeration Values -========================= - -.. warning:: - There is almost certainly a better way to do what you might be trying to do - by writing a custom enumeration value - for example consider using - enum-properties_ to make your enumeration types more robust by pushing more - of this functionality on the Enum_ class itself. - -If you must use a custom value type, you can by specifying a symmetrically -coercible primitive type. For example Path is already symmetrically coercible -to str so this works: - -.. code-block:: python - - class MyModel(models.Model): - - class PathEnum(Enum): - - USR = Path('/usr') - USR_LOCAL = Path('/usr/local') - USR_LOCAL_BIN = Path('/usr/local/bin') - - path = EnumField(PathEnum, primitive=str) - - -A fully custom value might look like the following (admittedly contrived) -example: - -.. code-block:: python - - class StrProps: - """ - Wrap a string with some properties. - """ - - _str = '' - - def __init__(self, string): - self._str = string - - def __str__(self): - """ coercion to str - str(StrProps('str1')) == 'str1' """ - return self._str - - @property - def upper(self): - return self._str.upper() - - @property - def lower(self): - return self._str.lower() - - def __eq__(self, other): - """ Make sure StrProps('str1') == 'str1' """ - if isinstance(other, str): - return self._str == other - if other is not None: - return self._str == other._str - return False - - def deconstruct(self): - """Necessary to construct choices and default in migration files""" - return 'my_module.StrProps', (self._str,), {} - - - class MyModel(models.Model): - - class StrPropsEnum(Enum): - - STR1 = StrProps('str1') - STR2 = StrProps('str2') - STR3 = StrProps('str3') - - str_props = EnumField(StrPropsEnum, primitive=str) - diff --git a/doc/source/examples.rst b/doc/source/examples.rst deleted file mode 100644 index 36b2180..0000000 --- a/doc/source/examples.rst +++ /dev/null @@ -1,107 +0,0 @@ -.. include:: refs.rst - -.. _examples: - -======== -Examples -======== - -Enumerations in Python can provide rich class based interfaces, well suited -to many scenarios. A real world example is presented here that leverages -``IntegerChoices`` integration with enum-properties_ to encapsulate more -information and get our enum to do more work. - -Map Box Style -_____________ - -`Mapbox `_ is a leading web mapping platform. It comes with -a handful of default map styles. An enumeration is a natural choice to -represent these styles but the styles are complicated by the fact that they are -versioned and that when used as a parameter in the mapbox API they are in a URI -format that is overly verbose for a human friendly user interface. - -Each mapbox style enumeration is therefore composed of 4 primary properties. A -a human friendly label for the style, a name slug used in the URI, a version -number for the style and the full URI specification of the style. We might -implement our style enumeration like so: - -.. code-block:: python - - import typing as t - from django.db import models - from django_enum import EnumField - from enum_properties import IntEnumProperties, Symmetric - - class Map(models.Model): - - class MapBoxStyle(IntEnumProperties): - """ - https://docs.mapbox.com/api/maps/styles/ - """ - _symmetric_builtins_ = ['name', 'uri'] - - label: t.Annotated[str, Symmetric()] - slug: t.Annotated[str, Symmetric(case_fold=True)] - version: int - - # name value label slug version - STREETS = 1, 'Streets', 'streets', 12 - OUTDOORS = 2, 'Outdoors', 'outdoors', 12 - LIGHT = 3, 'Light', 'light', 11 - DARK = 4, 'Dark', 'dark', 11 - SATELLITE = 5, 'Satellite', 'satellite', 9 - SATELLITE_STREETS = 6, 'Satellite Streets', 'satellite-streets', 12 - NAVIGATION_DAY = 7, 'Navigation Day', 'navigation-day', 1 - NAVIGATION_NIGHT = 8, 'Navigation Night', 'navigation-night', 1 - - @property - def uri(self): - return f'mapbox://styles/mapbox/{self.slug}-v{self.version}' - - def __str__(self): - return self.uri - - style = EnumField(MapBoxStyle, default=MapBoxStyle.STREETS) - - -We've used a small integer as the value of the enumeration to save storage -space. We've also added a symmetric case insensitive slug and a non-symmetric -version property. We do not need to specify the label property because we're -inheriting from Django's Choices type which provides a ``label`` property as -the second element in the value tuple. - -The version numbers will increment over time, but we're only concerned with the -most recent versions, so we'll increment their values in this enumeration as -they change. Any version number updates exist only in code and will be picked -up as those persisted values are re-instantiated as ``MapBoxStyle`` -enumerations. - -The last property we've added is the ``uri`` property. We've added it as -concrete property on the class because it can be created from the slug and -version. We could have specified it in the value tuple but that would be very -verbose and less -`DRY `_. To make this -property symmetric we added it to the ``_symmetric_builtins_`` list. - -We can use our enumeration like so: - -.. code-block:: python - - map = Map.objects.create() - - assert map.style.uri == 'mapbox://styles/mapbox/streets-v11' - - # uri's are symmetric - map.style = 'mapbox://styles/mapbox/light-v10' - map.full_clean() - assert map.style is Map.MapBoxStyle.LIGHT - assert map.style == 3 - assert map.style == 'light' - - # so are labels (also case insensitive) - map.style = 'satellite streets' - map.full_clean() - assert map.style == Map.MapBoxStyle.SATELLITE_STREETS - - # when used in API calls (coerced to strings) - they "do the right thing" - assert str(map.style) == 'mapbox://styles/mapbox/satellite-streets-v11' diff --git a/doc/source/howto/external.rst b/doc/source/howto/external.rst new file mode 100644 index 0000000..4175d38 --- /dev/null +++ b/doc/source/howto/external.rst @@ -0,0 +1,31 @@ +.. include:: ../refs.rst + +.. _external: + +================== +Use External Enums +================== + +:class:`enum.Enum` classes defined externally to your code base or enum classes that otherwise do +not inherit from Django's :ref:`field-choices-enum-types`, are supported. When no choices are +present on an :class:`enum.Enum` type, :class:`~django_enum.fields.EnumField` will attempt to use +the ``label`` member on each enumeration value if it is present, otherwise the labels will be based +off the enumeration name. Choices can also be overridden at the +:class:`~django_enum.fields.EnumField` declaration. + +:class:`~django_enum.fields.EnumField` should work with any subclass of :class:`enum.Enum`. + +.. literalinclude:: ../../../tests/examples/models/extern.py + +The list of choice tuples for each field are: + +.. literalinclude:: ../../../tests/examples/extern_howto.py + :lines: 3- + +.. warning:: + + One nice feature of Django's :ref:`field-choices-enum-types` are that they disable + :class:`enum.auto` on :class:`enum.Enum` fields. :class:`enum.auto` can be dangerous because the + values assigned depend on the order of declaration. This means that if the order changes + existing database values will no longer align with the enumeration values. When control over the + values is not certain it is a good idea to add integration tests that look for value changes. diff --git a/doc/source/howto/flags.rst b/doc/source/howto/flags.rst new file mode 100644 index 0000000..69e01fa --- /dev/null +++ b/doc/source/howto/flags.rst @@ -0,0 +1,62 @@ +.. include:: ../refs.rst + +.. _flag_enums: + +===================== +Use Flags (BitFields) +===================== + +Python supports `bit fields `_ through the +:class:`enum.Flag` extension to :class:`enum.Enum`. + +These enumerations are fully supported and will render as multi select form fields by default. For +example: + +.. literalinclude:: ../../../tests/examples/models/flag_howto.py + :lines: 2- + +.. literalinclude:: ../../../tests/examples/flag_howto.py + :lines: 14-22 + +**Two new field lookups are provided for flag enumerations:** :ref:`has_any` **and** :ref:`has_all`. + +.. _has_any: + +has_any +------- + +The :ref:`has_any` lookup will return any object that has at least one of the flags in the +referenced enumeration. For example: + +.. literalinclude:: ../../../tests/examples/flag_howto.py + :lines: 23-30 + +.. _has_all: + +has_all +------- + +The :ref:`has_all` lookup will return any object that has at least all of the flags in the +referenced enumeration. For example: + +.. literalinclude:: ../../../tests/examples/flag_howto.py + :lines: 32- + +**There are performance considerations when using a bit mask like a Flag enumeration instead of +multiple boolean columns.** See :ref:`flag performance ` for discussion and +benchmarks. + +.. _large_flags: + +Flags with more than 64 bits +---------------------------- + +Flag enumerations of arbitrary size are supported, however if the enum has more than 64 flags it +will be stored as a :class:`~django.db.models.BinaryField`. It is therefore strongly recommended to +keep your :class:`enum.IntFlag` enumerations at 64 bits or less. + +.. warning:: + + Support for extra large flag fields is experimental. :ref:`has_any` and :ref:`has_all` do not + work. Most RDBMS systems do not support bitwise operations on binary fields. Future work may + involve exploring support for this as a Postgres extension. diff --git a/doc/source/howto/forms.rst b/doc/source/howto/forms.rst new file mode 100644 index 0000000..41308a9 --- /dev/null +++ b/doc/source/howto/forms.rst @@ -0,0 +1,80 @@ +.. include:: ../refs.rst + +.. _forms: + +========= +Use Forms +========= + +Two form classes are provided that enable the use of :class:`~django_enum.fields.EnumField` in +:class:`~django.forms.ModelForm` and :class:`~django.forms.Form` classes. +:class:`~django.forms.ModelForm` will use :class:`~django_enum.forms.EnumChoiceField` by default +to represent standard :class:`~django_enum.fields.EnumField` and +:class:`~django_enum.forms.EnumFlagField` to represent instances of +:class:`~django_enum.fields.FlagField`. + +These form field types enable symmetric value resolution and will automatically coerce any set value +to the underlying enumeration type. + +.. tip:: + + See :class:`~django_enum.forms.ChoiceFieldMixin` for the list of parameters accepted by the + form fields. These parameters mirror the parameters for :class:`~django_enum.fields.EnumField`. + +Using :class:`~django_enum.forms.EnumChoiceField` +------------------------------------------------- + +The form fields can be used directly on any form and we can use the configuration parameters to +alter behavior including, modifying the choices list or allowing non-strict values. For example, +using our :ref:`TextChoicesExample ` - if ``color_ext`` was declared with +`strict=False`, we could add additional choices and set the form field to any string like so: + +.. literalinclude:: ../../../tests/examples/choice_form_howto.py + :lines: 2- + +When we render this form using ``{{ form.as_p }}`` we get: + +.. code-block:: html + +
+

+ + +

+

+ + +

+ +
+ +``color_ext`` will validate any string value, but ``color`` will raise a +:exc:`~django.core.exceptions.ValidationError` if anything other than a valid ``Color`` enum is set. + +Using :class:`~django_enum.forms.EnumFlagField` +----------------------------------------------- + +.. todo:: + + Add an example of using EnumFlagField diff --git a/doc/source/howto/index.rst b/doc/source/howto/index.rst new file mode 100644 index 0000000..789dd86 --- /dev/null +++ b/doc/source/howto/index.rst @@ -0,0 +1,47 @@ +.. include:: ../refs.rst + +====== +How To +====== + +:class:`~django_enum.fields.EnumField` infers the primitive enumeration type and maps to the most +appropriate Django_ field type. For example :class:`enum.StrEnum` types would become +:class:`~django.db.models.CharField` and :class:`enum.IntEnum` types would become +:class:`~django.db.models.PositiveSmallIntegerField` or +:class:`~django.db.models.PositiveIntegerField` depending on the maximum enumeration value. + +This means that :class:`~django_enum.fields.EnumField` columns will behave as expected and integrate +broadly with third party libraries. When issues arise it tends to be because the primitive type was +marshalled into an :class:`enum.Enum` instance. :ref:`integrations` with some popular third party +libraries are provided. + +For example: + +.. literalinclude:: ../../../tests/examples/models/equivalency.py + + +``txt_enum`` and ``txt_choices`` fields are equivalent in all ways with the +following exceptions: + +.. literalinclude:: ../../../tests/examples/equivalency_howto.py + :lines: 5- + + +:class:`~django.forms.ModelForm` classes, DRF_ serializers and filters will behave the same way +with ``txt_enum`` and ``txt_choices``. A few types are provided for deeper integration with forms +and django-filter_ but their usage is optional. See :ref:`forms` and :ref:`filtering`. + +Very rich enumeration fields that encapsulate much more functionality in a simple declarative syntax +are possible with :class:`~django_enum.fields.EnumField`. See :ref:`enum_props`. + +.. toctree:: + :maxdepth: 2 + :caption: How Tos: + + external + options + integrations + flags + forms + migrations + urls diff --git a/doc/source/howto/integrations.rst b/doc/source/howto/integrations.rst new file mode 100644 index 0000000..677a156 --- /dev/null +++ b/doc/source/howto/integrations.rst @@ -0,0 +1,138 @@ +.. include:: ../refs.rst + +.. _integrations: + +================== +Integrate with ... +================== + +.. tip:: + + :class:`~django_enum.fields.EnumField` instances ultimately inherit from existing core Django_ + model fields and set the :ref:`choices attribute `. Therefore + :class:`~django_enum.fields.EnumField` *should work with any third party libraries and will + behave as a core field with a defined choice tuple list would*. + +However, you may want to take advantage of some of the extra features provided by django-enum_. We +provide out-of-the-box integration with the following libraries: + +.. _enum_props: + +enum-properties +--------------- + +Almost any :class:`enum.Enum` type is supported, so you may make use of :class:`enum.Enum` +extension libraries like :doc:`enum-properties ` to define very rich +enumeration fields. You will need to install the properties optional dependency set: + +.. code:: bash + + pip install "django-enum[properties]" + +:doc:`enum-properties ` is an extension to :class:`enum.Enum` that allows +properties to be added to enumeration instances using a simple declarative syntax. This is a less +awkward and more compatible alternative to :mod:`dataclass enumerations `. + +If you find yourself considering a :mod:`dataclass enumeration `, consider using +:doc:`enum-properties ` instead. Dataclass value types do not work with +:class:`~django_enum.fields.EnumField`. Most libraries that work with enumerations expect the +:attr:`~enum.Enum.value` attribute to be a primitive serializable type. + +:doc:`enum-properties ` also allows for +:ref:`symmetric properties ` which compare as equivalent +to the enumeration values and can be used to instantiate enumeration instances. + +**For a real-world example see the** :ref:`properties tutorial `. + +It should be unnecessary, but if you need to integrate with code that expects an interface fully +compatible with Django's +`enumeration types `_ +(``TextChoices`` and ``IntegerChoices`` django-enum_ provides +:class:`~django_enum.choices.TextChoices`, :class:`~django_enum.choices.IntegerChoices`, +:class:`~django_enum.choices.FlagChoices` and :class:`~django_enum.choices.FloatChoices` types that +derive from :doc:`enum-properties ` and Django's ``Choices``. For instance, +you may be using a third party library that uses :func:`isinstance` checks on your enum types +instead of duck typing. For compatibility in these cases simply use django-enum_'s ``Choices`` types +as the base class for your enumeration instead: + +.. literalinclude:: ../../../tests/examples/models/text_choices.py + :lines: 2- + +All of the expected :doc:`enum-properties ` behavior works: + +.. literalinclude:: ../../../tests/examples/text_choices_howto.py + :lines: 4- + +.. note:: + + To make your non-choices derived enum :ref:`quack like one `, you will need to + add: + + 1. a ``choices`` property that returns the choices tuple list + 2. a ``label`` property that returns the list of labels + 3. a ``name`` property that returns the list of names + 4. a ``value`` property that returns the list of values + + +.. _rest_framework: + +Django Rest Framework +--------------------- + +By default `DRF ModelSerializer +`_ will use a +`ChoiceField `_ to represent an +:class:`~django_enum.fields.EnumField`. This works great, but it will not accept :ref:`symmetric +enumeration values `. A serializer field +:class:`~django_enum.drf.EnumField` is provided that will. The dependency on DRF_ is optional so +to use the provided serializer field you must install DRF_: + +.. code:: bash + + pip install djangorestframework + + +.. literalinclude:: ../../../tests/examples/drf_serializer_howto.py + :lines: 3- + + + +The serializer :class:`~django_enum.drf.EnumField` accepts any arguments that +`ChoiceField `_ does. It also +accepts the ``strict`` parameter which behaves the same way as it does on the model field. + +.. tip:: + + You only need to use :class:`django_enum.drf.EnumField` if: + + 1. You are integrating with :doc:`enum-properties ` and want symmetric + properties to work. + 2. You have non-strict model fields and want to allow your API to accept values outside of + the enumeration. + +.. _filtering: + +django-filter +------------- + +As shown above, filtering by any value, enumeration type instance or symmetric value works with +:doc:`Django's ORM `. This is not natively true for the default +:doc:`FilterSet ` from :doc:`django-filter `. +Those filter sets will only be filterable by direct enumeration value by default. An +:class:`~django_enum.filters.EnumFilter` class is provided to enable filtering by symmetric property +values, but since the dependency on :doc:`django-filter ` is optional, you must +first install it: + +.. code:: bash + + pip install django-filter + +.. literalinclude:: ../../../tests/examples/filterfield_howto.py + :lines: 3- + +An :class:`~django_enum.filters.FilterSet` class is also provided that uses +:class:`~django_enum.filters.EnumFilter` for :class:`~django_enum.fields.EnumField` by default. +So the above is also equivalent to: + +.. literalinclude:: ../../../tests/examples/filterset_howto.py + :lines: 3- diff --git a/doc/source/howto/migrations.rst b/doc/source/howto/migrations.rst new file mode 100644 index 0000000..35e7d5e --- /dev/null +++ b/doc/source/howto/migrations.rst @@ -0,0 +1,23 @@ +.. include:: ../refs.rst + +.. _migrations: + +================ +Write Migrations +================ + +.. important:: + + There is one rule for writing custom migration files for EnumFields: + *Never reference or import your enumeration classes in a migration file, + work with the primitive values instead*. + +The deconstructed :class:`~django_enum.fields.EnumField` only include the choices tuple in the +migration files. This is because :class:`enum.Enum` classes may come and go or be +altered but the earlier migration files must still work. Simply treat any +custom migration routines as if they were operating on a normal model field +with choices. + +:class:`~django_enum.fields.EnumField` in migration files will not resolve the field values to +enumeration types. The fields will be the primitive enumeration values as they +are with any field with choices. diff --git a/doc/source/howto/options.rst b/doc/source/howto/options.rst new file mode 100644 index 0000000..7f4286a --- /dev/null +++ b/doc/source/howto/options.rst @@ -0,0 +1,67 @@ +.. include:: ../refs.rst + +.. _options: + +==================== +Configure EnumFields +==================== + +All parameters available to the equivalent model field with choices may be set directly in the +:class:`~django_enum.fields.EnumField` instantiation. If not provided +:class:`~django_enum.fields.EnumField` will set ``choices`` and ``max_length`` automatically. + +The following :class:`~django_enum.fields.EnumField` specific parameters are available: + +``strict`` +---------- + +By default all :class:`~django_enum.fields.EnumField` are ``strict``. This means a +:exc:`~django.core.exceptions.ValidationError` will be thrown anytime +:meth:`~django.db.models.Model.full_clean` is run on a model and a value is set for the field that +can not be coerced to its native :class:`~enum.Enum` type. To allow the field to store values that +are not present in the fields :class:`~enum.Enum` type we can pass `strict=False`. + +Non-strict fields will be instances of the enumeration where a valid :class:`~enum.Enum` value is +present and the plain old data where no :class:`~enum.Enum` type coercion is possible. + +.. literalinclude:: ../../../tests/examples/models/strict.py + +.. literalinclude:: ../../../tests/examples/strict_howto.py + :lines: 4- + + +``constrained`` +--------------- + +By default all strict :class:`~django_enum.fields.EnumField` are ``constrained``. This means that +:doc:`CheckConstraints ` will be generated at the database level +to ensure that the column will reject any value that is not present in the enumeration. This is a +good idea for most use cases, but it can be turned off by setting ``constrained`` to ``False``. + +.. note:: + + This is new in version 2.0. If you are upgrading from a previous version, you may set + this parameter to ``False`` to maintain the previous behavior. + +``primitive`` +------------- + +:class:`~django_enum.fields.EnumField` dynamically determines the database column type by +determining the most appropriate primitive type for the enumeration based on the enumeration +values. You may override the primitive determined by :class:`~django_enum.fields.EnumField` by +passing a type to the ``primitive`` parameter. You will likely not need to do this unless your +enumeration is :ref:`eccentric ` in some way. + +``coerce`` +---------- + +Setting this parameter to ``False`` will turn off the automatic conversion to +the field's :class:`~enum.Enum` type while leaving all validation checks in place. It will +still be possible to set the field directly as an :class:`~enum.Enum` instance and to +filter by :class:`~enum.Enum` instance or any symmetric value: + +.. literalinclude:: ../../../tests/examples/models/no_coerce.py + :lines: 13- + +.. literalinclude:: ../../../tests/examples/no_coerce_howto.py + :lines: 6- diff --git a/doc/source/howto/urls.rst b/doc/source/howto/urls.rst new file mode 100644 index 0000000..fc3fb2f --- /dev/null +++ b/doc/source/howto/urls.rst @@ -0,0 +1,21 @@ +.. include:: ../refs.rst + +.. _urls_howto: + +================== +Use Enums in URLs +================== + +django-enum_ provides a :ref:`converter ` that can be used to register enum url parameters +with the Django_ path resolver. + +.. literalinclude:: ../../../tests/examples/urls.py + +By default the converter will use the value property of the enumeration to resolve the enumeration, +but this can be overridden by passing the `prop` parameter, so we could for example use the +name or label instead. + +The reversals for the above paths would look like this: + +.. literalinclude:: ../../../tests/examples/urls_howto.py + :lines: 3- diff --git a/doc/source/index.rst b/doc/source/index.rst index 8f83253..6506a78 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -5,7 +5,7 @@ Django Enum =========== |MIT license| |Ruff| |PyPI version fury.io| |PyPI pyversions| |PyPi djversions| |PyPI status| -|Documentation Status| |Code Cov| |Test Status| +|Documentation Status| |Code Cov| |Test Status| |Django Packages| |Postgres| |MySQL| |MariaDB| |SQLite| |Oracle| @@ -40,6 +40,9 @@ Django Enum .. |Lint Status| image:: https://github.com/bckohan/django-enum/workflows/lint/badge.svg :target: https://github.com/bckohan/django-enum/actions/workflows/lint.yml +.. |Django Packages| image:: https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26 + :target: https://djangopackages.org/packages/p/django-enum/ + .. |Postgres| image:: https://img.shields.io/badge/Postgres-9.6%2B-blue :target: https://www.postgresql.org/ @@ -61,201 +64,102 @@ Django Enum See :ref:`migration_1.x_to_2.x` for how to update from 1.x to 2.x. -Full and natural support for enumerations_ as Django_ model fields. +Full and natural support for PEP435_ :class:`enumerations ` as Django_ model fields. Many packages aim to ease usage of Python enumerations as model fields. Most were superseded when -Django provided ``TextChoices`` and ``IntegerChoices`` types. The motivation for django-enum_ was -to: - -* Work with any Python PEP 435 Enum including those that do not derive from Django's - ``TextChoices`` and ``IntegerChoices``. -* Coerce fields to instances of the Enum type by default. -* Allow strict adherence to Enum values to be disabled. +Django provided :ref:`TextChoices ` and +:ref:`IntegerChoices ` types. The motivation for django-enum_ was to: + +* Work with any :class:`~enum.Enum` including those that do not derive from + Django's :ref:`TextChoices ` and + :ref:`IntegerChoices `. +* Coerce fields to instances of the :class:`~enum.Enum` type by default. +* Allow strict adherence to :class:`~enum.Enum` values to be disabled. * Handle migrations appropriately. (See :ref:`migrations`) -* Integrate as fully as possible with Django's existing level of enum support. -* Support enum-properties_ to enable richer enumeration types. (A less awkward alternative to - dataclass enumerations with more features) +* Integrate as fully as possible with Django's + :ref:`existing level of enum support `. +* Support :doc:`enum-properties:index` to enable richer enumeration types. (A less awkward + alternative to :ref:`dataclass enumerations ` with more features) * Represent enum fields with the smallest possible column type. -* Support bit mask queries using standard Python Flag enumerations. +* Support `bit field `_ queries using standard + :class:`Python Flag enumerations `. * Be as simple and light-weight an extension to core Django_ as possible. -* Enforce enumeration value consistency at the database level using check constraints by default. +* Enforce enumeration value consistency at the database level using + :doc:`check constraints ` by default. * (TODO) Support native database enumeration column types when available. django-enum_ provides a new model field type, :class:`~django_enum.fields.EnumField`, that allows -you to treat almost any PEP 435 enumeration as a database column. +you to treat almost any PEP435_ enumeration as a database column. :class:`~django_enum.fields.EnumField` resolves the correct native Django_ field type for the given -enumeration based on its value type and range. For example, ``IntegerChoices`` that contain values -between 0 and 32767 become `PositiveSmallIntegerField `_. - -.. code-block:: python - - from django.db import models - from django_enum import EnumField - - class MyModel(models.Model): - - class TextEnum(models.TextChoices): - - VALUE0 = 'V0', 'Value 0' - VALUE1 = 'V1', 'Value 1' - VALUE2 = 'V2', 'Value 2' - - class IntEnum(models.IntegerChoices): - - ONE = 1, 'One' - TWO = 2, 'Two', - THREE = 3, 'Three' - - # this is equivalent to: - # CharField(max_length=2, choices=TextEnum.choices, null=True, blank=True) - txt_enum = EnumField(TextEnum, null=True, blank=True) - - # this is equivalent to - # PositiveSmallIntegerField(choices=IntEnum.choices, default=IntEnum.ONE.value) - int_enum = EnumField(IntEnum, default=IntEnum.ONE) +enumeration based on its value type and range. For example, +:ref:`IntegerChoices ` that contain values between 0 and 32767 become +:class:`~django.db.models.PositiveSmallIntegerField`. +.. literalinclude:: ../../tests/examples/models/basic.py + :language: python + :lines: 2- :class:`~django_enum.fields.EnumField` **is more than just an alias. The fields are now assignable and accessible as their enumeration type rather than by-value:** -.. code-block:: python - - instance = MyModel.objects.create( - txt_enum=MyModel.TextEnum.VALUE1, - int_enum=3 # by-value assignment also works - ) +.. literalinclude:: ../../tests/examples/basic_example.py + :language: python + :lines: 2- - assert instance.txt_enum is MyModel.TextEnum('V1') - assert instance.txt_enum.label is 'Value 1' +Flag Support (BitFields) +======================== - assert instance.int_enum is MyModel.IntEnum['THREE'] - assert instance.int_enum.value is 3 +:class:`enum.Flag` types are also seamlessly supported! This allows a database column to behave +like a bit field and is an alternative to having multiple boolean columns. There are positive +performance implications for using a bit field instead of booleans proportional on the size of the +bit field and the types of queries you will run against it. For bit fields more than a few bits long +the size reduction both speeds up queries and reduces the required storage space. See the +documentation for :ref:`discussion and benchmarks `. +.. literalinclude:: ../../tests/examples/models/flag.py + :language: python + :lines: 2- -Flag Support -============ - -Flag_ types are also seamlessly supported! This allows a database column to behave like a bit mask -and is an alternative to multiple boolean columns. There are mostly positive performance -implications for using a bit mask instead of booleans depending on the size of the bit mask and the -types of queries you will run against it. For bit masks more than a few bits long the size -reduction both speeds up queries and reduces the required storage space. See the documentation for -:ref:`discussion and benchmarks `. - -.. code-block:: python - - class Permissions(IntFlag): - - READ = 1**2 - WRITE = 2**2 - EXECUTE = 3**2 - - - class FlagExample(models.Model): - - permissions = EnumField(Permissions) - - - FlagExample.objects.create(permissions=Permissions.READ | Permissions.WRITE) - - # get all models with RW: - FlagExample.objects.filter(permissions__has_all=Permissions.READ | Permissions.WRITE) +.. literalinclude:: ../../tests/examples/flag_example.py + :language: python + :lines: 4- .. note:: The :ref:`has_all` and :ref:`has_any` field lookups are only available for Flag enumerations. -Complex Enumerations -==================== +Enums with Properties +===================== -django-enum_ supports enum types that do not derive from Django's ``IntegerChoices`` and -``TextChoices``. This allows us to use other libs like enum-properties_ which makes possible very +django-enum_ supports enum types that do not derive from Django's +:ref:`IntegerChoices ` and :ref:`TextChoices `. +This allows us to use other libs like :doc:`enum-properties:index` which makes possible very rich enumeration fields: -.. code-block:: console - - ?> pip install enum-properties +.. code-block:: bash -.. code-block:: python + > pip install enum-properties - from enum_properties import StrEnumProperties - from django.db import models - class TextChoicesExample(models.Model): +.. literalinclude:: ../../tests/examples/models/properties.py + :language: python + :lines: 2- - class Color(StrEnumProperties): +.. literalinclude:: ../../tests/examples/properties_example.py + :language: python + :lines: 4- - label: Annotated[str, Symmetric()] - rgb: Annotated[t.Tuple[int, int, int], Symmetric()] - hex: Annotated[str, Symmetric(case_fold=True)] +While they should be unnecessary, if you need to integrate with code that expects an interface fully +compatible with Django's :ref:`TextChoices ` and +:ref:`IntegerChoices ` django-enum_ +provides :class:`~django_enum.choices.TextChoices`, :class:`~django_enum.choices.IntegerChoices`, +:class:`~django_enum.choices.FlagChoices` and :class:`~django_enum.choices.FloatChoices` types that +derive from :doc:`enum-properties:index` and Django's ``Choices``. So the above enumeration could +also be written: - # name value label rgb hex - RED = "R", "Red", (1, 0, 0), "ff0000" - GREEN = "G", "Green", (0, 1, 0), "00ff00" - BLUE = "B", "Blue", (0, 0, 1), "0000ff" - - # any named s() values in the Enum's inheritance become properties on - # each value, and the enumeration value may be instantiated from the - # property's value - - color = EnumField(Color) - - instance = TextChoicesExample.objects.create( - color=TextChoicesExample.Color('FF0000') - ) - assert instance.color is TextChoicesExample.Color('Red') - assert instance.color is TextChoicesExample.Color('R') - assert instance.color is TextChoicesExample.Color((1, 0, 0)) - - # direct comparison to any symmetric value also works - assert instance.color == 'Red' - assert instance.color == 'R' - assert instance.color == (1, 0, 0) - - # save by any symmetric value - instance.color = 'FF0000' - - # access any enum property right from the model field - assert instance.color.hex == 'ff0000' - - # this also works! - assert instance.color == 'ff0000' - - # and so does this! - assert instance.color == 'FF0000' - - instance.save() - - # filtering works by any symmetric value or enum type instance - assert TextChoicesExample.objects.filter( - color=TextChoicesExample.Color.RED - ).first() == instance - - assert TextChoicesExample.objects.filter(color=(1, 0, 0)).first() == instance - - assert TextChoicesExample.objects.filter(color='FF0000').first() == instance - - -While they should be unnecessary if you need to integrate with code that expects an interface fully -compatible with Django's ``TextChoices`` and ``IntegerChoices`` django-enum_ provides -``TextChoices``, ``IntegerChoices``, ``FlagChoices`` and ``FloatChoices`` types that derive from -enum-properties_ and Django's ``Choices``. So the above enumeration could also be written: - -.. code-block:: python - - from django_enum.choices import TextChoices - - class Color(TextChoices): - - # label is added as a symmetric property by the base class - - rgb: Annotated[t.Tuple[int, int, int], Symmetric()] - hex: Annotated[str, Symmetric(case_fold=True)] - - # name value label rgb hex - RED = "R", "Red", (1, 0, 0), "ff0000" - GREEN = "G", "Green", (0, 1, 0), "00ff00" - BLUE = "B", "Blue", (0, 0, 1), "0000ff" +.. literalinclude:: ../../tests/examples/models/properties_choices.py + :language: python + :lines: 7- Installation @@ -263,45 +167,49 @@ Installation 1. Clone django-enum from GitHub_ or install a release off PyPI_: -.. code-block:: console +.. code-block:: bash + + > pip install django-enum - ?> pip install django-enum +django-enum_ has several optional dependencies that are not installed by default. +:class:`~django_enum.fields.EnumField` works seamlessly with all Django apps that work with model +fields with choices without any additional work. Optional integrations are provided with several +popular libraries to extend this basic functionality, these include: -django-enum_ has several optional dependencies that are not pulled in by default. ``EnumFields`` -work seamlessly with all Django apps that work with model fields with choices without any -additional work. Optional integrations are provided with several popular libraries to extend this -basic functionality. +* :doc:`enum-properties ` + .. code-block:: bash -Integrations are provided that leverage enum-properties_ to make enumerations do more work and to -provide extended functionality for django-filter_ and djangorestframework_. + > pip install "django-enum[properties]" -.. code-block:: console +* django-filter_ +* djangorestframework_. - ?> pip install enum-properties - ?> pip install django-filter - ?> pip install djangorestframework +Database Support +================ -Continuous Integration -====================== +|Postgres| |MySQL| |MariaDB| |SQLite| |Oracle| -Like with Django, Postgres is the preferred database for support. The full test suite is run -against all combinations of currently supported versions of Django, Python, and Postgres as well as -psycopg3 and psycopg2. The other RDBMS supported by Django are also tested including SQLite, MySQL, -MariaDB and Oracle. For these RDBMS (with the exception of Oracle), tests are run against the -minimum and maximum supported version combinations to maximize coverage breadth. +Like with Django, PostgreSQL_ is the preferred database for support. The full test suite is run +against all combinations of currently supported versions of Django_, Python_, and PostgreSQL_ as +well as psycopg3_ and psycopg2_. The other RDBMS supported by Django_ are also tested including +SQLite_, MySQL_, MariaDB_ and Oracle_. For these RDBMS (with the exception of Oracle_), tests are +run against the minimum and maximum supported version combinations to maximize coverage breadth. **See the** `latest test runs `_ **for our current test matrix** -*For Oracle, only the latest version of the free database is tested against the minimum and -maximum supported versions of Python, Django and the cx-Oracle driver.* +.. note:: + + For Oracle_, only the latest version of the free database is tested against the minimum and + maximum supported versions of Python, Django and the cx-Oracle_ driver. Further Reading =============== -Consider using django-render-static_ to make your enumerations DRY_ across the full stack! +Consider using :doc:`django-render-static ` to make your enumerations +DRY_ across the full stack! Please report bugs and discuss features on the `issues page `_. @@ -313,9 +221,9 @@ Please report bugs and discuss features on the :maxdepth: 2 :caption: Contents: - usage - examples + tutorials/index.rst + howto/index.rst performance - eccentric_enums - reference + eccentric + reference/index changelog diff --git a/doc/source/performance.rst b/doc/source/performance.rst index 6794b2c..9955911 100644 --- a/doc/source/performance.rst +++ b/doc/source/performance.rst @@ -9,24 +9,18 @@ Performance Enums ===== -The cost to resolve a raw database value into an Enum_ type object is -non-zero but negligible and swamped by I/O in most scenarios. +The cost to resolve a raw database value into an :class:`enum.Enum` type object is non-zero but +negligible and swamped by I/O in most scenarios. -An effort is made to characterize and monitor the performance penalty of -using ``EnumFields`` over a Django_ native field with choices and benchmark -tests ensure performance of future releases will remain stable or improve. - -For the nominal case the marshalling penalty is roughly equivalent to a map -lookup, but may involve several exception stack unwinds in unusual non-strict -or eccentric enumeration cases. +For the nominal case the marshalling penalty is roughly equivalent to a map lookup, but may involve +several exception stack unwinds in unusual non-strict or eccentric enumeration cases. .. note:: - The marshalling penalty can be eliminated by setting ``coerce`` to - ``False``. This will require the developer to manually coerce the - :class:`~django_enum.fields.EnumField` value to an Enum_ type object and is therefore usually - not recommended - but may be appropriate if the dominate use case involves - high volume serialization to a raw value instead. + The marshalling penalty can be eliminated by setting ``coerce`` to ``False``. This will require + you to manually coerce the :class:`~django_enum.fields.EnumField` value to an :class:`enum.Enum` + type object and is therefore usually not recommended - but may be appropriate if the dominate + use case involves high volume serialization to a primitive value instead. .. _flag_performance: @@ -114,11 +108,11 @@ Indexed All/Any Queries ----------------------- :class:`~django_enum.fields.EnumField` supplies new field lookups :ref:`has_all` and :ref:`has_any` -for fields with Flag_ enums. :ref:`has_all` will return rows where all the flags in the supplied -value are present on the row's column and :ref:`has_any` will return all rows where any flags on -the queried value are present on the row's column. These lookups pose challenges for indexing. The -plot below compares the performance of two indexing strategies for boolean columns to the single -index strategy for a flag :class:`~django_enum.fields.EnumField`. +for fields with :class:`enum.Flag` enums. :ref:`has_all` will return rows where all the flags in +the supplied value are present on the row's column and :ref:`has_any` will return all rows where +any flags on the queried value are present on the row's column. These lookups pose challenges for +indexing. The plot below compares the performance of two indexing strategies for boolean columns +to the single index strategy for a flag :class:`~django_enum.fields.EnumField`. The test compares a 16 flag bitmask to 16 boolean columns. In the multi index case, all boolean columns are indexed together. diff --git a/doc/source/reference.rst b/doc/source/reference.rst deleted file mode 100644 index 67973cc..0000000 --- a/doc/source/reference.rst +++ /dev/null @@ -1,90 +0,0 @@ -.. include:: refs.rst - -.. _reference: - -========= -Reference -========= - -.. _fields: - -Fields ------- - -.. automodule:: django_enum.fields - :members: - :undoc-members: - :show-inheritance: - :private-members: - - -Choices -------- - -.. automodule:: django_enum.choices - :members: - :undoc-members: - :show-inheritance: - :private-members: - -Filters -------- - -.. automodule:: django_enum.filters - :members: - :undoc-members: - :show-inheritance: - :private-members: - - -Forms ------ - -.. automodule:: django_enum.forms - :members: - :undoc-members: - :show-inheritance: - :private-members: - - -Query ------ - -.. automodule:: django_enum.query - :members: - :undoc-members: - :show-inheritance: - :private-members: - - -Serializer Fields ------------------ - -.. automodule:: django_enum.drf - :members: - :undoc-members: - :show-inheritance: - :private-members: - - -.. _urls: - -URLs ----- - -.. automodule:: django_enum.urls - :members: - :undoc-members: - :show-inheritance: - :private-members: - -.. _utilities: - -utilities ---------- - -.. automodule:: django_enum.utils - :members: - :undoc-members: - :show-inheritance: - :private-members: diff --git a/doc/source/reference/DRF.rst b/doc/source/reference/DRF.rst new file mode 100644 index 0000000..d33edff --- /dev/null +++ b/doc/source/reference/DRF.rst @@ -0,0 +1,13 @@ +.. include:: ../refs.rst + +.. _drf_ref: + +=== +DRF +=== + +.. automodule:: django_enum.drf + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/doc/source/reference/choices.rst b/doc/source/reference/choices.rst new file mode 100644 index 0000000..eded631 --- /dev/null +++ b/doc/source/reference/choices.rst @@ -0,0 +1,11 @@ +.. include:: ../refs.rst + +.. _choices_ref: + +======= +Choices +======= + +.. automodule:: django_enum.choices + :members: + :show-inheritance: diff --git a/doc/source/reference/fields.rst b/doc/source/reference/fields.rst new file mode 100644 index 0000000..b181f4c --- /dev/null +++ b/doc/source/reference/fields.rst @@ -0,0 +1,17 @@ +.. include:: ../refs.rst + +.. _fields: + +====== +Fields +====== + +.. note:: + + :class:`~django_enum.fields.EnumField` automatically determines the most appropriate database + column type based on the :class:`~enum.Enum` subclass it is assigned to. It is not recommended to + use the specialized field types listed here directly. + +.. automodule:: django_enum.fields + :members: + :show-inheritance: diff --git a/doc/source/reference/filters.rst b/doc/source/reference/filters.rst new file mode 100644 index 0000000..fc1ad70 --- /dev/null +++ b/doc/source/reference/filters.rst @@ -0,0 +1,14 @@ +.. include:: ../refs.rst + +.. _filters: + +======= +Filters +======= + +.. automodule:: django_enum.filters + :members: + :undoc-members: + :show-inheritance: + :private-members: + diff --git a/doc/source/reference/forms.rst b/doc/source/reference/forms.rst new file mode 100644 index 0000000..592ee59 --- /dev/null +++ b/doc/source/reference/forms.rst @@ -0,0 +1,13 @@ +.. include:: ../refs.rst + +.. _forms_ref: + +===== +Forms +===== + +.. automodule:: django_enum.forms + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 0000000..58819df --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,24 @@ +.. include:: ../refs.rst + +.. _reference: + +========= +Reference +========= + +.. automodule:: django_enum + +| + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + fields + choices + filters + forms + query + DRF + urls + utils diff --git a/doc/source/reference/query.rst b/doc/source/reference/query.rst new file mode 100644 index 0000000..97a8a85 --- /dev/null +++ b/doc/source/reference/query.rst @@ -0,0 +1,13 @@ +.. include:: ../refs.rst + +.. _query: + +===== +Query +===== + +.. automodule:: django_enum.query + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/doc/source/reference/urls.rst b/doc/source/reference/urls.rst new file mode 100644 index 0000000..1325ac3 --- /dev/null +++ b/doc/source/reference/urls.rst @@ -0,0 +1,13 @@ +.. include:: ../refs.rst + +.. _urls: + +==== +URLS +==== + +.. automodule:: django_enum.urls + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/doc/source/reference/utils.rst b/doc/source/reference/utils.rst new file mode 100644 index 0000000..f0fea24 --- /dev/null +++ b/doc/source/reference/utils.rst @@ -0,0 +1,13 @@ +.. include:: ../refs.rst + +.. _utilities: + +===== +Utils +===== + +.. automodule:: django_enum.utils + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/doc/source/refs.rst b/doc/source/refs.rst index 4306991..6d75e87 100644 --- a/doc/source/refs.rst +++ b/doc/source/refs.rst @@ -1,26 +1,19 @@ - -.. _Django: https://www.djangoproject.com/ +.. _Python: https://www.python.org +.. _Django: https://www.djangoproject.com .. _GitHub: https://github.com/bckohan/django-enum .. _PyPI: https://pypi.python.org/pypi/django-enum -.. _Enum: https://docs.python.org/3/library/enum.html#enum.Enum -.. _Flag: https://docs.python.org/3/library/enum.html#enum.Flag -.. _enumerations: https://docs.python.org/3/library/enum.html#enum.Enum -.. _ValueError: https://docs.python.org/3/library/exceptions.html#ValueError -.. _get_db_prep_value: https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.Field.get_db_prep_value -.. _from_db_value: https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.Field.from_db_value -.. _deconstruct: https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.Field.deconstruct -.. _to_python: https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.Field.to_python -.. _get_prep_value: https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.Field.get_prep_value -.. _full_clean: https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.full_clean +.. _PEP435: https://peps.python.org/pep-0435 .. _DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself -.. _enum-properties: https://pypi.org/project/enum-properties +.. _DRF: https://www.django-rest-framework.org .. _django-enum: https://pypi.org/project/django-enum .. _django-filter: https://pypi.org/project/django-filter .. _djangorestframework: https://pypi.org/project/djangorestframework -.. _django-render-static: https://pypi.org/project/django-render-static -.. _DRF: https://www.django-rest-framework.org -.. _Choices: https://docs.djangoproject.com/en/4.1/ref/models/fields/#enumeration-types -.. _TextChoices: https://docs.djangoproject.com/en/4.1/ref/models/fields/#enumeration-types -.. _IntegerChoices: https://docs.djangoproject.com/en/4.1/ref/models/fields/#enumeration-types - - +.. _PostgreSQL: https://www.postgresql.org +.. _MySQL: https://www.mysql.com +.. _MariaDB: https://mariadb.org +.. _Oracle: https://www.oracle.com/database +.. _SQLite: https://www.sqlite.org +.. _cx-Oracle: https://pypi.org/project/cx-Oracle +.. _psycopg2: https://pypi.org/project/psycopg2 +.. _psycopg3: https://pypi.org/project/psycopg +.. _mysqlclient: https://pypi.org/project/mysqlclient diff --git a/doc/source/tutorials/flags.rst b/doc/source/tutorials/flags.rst new file mode 100644 index 0000000..44338db --- /dev/null +++ b/doc/source/tutorials/flags.rst @@ -0,0 +1,55 @@ +.. include:: ../refs.rst + +.. _flags_bitfields: + +================= +Flags (BitFields) +================= + +There are many different Global Satellite Navigation Systems (GNSS) in operation today: + +* `GPS `_ +* `GLONASS `_ +* `Galileo `_ +* `BeiDou `_ +* `QZSS `_ +* `IRNSS `_ + +GNSS receivers may understand or be configured to track one or more of these systems. If we wanted +to build a data model of a GNSS receiver we would want to know which systems it can track. In +Django_ we might do this using a collection of boolean fields like this: + +.. literalinclude:: ../../../tests/examples/models/gnss_vanilla.py + +Which would allow us to check for receiver compatibility and filter requirements like this: + +.. literalinclude:: ../../../tests/examples/gnss_vanilla_tutorial.py + :lines: 3- + +This works pretty well. As our data scales though the waste of using an entire column for each +boolean can add up. We can do better by using a single column as a bit field. + +Python has a built-in :class:`enum.IntFlag` type that is used to represent bit fields. Bit fields +are useful for storing multiple boolean values in a single column. This is :ref:`much more space +efficient `. :class:`~django_enum.fields.EnumField` supports :class:`enum.IntFlag` +types out of the box. We could rewrite our GNSS receiver model like this: + +.. literalinclude:: ../../../tests/examples/models/gnss.py + :lines: 2- + +And use it like this: + +.. literalinclude:: ../../../tests/examples/gnss_tutorial.py + :lines: 3- + +The bit field model is much more compact and it does better in +:ref:`space efficiency and query performance `. We also get some additional lookup +types for :class:`~django_enum.fields.EnumField` that represent flags: :ref:`has_all` and +:ref:`has_any`. + +The defined indexes are not necessarily used in our examples. They may be partially engaged for the +:ref:`has_all` lookups but not for :ref:`has_any`. Our flag lookups perform optimized bit mask +operations in the database. The bit field example will out perform the boolean example because it +consumes much less memory while doing table scans with particular improvements in hard to index +:ref:`has_any` queries. + diff --git a/doc/source/tutorials/index.rst b/doc/source/tutorials/index.rst new file mode 100644 index 0000000..ecb8fb6 --- /dev/null +++ b/doc/source/tutorials/index.rst @@ -0,0 +1,29 @@ +.. include:: ../refs.rst + +.. _tutorials: + +========= +Tutorials +========= + +Enumerations in Python can provide rich class based interfaces well suited to many scenarios. We +present several real world scenarios here that demonstrate the capability of django-enum_ to get +your :class:`~django_enum.fields.EnumField` to do more work. + +In the :ref:`properties ` tutorial, we leverage :doc:`enum-properties:index` to +encapsulate more information onto our :class:`~enum.Enum` values so that any information needed in +different contexts is readily available without brittle mapping boilerplate. We also demonstrate +symmetric properties that are comparison equivalent to our enumeration values. + +In the :ref:`flags ` tutorial, we demonstrate how to use :class:`enum.Flag` +enumerations to represent bitfields in a database. This is a common pattern for storing multiple +boolean values in a single column. + +| + +.. toctree:: + :maxdepth: 2 + :caption: Tutorials: + + properties + flags diff --git a/doc/source/tutorials/properties.rst b/doc/source/tutorials/properties.rst new file mode 100644 index 0000000..8d7e3df --- /dev/null +++ b/doc/source/tutorials/properties.rst @@ -0,0 +1,58 @@ +.. include:: ../refs.rst + +.. _properties: + +========== +Properties +========== + +To run this example, we'll need to install django-enum_ with +:doc:`property support `: + +.. code-block:: bash + + > pip install "django-enum[properties]" + +MapBox Styles +------------- + +`MapBox `_ is a leading web mapping platform. It comes with a handful of default +`map styles `_. An enumeration is a +natural choice to represent these styles but the styles are complicated by versioning and are +identified by different properties depending on context. When used as a parameter in the MapBox API +they are in URI format, but in our interface we would prefer a more human friendly label, and in +code we prefer the brevity and reliability of an :class:`~enum.Enum` value attribute. + +Each MapBox style enumeration is therefore composed of 4 primary properties: + +1) A a human friendly label for the style +2) A name slug used in the URI +3) A version number for the style +4) The full URI specification of the style. + +Leveraging :class:`~enum_properties.IntEnumProperties` We might implement our style enumeration like +so: + +.. literalinclude:: ../../../tests/examples/models/mapbox.py + :language: python + :lines: 2- + +We've used a small integer as the value of the enumeration to save storage space. We've also added a +symmetric case insensitive slug and a version property. + +The version numbers will increment over time, but we're only concerned with the most recent +versions, so we'll increment their values in this enumeration as they change. Any version number +updates exist only in code and will be picked up as those persisted values are re-instantiated as +``MapBoxStyle`` enumerations. + +The last property we've added is the ``uri`` property. We've added it as concrete property on the +class because it can be created from the slug and version. We could have specified it in the value +tuple but that would be very verbose and less +`DRY `_. To make this property symmetric we +decorated it with :func:`~enum_properties.symmetric`. + +We can use our enumeration like so: + +.. literalinclude:: ../../../tests/examples/mapbox_tutorial.py + :language: python + :lines: 3- diff --git a/doc/source/usage.rst b/doc/source/usage.rst deleted file mode 100644 index e4a76e7..0000000 --- a/doc/source/usage.rst +++ /dev/null @@ -1,619 +0,0 @@ -.. include:: refs.rst - -===== -Usage -===== - -:class:`~django_enum.fields.EnumField` inherits from the appropriate native Django_ field and sets -the correct choice tuple set based on the enumeration type. This means ``EnumFields`` are -compatible with all modules, utilities and libraries that fields defined with a choice tuple are. -For example: - -.. code:: python - - from django.db import models - from django_enum import EnumField - - class MyModel(models.Model): - - class TextEnum(models.TextChoices): - - VALUE0 = 'V0', 'Value 0' - VALUE1 = 'V1', 'Value 1' - VALUE2 = 'V2', 'Value 2' - - txt_enum = EnumField(TextEnum, null=True, blank=True, default=None) - - txt_choices = models.CharField( - max_length=2, - choices=MyModel.TextEnum.choices, - null=True, - blank=True, - default=None - ) - -``txt_enum`` and ``txt_choices`` fields are equivalent in all ways with the -following exceptions: - -.. code:: python - - # txt_enum fields will always be an instance of the TextEnum type, unless - # set to a value that is not part of the enumeration - - assert isinstance(MyModel.objects.first().txt_enum, MyModel.TextEnum) - assert not isinstance(MyModel.objects.first().txt_choices, MyModel.TextEnum) - - # by default EnumFields are more strict, this is possible: - MyModel.objects.create(txt_choices='AA') - - # but this will throw a ValueError (unless strict=False) - MyModel.objects.create(txt_enum='AA') - - # and this will throw a ValidationError - MyModel(txt_enum='AA').full_clean() - -Any ``ModelForms``, DRF serializers and filters will behave the same way with -``txt_enum`` and ``txt_choices``. A few types are provided for deeper -integration with forms and django-filter_ but their usage is optional. -See :ref:`forms` and :ref:`filtering`. - -Very rich enumeration fields that encapsulate much more functionality in a -simple declarative syntax are possible with :class:`~django_enum.fields.EnumField`. See -:ref:`enum_props`. - - -External Enum Types -################### - -Enum_ classes defined externally to your code base or enum classes that otherwise do not inherit -from Django's Choices_ type, are supported. When no choices are present on an Enum_ type, -:class:`~django_enum.fields.EnumField` will attempt to use the ``label`` member on each -enumeration value if it is present, otherwise the labels will be based off the enumeration name. -Choices can also be overridden at the :class:`~django_enum.fields.EnumField` declaration. - -In short, :class:`~django_enum.fields.EnumField` should work with any subclass of Enum_. - -.. code:: python - - from enum import Enum - from django.db import models - from django_enum import EnumField - - class MyModel(models.Model): - - class TextEnum(str, Enum) - - VALUE0 = 'V0' - VALUE1 = 'V1' - VALUE2 = 'V2' - - txt_enum = EnumField(TextEnum) - -The above code will produce a choices set like ``[('V0', 'VALUE0'), ...]``. - -.. warning:: - - One nice feature of Django's Choices_ type is that it disables - ``auto()`` on Enum_ fields. ``auto()`` can be dangerous because the - values assigned depend on the order of declaration. This means that if the - order changes existing database values will no longer align with the - enumeration values. When using ``Enums`` where control over the values is - not certain it is a good idea to add integration tests that look for value - changes. - - -Parameters -########## - -All parameters available to the equivalent model field with choices may be set directly in the -:class:`~django_enum.fields.EnumField` instantiation. If not provided -:class:`~django_enum.fields.EnumField` will set ``choices`` and ``max_length`` automatically. - -The following :class:`~django_enum.fields.EnumField` specific parameters are available: - -``strict`` ----------- - -By default all ``EnumFields`` are ``strict``. This means a ``ValidationError`` -will be thrown anytime full_clean is run on a model and a value is set for the -field that can not be coerced to its native Enum_ type. To allow the field -to store values that are not present in the fields Enum_ type we can pass -`strict=False`. - -Non-strict fields that have values outside of the enumeration will be instances -of the enumeration where a valid Enum_ value is present and the plain old -data where no Enum_ type coercion is possible. - -.. code-block:: python - - class StrictExample(models.Model): - - class EnumType(TextChoices): - - ONE = '1', 'One' - TWO = '2', 'Two' - - non_strict = EnumField( - EnumType, - strict=False, - # it might be necessary to override max_length also, otherwise - # max_length will be 1 - max_length=10 - ) - - obj = StrictExample() - - # set to a valid EnumType value - obj.non_strict = '1' - # when accessed will be an EnumType instance - assert obj.non_strict is StrictExample.EnumType.ONE - - # we can also store any string less than or equal to length 10 - obj.non_strict = 'arbitrary' - obj.full_clean() # no errors - # when accessed will be a str instance - assert obj.non_strict == 'arbitrary' - -``constrained`` ---------------- - -By default all strict ``EnumFields`` are ``constrained``. This means that -`CheckConstraints `_ will be -generated at the database level to ensure that the column will reject any value that is not -present in the enumeration. This is a good idea for most use cases, but it can be turned off -by setting ``constrained`` to ``False``. - -.. note:: - - This is new in version 2.0. If you are upgrading from a previous version, you may set - this parameter to ``False`` to maintain the previous behavior. - -``primitive`` -------------- - -``EnumFields`` dynamically determine the database column type by determining the most appropriate -primitive type for the enumeration based on the enumeration values. You may override the primitive -determined by :class:`~django_enum.fields.EnumField` by passing a type to the ``primitive`` -parameter. You will likely not need to do this unless your enumeration is -:ref:`eccentric ` in some way. - -``coerce`` ----------- - -Setting this parameter to ``False`` will turn off the automatic conversion to -the field's Enum_ type while leaving all validation checks in place. It will -still be possible to set the field directly as an Enum_ instance and to -filter by Enum_ instance or any symmetric value: - -.. code-block:: python - - non_strict = EnumField( - EnumType, - strict=False, - coerce=False, - # it might be necessary to override max_length also, otherwise - # max_length will be 1 - max_length=10 - ) - - # set to a valid EnumType value - obj.non_strict = '1' - - # when accessed will be the primitive value - assert obj.non_strict == '1' - assert isinstance(obj.non_strict, str) - assert not isinstance(obj.non_strict, StrictExample.EnumType) - - -.. _enum_props: - -enum-properties -############### - -Almost any Enum_ type is supported, so you may make use of Enum_ extension libraries like -enum-properties_ to define very rich enumeration fields: - -.. code:: bash - - pip install enum-properties - -enum-properties_ is an extension to Enum_ that allows properties to be added to enumeration -instances using a simple declarative syntax. This is a less awkward and more compatible alternative -than dataclass enumerations. - -If you find yourself considering a dataclass enumeration, consider using enum-properties_ instead. -dataclass enumerations do not work with :class:`~django_enum.fields.EnumField` because their value -type is a dataclass. Futher, most libraries that expect to be able to work with enumerations expect -the ``value`` attribute to be a primitive serializable type. - -.. code-block:: python - - import typing as t - from enum_properties import StrEnumProperties, Symmetric - from django_enum.choices import TextChoices # use instead of Django's TextChoices - from django.db import models - - class TextChoicesExample(models.Model): - - class Color(StrEnumProperties): - - label: t.Annotated[str, Symmetric()] - rgb: t.Annotated[t.Tuple[int, int, int], Symmetric()] - hex: t.Annotated[str, Symmetric(case_fold=True)] - - # name value label rgb hex - RED = 'R', 'Red', (1, 0, 0), 'ff0000' - GREEN = 'G', 'Green', (0, 1, 0), '00ff00' - BLUE = 'B', 'Blue', (0, 0, 1), '0000ff' - - # any named s() values in the Enum's inheritance become properties on - # each value, and the enumeration value may be instantiated from the - # property's value - - color = EnumField(Color) - - instance = TextChoicesExample.objects.create( - color=TextChoicesExample.Color('FF0000') - ) - assert instance.color is TextChoicesExample.Color('Red') - assert instance.color is TextChoicesExample.Color('R') - assert instance.color is TextChoicesExample.Color((1, 0, 0)) - - # direct comparison to any symmetric value also works - assert instance.color == 'Red' - assert instance.color == 'R' - assert instance.color == (1, 0, 0) - - # save by any symmetric value - instance.color = 'FF0000' - - # access any enum property right from the model field - assert instance.color.hex == 'ff0000' - - # this also works! - assert instance.color == 'ff0000' - - # and so does this! - assert instance.color == 'FF0000' - - instance.save() - - # filtering works by any symmetric value or enum type instance - assert TextChoicesExample.objects.filter( - color=TextChoicesExample.Color.RED - ).first() == instance - - assert TextChoicesExample.objects.filter(color=(1, 0, 0)).first() == instance - - assert TextChoicesExample.objects.filter(color='FF0000').first() == instance - -For a real-world example see :ref:`examples`. - -It should be unnecessary, but if you need to integrate with code that expects an interface fully -compatible with Django's -`enumeration types `_ -(``TextChoices`` and ``IntegerChoices`` django-enum_ provides -:class:`~django_enum.choices.TextChoices`, :class:`~django_enum.choices.IntegerChoices`, -:class:`~django_enum.choices.FlagChoices` and :class:`~django_enum.choices.FloatChoices` types that -derive from enum-properties_ and Django's ``Choices``. So the above enumeration could also be -written: - -.. code-block:: python - - from django_enum.choices import TextChoices - - class Color(TextChoices): - - # label is added as a symmetric property by the base class - - rgb: Annotated[t.Tuple[int, int, int], Symmetric()] - hex: Annotated[str, Symmetric(case_fold=True)] - - # name value label rgb hex - RED = "R", "Red", (1, 0, 0), "ff0000" - GREEN = "G", "Green", (0, 1, 0), "00ff00" - BLUE = "B", "Blue", (0, 0, 1), "0000ff" - -.. note:: - - To use these ``Choices`` extensions you will need to install enum-properties_ which is an - optional dependency. - -.. _forms: - -Forms -##### - -An ``EnumChoiceField`` type is provided that enables symmetric value resolution -and will automatically coerce any set value to the underlying enumeration type. -Django_'s ``ModelForms`` will use this form field type to represent -``EnumFields`` by default. For most scenarios this is sufficient. The -``EnumChoiceField`` can also be explicitly used. For example, using our -``TextChoicesExample`` from above - if ``color`` was declared with -`strict=False`, we could add additional choices to our form field like so: - -.. code-block:: - - from django_enum.forms import EnumChoiceField - - class TextChoicesExampleForm(ModelForm): - - color = EnumChoiceField( - TextChoicesExample.Color, - strict=False, - choices=[ - ('P', 'Purple'), - ('O', 'Orange'), - ] + TextChoicesExample.Color.choices - ) - - class Meta: - model = TextChoicesExample - fields = '__all__' - - # when this form is rendered in a template it will include a selected - # option for the value 'Y' that is not part of our Color enumeration. - # since our field is not strict, we can set it to a value not in our - # enum or choice tuple. - form = TextChoicesExampleForm( - instance=TextChoicesExample.objects.create(color='Y') - ) - - -.. code-block:: html - - - - - -.. _rest_framework: - -Django Rest Framework -##################### - -By default DRF_ ``ModelSerializer`` will use a ``ChoiceField`` to represent an -:class:`~django_enum.fields.EnumField`. This works great, but it will not accept symmetric enumeration -values. A serializer field :class:`~django_enum.fields.EnumField` is provided that will. The dependency -on DRF_ is optional so to use the provided serializer field you must install -DRF_: - -.. code:: bash - - pip install djangorestframework - -.. code-block:: - - from django_enum.drf import EnumField - from rest_framework import serializers - - class ExampleSerializer(serializers.Serializer): - - color = EnumField(TextChoicesExample.Color) - - ser = ExampleSerializer(data={'color': (1, 0, 0)}) - assert ser.is_valid() - -The serializer :class:`~django_enum.fields.EnumField` accepts any arguments that ``ChoiceField`` -does. It also accepts the ``strict`` parameter which behaves the same way as it does -on the model field. - -.. _filtering: - -Filtering -######### - -As shown above, filtering by any value, enumeration type instance or symmetric -value works with Django_'s ORM. This is not natively true for automatically -generated ``FilterSets`` from django-filter_. Those filter sets will only be -filterable by direct enumeration value by default. An ``EnumFilter`` type is -provided to enable filtering by symmetric property values, but since the -dependency on django-filter_ is optional, you must first install it: - -.. code:: bash - - pip install django-filter - - -.. code-block:: - - from django_enum.filters import EnumFilter - from django_filters.views import FilterView - from django_filters import FilterSet - - class TextChoicesExampleFilterViewSet(FilterView): - - class TextChoicesExampleFilter(FilterSet): - - color = EnumFilter(TextChoicesExample.Color) - - class Meta: - model = TextChoicesExample - fields = '__all__' - - filterset_class = TextChoicesExampleFilter - model = TextChoicesExample - - # now filtering by symmetric value in url parameters works: - # e.g.: /?color=FF0000 - -An ``EnumFilterSet`` type is also provided that uses ``EnumFilter`` for ``EnumFields`` -by default. So the above is also equivalent to: - -.. code-block:: - - from django_enum.filters import FilterSet as EnumFilterSet - from django_filters.views import FilterView - - class TextChoicesExampleFilterViewSet(FilterView): - - class TextChoicesExampleFilter(EnumFilterSet): - class Meta: - model = TextChoicesExample - fields = '__all__' - - filterset_class = TextChoicesExampleFilter - model = TextChoicesExample - -.. _migrations: - -Migrations -########## - -.. important:: - - There is one rule for writing custom migration files for EnumFields: - *Never reference or import your enumeration classes in a migration file, - work with the primitive values instead*. - -The deconstructed ``EnumFields`` only include the choices tuple in the -migration files. This is because Enum_ classes may come and go or be -altered but the earlier migration files must still work. Simply treat any -custom migration routines as if they were operating on a normal model field -with choices. - -``EnumFields`` in migration files will not resolve the field values to -enumeration types. The fields will be the primitive enumeration values as they -are with any field with choices. - -Flag Enumerations -################# - -Python supports `bit masks `_ through the -`Flag `_ extension to Enum_. - -These enumerations are fully supported and will render as multi select form fields -by default. For example: - -.. code-block:: python - - from enum import IntFlag - from django_enum import EnumField - from django.db import models - - class MyModel(models.Model): - - class GNSSConstellation(IntFlag): - - GPS = 2**1 - GLONASS = 2**2 - GALILEO = 2**3 - BEIDOU = 2**4 - QZSS = 2**5 - - constellation = EnumField(GNSSConstellation) - - obj1 = MyModel.objects.create( - constellation=( - GNSSConstellation.GPS | - GNSSConstellation.GLONASS | - GNSSConstellation.GALILEO - ) - ) - obj2 = MyModel.objects.create(constellation=GNSSConstellation.GPS) - - assert GNSSConstellation.GPS in obj1.constellation - assert GNSSConstellation.GLONASS in obj1.constellation - -**Two new field lookups are provided for flag enumerations:** ``has_any`` **and** -``has_all``. - -.. _has_any: - -has_any -------- - -The ``has_any`` lookup will return any object that has at least one of the flags in the referenced -enumeration. For example: - -.. code-block:: python - - # this will return both obj1 and obj2 - MyModel.objects.filter( - constellation__has_any=GNSSConstellation.GPS | GNSSConstellation.QZSS - ) - -.. _has_all: - -has_all -------- - -The ``has_all`` lookup will return any object that has at least all of the flags in the referenced -enumeration. For example: - -.. code-block:: python - - # this will return only obj1 - MyModel.objects.filter( - constellation__has_all=GNSSConstellation.GPS | GNSSConstellation.GLONASS - ) - -**There are performance considerations when using a bit mask like a Flag enumeration instead of -multiple boolean columns.** See :ref:`flag performance ` for discussion and -benchmarks. - -Flags with more than 64 bits ----------------------------- - -Flag enumerations of arbitrary size are supported, however if the enum has more -than 64 flags it will be stored as a `BinaryField `_. -It is therefore strongly recommended to keep your Flag_ enumerations at 64 bits or less. - -.. warning:: - - Support for extra large flag fields is experimental. ``has_any`` and ``has_all`` do not work. - Most RDBMS systems do not support bitwise operations on binary fields. Future work may - involve exploring support for this as a Postgres extension. - - -URLs -#### - -django-enum_ provides a :ref:`converter ` that can be used to register enum url parameters -with the Django_ path resolver. - -.. code-block:: python - - from enum import IntEnum - - from django.http import HttpResponse - from django.urls import path - - from django_enum.urls import register_enum_converter - - - class Enum1(IntEnum): - A = 1 - B = 2 - - register_enum_converter(Enum1) - - def enum_converter_view(request, enum): - assert isinstance(enum, Enum1) - return HttpResponse(status=200) - - - # this will match paths /1/ and /2/ - urlpatterns = [ - path("", register_enum_converter, name="enum1_view"), - ] - -By default the converter will use the value property of the enumeration to resolve the enumeration, -but this can be overridden by passing the `prop` parameter, so we could for example use the label -instead. diff --git a/justfile b/justfile index 8cb0766..6fa3cfb 100644 --- a/justfile +++ b/justfile @@ -28,10 +28,11 @@ install_uv: install_uv: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" -# setup the venv and pre-commit hooks +# setup the venv, pre-commit hooks and playwright dependencies setup python="python": uv venv -p {{ python }} @just run pre-commit install + @just run playwright install # install git pre-commit hooks install-precommit: @@ -95,7 +96,7 @@ build-docs-html: install-docs _open-pdf-docs: import webbrowser from pathlib import Path - webbrowser.open(f"file://{Path('./doc/build/pdf/django-render-static.pdf').absolute()}") + webbrowser.open(f"file://{Path('./doc/build/pdf/django-enum.pdf').absolute()}") # build pdf documentation build-docs-pdf: install-docs diff --git a/pyproject.toml b/pyproject.toml index 0347b31..50dd8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "django-enum" -version = "2.1.0" +version = "2.2.0" description = "Full and natural support for enumerations as Django model fields." requires-python = ">=3.9,<4.0" authors = [ @@ -28,6 +28,7 @@ classifiers = [ "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", @@ -80,8 +81,10 @@ dev = [ "coverage>=7.6.12", "darglint>=1.8.1", "deepdiff>=8.2.0", + "django-extensions>=3.2.3", "django-stubs[compatible-mypy]>=5.1.3", - "django-test-migrations>=1.4.0", + "django-test-migrations @ git+https://github.com/bckohan/django-test-migrations.git@issue-503#egg=django-test-migrations", + "djlint>=1.36.4", "ipdb>=0.13.13", "matplotlib>=3.9.4", "mypy>=1.15.0", @@ -91,6 +94,8 @@ dev = [ "pytest>=8.3.4", "pytest-cov>=6.0.0", "pytest-django>=4.10.0", + "pytest-env>=1.1.5", + "pytest-playwright>=0.7.0", "python-dateutil>=2.9.0.post0", "ruff>=0.9.7", "tomlkit>=0.13.2", @@ -106,6 +111,7 @@ docs = [ "sphinx>=7.4.7", "sphinx-autobuild>=2024.10.3", "sphinx-tabs>=3.4.7", + "sphinxcontrib-django>=2.5", ] psycopg2 = [ "psycopg2>=2.9.10", @@ -142,6 +148,9 @@ sphinx = true [tool.pytest.ini_options] # py.test options: DJANGO_SETTINGS_MODULE = "tests.settings" +env = [ + "IS_PYTEST_RUN=1" +] python_files = "test*.py" norecursedirs = "*.egg .eggs dist build docs .tox .git __pycache__" diff --git a/src/django_enum/__init__.py b/src/django_enum/__init__.py index 1e2a2c3..4eb8193 100644 --- a/src/django_enum/__init__.py +++ b/src/django_enum/__init__.py @@ -1,19 +1,22 @@ r""" -******************************************************************************* - ___ _ ____ - / _ \ (_)__ ____ ___ ____ / __/__ __ ____ _ - / // / / / _ `/ _ \/ _ `/ _ \ / _// _ \/ // / ' \ -/____/_/ /\_,_/_//_/\_, /\___/ /___/_//_/\_,_/_/_/_/ - |___/ /___/ +:: -******************************************************************************* + ____ _ _____ + | _ \(_) __ _ _ __ __ _ ___ | ____|_ __ _ _ _ __ ___ + | | | | |/ _` | '_ \ / _` |/ _ \ | _| | '_ \| | | | '_ ` _ \ + | |_| | | (_| | | | | (_| | (_) | | |___| | | | |_| | | | | | | + |____// |\__,_|_| |_|\__, |\___/ |_____|_| |_|\__,_|_| |_| |_| + |__/ |___/ + + +Full and natural support for enumerations as Django model fields. """ from django_enum.fields import EnumField __all__ = ["EnumField"] -VERSION = (2, 1, 0) +VERSION = (2, 2, 0) __title__ = "Django Enum" __version__ = ".".join(str(i) for i in VERSION) diff --git a/src/django_enum/choices.py b/src/django_enum/choices.py index 2f84f75..8ab9bdf 100644 --- a/src/django_enum/choices.py +++ b/src/django_enum/choices.py @@ -5,6 +5,7 @@ """ import enum +import typing as t from django import VERSION as django_version from django.db.models import Choices @@ -33,18 +34,22 @@ class DjangoEnumPropertiesMeta(EnumPropertiesMeta, ChoicesType): # type: ignore """ @property - def names(self): + def names(self) -> t.List[str]: """ For some eccentric enums list(Enum) is empty, so we override names - if empty + if empty. + + :returns: list of enum value names """ return super().names or names(self, override=True) @property - def choices(self): + def choices(self) -> t.List[t.Tuple[t.Any, str]]: """ For some eccentric enums list(Enum) is empty, so we override choices if empty + + :returns: list of enum value choices """ return super().choices or choices(self, override=True) @@ -69,8 +74,6 @@ class TextChoices( def __hash__(self): return DjangoTextChoices.__hash__(self) - label: str - class IntegerChoices( DjangoSymmetricMixin, DjangoIntegerChoices, metaclass=DjangoEnumPropertiesMeta @@ -83,8 +86,6 @@ class IntegerChoices( def __hash__(self): return DjangoIntegerChoices.__hash__(self) - label: str - class FloatChoices( DjangoSymmetricMixin, float, Choices, metaclass=DjangoEnumPropertiesMeta @@ -100,8 +101,6 @@ def __hash__(self): def __str__(self): return str(self.value) - label: str - # mult inheritance type hint bug class FlagChoices( # type: ignore @@ -121,5 +120,3 @@ class FlagChoices( # type: ignore def __hash__(self): return enum.IntFlag.__hash__(self) - - label: str diff --git a/src/django_enum/drf.py b/src/django_enum/drf.py index 4d31603..9eec86b 100644 --- a/src/django_enum/drf.py +++ b/src/django_enum/drf.py @@ -70,9 +70,10 @@ def __getitem__(self, key: Any) -> Optional[Any]: class EnumField(ChoiceField): """ A djangorestframework serializer field for Enumeration types. If - unspecified ModelSerializers will assign django_enum.fields.EnumField - model field types to ChoiceFields. ChoiceFields do not accept - symmetrical values, this field will. + unspecified ModelSerializers will assign :class:`~django_enum.fields.EnumField` + model field types to `ChoiceField + `_ which will + not accept symmetrical values, this field will. :param enum: The type of the Enumeration of the field :param strict: If True (default) only values in the Enumeration type @@ -143,6 +144,9 @@ def __init__(self, enum: Type[Enum], strict: bool = strict, **kwargs): def to_internal_value(self, data: Any) -> Union[Enum, Any]: """ Transform the *incoming* primitive data into an enum instance. + + :return: The enum instance or the primitive value if the enum + instance could not be found. """ if data == "" and self.allow_blank: return "" diff --git a/src/django_enum/fields.py b/src/django_enum/fields.py index 7656fc2..2f249e0 100644 --- a/src/django_enum/fields.py +++ b/src/django_enum/fields.py @@ -334,7 +334,9 @@ class EnumField( and convert any values to the Enumeration type in question. :param enum: The enum class - :param strict: If True (default) the field will throw ValueErrors if the + :param primitive: The primitive type of the enumeration if different than the + default + :param strict: If True (default) the field will throw a :exc:`ValueError` if the value is not coercible to a valid enumeration type. :param coerce: If True (default) the field will always coerce values to the enum type when possible. If False, the field will contain the primitive @@ -462,7 +464,7 @@ def _try_coerce(self, value: Any, force: bool = False) -> Union[Enum, Any]: """ Attempt coercion of value to enumeration type instance, if unsuccessful and non-strict, coercion to enum's primitive type will be done, - otherwise a ValueError is raised. + otherwise a :exc:`ValueError` is raised. """ if self.enum is None: return value @@ -519,7 +521,7 @@ def deconstruct(self) -> Tuple[str, str, List, dict]: the choices tuple, which is plain old data and entirely sufficient to de/reconstruct our field. - See deconstruct_ + See :meth:`django.db.models.Field.deconstruct` """ name, path, args, kwargs = super().deconstruct() if self.enum is not None: @@ -544,7 +546,7 @@ def get_prep_value(self, value: Any) -> Any: """ Convert the database field value into the Enum type. - See get_prep_value_ + See :meth:`django.db.models.Field.get_prep_value` """ value = Field.get_prep_value(self, value) if self.enum: @@ -562,7 +564,7 @@ def get_db_prep_value(self, value, connection, prepared=False) -> Any: Convert the field value into the Enum type and then pull its value out. - See get_db_prep_value_ + See :meth:`django.db.models.Field.get_db_prep_value` """ if not prepared: value = self.get_prep_value(value) @@ -577,7 +579,7 @@ def from_db_value( """ Convert the database field value into the Enum type. - See from_db_value_ + See :meth:`django.db.models.Field.from_db_value` """ # give the super class converter a first whack if it exists value = getattr(super(), "from_db_value", lambda v: v)(value) @@ -595,7 +597,7 @@ def to_python(self, value: Any) -> Union[Enum, Any]: """ Converts the value in the enumeration type. - See to_python_ + See :meth:`django.db.models.Field.to_python` :param value: The value to convert :return: The converted value @@ -633,7 +635,7 @@ def validate(self, value: Any, model_instance: Optional[Model]): validation routines then tries to convert the value to a valid enumeration instance. - See full_clean_ + See :meth:`django.db.models.Model.full_clean` :param value: The value to validate :param model_instance: The model instance holding the value @@ -1264,7 +1266,7 @@ def get_prep_value(self, value: Any) -> Any: Convert the database field value into the Enum type then convert that enum value into the smallest number of bytes that can hold it. - See get_prep_value_ + See :meth:`django.db.models.Field.get_prep_value` """ if value is None or isinstance(value, (bytes, memoryview, bytearray)): return value @@ -1281,7 +1283,7 @@ def get_db_prep_value(self, value: Any, connection, prepared=False): Convert the field value into the Enum type and then pull its value out. - See get_db_prep_value_ + See :meth:`django.db.models.Field.get_db_prep_value` """ if value is None or isinstance(value, (bytes, memoryview, bytearray)): return value @@ -1302,7 +1304,7 @@ def from_db_value( """ Convert the database field value into the Enum type. - See from_db_value_ + See :meth:`django.db.models.Field.from_db_value` """ if value is None: return value diff --git a/src/django_enum/filters.py b/src/django_enum/filters.py index 957d9d4..2fce11c 100644 --- a/src/django_enum/filters.py +++ b/src/django_enum/filters.py @@ -1,4 +1,6 @@ -"""Support for django-filter""" +""" +Support for :doc:`django-filter `. +""" from typing import Tuple, Type @@ -11,24 +13,28 @@ class EnumFilter(TypedChoiceFilter): """ - Use this filter class instead of ``ChoiceFilter`` to get filters to - accept Enum labels and symmetric properties. + Use this filter class instead of :ref:`ChoiceFilter ` + to get filters to accept :class:`~enum.Enum` labels and symmetric properties. For example if we have an enumeration field defined with the following Enum: - .. code-block:: + .. code-block:: python - class Color(TextChoices, s('rgb'), s('hex', case_fold=True)): - RED = 'R', 'Red', (1, 0, 0), 'ff0000' + class Color(TextChoices): + + rgb: Annotated[Tuple[int, int, int], Symmetric()] + hex: Annotated[str, Symmetric(case_fold=True)] + + RED = 'R', 'Red', (1, 0, 0), 'ff0000' GREEN = 'G', 'Green', (0, 1, 0), '00ff00' - BLUE = 'B', 'Blue', (0, 0, 1), '0000ff' + BLUE = 'B', 'Blue', (0, 0, 1), '0000ff' color = EnumField(Color) - The default ``ChoiceFilter`` will only work with the enumeration - values: ?color=R, ?color=G, ?color=B. ``EnumFilter`` will accept query - parameter values from any of the symmetric properties: ?color=Red, + The default :ref:`ChoiceFilter ` will only work with + the enumeration values: ?color=R, ?color=G, ?color=B. ``EnumFilter`` will accept + query parameter values from any of the symmetric properties: ?color=Red, ?color=ff0000, etc... :param enum: The class of the enumeration containing the values to @@ -52,9 +58,11 @@ def __init__(self, *, enum, strict=False, **kwargs): class FilterSet(filterset.FilterSet): """ - Use this class instead of django-filter's ``FilterSet`` class to - automatically set all ``EnumField`` filters to ``EnumFilter`` by - default instead of ``ChoiceFilter``. + Use this class instead of the :doc:`django-filter ` + :doc:`FilterSet ` class to automatically set all + :class:`~django_enum.fields.EnumField` filters to + :class:`~django_enum.filters.EnumFilter` by default instead of + :ref:`ChoiceFilter `. """ @classmethod diff --git a/src/django_enum/forms.py b/src/django_enum/forms.py index b8de450..04fcdba 100644 --- a/src/django_enum/forms.py +++ b/src/django_enum/forms.py @@ -2,11 +2,10 @@ from copy import copy from decimal import DecimalException -from enum import Enum +from enum import Enum, Flag from typing import Any, Iterable, List, Optional, Protocol, Sequence, Tuple, Type, Union from django.core.exceptions import ValidationError -from django.db.models import Choices from django.forms.fields import ( Field, TypedChoiceField, @@ -21,6 +20,7 @@ "NonStrictSelect", "NonStrictSelectMultiple", "FlagSelectMultiple", + "ChoiceFieldMixin", "EnumChoiceField", "EnumFlagField", ] @@ -316,7 +316,7 @@ class EnumFlagField(ChoiceFieldMixin, TypedMultipleChoiceField): # type: ignore def __init__( self, - enum: Optional[Type[Choices]] = None, + enum: Optional[Type[Flag]] = None, *, empty_value: Any = _Unspecified, strict: bool = ChoiceFieldMixin._strict_, diff --git a/src/django_enum/urls.py b/src/django_enum/urls.py index 8579c26..20e5523 100644 --- a/src/django_enum/urls.py +++ b/src/django_enum/urls.py @@ -41,9 +41,14 @@ def register_enum_converter(enum: Type[Enum], type_name="", prop="value"): Register an enum converter for Django's URL dispatcher. :param enum: The enumeration type to register. - :param type_name: the name to use for the converter - :param prop: The property name to use to generate urls - by default the - value is used. + :param type_name: the name to use for the converter, defaults to the enum class + name: + + .. code-block:: python + + path("", view, view_name) + + :param prop: The property name to use in the urls - by default the value is used. """ register_converter( type( diff --git a/tests/converters/urls.py b/tests/converters/urls.py index 6cb585c..68da594 100644 --- a/tests/converters/urls.py +++ b/tests/converters/urls.py @@ -7,12 +7,12 @@ from tests.djenum.enums import Constants, DecimalEnum -class Enum1(IntEnum): +class TestEnum(IntEnum): A = 1 B = 2 -register_enum_converter(Enum1) +register_enum_converter(TestEnum) register_enum_converter(DecimalEnum, "decimal_enum") register_enum_converter(Constants, prop="label") @@ -25,7 +25,7 @@ def enum_converter_view(request, enum): urlpatterns = [ - path("", enum_converter_view, name="enum1_view"), + path("", enum_converter_view, name="enum1_view"), path("", enum_converter_view, name="decimal_enum_view"), path("", enum_converter_view, name="constants_view"), ] diff --git a/tests/examples/admin.py b/tests/examples/admin.py index 189d5c4..1d44590 100644 --- a/tests/examples/admin.py +++ b/tests/examples/admin.py @@ -1,17 +1,22 @@ +import sys from django.contrib import admin from tests.examples.models import ( - BitFieldExample, Map, - MyModel, + BasicExample, + FlagExample, NoCoerceExample, StrictExample, - TextChoicesExample, + PropertyExample, + ChoicesWithProperties, + TextChoicesExample ) admin.site.register(Map) admin.site.register(StrictExample) admin.site.register(NoCoerceExample) +admin.site.register(PropertyExample) +admin.site.register(BasicExample) +admin.site.register(FlagExample) +admin.site.register(ChoicesWithProperties) admin.site.register(TextChoicesExample) -admin.site.register(MyModel) -admin.site.register(BitFieldExample) diff --git a/tests/examples/basic_example.py b/tests/examples/basic_example.py new file mode 100644 index 0000000..34f4c54 --- /dev/null +++ b/tests/examples/basic_example.py @@ -0,0 +1,12 @@ +from .models.basic import BasicExample + +instance = BasicExample.objects.create( + txt_enum=BasicExample.TextEnum.VALUE1, + int_enum=3 # by-value assignment also works +) + +assert instance.txt_enum is BasicExample.TextEnum('V1') +assert instance.txt_enum.label == 'Value 1' + +assert instance.int_enum is BasicExample.IntEnum.THREE +assert instance.int_enum.value == 3 diff --git a/tests/examples/choice_form_howto.py b/tests/examples/choice_form_howto.py new file mode 100644 index 0000000..a7074b0 --- /dev/null +++ b/tests/examples/choice_form_howto.py @@ -0,0 +1,29 @@ +from .models.text_choices import TextChoicesExample +from django.forms import Form +from django_enum.forms import EnumChoiceField + + +class TextChoicesExampleForm(Form): + + color = EnumChoiceField(TextChoicesExample.Color) + + color_ext = EnumChoiceField( + TextChoicesExample.Color, + strict=False, + choices=[ + ('P', 'Purple'), + ('O', 'Orange'), + ] + TextChoicesExample.Color.choices + ) + + +# when this form is rendered in a template it will include a selected +# option for the value 'Y' that is not part of our Color enumeration. +# since our field is not strict, we can set it to a value not in our +# enum or choice tuple. +form = TextChoicesExampleForm( + initial={ + "color": TextChoicesExample.Color.RED, + "color_ext": "Y" + } +) diff --git a/tests/examples/custom_value_example.py b/tests/examples/custom_value_example.py new file mode 100644 index 0000000..2d56051 --- /dev/null +++ b/tests/examples/custom_value_example.py @@ -0,0 +1,12 @@ +from .models.custom_value import CustomValueExample +from django.db import models + +obj = CustomValueExample.objects.create( + str_props=CustomValueExample.StrPropsEnum.STR2 +) + +assert isinstance(obj._meta.get_field("str_props"), models.CharField) +assert obj.str_props is CustomValueExample.StrPropsEnum.STR2 +assert obj.str_props.value.upper == "STR2" +assert obj.str_props.value.lower == "str2" +print(f"{obj.str_props=}") diff --git a/tests/examples/drf_serializer_howto.py b/tests/examples/drf_serializer_howto.py new file mode 100644 index 0000000..3e4c547 --- /dev/null +++ b/tests/examples/drf_serializer_howto.py @@ -0,0 +1,13 @@ +from .models import TextChoicesExample + +from django_enum.drf import EnumField +from rest_framework import serializers + + +class ExampleSerializer(serializers.Serializer): + + color = EnumField(TextChoicesExample.Color) + + +ser = ExampleSerializer(data={'color': (1, 0, 0)}) +assert ser.is_valid() diff --git a/tests/examples/equivalency_howto.py b/tests/examples/equivalency_howto.py new file mode 100644 index 0000000..faed112 --- /dev/null +++ b/tests/examples/equivalency_howto.py @@ -0,0 +1,34 @@ +from .models.equivalency import EquivalencyExample +from django.core.exceptions import ValidationError + + +EquivalencyExample.objects.create(txt_enum='V0', txt_choices='V0') + +# txt_enum fields will always be an instance of the TextEnum type, unless +# set to a value that is not part of the enumeration + +assert isinstance( + EquivalencyExample.objects.first().txt_enum, + EquivalencyExample.TextEnum +) +assert isinstance( + EquivalencyExample.objects.first().txt_choices, + str +) + +# by default EnumFields are more strict, this is possible: +EquivalencyExample.objects.create(txt_choices='AA') + +# but this will throw a ValueError (unless strict=False) +try: + EquivalencyExample.objects.create(txt_enum='AA') + assert False +except ValueError: + assert True + +# and this will throw a ValidationError +try: + EquivalencyExample(txt_enum='AA').full_clean() + assert False +except ValidationError: + assert True diff --git a/tests/examples/extern_howto.py b/tests/examples/extern_howto.py new file mode 100644 index 0000000..9f1df89 --- /dev/null +++ b/tests/examples/extern_howto.py @@ -0,0 +1,13 @@ +from .models.extern import ExternalChoices + +assert ExternalChoices._meta.get_field('txt_enum1').choices == [ + ('V0', 'VALUE0'), + ('V1', 'VALUE1'), + ('V2', 'VALUE2') +] + +assert ExternalChoices._meta.get_field('txt_enum2').choices == [ + ('V0', 'Value0'), + ('V1', 'Value1'), + ('V2', 'Value2') +] diff --git a/tests/examples/filterfield_howto.py b/tests/examples/filterfield_howto.py new file mode 100644 index 0000000..7dfe024 --- /dev/null +++ b/tests/examples/filterfield_howto.py @@ -0,0 +1,21 @@ +from .models import TextChoicesExample +from django_enum.filters import EnumFilter +from django_filters.views import FilterView +from django_filters import FilterSet + + +class TextChoicesExampleFilterViewSet(FilterView): + + class TextChoicesExampleFilter(FilterSet): + + color = EnumFilter(enum=TextChoicesExample.Color) + + class Meta: + model = TextChoicesExample + fields = '__all__' + + filterset_class = TextChoicesExampleFilter + model = TextChoicesExample + +# now filtering by symmetric value in url parameters works: +# e.g.: /?color=FF0000 diff --git a/tests/examples/filterset_howto.py b/tests/examples/filterset_howto.py new file mode 100644 index 0000000..4326412 --- /dev/null +++ b/tests/examples/filterset_howto.py @@ -0,0 +1,14 @@ +from .models import TextChoicesExample +from django_enum.filters import FilterSet as EnumFilterSet +from django_filters.views import FilterView + + +class TextChoicesExampleFilterViewSet(FilterView): + + class TextChoicesExampleFilter(EnumFilterSet): + class Meta: + model = TextChoicesExample + fields = '__all__' + + filterset_class = TextChoicesExampleFilter + model = TextChoicesExample diff --git a/tests/examples/flag_example.py b/tests/examples/flag_example.py new file mode 100644 index 0000000..b4e039e --- /dev/null +++ b/tests/examples/flag_example.py @@ -0,0 +1,12 @@ +from .models import FlagExample +from .models.flag import Permissions + + +instance = FlagExample.objects.create( + permissions=Permissions.READ | Permissions.WRITE | Permissions.EXECUTE +) + +# get all models with at least RW: +assert instance in FlagExample.objects.filter( + permissions__has_all=Permissions.READ | Permissions.WRITE +) diff --git a/tests/examples/flag_form_howto.py b/tests/examples/flag_form_howto.py new file mode 100644 index 0000000..965a8b2 --- /dev/null +++ b/tests/examples/flag_form_howto.py @@ -0,0 +1,24 @@ +from .models.flag_howto import Group +from django.forms import Form +from django_enum.forms import EnumFlagField +from django_enum import utils + + +class PermissionsExampleForm(Form): + + permissions = EnumFlagField(Group.Permissions) + + permissions_ext = EnumFlagField( + Group.Permissions, + choices=[ + ((Group.Permissions.READ | Group.Permissions.WRITE).value, 'RW') + ] + utils.choices(Group.Permissions) + ) + + +form = PermissionsExampleForm( + initial={ + "permissions": Group.Permissions.READ | Group.Permissions.EXECUTE, + "permissions_ext": Group.Permissions.READ | Group.Permissions.WRITE + } +) diff --git a/tests/examples/flag_howto.py b/tests/examples/flag_howto.py new file mode 100644 index 0000000..7d6bc47 --- /dev/null +++ b/tests/examples/flag_howto.py @@ -0,0 +1,39 @@ +from .models.flag_howto import Group + +group = Group.objects.create( + permissions=(Group.Permissions.READ | Group.Permissions.EXECUTE) +) + +group1 = Group.objects.create( + permissions=(Group.Permissions.READ | Group.Permissions.EXECUTE) +) +group2 = Group.objects.create( + permissions=Group.Permissions.RWX +) + +assert Group.Permissions.READ in group1.permissions +assert Group.Permissions.WRITE not in group1.permissions +assert Group.Permissions.EXECUTE in group1.permissions + +assert Group.Permissions.READ in group2.permissions +assert Group.Permissions.WRITE in group2.permissions +assert Group.Permissions.EXECUTE in group2.permissions +assert group2.permissions is Group.Permissions.RWX + +# this will return both group1 and group2 +read_or_write = Group.objects.filter( + permissions__has_any=Group.Permissions.READ | Group.Permissions.WRITE +) +assert ( + group1 in read_or_write and + group2 in read_or_write +) + +# this will return only group2 +read_and_write = Group.objects.filter( + permissions__has_all=Group.Permissions.READ | Group.Permissions.WRITE +) +assert ( + group1 not in read_and_write and + group2 in read_and_write +) diff --git a/tests/examples/gnss_tutorial.py b/tests/examples/gnss_tutorial.py new file mode 100644 index 0000000..54c7bc7 --- /dev/null +++ b/tests/examples/gnss_tutorial.py @@ -0,0 +1,41 @@ +from .models import GNSSReceiver, Constellation + +receiver1 = GNSSReceiver.objects.create( + constellations=Constellation.GPS | Constellation.GLONASS +) + +receiver2 = GNSSReceiver.objects.create( + constellations=( + Constellation.GPS | + Constellation.GLONASS | + Constellation.GALILEO | + Constellation.BEIDOU + ) +) + +wanted = Constellation.GPS | Constellation.BEIDOU + +# check for GPS and BEIDOU +assert not ( + Constellation.GPS in receiver1.constellations and + Constellation.BEIDOU in receiver1.constellations +) +assert ( + Constellation.GPS in receiver2.constellations and + Constellation.BEIDOU in receiver2.constellations +) + +# we can treat IntFlags like bit masks so we can also check for having at +# least GPS and BEIDOU like this: +assert not wanted & receiver1.constellations == wanted +assert wanted & receiver2.constellations == wanted + +# get all receives that have at least GPS and BEIDOU +qry = GNSSReceiver.objects.filter(constellations__has_all=wanted) +assert receiver1 not in qry +assert receiver2 in qry + +# get all receivers that have either GPS or BEIDOU +qry = GNSSReceiver.objects.filter(constellations__has_any=wanted) +assert receiver1 in qry +assert receiver2 in qry diff --git a/tests/examples/gnss_vanilla_tutorial.py b/tests/examples/gnss_vanilla_tutorial.py new file mode 100644 index 0000000..95d7600 --- /dev/null +++ b/tests/examples/gnss_vanilla_tutorial.py @@ -0,0 +1,28 @@ +from .models import GNSSReceiverBasic +from django.db.models import Q + +receiver1 = GNSSReceiverBasic.objects.create( + gps=True, + glonass=True +) + +receiver2 = GNSSReceiverBasic.objects.create( + gps=True, + glonass=True, + galileo=True, + beidou=True +) + +# check for GPS and BEIDOU +assert not (receiver1.gps and receiver1.beidou) +assert receiver2.gps and receiver2.beidou + +# get all receives that have at least GPS and BEIDOU +qry = GNSSReceiverBasic.objects.filter(Q(gps=True) & Q(beidou=True)) +assert receiver1 not in qry +assert receiver2 in qry + +# get all receivers that have either GPS or BEIDOU +qry = GNSSReceiverBasic.objects.filter(Q(gps=True) | Q(beidou=True)) +assert receiver1 in qry +assert receiver2 in qry diff --git a/tests/examples/mapbox_tutorial.py b/tests/examples/mapbox_tutorial.py new file mode 100644 index 0000000..9e3e4dd --- /dev/null +++ b/tests/examples/mapbox_tutorial.py @@ -0,0 +1,23 @@ +from .models.mapbox import Map + +map = Map.objects.create() + +assert map.style.uri == 'mapbox://styles/mapbox/streets-v12' + +# uri's are symmetric +map.style = 'mapbox://styles/mapbox/light-v11' +map.full_clean() +assert map.style is Map.MapBoxStyle.LIGHT + +# comparisons can be made directly to symmetric property values +assert map.style == 3 +assert map.style == 'light' +assert map.style == 'mapbox://styles/mapbox/light-v11' + +# so are labels (also case insensitive) +map.style = 'satellite streets' +map.full_clean() +assert map.style is Map.MapBoxStyle.SATELLITE_STREETS + +# when used in API calls (coerced to strings) - they "do the right thing" +assert str(map.style) == 'mapbox://styles/mapbox/satellite-streets-v12' diff --git a/tests/examples/mixed_value_example.py b/tests/examples/mixed_value_example.py new file mode 100644 index 0000000..a8a5283 --- /dev/null +++ b/tests/examples/mixed_value_example.py @@ -0,0 +1,20 @@ +from .models.mixed_value import MixedValueExample, MixedValueEnum +from django.db import models + +obj = MixedValueExample.objects.create( + eccentric_str=MixedValueEnum.NONE, + eccentric_float=MixedValueEnum.NONE +) + +assert isinstance(obj._meta.get_field("eccentric_str"), models.CharField) +assert isinstance(obj._meta.get_field("eccentric_float"), models.FloatField) + +for en in list(MixedValueEnum): + obj.eccentric_str = en + obj.eccentric_float = en + obj.save() + obj.refresh_from_db() + assert obj.eccentric_str is en + assert obj.eccentric_float is en + print(f"{obj.eccentric_str=}") + print(f"{obj.eccentric_float=}") diff --git a/tests/examples/models.py b/tests/examples/models.py deleted file mode 100644 index e2ca534..0000000 --- a/tests/examples/models.py +++ /dev/null @@ -1,235 +0,0 @@ -from django.db import models -from enum import IntFlag -from enum_properties import s, Symmetric -import typing as t -from typing_extensions import Annotated -from django_enum import EnumField -from django_enum.choices import FlagChoices, IntegerChoices, TextChoices - - -class Map(models.Model): - - class MapBoxStyle(IntegerChoices): - """ - https://docs.mapbox.com/api/maps/styles/ - """ - - slug: Annotated[str, Symmetric(case_fold=True)] - version: int - - _symmetric_builtins_ = ["name", s("label", case_fold=True), "uri"] - - # name value label slug version - STREETS = 1, "Streets", "streets", 11 - OUTDOORS = 2, "Outdoors", "outdoors", 11 - LIGHT = 3, "Light", "light", 10 - DARK = 4, "Dark", "dark", 10 - SATELLITE = 5, "Satellite", "satellite", 9 - SATELLITE_STREETS = 6, "Satellite Streets", "satellite-streets", 11 - NAVIGATION_DAY = 7, "Navigation Day", "navigation-day", 1 - NAVIGATION_NIGHT = 8, "Navigation Night", "navigation-night", 1 - - @property - def uri(self): - return f"mapbox://styles/mapbox/{self.slug}-v{self.version}" - - def __str__(self): - return self.uri - - style = EnumField(MapBoxStyle, default=MapBoxStyle.STREETS) - - -class StrictExample(models.Model): - - class EnumType(TextChoices): - - ONE = "1", "One" - TWO = "2", "Two" - - non_strict = EnumField( - EnumType, - strict=False, - # it might be necessary to override max_length also, otherwise - # max_length will be 1 - max_length=10, - ) - - -class NoCoerceExample(models.Model): - - class EnumType(TextChoices): - - ONE = "1", "One" - TWO = "2", "Two" - - non_strict = EnumField( - EnumType, - strict=False, - coerce=False, - # it might be necessary to override max_length also, otherwise - # max_length will be 1 - max_length=10, - ) - - -class TextChoicesExample(models.Model): - - class Color(TextChoices): - - rgb: Annotated[t.Tuple[int, int, int], Symmetric()] - hex: Annotated[str, Symmetric(case_fold=True)] - - # name value label rgb hex - RED = "R", "Red", (1, 0, 0), "ff0000" - GREEN = "G", "Green", (0, 1, 0), "00ff00" - BLUE = "B", "Blue", (0, 0, 1), "0000ff" - - # any named s() values in the Enum's inheritance become properties on - # each value, and the enumeration value may be instantiated from the - # property's value - - color = EnumField(Color) - - -class MyModel(models.Model): - - class TextEnum(models.TextChoices): - - VALUE0 = "V0", "Value 0" - VALUE1 = "V1", "Value 1" - VALUE2 = "V2", "Value 2" - - class IntEnum(models.IntegerChoices): - - ONE = 1, "One" - TWO = ( - 2, - "Two", - ) - THREE = 3, "Three" - - - class Permissions(IntFlag): - - READ = 1**2 - WRITE = 2**2 - EXECUTE = 3**2 - - # this is equivalent to: - # CharField(max_length=2, choices=TextEnum.choices, null=True, blank=True) - txt_enum = EnumField(TextEnum, null=True, blank=True, default=None) - - # this is equivalent to - # PositiveSmallIntegerField(choices=IntEnum.choices, default=IntEnum.ONE.value) - int_enum = EnumField(IntEnum, default=IntEnum.ONE) - - permissions = EnumField(Permissions, null=True, blank=True) - - -class BitFieldExample(models.Model): - - class GPSRinexObservables(FlagChoices): - """ - https://files.igs.org/pub/data/format/rinex305.pdf - """ - - # name value label - C1C = 2**0, "C1C" - C1S = 2**1, "C1S" - C1L = 2**2, "C1L" - C1X = 2**3, "C1X" - C1P = 2**4, "C1P" - C1W = 2**5, "C1W" - C1Y = 2**6, "C1Y" - C1M = 2**7, "C1M" - - C2C = 2**8, "C2C" - C2D = 2**9, "C2D" - C2S = 2**10, "C2S" - C2L = 2**11, "C2L" - C2X = 2**12, "C2X" - C2P = 2**13, "C2P" - C2W = 2**14, "C2W" - C2Y = 2**15, "C2Y" - C2M = 2**16, "C2M" - - C5I = 2**17, "C5I" - C5Q = 2**18, "C5Q" - C5X = 2**19, "C5X" - - L1C = 2**20, "L1C" - L1S = 2**21, "L1S" - L1L = 2**22, "L1L" - L1X = 2**23, "L1X" - L1P = 2**24, "L1P" - L1W = 2**25, "L1W" - L1Y = 2**26, "L1Y" - L1M = 2**27, "L1M" - L1N = 2**28, "L1N" - - L2C = 2**29, "L2C" - L2D = 2**30, "L2D" - L2S = 2**31, "L2S" - L2L = 2**32, "L2L" - L2X = 2**33, "L2X" - L2P = 2**34, "L2P" - L2W = 2**35, "L2W" - L2Y = 2**36, "L2Y" - L2M = 2**37, "L2M" - L2N = 2**38, "L2N" - - L5I = 2**39, "L5I" - L5Q = 2**40, "L5Q" - L5X = 2**41, "L5X" - - D1C = 2**42, "D1C" - D1S = 2**43, "D1S" - D1L = 2**44, "D1L" - D1X = 2**45, "D1X" - D1P = 2**46, "D1P" - D1W = 2**47, "D1W" - D1Y = 2**48, "D1Y" - D1M = 2**49, "D1M" - D1N = 2**50, "D1N" - - D2C = 2**51, "D2C" - D2D = 2**52, "D2D" - D2S = 2**53, "D2S" - D2L = 2**54, "D2L" - D2X = 2**55, "D2X" - D2P = 2**56, "D2P" - D2W = 2**57, "D2W" - D2Y = 2**58, "D2Y" - D2M = 2**59, "D2M" - D2N = 2**60, "D2N" - - D5I = 2**61, "D5I" - D5Q = 2**62, "D5Q" - D5X = 2**63, "D5X" - - S1C = 2**64, "S1C" - S1S = 2**65, "S1S" - S1L = 2**66, "S1L" - S1X = 2**67, "S1X" - S1P = 2**68, "S1P" - S1W = 2**69, "S1W" - S1Y = 2**70, "S1Y" - S1M = 2**71, "S1M" - S1N = 2**72, "S1N" - - S2C = 2**73, "S2C" - S2D = 2**74, "S2D" - S2S = 2**75, "S2S" - S2L = 2**76, "S2L" - S2X = 2**77, "S2X" - S2P = 2**78, "S2P" - S2W = 2**79, "S2W" - S2Y = 2**80, "S2Y" - S2M = 2**81, "S2M" - S2N = 2**82, "S2N" - - S5I = 2**83, "S5I" - S5Q = 2**84, "S5Q" - S5X = 2**85, "S5X" - - observables = EnumField(GPSRinexObservables) diff --git a/tests/examples/models/__init__.py b/tests/examples/models/__init__.py new file mode 100644 index 0000000..27b560c --- /dev/null +++ b/tests/examples/models/__init__.py @@ -0,0 +1,17 @@ +import sys +from .mapbox import Map +from .strict import StrictExample +from .no_coerce import NoCoerceExample +from .properties import PropertyExample +from .properties_choices import ChoicesWithProperties +from .basic import BasicExample +from .flag import FlagExample +from .mixed_value import MixedValueExample +from .path_value import PathValueExample +from .custom_value import CustomValueExample +from .gnss import GNSSReceiver, Constellation +from .gnss_vanilla import GNSSReceiverBasic +from .equivalency import EquivalencyExample +from .extern import ExternalChoices +from .flag_howto import Group +from .text_choices import TextChoicesExample diff --git a/tests/examples/models/basic.py b/tests/examples/models/basic.py new file mode 100644 index 0000000..a32774f --- /dev/null +++ b/tests/examples/models/basic.py @@ -0,0 +1,28 @@ +# flake8: noqa +from django.db import models +from django_enum import EnumField + + +class BasicExample(models.Model): + + class TextEnum(models.TextChoices): + + VALUE0 = "V0", "Value 0" + VALUE1 = "V1", "Value 1" + VALUE2 = "V2", "Value 2" + + class IntEnum(models.IntegerChoices): + + # fmt: off + ONE = 1, "One" + TWO = 2, "Two" + THREE = 3, "Three" + # fmt: on + + # this is equivalent to: + # CharField(max_length=2, choices=TextEnum.choices, null=True, blank=True) + txt_enum = EnumField(TextEnum, null=True, blank=True, default=None) + + # this is equivalent to + # PositiveSmallIntegerField(choices=IntEnum.choices, default=IntEnum.ONE.value) + int_enum = EnumField(IntEnum, default=IntEnum.ONE) diff --git a/tests/examples/models/custom_value.py b/tests/examples/models/custom_value.py new file mode 100644 index 0000000..756bb4d --- /dev/null +++ b/tests/examples/models/custom_value.py @@ -0,0 +1,59 @@ +from enum import Enum +from django.db import models +from django_enum import EnumField + + +class StrProps: + """ + Wrap a string with some properties. + """ + + _str = '' + + def __init__(self, string): + self._str = string + + def __str__(self): + """ + coercion to str - str(StrProps('str1')) == 'str1' + """ + return self._str + + @property + def upper(self): + return self._str.upper() + + @property + def lower(self): + return self._str.lower() + + def __eq__(self, other): + """ + Make sure StrProps('str1') == 'str1' + """ + if isinstance(other, str): + return self._str == other + if other is not None: + return self._str == other._str + return False + + def deconstruct(self): + """ + Necessary to construct choices and default in migration files + """ + return ( + f'{self.__class__.__module__}.{self.__class__.__qualname__}', + (self._str,), + {} + ) + + +class CustomValueExample(models.Model): + + class StrPropsEnum(Enum): + + STR1 = StrProps('str1') + STR2 = StrProps('str2') + STR3 = StrProps('str3') + + str_props = EnumField(StrPropsEnum, primitive=str) diff --git a/tests/examples/models/equivalency.py b/tests/examples/models/equivalency.py new file mode 100644 index 0000000..35232f4 --- /dev/null +++ b/tests/examples/models/equivalency.py @@ -0,0 +1,21 @@ +from django.db import models +from django_enum import EnumField + + +class EquivalencyExample(models.Model): + + class TextEnum(models.TextChoices): + + VALUE0 = 'V0', 'Value 0' + VALUE1 = 'V1', 'Value 1' + VALUE2 = 'V2', 'Value 2' + + txt_enum = EnumField(TextEnum, null=True, blank=True, default=None) + + txt_choices = models.CharField( + max_length=2, + choices=TextEnum.choices, + null=True, + blank=True, + default=None + ) diff --git a/tests/examples/models/extern.py b/tests/examples/models/extern.py new file mode 100644 index 0000000..7663e81 --- /dev/null +++ b/tests/examples/models/extern.py @@ -0,0 +1,21 @@ +from enum import Enum +from django.db import models +from django_enum import EnumField + + +class ExternalChoices(models.Model): + + class TextEnum(str, Enum): + + VALUE0 = 'V0' + VALUE1 = 'V1' + VALUE2 = 'V2' + + # choices will default to (value, name) pairs + txt_enum1 = EnumField(TextEnum) + + # you can also override choices + txt_enum2 = EnumField( + TextEnum, + choices=[(en.value, en.name.title()) for en in TextEnum] + ) diff --git a/tests/examples/models/flag.py b/tests/examples/models/flag.py new file mode 100644 index 0000000..7af8334 --- /dev/null +++ b/tests/examples/models/flag.py @@ -0,0 +1,18 @@ +# flake8: noqa +from django.db import models +from enum import IntFlag +from django_enum import EnumField + + +class Permissions(IntFlag): + + # fmt: off + READ = 1<<0 + WRITE = 1<<1 + EXECUTE = 1<<2 + # fmt: on + + +class FlagExample(models.Model): + + permissions = EnumField(Permissions, null=True, blank=True) diff --git a/tests/examples/models/flag_howto.py b/tests/examples/models/flag_howto.py new file mode 100644 index 0000000..feee98e --- /dev/null +++ b/tests/examples/models/flag_howto.py @@ -0,0 +1,20 @@ +# flake8: noqa +from enum import IntFlag +from django_enum import EnumField +from django.db import models + + +class Group(models.Model): + + class Permissions(IntFlag): + + # fmt: off + READ = 1 << 0 + WRITE = 1 << 1 + EXECUTE = 1 << 2 + + # IntFlags can have composite values! + RWX = READ | WRITE | EXECUTE + # fmt: on + + permissions = EnumField(Permissions) diff --git a/tests/examples/models/gnss.py b/tests/examples/models/gnss.py new file mode 100644 index 0000000..282e03d --- /dev/null +++ b/tests/examples/models/gnss.py @@ -0,0 +1,21 @@ +# flake8: noqa +from django.db import models +from enum import IntFlag +from django_enum import EnumField + + +class Constellation(IntFlag): + + # fmt: off + GPS = 1 << 0 # 1 + GLONASS = 1 << 1 # 2 + GALILEO = 1 << 2 # 4 + BEIDOU = 1 << 3 # 8 + QZSS = 1 << 4 # 16 + IRNSS = 1 << 5 # 32 + # fmt: on + + +class GNSSReceiver(models.Model): + + constellations = EnumField(Constellation, db_index=True) diff --git a/tests/examples/models/gnss_vanilla.py b/tests/examples/models/gnss_vanilla.py new file mode 100644 index 0000000..d0228f0 --- /dev/null +++ b/tests/examples/models/gnss_vanilla.py @@ -0,0 +1,28 @@ +from django.db import models + + +class GNSSReceiverBasic(models.Model): + + gps = models.BooleanField(default=False) + glonass = models.BooleanField(default=False) + galileo = models.BooleanField(default=False) + beidou = models.BooleanField(default=False) + qzss = models.BooleanField(default=False) + irnss = models.BooleanField(default=False) + sbas = models.BooleanField(default=False) + + class Meta: + + # we can create an index for all fields, which will speed up queries for + # exact matches on these fields + indexes = [ + models.Index(fields=[ + 'gps', + 'glonass', + 'galileo', + 'beidou', + 'qzss', + 'irnss', + 'sbas' + ]) + ] diff --git a/tests/examples/models/mapbox.py b/tests/examples/models/mapbox.py new file mode 100644 index 0000000..7edc7a4 --- /dev/null +++ b/tests/examples/models/mapbox.py @@ -0,0 +1,39 @@ +# flake8: noqa +import typing as t +from django.db import models +from enum_properties import symmetric, Symmetric, IntEnumProperties +from django_enum import EnumField + + +class Map(models.Model): + + class MapBoxStyle(IntEnumProperties): + """ + https://docs.mapbox.com/api/maps/styles/ + """ + + label: t.Annotated[str, Symmetric(case_fold=True)] + slug: t.Annotated[str, Symmetric(case_fold=True)] + version: int + + # fmt: off + # name value label slug version + STREETS = 1, "Streets", "streets", 12 + OUTDOORS = 2, "Outdoors", "outdoors", 12 + LIGHT = 3, "Light", "light", 11 + DARK = 4, "Dark", "dark", 11 + SATELLITE = 5, "Satellite", "satellite", 9 + SATELLITE_STREETS = 6, "Satellite Streets", "satellite-streets", 12 + NAVIGATION_DAY = 7, "Navigation Day", "navigation-day", 1 + NAVIGATION_NIGHT = 8, "Navigation Night", "navigation-night", 1 + # fmt: on + + @symmetric() + @property + def uri(self): + return f"mapbox://styles/mapbox/{self.slug}-v{self.version}" + + def __str__(self): + return self.uri + + style = EnumField(MapBoxStyle, default=MapBoxStyle.STREETS) diff --git a/tests/examples/models/mixed_value.py b/tests/examples/models/mixed_value.py new file mode 100644 index 0000000..b15b8dc --- /dev/null +++ b/tests/examples/models/mixed_value.py @@ -0,0 +1,25 @@ +from decimal import Decimal +from enum import Enum +from django.db import models +from django_enum import EnumField + + +class MixedValueEnum(Enum): + + NONE = None + VAL1 = 1 + VAL2 = '2.0' + VAL3 = 3.0 + VAL4 = Decimal('4.5') + + +class MixedValueExample(models.Model): + + # Since None is an enumeration value, EnumField will automatically set + # null=True on these model fields. + + # column will be a CharField + eccentric_str = EnumField(MixedValueEnum) + + # column will be a FloatField + eccentric_float = EnumField(MixedValueEnum, primitive=float) diff --git a/tests/examples/models/no_coerce.py b/tests/examples/models/no_coerce.py new file mode 100644 index 0000000..97a5bba --- /dev/null +++ b/tests/examples/models/no_coerce.py @@ -0,0 +1,20 @@ +from django.db import models +from django_enum import EnumField +from django.db.models import TextChoices + + +class NoCoerceExample(models.Model): + + class EnumType(TextChoices): + + ONE = "1", "One" + TWO = "2", "Two" + + non_strict = EnumField( + EnumType, + strict=False, + coerce=False, + # it might be necessary to override max_length also, otherwise + # max_length will be 1 + max_length=10, + ) diff --git a/tests/examples/models/path_value.py b/tests/examples/models/path_value.py new file mode 100644 index 0000000..4288ca3 --- /dev/null +++ b/tests/examples/models/path_value.py @@ -0,0 +1,15 @@ +from django.db import models +from enum import Enum +from django_enum import EnumField +from pathlib import Path + + +class PathValueExample(models.Model): + + class PathEnum(Enum): + + USR = Path('/usr') + USR_LOCAL = Path('/usr/local') + USR_LOCAL_BIN = Path('/usr/local/bin') + + path = EnumField(PathEnum, primitive=str) diff --git a/tests/examples/models/properties.py b/tests/examples/models/properties.py new file mode 100644 index 0000000..656bb68 --- /dev/null +++ b/tests/examples/models/properties.py @@ -0,0 +1,28 @@ +# flake8: noqa +import typing as t +from django.db import models +from enum_properties import Symmetric, StrEnumProperties +from typing_extensions import Annotated +from django_enum import EnumField + + +class PropertyExample(models.Model): + + class Color(StrEnumProperties): + + label: str + rgb: Annotated[t.Tuple[int, int, int], Symmetric()] + hex: Annotated[str, Symmetric(case_fold=True)] + + # fmt: off + # name value label rgb hex + RED = "R", "Red", (1, 0, 0), "ff0000" + GREEN = "G", "Green", (0, 1, 0), "00ff00" + BLUE = "B", "Blue", (0, 0, 1), "0000ff" + # fmt: on + + # any type hints before the values in the Enum's definition become + # properties on each value, and the enumeration value may be + # instantiated from any symmetric property's value + + color = EnumField(Color) diff --git a/tests/examples/models/properties_choices.py b/tests/examples/models/properties_choices.py new file mode 100644 index 0000000..30521c7 --- /dev/null +++ b/tests/examples/models/properties_choices.py @@ -0,0 +1,24 @@ +# flake8: noqa +import typing as t +from django.db import models +from enum_properties import Symmetric +from typing_extensions import Annotated +from django_enum import EnumField +from django_enum.choices import TextChoices + +class ChoicesWithProperties(models.Model): + + class Color(TextChoices): + + # label is added as a symmetric property by the base class + rgb: Annotated[t.Tuple[int, int, int], Symmetric()] + hex: Annotated[str, Symmetric(case_fold=True)] + + # fmt: off + # name value label rgb hex + RED = "R", "Red", (1, 0, 0), "ff0000" + GREEN = "G", "Green", (0, 1, 0), "00ff00" + BLUE = "B", "Blue", (0, 0, 1), "0000ff" + # fmt: on + + color = EnumField(Color) diff --git a/tests/examples/models/strict.py b/tests/examples/models/strict.py new file mode 100644 index 0000000..831cdb4 --- /dev/null +++ b/tests/examples/models/strict.py @@ -0,0 +1,18 @@ +from django.db import models +from django_enum import EnumField + + +class StrictExample(models.Model): + + class EnumType(models.TextChoices): + + ONE = "1", "One" + TWO = "2", "Two" + + non_strict = EnumField( + EnumType, + strict=False, + # it might be necessary to override max_length also, + # otherwise max_length will be 1 + max_length=10, + ) diff --git a/tests/examples/models/text_choices.py b/tests/examples/models/text_choices.py new file mode 100644 index 0000000..77ccb72 --- /dev/null +++ b/tests/examples/models/text_choices.py @@ -0,0 +1,33 @@ +# flake8: noqa +import typing as t +from django.db import models +from typing_extensions import Annotated +from django_enum import EnumField +from django_enum.choices import TextChoices +from enum_properties import Symmetric, symmetric + + +class TextChoicesExample(models.Model): + + class Color(TextChoices): + + # no need to specify label because it is built in + rgb: Annotated[t.Tuple[int, int, int], Symmetric()] + hex: Annotated[str, Symmetric(case_fold=True)] + + # fmt: off + # name value label rgb hex + RED = "R", "Red", (1, 0, 0), "ff0000" + GREEN = "G", "Green", (0, 1, 0), "00ff00" + BLUE = "B", "Blue", (0, 0, 1), "0000ff" + # fmt: on + + # by default label is symmetric, but case sensitive + # to make it case-insensitive we can override the property + # and mark it like this: + @symmetric(case_fold=True) + @property + def label(self) -> str: + return self._label_ + + color = EnumField(Color) diff --git a/tests/examples/no_coerce_howto.py b/tests/examples/no_coerce_howto.py new file mode 100644 index 0000000..adb74e8 --- /dev/null +++ b/tests/examples/no_coerce_howto.py @@ -0,0 +1,12 @@ +from .models import NoCoerceExample + + +obj = NoCoerceExample() + +# set to a valid EnumType value +obj.non_strict = '1' + +# when accessed will be the primitive value +assert obj.non_strict == '1' +assert isinstance(obj.non_strict, str) +assert not isinstance(obj.non_strict, NoCoerceExample.EnumType) diff --git a/tests/examples/path_value_example.py b/tests/examples/path_value_example.py new file mode 100644 index 0000000..546227e --- /dev/null +++ b/tests/examples/path_value_example.py @@ -0,0 +1,9 @@ +from .models.path_value import PathValueExample +from django.db import models + +obj = PathValueExample.objects.create( + path=PathValueExample.PathEnum.USR_LOCAL_BIN, +) + +assert isinstance(obj._meta.get_field("path"), models.CharField) +assert obj.path is PathValueExample.PathEnum.USR_LOCAL_BIN diff --git a/tests/examples/properties_example.py b/tests/examples/properties_example.py new file mode 100644 index 0000000..89d2d82 --- /dev/null +++ b/tests/examples/properties_example.py @@ -0,0 +1,40 @@ +from .models import PropertyExample + + +instance = PropertyExample.objects.create( + color=PropertyExample.Color('FF0000') +) +assert instance.color is PropertyExample.Color['RED'] +assert instance.color is PropertyExample.Color('R') +assert instance.color is PropertyExample.Color((1, 0, 0)) +# note that we did not make label symmetric, so this does not work: +# PropertyExample.Color('Red') + +# direct comparison to any symmetric value also works +assert instance.color == 'FF0000' +assert instance.color == 'R' +assert instance.color == (1, 0, 0) +assert instance.color != 'Red' # because label is not symmetric + +# save by any symmetric value +instance.color = 'FF0000' + +# access any enum property right from the model field +assert instance.color.hex == 'ff0000' + +# this also works! +assert instance.color == 'ff0000' + +# and so does this! +assert instance.color == 'FF0000' + +instance.save() + +# filtering works by any symmetric value or enum type instance +assert PropertyExample.objects.filter( + color=PropertyExample.Color.RED +).first() == instance + +assert PropertyExample.objects.filter(color=(1, 0, 0)).first() == instance + +assert PropertyExample.objects.filter(color='FF0000').first() == instance diff --git a/tests/examples/strict_howto.py b/tests/examples/strict_howto.py new file mode 100644 index 0000000..8fe7b9e --- /dev/null +++ b/tests/examples/strict_howto.py @@ -0,0 +1,15 @@ +from .models import StrictExample + + +obj = StrictExample() + +# set to a valid EnumType value +obj.non_strict = '1' +# when accessed will be an EnumType instance +assert obj.non_strict is StrictExample.EnumType.ONE + +# we can also store any string less than or equal to length 10 +obj.non_strict = 'arbitrary' +obj.full_clean() # no errors +# when accessed will be a str instance +assert obj.non_strict == 'arbitrary' diff --git a/tests/examples/templates/tests_examples/choice_form_howto.html b/tests/examples/templates/tests_examples/choice_form_howto.html new file mode 100644 index 0000000..138f314 --- /dev/null +++ b/tests/examples/templates/tests_examples/choice_form_howto.html @@ -0,0 +1,15 @@ + + + + + + EnumChoiceField Form + + +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + diff --git a/tests/examples/templates/tests_examples/flag_form_howto.html b/tests/examples/templates/tests_examples/flag_form_howto.html new file mode 100644 index 0000000..73d2d17 --- /dev/null +++ b/tests/examples/templates/tests_examples/flag_form_howto.html @@ -0,0 +1,15 @@ + + + + + + EnumFlagField Form + + +
+ {% csrf_token %} + {{ form.as_p }} + +
+ + diff --git a/tests/examples/templates/tests_examples/textchoicesexample_filter.html b/tests/examples/templates/tests_examples/textchoicesexample_filter.html new file mode 100644 index 0000000..3e532af --- /dev/null +++ b/tests/examples/templates/tests_examples/textchoicesexample_filter.html @@ -0,0 +1,24 @@ + + + + + + Filtered List + + +

Filtered Objects

+ +
+ {{ filter.form.as_p }} + +
+ +
    + {% for obj in filter.qs %} +
  • {{ obj.color.label }}
  • + {% empty %} +
  • No results found.
  • + {% endfor %} +
+ + diff --git a/tests/examples/text_choices_howto.py b/tests/examples/text_choices_howto.py new file mode 100644 index 0000000..ed3e1b6 --- /dev/null +++ b/tests/examples/text_choices_howto.py @@ -0,0 +1,14 @@ +from .models import TextChoicesExample + + +obj = TextChoicesExample.objects.create(color=TextChoicesExample.Color.RED) + +assert obj.color is TextChoicesExample.Color.RED +assert obj.color.label == 'Red' +assert obj.color.rgb == (1, 0, 0) +assert obj.color.hex == 'ff0000' + +# enum-properties symmetric properties work as expected +assert obj.color == 'Red' +assert obj.color == (1, 0, 0) +assert obj.color == 'ff0000' diff --git a/tests/examples/url_converter_howto.py b/tests/examples/url_converter_howto.py new file mode 100644 index 0000000..17d95e8 --- /dev/null +++ b/tests/examples/url_converter_howto.py @@ -0,0 +1,9 @@ +from django.urls import reverse +from tests.examples.urls import Enum1 + + +assert reverse("examples:enum_default", kwargs={"enum": Enum1.A}) == "/1" +assert reverse("examples:enum_default", kwargs={"enum": Enum1.B}) == "/2" + +assert reverse("examples:enum_by_name", kwargs={"enum": Enum1.A}) == "/A" +assert reverse("examples:enum_by_name", kwargs={"enum": Enum1.B}) == "/B" diff --git a/tests/examples/urls.py b/tests/examples/urls.py new file mode 100644 index 0000000..c080e9c --- /dev/null +++ b/tests/examples/urls.py @@ -0,0 +1,31 @@ +from enum import IntEnum + +from django.http import HttpResponse +from django.urls import path + +from django_enum.urls import register_enum_converter + + +class Enum1(IntEnum): + A = 1 + B = 2 + + +register_enum_converter(Enum1) +register_enum_converter(Enum1, type_name="Enum1ByName", prop="name") + + +def enum_converter_view(request, enum): + assert isinstance(enum, Enum1) + return HttpResponse(status=200, content=f"{enum=}") + + +app_name = "examples" + +urlpatterns = [ + # this will match paths /1 and /2 + path("", enum_converter_view, name="enum_default"), + + # this will match paths /A and /B + path("", enum_converter_view, name="enum_by_name"), +] diff --git a/tests/examples/urls_forms.py b/tests/examples/urls_forms.py new file mode 100644 index 0000000..c165c47 --- /dev/null +++ b/tests/examples/urls_forms.py @@ -0,0 +1,44 @@ +from django.urls import path +try: + from django.shortcuts import render + from .choice_form_howto import TextChoicesExampleForm, TextChoicesExample + from .flag_form_howto import PermissionsExampleForm, Group + + def choice_form_view(request): + if request.method == "POST": + form = TextChoicesExampleForm(request.POST) + if form.is_valid(): + # Process the form data (for now, just print it) + print("Valid form data:", form.cleaned_data) + else: + form = TextChoicesExampleForm( + initial={ + "color": TextChoicesExample.Color.RED, + "color_ext": "Y" + } + ) + return render(request, "tests_examples/choice_form_howto.html", {"form": form}) + + def flag_form_view(request): + if request.method == "POST": + form = PermissionsExampleForm(request.POST) + if form.is_valid(): + # Process the form data (for now, just print it) + print("Valid form data:", form.cleaned_data) + else: + form = PermissionsExampleForm( + initial={ + "permissions": Group.Permissions.READ | Group.Permissions.EXECUTE, + "permissions_ext": Group.Permissions.READ | Group.Permissions.WRITE + } + ) + return render(request, "tests_examples/flag_form_howto.html", {"form": form}) + + app_name = "howto_forms" + + urlpatterns = [ + path("choice/", choice_form_view, name="choice"), + path("flag/", flag_form_view, name="flag"), + ] +except ImportError: + urlpatterns = [] diff --git a/tests/examples/urls_howto.py b/tests/examples/urls_howto.py new file mode 100644 index 0000000..13af83e --- /dev/null +++ b/tests/examples/urls_howto.py @@ -0,0 +1,23 @@ +from django.urls import path +try: + from .filterfield_howto import TextChoicesExampleFilterViewSet + from .filterset_howto import ( + TextChoicesExampleFilterViewSet as TextChoicesExampleFilterSetViewSet + ) + + app_name = "howto" + + urlpatterns = [ + path( + 'filterfield/', + TextChoicesExampleFilterViewSet.as_view(), + name='filterfield' + ), + path( + 'filterset/', + TextChoicesExampleFilterSetViewSet.as_view(), + name='filterset' + ), + ] +except ImportError: + urlpatterns = [] diff --git a/tests/settings.py b/tests/settings.py index 1288294..6ab6fb6 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -5,6 +5,7 @@ from django import VERSION as django_version +DEBUG = not os.environ.get("IS_PYTEST_RUN", False) SECRET_KEY = "psst" SITE_ID = 1 USE_TZ = False @@ -96,6 +97,7 @@ MIDDLEWARE = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", + # "debug_toolbar.middleware.DebugToolbarMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", @@ -109,6 +111,8 @@ "tests.converters", "tests.djenum", "tests.tmpls", + # "debug_toolbar", + "django_extensions", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", @@ -117,6 +121,11 @@ "django.contrib.staticfiles", "django.contrib.admin", ] +INTERNAL_IPS = [ + # ... + "127.0.0.1", + # ... +] if django_version[0:2] >= (5, 0): # pragma: no cover INSTALLED_APPS.insert(0, "tests.db_default") diff --git a/tests/test_converter.py b/tests/test_converter.py index 3abfa04..d539dbc 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -7,22 +7,24 @@ def test_enum_converter(self): from django.urls import reverse from django.urls.converters import get_converters - from tests.converters.urls import Enum1, record + from tests.converters.urls import TestEnum, record from tests.djenum.enums import Constants, DecimalEnum - converter = get_converters()["Enum1"] + converter = get_converters()["TestEnum"] self.assertEqual(converter.regex, "1|2") - self.assertEqual(converter.to_python("1"), Enum1.A) - self.assertEqual(converter.to_python("2"), Enum1.B) + self.assertEqual(converter.to_python("1"), TestEnum.A) + self.assertEqual(converter.to_python("2"), TestEnum.B) self.assertEqual(converter.primitive, int) - self.assertEqual(converter.enum, Enum1) + self.assertEqual(converter.enum, TestEnum) self.assertEqual(converter.prop, "value") - self.assertEqual(reverse("enum1_view", kwargs={"enum": Enum1.A}), "/1") + self.assertEqual( + reverse("enum1_view", kwargs={"enum": TestEnum.A}), "/converters/1" + ) - response = self.client.get("/1") + response = self.client.get("/converters/1") self.assertEqual(response.status_code, 200) - self.assertEqual(record[0], Enum1.A) + self.assertEqual(record[0], TestEnum.A) converter = get_converters()["decimal_enum"] self.assertEqual(converter.regex, "0.99|0.999|0.9999|99.9999|999") @@ -33,10 +35,11 @@ def test_enum_converter(self): self.assertEqual(converter.prop, "value") self.assertEqual( - reverse("decimal_enum_view", kwargs={"enum": DecimalEnum.ONE}), "/0.99" + reverse("decimal_enum_view", kwargs={"enum": DecimalEnum.ONE}), + "/converters/0.99", ) - response = self.client.get("/0.99") + response = self.client.get("/converters/0.99") self.assertEqual(response.status_code, 200) self.assertEqual(record[1], DecimalEnum.ONE) @@ -51,9 +54,9 @@ def test_enum_converter(self): self.assertEqual( reverse("constants_view", kwargs={"enum": Constants.GOLDEN_RATIO}), - "/Golden%20Ratio", + "/converters/Golden%20Ratio", ) - response = self.client.get("/Euler's Number") + response = self.client.get("/converters/Euler's Number") self.assertEqual(response.status_code, 200) self.assertEqual(record[2], Constants.e) diff --git a/tests/test_examples.py b/tests/test_examples.py index eaf8929..59b3e68 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -3,14 +3,15 @@ pytest.importorskip("enum_properties") from django.test import TestCase - -from tests.enum_prop.models import MyModel from django.core.exceptions import ValidationError from django.forms import ModelForm +from tests.utils import DJANGO_FILTERS, DJANGO_REST_FRAMEWORK -class TestExamples(TestCase): +class ExampleTests(TestCase): # pragma: no cover - why is this necessary? def test_readme(self): + from tests.enum_prop.models import MyModel + instance = MyModel.objects.create( txt_enum=MyModel.TextEnum.VALUE1, int_enum=3, # by-value assignment also works @@ -36,29 +37,16 @@ def test_readme(self): self.assertEqual(instance.color.hex, "ff0000") - """ - # Django breaks auto - def test_auto_enum(self): - from django_enum import IntegerChoices - from enum import auto - - class AutoEnum(IntegerChoices): - ONE = auto(), 'One' - TWO = auto(), 'Two' - THREE = auto(), 'Three' - """ - - -class ExampleTests(TestCase): # pragma: no cover - why is this necessary? def test_mapboxstyle(self): - from tests.examples.models import Map + from tests.examples import mapbox_tutorial + from tests.examples.models.mapbox import Map map_obj = Map.objects.create() - self.assertTrue(map_obj.style.uri == "mapbox://styles/mapbox/streets-v11") + self.assertTrue(map_obj.style.uri == "mapbox://styles/mapbox/streets-v12") # uri's are symmetric - map_obj.style = "mapbox://styles/mapbox/light-v10" + map_obj.style = "mapbox://styles/mapbox/light-v11" self.assertTrue(map_obj.style == Map.MapBoxStyle.LIGHT) self.assertTrue(map_obj.style == 3) self.assertTrue(map_obj.style == "light") @@ -70,23 +58,87 @@ def test_mapboxstyle(self): # when used in API calls (coerced to strings) - they "do the right # thing" self.assertTrue( - str(map_obj.style) == "mapbox://styles/mapbox/satellite-streets-v11" + str(map_obj.style) == "mapbox://styles/mapbox/satellite-streets-v12" ) - def test_color(self): - from tests.examples.models import TextChoicesExample + def test_properties(self): + from tests.examples import properties_example + from tests.examples.models import PropertyExample + + PropertyExample.objects.all().delete() + + instance = PropertyExample.objects.create(color=PropertyExample.Color("FF0000")) + self.assertTrue( + instance.color + == PropertyExample.Color("RED") + == PropertyExample.Color("R") + == PropertyExample.Color((1, 0, 0)) + ) + + # direct comparison to any symmetric value also works + self.assertTrue(instance.color == "FF0000") + self.assertFalse(instance.color == "Red") + self.assertTrue(instance.color == "R") + self.assertTrue(instance.color == (1, 0, 0)) + + # save by any symmetric value + instance.color = "FF0000" + + # access any property right from the model field + self.assertTrue(instance.color.hex == "ff0000") + + # this also works! + self.assertTrue(instance.color == "ff0000") + + # and so does this! + self.assertTrue(instance.color == "FF0000") + + instance.save() + + self.assertTrue( + PropertyExample.objects.filter(color=PropertyExample.Color.RED).first() + == instance + ) + + self.assertTrue( + PropertyExample.objects.filter(color=(1, 0, 0)).first() == instance + ) + + self.assertTrue( + PropertyExample.objects.filter(color="FF0000").first() == instance + ) + + from django_enum.forms import EnumChoiceField + + class PropertyExampleForm(ModelForm): + color = EnumChoiceField(PropertyExample.Color) + + class Meta: + model = PropertyExample + fields = "__all__" + + # this is possible + form = PropertyExampleForm({"color": "FF0000"}) + form.save() + self.assertTrue(form.instance.color == PropertyExample.Color.RED) + + def test_properties_with_choices(self): + from tests.examples.models import ChoicesWithProperties - instance = TextChoicesExample.objects.create( - color=TextChoicesExample.Color("FF0000") + ChoicesWithProperties.objects.all().delete() + + instance = ChoicesWithProperties.objects.create( + color=ChoicesWithProperties.Color("FF0000") ) self.assertTrue( instance.color - == TextChoicesExample.Color("Red") - == TextChoicesExample.Color("R") - == TextChoicesExample.Color((1, 0, 0)) + == ChoicesWithProperties.Color("Red") + == ChoicesWithProperties.Color("R") + == ChoicesWithProperties.Color((1, 0, 0)) ) # direct comparison to any symmetric value also works + self.assertTrue(instance.color == "FF0000") self.assertTrue(instance.color == "Red") self.assertTrue(instance.color == "R") self.assertTrue(instance.color == (1, 0, 0)) @@ -106,33 +158,33 @@ def test_color(self): instance.save() self.assertTrue( - TextChoicesExample.objects.filter( - color=TextChoicesExample.Color.RED + ChoicesWithProperties.objects.filter( + color=ChoicesWithProperties.Color.RED ).first() == instance ) self.assertTrue( - TextChoicesExample.objects.filter(color=(1, 0, 0)).first() == instance + ChoicesWithProperties.objects.filter(color=(1, 0, 0)).first() == instance ) self.assertTrue( - TextChoicesExample.objects.filter(color="FF0000").first() == instance + ChoicesWithProperties.objects.filter(color="FF0000").first() == instance ) from django_enum.forms import EnumChoiceField - class TextChoicesExampleForm(ModelForm): - color = EnumChoiceField(TextChoicesExample.Color) + class ChoicesWithPropertiesForm(ModelForm): + color = EnumChoiceField(ChoicesWithProperties.Color) class Meta: - model = TextChoicesExample + model = ChoicesWithProperties fields = "__all__" # this is possible - form = TextChoicesExampleForm({"color": "FF0000"}) + form = ChoicesWithPropertiesForm({"color": "FF0000"}) form.save() - self.assertTrue(form.instance.color == TextChoicesExample.Color.RED) + self.assertTrue(form.instance.color == ChoicesWithProperties.Color.RED) def test_strict(self): from tests.examples.models import StrictExample @@ -151,20 +203,23 @@ def test_strict(self): self.assertTrue(obj.non_strict == "arbitrary") def test_basic(self): - from tests.examples.models import MyModel + from tests.examples import basic_example + from tests.examples.models import BasicExample - instance = MyModel.objects.create( - txt_enum=MyModel.TextEnum.VALUE1, + instance = BasicExample.objects.create( + txt_enum=BasicExample.TextEnum.VALUE1, int_enum=3, # by-value assignment also works ) - self.assertTrue(instance.txt_enum == MyModel.TextEnum("V1")) + self.assertTrue(instance.txt_enum == BasicExample.TextEnum("V1")) self.assertTrue(instance.txt_enum.label == "Value 1") - self.assertTrue(instance.int_enum == MyModel.IntEnum["THREE"]) + self.assertTrue(instance.int_enum == BasicExample.IntEnum["THREE"]) self.assertTrue(instance.int_enum.value == 3) - self.assertRaises(ValueError, MyModel.objects.create, txt_enum="AA", int_enum=3) + self.assertRaises( + ValueError, BasicExample.objects.create, txt_enum="AA", int_enum=3 + ) instance.txt_enum = "AA" self.assertRaises(ValidationError, instance.full_clean) @@ -183,20 +238,202 @@ def test_no_coerce(self): self.assertFalse(isinstance(obj.non_strict, NoCoerceExample.EnumType)) def test_flag_readme_ex(self): - from tests.examples.models import MyModel + from tests.examples.models import FlagExample + from tests.examples.models.flag import Permissions + from tests.examples import flag_example - MyModel.objects.create( - permissions=MyModel.Permissions.READ | MyModel.Permissions.WRITE, + FlagExample.objects.create( + permissions=Permissions.READ | Permissions.WRITE, ) - MyModel.objects.create( - permissions=MyModel.Permissions.READ | MyModel.Permissions.EXECUTE, + FlagExample.objects.create( + permissions=Permissions.READ | Permissions.EXECUTE, ) self.assertEqual( - MyModel.objects.filter( - permissions__has_all=MyModel.Permissions.READ - | MyModel.Permissions.WRITE + FlagExample.objects.count(), + 3, + ) + + self.assertEqual( + FlagExample.objects.filter( + permissions__has_all=Permissions.READ | Permissions.WRITE ).count(), - 1, + 2, + ) + + self.assertEqual( + FlagExample.objects.filter( + permissions__has_all=Permissions.EXECUTE + ).count(), + 2, + ) + + def test_mixed_value(self): + from tests.examples import mixed_value_example + + def test_path_value(self): + from tests.examples import path_value_example + + def test_custom_value(self): + from tests.examples import custom_value_example + + def test_gnss_tutorial(self): + from tests.examples import gnss_tutorial + + def test_gnss_tutorial_vanilla(self): + from tests.examples import gnss_vanilla_tutorial + + def test_equivalency_howto(self): + from tests.examples import equivalency_howto + + def test_extern_howto(self): + from tests.examples import extern_howto + + def test_urls_howto(self): + from tests.examples import url_converter_howto + + self.assertEqual(self.client.get("/A").content.decode(), "enum=") + self.assertEqual(self.client.get("/B").content.decode(), "enum=") + self.assertEqual(self.client.get("/1").content.decode(), "enum=") + self.assertEqual(self.client.get("/2").content.decode(), "enum=") + + def test_text_choices_howto(self): + from tests.examples import text_choices_howto + + @pytest.mark.skipif(not DJANGO_REST_FRAMEWORK, reason="DRF not installed") + def test_drf_serializer_howto(self): + from tests.examples import drf_serializer_howto + + def _filterset_webcheck(self, name): + from tests.examples.models import TextChoicesExample + from django.urls import reverse + from playwright.sync_api import sync_playwright + + TextChoicesExample.objects.create(color=TextChoicesExample.Color.RED) + TextChoicesExample.objects.create(color=TextChoicesExample.Color.BLUE) + TextChoicesExample.objects.create(color=TextChoicesExample.Color.GREEN) + + url = reverse(name) + + red_content = self.client.get(f"{url}?color=ReD").content.decode() + green_content = self.client.get(f"{url}?color=00Ff00").content.decode() + blue_content = self.client.get(f"{url}?color=B").content.decode() + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + + page.set_content(red_content) + list_items = page.locator("ul li").all_text_contents() + self.assertTrue("Red" in list_items) + self.assertFalse("Green" in list_items) + self.assertFalse("Blue" in list_items) + self.assertEqual( + page.locator("select#id_color option:checked").text_content(), "Red" + ) + + page.set_content(green_content) + list_items = page.locator("ul li").all_text_contents() + self.assertFalse("Red" in list_items) + self.assertTrue("Green" in list_items) + self.assertFalse("Blue" in list_items) + self.assertEqual( + page.locator("select#id_color option:checked").text_content(), "Green" + ) + + page.set_content(blue_content) + list_items = page.locator("ul li").all_text_contents() + self.assertFalse("Red" in list_items) + self.assertFalse("Green" in list_items) + self.assertTrue("Blue" in list_items) + self.assertEqual( + page.locator("select#id_color option:checked").text_content(), "Blue" + ) + + browser.close() + + @pytest.mark.skipif(not DJANGO_FILTERS, reason="django-filters not installed") + def test_filterfield_howto(self): + from tests.examples import filterfield_howto + + self._filterset_webcheck("howto:filterfield") + + @pytest.mark.skipif(not DJANGO_FILTERS, reason="django-filters not installed") + def test_filterset_howto(self): + from tests.examples import filterset_howto + + self._filterset_webcheck("howto:filterset") + + def test_flag_howto(self): + from tests.examples import flag_howto + + def test_choice_form_howto(self): + from tests.examples import choice_form_howto + from django.urls import reverse + from playwright.sync_api import sync_playwright + + url = reverse("howto_forms:choice") + + initial_content = self.client.get(url).content.decode() + + response = self.client.post(url, {"color": "00FF00", "color_ext": "P"}) + self.assertEqual(response.status_code, 200) + form = response.context["form"] + self.assertTrue(form.is_valid()) + self.assertEqual( + form.cleaned_data["color"], choice_form_howto.TextChoicesExample.Color.GREEN + ) + self.assertEqual(form.cleaned_data["color_ext"], "P") + post_green_purple_content = response.content.decode() + + response = self.client.post(url, {"color": "BlUe", "color_ext": "X"}) + self.assertEqual(response.status_code, 200) + form = response.context["form"] + self.assertTrue(form.is_valid()) + self.assertEqual( + form.cleaned_data["color"], choice_form_howto.TextChoicesExample.Color.BLUE ) + self.assertEqual(form.cleaned_data["color_ext"], "X") + post_blue_x_content = response.content.decode() + + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + + page.set_content(initial_content) + self.assertEqual( + page.locator("select#id_color option:checked").text_content(), "Red" + ) + self.assertEqual( + page.locator("select#id_color_ext option:checked").text_content(), "Y" + ) + + page.set_content(post_green_purple_content) + self.assertEqual( + page.locator("select#id_color option:checked").text_content(), "Green" + ) + self.assertEqual( + page.locator("select#id_color_ext option:checked").text_content(), + "Purple", + ) + + page.set_content(post_blue_x_content) + self.assertEqual( + page.locator("select#id_color option:checked").text_content(), "Blue" + ) + self.assertEqual( + page.locator("select#id_color_ext option:checked").text_content(), "X" + ) + + with self.assertRaises(ValidationError): + response = self.client.post(url, {"color": "X", "color_ext": "G"}) + + def test_flag_form_howto(self): + # TODO + pass + + def test_strict_example(self): + from tests.examples import strict_howto + + def test_no_coerce_example(self): + from tests.examples import no_coerce_howto diff --git a/tests/test_verify_environment.py b/tests/test_verify_environment.py index 55d482a..d8d92af 100644 --- a/tests/test_verify_environment.py +++ b/tests/test_verify_environment.py @@ -1,6 +1,6 @@ import os import sys -from django import VERSION +import django from django.db import connection import typing as t from django.test import TestCase @@ -44,11 +44,14 @@ def test(self): f"{expected_python}" ) - expected_django = tuple(int(v) for v in expected_django.split(".") if v) - assert VERSION[: len(expected_django)] == expected_django, ( - f"Django Version Mismatch: {VERSION[: len(expected_django)]} != " - f"{expected_django}" - ) + try: + expected_django = tuple(int(v) for v in expected_django.split(".") if v) + assert django.VERSION[: len(expected_django)] == expected_django, ( + f"Django Version Mismatch: {django.VERSION[: len(expected_django)]} != " + f"{expected_django}" + ) + except ValueError: + assert expected_django == django.__version__ if expected_db_ver: if rdbms == "postgres": diff --git a/tests/urls.py b/tests/urls.py index be40f84..1270a66 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -2,10 +2,14 @@ from django.contrib import admin from django.urls import include, path + urlpatterns = [ path("admin/", admin.site.urls), path("djenum/", include("tests.djenum.urls")), - path("", include("tests.converters.urls")), + path("converters/", include("tests.converters.urls")), + path("", include("tests.examples.urls")), + path("howto/", include("tests.examples.urls_howto")), + path("howto/forms/", include("tests.examples.urls_forms")), ] if "tests.enum_prop" in settings.INSTALLED_APPS: # pragma: no cover diff --git a/tests/utils.py b/tests/utils.py index 81dcf88..6355f69 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,6 +3,7 @@ from datetime import date, datetime, time, timedelta from decimal import Decimal, DecimalException from pathlib import Path +from importlib.util import find_spec from tests.oracle_patch import patch_oracle from dateutil import parser @@ -15,6 +16,10 @@ r"(-)?P(\d+)DT(\d{2})H(\d{2})M(\d{2})(?:\.(\d+))?S", re.IGNORECASE ) +ENUM_PROPERTIES = bool(find_spec("enum_properties")) +DJANGO_FILTERS = bool(find_spec("django_filters")) +DJANGO_REST_FRAMEWORK = bool(find_spec("rest_framework")) + def try_convert(primitive, value, raise_on_error=True): try: diff --git a/uv.lock b/uv.lock index f8ec705..0692966 100644 --- a/uv.lock +++ b/uv.lock @@ -3,35 +3,18 @@ revision = 1 requires-python = ">=3.9, <4.0" resolution-markers = [ "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version < '3.11'", ] [[package]] name = "alabaster" version = "0.7.16" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } wheels = [ { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, ] -[[package]] -name = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, -] - [[package]] name = "anyio" version = "4.8.0" @@ -327,11 +310,8 @@ wheels = [ name = "contourpy" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370 } wheels = [ @@ -401,74 +381,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838 }, ] -[[package]] -name = "contourpy" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "numpy", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466 }, - { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314 }, - { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003 }, - { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896 }, - { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814 }, - { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969 }, - { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162 }, - { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328 }, - { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861 }, - { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566 }, - { url = "https://files.pythonhosted.org/packages/12/bb/11250d2906ee2e8b466b5f93e6b19d525f3e0254ac8b445b56e618527718/contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", size = 269555 }, - { url = "https://files.pythonhosted.org/packages/67/71/1e6e95aee21a500415f5d2dbf037bf4567529b6a4e986594d7026ec5ae90/contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", size = 254549 }, - { url = "https://files.pythonhosted.org/packages/31/2c/b88986e8d79ac45efe9d8801ae341525f38e087449b6c2f2e6050468a42c/contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", size = 313000 }, - { url = "https://files.pythonhosted.org/packages/c4/18/65280989b151fcf33a8352f992eff71e61b968bef7432fbfde3a364f0730/contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6", size = 352925 }, - { url = "https://files.pythonhosted.org/packages/f5/c7/5fd0146c93220dbfe1a2e0f98969293b86ca9bc041d6c90c0e065f4619ad/contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85", size = 323693 }, - { url = "https://files.pythonhosted.org/packages/85/fc/7fa5d17daf77306840a4e84668a48ddff09e6bc09ba4e37e85ffc8e4faa3/contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c", size = 326184 }, - { url = "https://files.pythonhosted.org/packages/ef/e7/104065c8270c7397c9571620d3ab880558957216f2b5ebb7e040f85eeb22/contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291", size = 1268031 }, - { url = "https://files.pythonhosted.org/packages/e2/4a/c788d0bdbf32c8113c2354493ed291f924d4793c4a2e85b69e737a21a658/contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f", size = 1325995 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/a2f351a90d955f8b0564caf1ebe4b1451a3f01f83e5e3a414055a5b8bccb/contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375", size = 174396 }, - { url = "https://files.pythonhosted.org/packages/a8/7e/cd93cab453720a5d6cb75588cc17dcdc08fc3484b9de98b885924ff61900/contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9", size = 219787 }, - { url = "https://files.pythonhosted.org/packages/37/6b/175f60227d3e7f5f1549fcb374592be311293132207e451c3d7c654c25fb/contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509", size = 271494 }, - { url = "https://files.pythonhosted.org/packages/6b/6a/7833cfae2c1e63d1d8875a50fd23371394f540ce809d7383550681a1fa64/contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc", size = 255444 }, - { url = "https://files.pythonhosted.org/packages/7f/b3/7859efce66eaca5c14ba7619791b084ed02d868d76b928ff56890d2d059d/contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454", size = 307628 }, - { url = "https://files.pythonhosted.org/packages/48/b2/011415f5e3f0a50b1e285a0bf78eb5d92a4df000553570f0851b6e309076/contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80", size = 347271 }, - { url = "https://files.pythonhosted.org/packages/84/7d/ef19b1db0f45b151ac78c65127235239a8cf21a59d1ce8507ce03e89a30b/contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec", size = 318906 }, - { url = "https://files.pythonhosted.org/packages/ba/99/6794142b90b853a9155316c8f470d2e4821fe6f086b03e372aca848227dd/contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9", size = 323622 }, - { url = "https://files.pythonhosted.org/packages/3c/0f/37d2c84a900cd8eb54e105f4fa9aebd275e14e266736778bb5dccbf3bbbb/contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b", size = 1266699 }, - { url = "https://files.pythonhosted.org/packages/3a/8a/deb5e11dc7d9cc8f0f9c8b29d4f062203f3af230ba83c30a6b161a6effc9/contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d", size = 1326395 }, - { url = "https://files.pythonhosted.org/packages/1a/35/7e267ae7c13aaf12322ccc493531f1e7f2eb8fba2927b9d7a05ff615df7a/contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e", size = 175354 }, - { url = "https://files.pythonhosted.org/packages/a1/35/c2de8823211d07e8a79ab018ef03960716c5dff6f4d5bff5af87fd682992/contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d", size = 220971 }, - { url = "https://files.pythonhosted.org/packages/9a/e7/de62050dce687c5e96f946a93546910bc67e483fe05324439e329ff36105/contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", size = 271548 }, - { url = "https://files.pythonhosted.org/packages/78/4d/c2a09ae014ae984c6bdd29c11e74d3121b25eaa117eca0bb76340efd7e1c/contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", size = 255576 }, - { url = "https://files.pythonhosted.org/packages/ab/8a/915380ee96a5638bda80cd061ccb8e666bfdccea38d5741cb69e6dbd61fc/contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", size = 306635 }, - { url = "https://files.pythonhosted.org/packages/29/5c/c83ce09375428298acd4e6582aeb68b1e0d1447f877fa993d9bf6cd3b0a0/contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", size = 345925 }, - { url = "https://files.pythonhosted.org/packages/29/63/5b52f4a15e80c66c8078a641a3bfacd6e07106835682454647aca1afc852/contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", size = 318000 }, - { url = "https://files.pythonhosted.org/packages/9a/e2/30ca086c692691129849198659bf0556d72a757fe2769eb9620a27169296/contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", size = 322689 }, - { url = "https://files.pythonhosted.org/packages/6b/77/f37812ef700f1f185d348394debf33f22d531e714cf6a35d13d68a7003c7/contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", size = 1268413 }, - { url = "https://files.pythonhosted.org/packages/3f/6d/ce84e79cdd128542ebeb268f84abb4b093af78e7f8ec504676673d2675bc/contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", size = 1326530 }, - { url = "https://files.pythonhosted.org/packages/72/22/8282f4eae20c73c89bee7a82a19c4e27af9b57bb602ecaa00713d5bdb54d/contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", size = 175315 }, - { url = "https://files.pythonhosted.org/packages/e3/d5/28bca491f65312b438fbf076589dcde7f6f966b196d900777f5811b9c4e2/contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", size = 220987 }, - { url = "https://files.pythonhosted.org/packages/2f/24/a4b285d6adaaf9746e4700932f579f1a7b6f9681109f694cfa233ae75c4e/contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", size = 285001 }, - { url = "https://files.pythonhosted.org/packages/48/1d/fb49a401b5ca4f06ccf467cd6c4f1fd65767e63c21322b29b04ec40b40b9/contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", size = 268553 }, - { url = "https://files.pythonhosted.org/packages/79/1e/4aef9470d13fd029087388fae750dccb49a50c012a6c8d1d634295caa644/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", size = 310386 }, - { url = "https://files.pythonhosted.org/packages/b0/34/910dc706ed70153b60392b5305c708c9810d425bde12499c9184a1100888/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", size = 349806 }, - { url = "https://files.pythonhosted.org/packages/31/3c/faee6a40d66d7f2a87f7102236bf4780c57990dd7f98e5ff29881b1b1344/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", size = 321108 }, - { url = "https://files.pythonhosted.org/packages/17/69/390dc9b20dd4bb20585651d7316cc3054b7d4a7b4f8b710b2b698e08968d/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", size = 327291 }, - { url = "https://files.pythonhosted.org/packages/ef/74/7030b67c4e941fe1e5424a3d988080e83568030ce0355f7c9fc556455b01/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", size = 1263752 }, - { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 }, - { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 }, - { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605 }, - { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040 }, - { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221 }, -] - [[package]] name = "coverage" version = "7.6.12" @@ -544,6 +456,20 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cssbeautifier" +version = "1.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editorconfig" }, + { name = "jsbeautifier" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/01/fdf41c1e5f93d359681976ba10410a04b299d248e28ecce1d4e88588dde4/cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5", size = 25376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/51/ef6c5628e46092f0a54c7cee69acc827adc6b6aab57b55d344fefbdf28f1/cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98", size = 123667 }, +] + [[package]] name = "cx-oracle" version = "8.3.0" @@ -608,46 +534,24 @@ wheels = [ [[package]] name = "django" -version = "4.2.19" +version = "3.2.25" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "asgiref", marker = "python_full_version < '3.10'" }, - { name = "sqlparse", marker = "python_full_version < '3.10'" }, - { name = "tzdata", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/49/8e71f8a30aecc8e236bbb0ce056ff705a4782db4ab836e588c1a0e0a26aa/Django-4.2.19.tar.gz", hash = "sha256:6c833be4b0ca614f0a919472a1028a3bbdeb6f056fa04023aeb923346ba2c306", size = 10426865 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/db/6fc8c55f3b482776ac245623cd713b00ba894ad8f32cf438a40d9f1ea29e/Django-4.2.19-py3-none-any.whl", hash = "sha256:a104e13f219fc55996a4e416ef7d18ab4eeb44e0aa95174c192f16cda9f94e75", size = 7993670 }, -] - -[[package]] -name = "django" -version = "5.1.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "asgiref", marker = "python_full_version >= '3.10'" }, - { name = "sqlparse", marker = "python_full_version >= '3.10'" }, - { name = "tzdata", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "asgiref" }, + { name = "pytz" }, + { name = "sqlparse" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/e4/901f54ee114a080371a49bd08fa688d301aaffd9751febaf4ae855fc8fcd/Django-5.1.6.tar.gz", hash = "sha256:1e39eafdd1b185e761d9fab7a9f0b9fa00af1b37b25ad980a8aa0dac13535690", size = 10700620 } +sdist = { url = "https://files.pythonhosted.org/packages/ec/68/0e744f07b57bfdf99abbb6b3eb14fcba188867021c05f4a104e04f6d56b8/Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", size = 9836336 } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/6f/d2c216d00975e2604b10940937b0ba6b2c2d9b3cc0cc633e414ae3f14b2e/Django-5.1.6-py3-none-any.whl", hash = "sha256:8d203400bc2952fbfb287c2bbda630297d654920c72a73cc82a9ad7926feaad5", size = 8277066 }, + { url = "https://files.pythonhosted.org/packages/30/8e/cc23c762c5dcd1d367d73cf006a326e0df2bd0e785cba18b658b39904c1e/Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38", size = 7890550 }, ] [[package]] name = "django-enum" -version = "2.1.0" +version = "2.2.0" source = { editable = "." } dependencies = [ - { name = "django", version = "4.2.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "django", version = "5.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "django" }, ] [package.optional-dependencies] @@ -667,19 +571,21 @@ dev = [ { name = "coverage" }, { name = "darglint" }, { name = "deepdiff" }, + { name = "django-extensions" }, { name = "django-stubs", extra = ["compatible-mypy"] }, { name = "django-test-migrations" }, + { name = "djlint" }, { name = "ipdb" }, - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib" }, { name = "mypy" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, { name = "packaging" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-django" }, + { name = "pytest-env" }, + { name = "pytest-playwright" }, { name = "python-dateutil" }, { name = "ruff" }, { name = "tomlkit" }, @@ -692,11 +598,10 @@ docs = [ { name = "docutils" }, { name = "furo" }, { name = "readme-renderer", extra = ["md"] }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, { name = "sphinx-autobuild" }, { name = "sphinx-tabs" }, + { name = "sphinxcontrib-django" }, ] mysql = [ { name = "mysqlclient" }, @@ -726,8 +631,10 @@ dev = [ { name = "coverage", specifier = ">=7.6.12" }, { name = "darglint", specifier = ">=1.8.1" }, { name = "deepdiff", specifier = ">=8.2.0" }, + { name = "django-extensions", specifier = ">=3.2.3" }, { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1.3" }, - { name = "django-test-migrations", specifier = ">=1.4.0" }, + { name = "django-test-migrations", git = "https://github.com/bckohan/django-test-migrations.git?rev=issue-503" }, + { name = "djlint", specifier = ">=1.36.4" }, { name = "ipdb", specifier = ">=0.13.13" }, { name = "matplotlib", specifier = ">=3.9.4" }, { name = "mypy", specifier = ">=1.15.0" }, @@ -737,6 +644,8 @@ dev = [ { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-django", specifier = ">=4.10.0" }, + { name = "pytest-env", specifier = ">=1.1.5" }, + { name = "pytest-playwright", specifier = ">=0.7.0" }, { name = "python-dateutil", specifier = ">=2.9.0.post0" }, { name = "ruff", specifier = ">=0.9.7" }, { name = "tomlkit", specifier = ">=0.13.2" }, @@ -752,23 +661,35 @@ docs = [ { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-autobuild", specifier = ">=2024.10.3" }, { name = "sphinx-tabs", specifier = ">=3.4.7" }, + { name = "sphinxcontrib-django", specifier = ">=2.5" }, ] mysql = [{ name = "mysqlclient", specifier = ">=1.4.0" }] oracle = [{ name = "cx-oracle", specifier = ">=8.3.0" }] psycopg2 = [{ name = "psycopg2", specifier = ">=2.9.10" }] psycopg3 = [{ name = "psycopg" }] +[[package]] +name = "django-extensions" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/f1/318684c9466968bf9a9c221663128206e460c1a67f595055be4b284cde8a/django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", size = 277216 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/7e/ba12b9660642663f5273141018d2bec0a1cae1711f4f6d1093920e157946/django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401", size = 229868 }, +] + [[package]] name = "django-filter" -version = "25.1" +version = "23.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "4.2.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "django", version = "5.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/40/c702a6fe8cccac9bf426b55724ebdf57d10a132bae80a17691d0cf0b9bac/django_filter-25.1.tar.gz", hash = "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153", size = 143021 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/e874b30a66684e3e79b183b0197dd3d08f842a8674972e47a90c09a6ec9c/django-filter-23.5.tar.gz", hash = "sha256:67583aa43b91fe8c49f74a832d95f4d8442be628fd4c6d65e9f811f5153a4e5c", size = 141058 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/a6/70dcd68537c434ba7cb9277d403c5c829caf04f35baf5eb9458be251e382/django_filter-25.1-py3-none-any.whl", hash = "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80", size = 94114 }, + { url = "https://files.pythonhosted.org/packages/79/ff/4ae79361e09c3803562368700bc56672cca37bab7c1a8a91e7a225ce8fa0/django_filter-23.5-py3-none-any.whl", hash = "sha256:99122a201d83860aef4fe77758b69dda913e874cc5e0eaa50a86b0b18d708400", size = 94402 }, ] [[package]] @@ -777,8 +698,7 @@ version = "5.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, - { name = "django", version = "4.2.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "django", version = "5.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "django" }, { name = "django-stubs-ext" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "types-pyyaml" }, @@ -799,8 +719,7 @@ name = "django-stubs-ext" version = "5.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "4.2.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "django", version = "5.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "django" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/06/7b210e0073c6cb8824bde82afc25f268e8c410a99d3621297f44fa3f6a6c/django_stubs_ext-5.1.3.tar.gz", hash = "sha256:3e60f82337f0d40a362f349bf15539144b96e4ceb4dbd0239be1cd71f6a74ad0", size = 9613 } @@ -811,26 +730,63 @@ wheels = [ [[package]] name = "django-test-migrations" version = "1.4.0" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/bckohan/django-test-migrations.git?rev=issue-503#0b674ba78807a37f74e402e09d6302051eb6b7c3" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/28/e0d78605a48aea56a64c75baacfb10f3cbb33f699dadc3d0e8fc08009c85/django_test_migrations-1.4.0.tar.gz", hash = "sha256:f0c9c92864ed27d0c9a582e92056637e91227f54bd868a50cb9a1726668c563e", size = 20311 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/60/11c792485df2cc242bb13ab167dcd3cc1ff1d943bcb235b8320dd71c6bf6/django_test_migrations-1.4.0-py3-none-any.whl", hash = "sha256:294dff98f6d43d020d4046b971bac5339e7c71458a35e9ad6450c388fe16ed6b", size = 25211 }, -] [[package]] name = "djangorestframework" -version = "3.15.2" +version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", version = "4.2.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "django", version = "5.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420 } +sdist = { url = "https://files.pythonhosted.org/packages/ec/60/cc2dd985400293fe7bf3fa1b9a5d61f5b44200c33f7d31952f2c9fd79e8a/djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1", size = 1066194 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 }, + { url = "https://files.pythonhosted.org/packages/c0/7e/8c45ea7f85dd5d52ceddbacc6f56ecaca21ecbfc0e8c34c95618a14d5082/djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6", size = 1067096 }, +] + +[[package]] +name = "djlint" +version = "1.36.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama" }, + { name = "cssbeautifier" }, + { name = "jsbeautifier" }, + { name = "json5" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tqdm" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/89/ecf5be9f5c59a0c53bcaa29671742c5e269cc7d0e2622e3f65f41df251bf/djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1", size = 47849 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/71/6a3ce2b49a62e635b85dce30ccf3eb3a18fe79275d45535325a55a63d3a3/djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c", size = 354135 }, + { url = "https://files.pythonhosted.org/packages/72/47/308412dc579e277c910774f41b380308d582862b16763425583e69e0fc14/djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292", size = 328501 }, + { url = "https://files.pythonhosted.org/packages/9b/6f/428dc044d1e34363265b1301dc9b53253007acd858879d54b369d233aa96/djlint-1.36.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1", size = 415849 }, + { url = "https://files.pythonhosted.org/packages/d6/13/0d488e551d73ddf369552fc6f4c7702ea683e4bc1305bcf5c1d198fbdace/djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c", size = 360969 }, + { url = "https://files.pythonhosted.org/packages/04/68/18ecd1e4d54a523e1d077f01419d669116e5dede97f97f1eb8ddb918a872/djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7", size = 344261 }, + { url = "https://files.pythonhosted.org/packages/1e/03/005cf5c66e57ca2d26249f8385bc64420b2a95fea81c5eb619c925199029/djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7", size = 319580 }, + { url = "https://files.pythonhosted.org/packages/9f/88/aea3c81343a273a87362f30442abc13351dc8ada0b10e51daa285b4dddac/djlint-1.36.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483", size = 407070 }, + { url = "https://files.pythonhosted.org/packages/60/77/0f767ac0b72e9a664bb8c92b8940f21bc1b1e806e5bd727584d40a4ca551/djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08", size = 360775 }, + { url = "https://files.pythonhosted.org/packages/53/f5/9ae02b875604755d4d00cebf96b218b0faa3198edc630f56a139581aed87/djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b", size = 354886 }, + { url = "https://files.pythonhosted.org/packages/97/51/284443ff2f2a278f61d4ae6ae55eaf820ad9f0fd386d781cdfe91f4de495/djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e", size = 323237 }, + { url = "https://files.pythonhosted.org/packages/6d/5e/791f4c5571f3f168ad26fa3757af8f7a05c623fde1134a9c4de814ee33b7/djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675", size = 411719 }, + { url = "https://files.pythonhosted.org/packages/1f/11/894425add6f84deffcc6e373f2ce250f2f7b01aa58c7f230016ebe7a0085/djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08", size = 362076 }, + { url = "https://files.pythonhosted.org/packages/da/83/88b4c885812921739f5529a29085c3762705154d41caf7eb9a8886a3380c/djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2", size = 354384 }, + { url = "https://files.pythonhosted.org/packages/32/38/67695f7a150b3d9d62fadb65242213d96024151570c3cf5d966effa68b0e/djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835", size = 322971 }, + { url = "https://files.pythonhosted.org/packages/ac/7a/cd851393291b12e7fe17cf5d4d8874b8ea133aebbe9235f5314aabc96a52/djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f", size = 410972 }, + { url = "https://files.pythonhosted.org/packages/6c/31/56469120394b970d4f079a552fde21ed27702ca729595ab0ed459eb6d240/djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4", size = 362053 }, + { url = "https://files.pythonhosted.org/packages/9f/d1/8d855042b0758887f26ffd9e56230fde929669e10e4c423b53f644626957/djlint-1.36.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89678661888c03d7bc6cadd75af69db29962b5ecbf93a81518262f5c48329f04", size = 354057 }, + { url = "https://files.pythonhosted.org/packages/ac/da/f395175586f405c169039a9fea687836c78f2eff3b1c70bba8dbfe2c97af/djlint-1.36.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b01a98df3e1ab89a552793590875bc6e954cad661a9304057db75363d519fa0", size = 328319 }, + { url = "https://files.pythonhosted.org/packages/96/8a/417e3b41393180fba09cdd072a13076a7441bd49cf0421f14d517eac9ce3/djlint-1.36.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbb4f7b93223d471d09ae34ed515fef98b2233cbca2449ad117416c44b1351", size = 415780 }, + { url = "https://files.pythonhosted.org/packages/a8/a7/f3239f694bb2f6ecb80cae438be6a64b4ec7000a6117401ac79e9cc6a30a/djlint-1.36.4-cp39-cp39-win_amd64.whl", hash = "sha256:7a483390d17e44df5bc23dcea29bdf6b63f3ed8b4731d844773a4829af4f5e0b", size = 360729 }, + { url = "https://files.pythonhosted.org/packages/4b/67/f7aeea9be6fb3bd984487af8d0d80225a0b1e5f6f7126e3332d349fb13fe/djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd", size = 52290 }, ] [[package]] @@ -858,6 +814,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, ] +[[package]] +name = "editorconfig" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/29/785595a0d8b30ab8d2486559cfba1d46487b8dcbd99f74960b6b4cca92a4/editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2", size = 13369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/e5/8dba39ea24ca3de0e954e668107692f4dfc13a85300a531fa9a39e83fde4/EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc", size = 16276 }, +] + [[package]] name = "enum-properties" version = "2.2.2" @@ -950,9 +915,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "pygments" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, { name = "sphinx-basic-ng" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 } @@ -960,6 +923,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, ] +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, + { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, + { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, + { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, + { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, + { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, + { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, + { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, + { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, + { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, + { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, + { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, + { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, + { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, + { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, + { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, + { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, + { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, + { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, +] + [[package]] name = "h11" version = "0.14.0" @@ -1001,7 +1025,7 @@ name = "importlib-metadata" version = "8.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, + { name = "zipp", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } wheels = [ @@ -1035,8 +1059,7 @@ version = "0.13.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "decorator" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.32.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042 } @@ -1048,20 +1071,17 @@ wheels = [ name = "ipython" version = "8.18.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.10'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "jedi", marker = "python_full_version < '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, - { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "stack-data", marker = "python_full_version < '3.10'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330 } @@ -1069,32 +1089,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161 }, ] -[[package]] -name = "ipython" -version = "8.32.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.10'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "jedi", marker = "python_full_version >= '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" }, - { name = "pexpect", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "stack-data", marker = "python_full_version >= '3.10'" }, - { name = "traitlets", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/80/4d2a072e0db7d250f134bc11676517299264ebe16d62a8619d49a78ced73/ipython-8.32.0.tar.gz", hash = "sha256:be2c91895b0b9ea7ba49d33b23e2040c352b33eb6a519cca7ce6e0c743444251", size = 5507441 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e1/f4474a7ecdb7745a820f6f6039dc43c66add40f1bcc66485607d93571af6/ipython-8.32.0-py3-none-any.whl", hash = "sha256:cae85b0c61eff1fc48b0a8002de5958b6528fa9c8defb1894da63f42613708aa", size = 825524 }, -] - [[package]] name = "jedi" version = "0.19.2" @@ -1119,13 +1113,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, ] +[[package]] +name = "jsbeautifier" +version = "1.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editorconfig" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/98/d6cadf4d5a1c03b2136837a435682418c29fdeb66be137128544cecc5b7a/jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592", size = 75257 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528", size = 94707 }, +] + +[[package]] +name = "json5" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049 }, +] + [[package]] name = "kiwisolver" version = "1.4.7" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } wheels = [ { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440 }, @@ -1222,97 +1235,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811 }, ] -[[package]] -name = "kiwisolver" -version = "1.4.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, - { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, - { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, - { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, - { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, - { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, - { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, - { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, - { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, - { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, - { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, - { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, - { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, - { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, - { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, - { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, - { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, - { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, - { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, - { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, - { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, - { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, - { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, - { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, - { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, - { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, - { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, - { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, -] - [[package]] name = "markupsafe" version = "3.0.2" @@ -1385,20 +1307,17 @@ wheels = [ name = "matplotlib" version = "3.9.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "cycler", marker = "python_full_version < '3.10'" }, - { name = "fonttools", marker = "python_full_version < '3.10'" }, + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, { name = "importlib-resources", marker = "python_full_version < '3.10'" }, - { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529 } wheels = [ @@ -1444,62 +1363,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624 }, ] -[[package]] -name = "matplotlib" -version = "3.10.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "contourpy", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/dd/fa2e1a45fce2d09f4aea3cee169760e672c8262325aa5796c49d543dc7e6/matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278", size = 36686418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/ec/3cdff7b5239adaaacefcc4f77c316dfbbdf853c4ed2beec467e0fec31b9f/matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6", size = 8160551 }, - { url = "https://files.pythonhosted.org/packages/41/f2/b518f2c7f29895c9b167bf79f8529c63383ae94eaf49a247a4528e9a148d/matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e", size = 8034853 }, - { url = "https://files.pythonhosted.org/packages/ed/8d/45754b4affdb8f0d1a44e4e2bcd932cdf35b256b60d5eda9f455bb293ed0/matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5", size = 8446724 }, - { url = "https://files.pythonhosted.org/packages/09/5a/a113495110ae3e3395c72d82d7bc4802902e46dc797f6b041e572f195c56/matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6", size = 8583905 }, - { url = "https://files.pythonhosted.org/packages/12/b1/8b1655b4c9ed4600c817c419f7eaaf70082630efd7556a5b2e77a8a3cdaf/matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1", size = 9395223 }, - { url = "https://files.pythonhosted.org/packages/5a/85/b9a54d64585a6b8737a78a61897450403c30f39e0bd3214270bb0b96f002/matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3", size = 8025355 }, - { url = "https://files.pythonhosted.org/packages/0c/f1/e37f6c84d252867d7ddc418fff70fc661cfd363179263b08e52e8b748e30/matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363", size = 8171677 }, - { url = "https://files.pythonhosted.org/packages/c7/8b/92e9da1f28310a1f6572b5c55097b0c0ceb5e27486d85fb73b54f5a9b939/matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997", size = 8044945 }, - { url = "https://files.pythonhosted.org/packages/c5/cb/49e83f0fd066937a5bd3bc5c5d63093703f3637b2824df8d856e0558beef/matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef", size = 8458269 }, - { url = "https://files.pythonhosted.org/packages/b2/7d/2d873209536b9ee17340754118a2a17988bc18981b5b56e6715ee07373ac/matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683", size = 8599369 }, - { url = "https://files.pythonhosted.org/packages/b8/03/57d6cbbe85c61fe4cbb7c94b54dce443d68c21961830833a1f34d056e5ea/matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765", size = 9405992 }, - { url = "https://files.pythonhosted.org/packages/14/cf/e382598f98be11bf51dd0bc60eca44a517f6793e3dc8b9d53634a144620c/matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a", size = 8034580 }, - { url = "https://files.pythonhosted.org/packages/44/c7/6b2d8cb7cc251d53c976799cacd3200add56351c175ba89ab9cbd7c1e68a/matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59", size = 8172465 }, - { url = "https://files.pythonhosted.org/packages/42/2a/6d66d0fba41e13e9ca6512a0a51170f43e7e7ed3a8dfa036324100775612/matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a", size = 8043300 }, - { url = "https://files.pythonhosted.org/packages/90/60/2a60342b27b90a16bada939a85e29589902b41073f59668b904b15ea666c/matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95", size = 8448936 }, - { url = "https://files.pythonhosted.org/packages/a7/b2/d872fc3d753516870d520595ddd8ce4dd44fa797a240999f125f58521ad7/matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8", size = 8594151 }, - { url = "https://files.pythonhosted.org/packages/f4/bd/b2f60cf7f57d014ab33e4f74602a2b5bdc657976db8196bbc022185f6f9c/matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12", size = 9400347 }, - { url = "https://files.pythonhosted.org/packages/9f/6e/264673e64001b99d747aff5a288eca82826c024437a3694e19aed1decf46/matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc", size = 8039144 }, - { url = "https://files.pythonhosted.org/packages/72/11/1b2a094d95dcb6e6edd4a0b238177c439006c6b7a9fe8d31801237bf512f/matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25", size = 8173073 }, - { url = "https://files.pythonhosted.org/packages/0d/c4/87b6ad2723070511a411ea719f9c70fde64605423b184face4e94986de9d/matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908", size = 8043892 }, - { url = "https://files.pythonhosted.org/packages/57/69/cb0812a136550b21361335e9ffb7d459bf6d13e03cb7b015555d5143d2d6/matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2", size = 8450532 }, - { url = "https://files.pythonhosted.org/packages/ea/3a/bab9deb4fb199c05e9100f94d7f1c702f78d3241e6a71b784d2b88d7bebd/matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf", size = 8593905 }, - { url = "https://files.pythonhosted.org/packages/8b/66/742fd242f989adc1847ddf5f445815f73ad7c46aa3440690cc889cfa423c/matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae", size = 9399609 }, - { url = "https://files.pythonhosted.org/packages/fa/d6/54cee7142cef7d910a324a7aedf335c0c147b03658b54d49ec48166f10a6/matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442", size = 8039076 }, - { url = "https://files.pythonhosted.org/packages/43/14/815d072dc36e88753433bfd0385113405efb947e6895ff7b4d2e8614a33b/matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06", size = 8211000 }, - { url = "https://files.pythonhosted.org/packages/9a/76/34e75f364194ec352678adcb540964be6f35ec7d3d8c75ebcb17e6839359/matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff", size = 8087707 }, - { url = "https://files.pythonhosted.org/packages/c3/2b/b6bc0dff6a72d333bc7df94a66e6ce662d224e43daa8ad8ae4eaa9a77f55/matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593", size = 8477384 }, - { url = "https://files.pythonhosted.org/packages/c2/2d/b5949fb2b76e9b47ab05e25a5f5f887c70de20d8b0cbc704a4e2ee71c786/matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e", size = 8610334 }, - { url = "https://files.pythonhosted.org/packages/d6/9a/6e3c799d5134d9af44b01c787e1360bee38cf51850506ea2e743a787700b/matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede", size = 9406777 }, - { url = "https://files.pythonhosted.org/packages/0e/dd/e6ae97151e5ed648ab2ea48885bc33d39202b640eec7a2910e2c843f7ac0/matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c", size = 8109742 }, - { url = "https://files.pythonhosted.org/packages/32/5f/29def7ce4e815ab939b56280976ee35afffb3bbdb43f332caee74cb8c951/matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03", size = 8155500 }, - { url = "https://files.pythonhosted.org/packages/de/6d/d570383c9f7ca799d0a54161446f9ce7b17d6c50f2994b653514bcaa108f/matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea", size = 8032398 }, - { url = "https://files.pythonhosted.org/packages/c9/b4/680aa700d99b48e8c4393fa08e9ab8c49c0555ee6f4c9c0a5e8ea8dfde5d/matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef", size = 8587361 }, -] - [[package]] name = "matplotlib-inline" version = "0.1.7" @@ -1624,9 +1487,6 @@ wheels = [ name = "numpy" version = "2.0.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } wheels = [ { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, @@ -1675,72 +1535,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, ] -[[package]] -name = "numpy" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/e1/1816d5d527fa870b260a1c2c5904d060caad7515637bd54f495a5ce13ccd/numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71", size = 21232911 }, - { url = "https://files.pythonhosted.org/packages/29/46/9f25dc19b359f10c0e52b6bac25d3181eb1f4b4d04c9846a32cf5ea52762/numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787", size = 14371955 }, - { url = "https://files.pythonhosted.org/packages/72/d7/de941296e6b09a5c81d3664ad912f1496a0ecdd2f403318e5e35604ff70f/numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716", size = 5410476 }, - { url = "https://files.pythonhosted.org/packages/36/ce/55f685995110f8a268fdca0f198c9a84fa87b39512830965cc1087af6391/numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b", size = 6945730 }, - { url = "https://files.pythonhosted.org/packages/4f/84/abdb9f6e22576d89c259401c3234d4755b322539491bbcffadc8bcb120d3/numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3", size = 14350752 }, - { url = "https://files.pythonhosted.org/packages/e9/88/3870cfa9bef4dffb3a326507f430e6007eeac258ebeef6b76fc542aef66d/numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52", size = 16399386 }, - { url = "https://files.pythonhosted.org/packages/02/10/3f629682dd0b457525c131945329c4e81e2dadeb11256e6ce4c9a1a6fb41/numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b", size = 15561826 }, - { url = "https://files.pythonhosted.org/packages/da/18/fd35673ba9751eba449d4ce5d24d94e3b612cdbfba79348da71488c0b7ac/numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027", size = 18188593 }, - { url = "https://files.pythonhosted.org/packages/ce/4c/c0f897b580ea59484b4cc96a441fea50333b26675a60a1421bc912268b5f/numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094", size = 6590421 }, - { url = "https://files.pythonhosted.org/packages/e5/5b/aaabbfc7060c5c8f0124c5deb5e114a3b413a548bbc64e372c5b5db36165/numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb", size = 12925667 }, - { url = "https://files.pythonhosted.org/packages/96/86/453aa3949eab6ff54e2405f9cb0c01f756f031c3dc2a6d60a1d40cba5488/numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8", size = 21237256 }, - { url = "https://files.pythonhosted.org/packages/20/c3/93ecceadf3e155d6a9e4464dd2392d8d80cf436084c714dc8535121c83e8/numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b", size = 14408049 }, - { url = "https://files.pythonhosted.org/packages/8d/29/076999b69bd9264b8df5e56f2be18da2de6b2a2d0e10737e5307592e01de/numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a", size = 5408655 }, - { url = "https://files.pythonhosted.org/packages/e2/a7/b14f0a73eb0fe77cb9bd5b44534c183b23d4229c099e339c522724b02678/numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636", size = 6949996 }, - { url = "https://files.pythonhosted.org/packages/72/2f/8063da0616bb0f414b66dccead503bd96e33e43685c820e78a61a214c098/numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d", size = 14355789 }, - { url = "https://files.pythonhosted.org/packages/e6/d7/3cd47b00b8ea95ab358c376cf5602ad21871410950bc754cf3284771f8b6/numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb", size = 16411356 }, - { url = "https://files.pythonhosted.org/packages/27/c0/a2379e202acbb70b85b41483a422c1e697ff7eee74db642ca478de4ba89f/numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2", size = 15576770 }, - { url = "https://files.pythonhosted.org/packages/bc/63/a13ee650f27b7999e5b9e1964ae942af50bb25606d088df4229283eda779/numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b", size = 18200483 }, - { url = "https://files.pythonhosted.org/packages/4c/87/e71f89935e09e8161ac9c590c82f66d2321eb163893a94af749dfa8a3cf8/numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5", size = 6588415 }, - { url = "https://files.pythonhosted.org/packages/b9/c6/cd4298729826af9979c5f9ab02fcaa344b82621e7c49322cd2d210483d3f/numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f", size = 12929604 }, - { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 }, - { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 }, - { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 }, - { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 }, - { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 }, - { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 }, - { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 }, - { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 }, - { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 }, - { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 }, - { url = "https://files.pythonhosted.org/packages/0e/8b/88b98ed534d6a03ba8cddb316950fe80842885709b58501233c29dfa24a9/numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba", size = 20916001 }, - { url = "https://files.pythonhosted.org/packages/d9/b4/def6ec32c725cc5fbd8bdf8af80f616acf075fe752d8a23e895da8c67b70/numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50", size = 14130721 }, - { url = "https://files.pythonhosted.org/packages/20/60/70af0acc86495b25b672d403e12cb25448d79a2b9658f4fc45e845c397a8/numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1", size = 5130999 }, - { url = "https://files.pythonhosted.org/packages/2e/69/d96c006fb73c9a47bcb3611417cf178049aae159afae47c48bd66df9c536/numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5", size = 6665299 }, - { url = "https://files.pythonhosted.org/packages/5a/3f/d8a877b6e48103733ac224ffa26b30887dc9944ff95dffdfa6c4ce3d7df3/numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2", size = 14064096 }, - { url = "https://files.pythonhosted.org/packages/e4/43/619c2c7a0665aafc80efca465ddb1f260287266bdbdce517396f2f145d49/numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1", size = 16114758 }, - { url = "https://files.pythonhosted.org/packages/d9/79/ee4fe4f60967ccd3897aa71ae14cdee9e3c097e3256975cc9575d393cb42/numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304", size = 15259880 }, - { url = "https://files.pythonhosted.org/packages/fb/c8/8b55cf05db6d85b7a7d414b3d1bd5a740706df00bfa0824a08bf041e52ee/numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d", size = 17876721 }, - { url = "https://files.pythonhosted.org/packages/21/d6/b4c2f0564b7dcc413117b0ffbb818d837e4b29996b9234e38b2025ed24e7/numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693", size = 6290195 }, - { url = "https://files.pythonhosted.org/packages/97/e7/7d55a86719d0de7a6a597949f3febefb1009435b79ba510ff32f05a8c1d7/numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b", size = 12619013 }, - { url = "https://files.pythonhosted.org/packages/a6/1f/0b863d5528b9048fd486a56e0b97c18bf705e88736c8cea7239012119a54/numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890", size = 20944621 }, - { url = "https://files.pythonhosted.org/packages/aa/99/b478c384f7a0a2e0736177aafc97dc9152fc036a3fdb13f5a3ab225f1494/numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c", size = 14142502 }, - { url = "https://files.pythonhosted.org/packages/fb/61/2d9a694a0f9cd0a839501d362de2a18de75e3004576a3008e56bdd60fcdb/numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94", size = 5176293 }, - { url = "https://files.pythonhosted.org/packages/33/35/51e94011b23e753fa33f891f601e5c1c9a3d515448659b06df9d40c0aa6e/numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0", size = 6691874 }, - { url = "https://files.pythonhosted.org/packages/ff/cf/06e37619aad98a9d03bd8d65b8e3041c3a639be0f5f6b0a0e2da544538d4/numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610", size = 14036826 }, - { url = "https://files.pythonhosted.org/packages/0c/93/5d7d19955abd4d6099ef4a8ee006f9ce258166c38af259f9e5558a172e3e/numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76", size = 16096567 }, - { url = "https://files.pythonhosted.org/packages/af/53/d1c599acf7732d81f46a93621dab6aa8daad914b502a7a115b3f17288ab2/numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a", size = 15242514 }, - { url = "https://files.pythonhosted.org/packages/53/43/c0f5411c7b3ea90adf341d05ace762dad8cb9819ef26093e27b15dd121ac/numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf", size = 17872920 }, - { url = "https://files.pythonhosted.org/packages/5b/57/6dbdd45ab277aff62021cafa1e15f9644a52f5b5fc840bc7591b4079fb58/numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef", size = 6346584 }, - { url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 }, - { url = "https://files.pythonhosted.org/packages/0a/b5/a7839f5478be8f859cb880f13d90fcfe4b0ec7a9ebaff2bcc30d96760596/numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d", size = 21064244 }, - { url = "https://files.pythonhosted.org/packages/29/e8/5da32ffcaa7a72f7ecd82f90c062140a061eb823cb88e90279424e515cf4/numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9", size = 6809418 }, - { url = "https://files.pythonhosted.org/packages/a8/a9/68aa7076c7656a7308a0f73d0a2ced8c03f282c9fd98fa7ce21c12634087/numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e", size = 16215461 }, - { url = "https://files.pythonhosted.org/packages/17/7f/d322a4125405920401450118dbdc52e0384026bd669939484670ce8b2ab9/numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4", size = 12839607 }, -] - [[package]] name = "orderly-set" version = "5.3.0" @@ -1768,6 +1562,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + [[package]] name = "pbr" version = "6.1.1" @@ -1879,6 +1682,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] +[[package]] +name = "playwright" +version = "1.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/5e/068dea3c96e9c09929b45c92cf7e573403b52a89aa463f89b9da9b87b7a4/playwright-1.50.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:f36d754a6c5bd9bf7f14e8f57a2aea6fd08f39ca4c8476481b9c83e299531148", size = 40277564 }, + { url = "https://files.pythonhosted.org/packages/78/85/b3deb3d2add00d2a6ee74bf6f57ccefb30efc400fd1b7b330ba9a3626330/playwright-1.50.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40f274384591dfd27f2b014596250b2250c843ed1f7f4ef5d2960ecb91b4961e", size = 39521844 }, + { url = "https://files.pythonhosted.org/packages/f3/f6/002b3d98df9c84296fea84f070dc0d87c2270b37f423cf076a913370d162/playwright-1.50.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9922ef9bcd316995f01e220acffd2d37a463b4ad10fd73e388add03841dfa230", size = 40277563 }, + { url = "https://files.pythonhosted.org/packages/b9/63/c9a73736e434df894e484278dddc0bf154312ff8d0f16d516edb790a7d42/playwright-1.50.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:8fc628c492d12b13d1f347137b2ac6c04f98197ff0985ef0403a9a9ee0d39131", size = 45076712 }, + { url = "https://files.pythonhosted.org/packages/bd/2c/a54b5a64cc7d1a62f2d944c5977fb3c88e74d76f5cdc7966e717426bce66/playwright-1.50.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcff35f72db2689a79007aee78f1b0621a22e6e3d6c1f58aaa9ac805bf4497c", size = 44493111 }, + { url = "https://files.pythonhosted.org/packages/2b/4a/047cbb2ffe1249bd7a56441fc3366fb4a8a1f44bc36a9061d10edfda2c86/playwright-1.50.0-py3-none-win32.whl", hash = "sha256:3b906f4d351260016a8c5cc1e003bb341651ae682f62213b50168ed581c7558a", size = 34784543 }, + { url = "https://files.pythonhosted.org/packages/bc/2b/e944e10c9b18e77e43d3bb4d6faa323f6cc27597db37b75bc3fd796adfd5/playwright-1.50.0-py3-none-win_amd64.whl", hash = "sha256:1859423da82de631704d5e3d88602d755462b0906824c1debe140979397d2e8d", size = 34784546 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -1888,6 +1709,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "pprintpp" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/1a/7737e7a0774da3c3824d654993cf57adc915cb04660212f03406334d8c0b/pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403", size = 17995 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952 }, +] + [[package]] name = "pre-commit" version = "4.1.0" @@ -1973,6 +1803,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pyee" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/37/8fb6e653597b2b67ef552ed49b438d5398ba3b85a9453f8ada0fd77d455c/pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3", size = 30915 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/68/7e150cba9eeffdeb3c5cecdb6896d70c8edd46ce41c0491e12fb2b2256ff/pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef", size = 15527 }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -2008,6 +1850,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] +[[package]] +name = "pytest-base-url" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/1a/b64ac368de6b993135cb70ca4e5d958a5c268094a3a2a4cac6f0021b6c4f/pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", size = 6702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302 }, +] + [[package]] name = "pytest-cov" version = "6.0.0" @@ -2033,6 +1888,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/4c/a4fe18205926216e1aebe1f125cba5bce444f91b6e4de4f49fa87e322775/pytest_django-4.10.0-py3-none-any.whl", hash = "sha256:57c74ef3aa9d89cae5a5d73fbb69a720a62673ade7ff13b9491872409a3f5918", size = 23975 }, ] +[[package]] +name = "pytest-env" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141 }, +] + +[[package]] +name = "pytest-playwright" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "playwright" }, + { name = "pytest" }, + { name = "pytest-base-url" }, + { name = "python-slugify" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/47/38e292ad92134a00ea05e6fc4fc44577baaa38b0922ab7ea56312b7a6663/pytest_playwright-0.7.0.tar.gz", hash = "sha256:b3f2ea514bbead96d26376fac182f68dcd6571e7cb41680a89ff1673c05d60b6", size = 16666 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/96/5f8a4545d783674f3de33f0ebc4db16cc76ce77a4c404d284f43f09125e3/pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2", size = 16618 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2045,6 +1928,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051 }, +] + +[[package]] +name = "pytz" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -2117,6 +2021,91 @@ md = [ { name = "cmarkgfm" }, ] +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, + { url = "https://files.pythonhosted.org/packages/89/23/c4a86df398e57e26f93b13ae63acce58771e04bdde86092502496fa57f9c/regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", size = 482682 }, + { url = "https://files.pythonhosted.org/packages/3c/8b/45c24ab7a51a1658441b961b86209c43e6bb9d39caf1e63f46ce6ea03bc7/regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", size = 287679 }, + { url = "https://files.pythonhosted.org/packages/7a/d1/598de10b17fdafc452d11f7dada11c3be4e379a8671393e4e3da3c4070df/regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", size = 284578 }, + { url = "https://files.pythonhosted.org/packages/49/70/c7eaa219efa67a215846766fde18d92d54cb590b6a04ffe43cef30057622/regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", size = 782012 }, + { url = "https://files.pythonhosted.org/packages/89/e5/ef52c7eb117dd20ff1697968219971d052138965a4d3d9b95e92e549f505/regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", size = 820580 }, + { url = "https://files.pythonhosted.org/packages/5f/3f/9f5da81aff1d4167ac52711acf789df13e789fe6ac9545552e49138e3282/regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", size = 809110 }, + { url = "https://files.pythonhosted.org/packages/86/44/2101cc0890c3621b90365c9ee8d7291a597c0722ad66eccd6ffa7f1bcc09/regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", size = 780919 }, + { url = "https://files.pythonhosted.org/packages/ce/2e/3e0668d8d1c7c3c0d397bf54d92fc182575b3a26939aed5000d3cc78760f/regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", size = 771515 }, + { url = "https://files.pythonhosted.org/packages/a6/49/1bc4584254355e3dba930a3a2fd7ad26ccba3ebbab7d9100db0aff2eedb0/regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", size = 696957 }, + { url = "https://files.pythonhosted.org/packages/c8/dd/42879c1fc8a37a887cd08e358af3d3ba9e23038cd77c7fe044a86d9450ba/regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", size = 768088 }, + { url = "https://files.pythonhosted.org/packages/89/96/c05a0fe173cd2acd29d5e13c1adad8b706bcaa71b169e1ee57dcf2e74584/regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", size = 774752 }, + { url = "https://files.pythonhosted.org/packages/b5/f3/a757748066255f97f14506483436c5f6aded7af9e37bca04ec30c90ca683/regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", size = 838862 }, + { url = "https://files.pythonhosted.org/packages/5c/93/c6d2092fd479dcaeea40fc8fa673822829181ded77d294a7f950f1dda6e2/regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", size = 842622 }, + { url = "https://files.pythonhosted.org/packages/ff/9c/daa99532c72f25051a90ef90e1413a8d54413a9e64614d9095b0c1c154d0/regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", size = 772713 }, + { url = "https://files.pythonhosted.org/packages/13/5d/61a533ccb8c231b474ac8e3a7d70155b00dfc61af6cafdccd1947df6d735/regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", size = 261756 }, + { url = "https://files.pythonhosted.org/packages/dc/7b/e59b7f7c91ae110d154370c24133f947262525b5d6406df65f23422acc17/regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", size = 274110 }, +] + [[package]] name = "requests" version = "2.32.3" @@ -2141,15 +2130,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/48/9c/6d8035cafa2d2d314f34e6cd9313a299de095b26e96f1c7312878f988eec/restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45", size = 16723 } -[[package]] -name = "roman-numerals-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/78/9491ab144c9cb2d97aa74d6f632bd6f4be67957de03f945a23a67415d859/roman_numerals_py-3.0.0.tar.gz", hash = "sha256:91199c4373658c03d87d9fe004f4a5120a20f6cb192be745c2377cce274ef41c", size = 8970 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/d0/a3a2fed015e95b9e81619182adc472540f9786183febfaef8b7c5e909418/roman_numerals_py-3.0.0-py3-none-any.whl", hash = "sha256:a1421ce66b3eab7e8735065458de3fa5c4a46263d50f9f4ac8f0e5e7701dd125", size = 4416 }, -] - [[package]] name = "ruff" version = "0.9.7" @@ -2224,105 +2204,38 @@ wheels = [ name = "sphinx" version = "7.4.7" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] dependencies = [ - { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "babel", marker = "python_full_version < '3.10'" }, - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.10'" }, - { name = "imagesize", marker = "python_full_version < '3.10'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 }, ] -[[package]] -name = "sphinx" -version = "8.1.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "babel", marker = "python_full_version == '3.10.*'" }, - { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version == '3.10.*'" }, - { name = "imagesize", marker = "python_full_version == '3.10.*'" }, - { name = "jinja2", marker = "python_full_version == '3.10.*'" }, - { name = "packaging", marker = "python_full_version == '3.10.*'" }, - { name = "pygments", marker = "python_full_version == '3.10.*'" }, - { name = "requests", marker = "python_full_version == '3.10.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, -] - -[[package]] -name = "sphinx" -version = "8.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", -] -dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "imagesize", marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/4b/95bdb36eaee30698f2d244d52e1b9e58642af56525d4b02fcd0f7312c27c/sphinx-8.2.1.tar.gz", hash = "sha256:e4b932951b9c18b039f73b72e4e63afe967d90408700ec222b981ac24647c01e", size = 8321376 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/aa/282768cff0039b227a923cb65686539bb606e448c594d4fdee4d2c7765a1/sphinx-8.2.1-py3-none-any.whl", hash = "sha256:b5d2bb3cdf6207fcacde9f92085d2b97667b05b9c346eaec426ca4be8af505e9", size = 3589415 }, -] - [[package]] name = "sphinx-autobuild" version = "2024.10.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, { name = "starlette" }, { name = "uvicorn" }, { name = "watchfiles" }, @@ -2338,9 +2251,7 @@ name = "sphinx-basic-ng" version = "1.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 } wheels = [ @@ -2354,9 +2265,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "pygments" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/53/a9a91995cb365e589f413b77fc75f1c0e9b4ac61bfa8da52a779ad855cc0/sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d", size = 15891 } wheels = [ @@ -2381,6 +2290,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, ] +[[package]] +name = "sphinxcontrib-django" +version = "2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "pprintpp" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/28/92f6d685899fbd74a6c575c50dcc1abb8ab69c6da0160bc99d557d2104d1/sphinxcontrib-django-2.5.tar.gz", hash = "sha256:45a54c0cc1f641d6c15872828862f0738348ca8d7d5b92777bcaa530678c2cc4", size = 23788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/3c/2de4b64abb71cf0f27f9e4401695d8843efed9fdd89ab49957f157a23519/sphinxcontrib_django-2.5-py3-none-any.whl", hash = "sha256:70148af4ccbb5184c5b1add939c3827c79a29a7d20ed18c25ceab347e3f14ca8", size = 21538 }, +] + [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" @@ -2465,6 +2388,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533 }, ] +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154 }, +] + [[package]] name = "tomli" version = "2.2.1"