├── tests ├── taxonomy │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ └── models.py ├── test_label.py ├── test_path_value.py ├── test_register.py ├── test_path_field.py ├── conftest.py ├── test_path_creation.py └── test_model.py ├── src └── django_ltree │ ├── migrations │ ├── __init__.py │ └── 0001_create_extension.py │ ├── __init__.py │ ├── apps.py │ ├── functions.py │ ├── querysets.py │ ├── checks.py │ ├── lookups.py │ ├── models.py │ ├── managers.py │ ├── paths.py │ └── fields.py ├── .dockerignore ├── .gitattributes ├── CONTRIBUTORS ├── .pre-commit-config.yaml ├── docs ├── manager.md ├── Makefile ├── index.rst ├── installation.md ├── indexes.md ├── make.bat ├── conf.py └── usage.md ├── .github ├── dependabot.yml └── workflows │ └── CI.yml ├── LICENSE ├── .readthedocs.yml ├── .gitignore ├── pyproject.toml ├── README.md └── poetry.lock /tests/taxonomy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/taxonomy/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/django_ltree/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !python-versions.txt 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/django_ltree/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "django_ltree.apps.DjangoLtreeConfig" 2 | -------------------------------------------------------------------------------- /tests/taxonomy/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TaxonomyConfig(AppConfig): 5 | name = "taxonomy" 6 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | "M. César Señoranis" (github.com/mariocesar) mariocesar@humanzilla.com 2 | "Borys Szefczyk" (github.com/boryszef) boryszef@gmail.com 3 | "Thomas Stephenson" (github.com/ovangle) ovangle 4 | "Baseplate Admin" (github.com/baseplate-admin) baseplate-admin 5 | -------------------------------------------------------------------------------- /tests/taxonomy/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_ltree.models import TreeModel 4 | 5 | 6 | class Taxonomy(TreeModel): 7 | name = models.TextField() 8 | 9 | def __str__(self): 10 | return f"{self.path}: {self.name}" 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.14.10 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | args: [--fix] 9 | # Run the formatter. 10 | - id: ruff-format 11 | -------------------------------------------------------------------------------- /src/django_ltree/migrations/0001_create_extension.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | initial = True 6 | 7 | dependencies = [] 8 | 9 | operations = [ 10 | migrations.RunSQL( 11 | "CREATE EXTENSION IF NOT EXISTS ltree;", "DROP EXTENSION ltree;" 12 | ) 13 | ] 14 | -------------------------------------------------------------------------------- /src/django_ltree/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoLtreeConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "django_ltree" 7 | 8 | def ready(self): 9 | from . import checks as checks 10 | from . import lookups as lookups 11 | from . import functions as functions 12 | -------------------------------------------------------------------------------- /src/django_ltree/functions.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Transform 2 | from django.db.models import fields 3 | from .fields import PathField 4 | 5 | 6 | __all__ = ("NLevel",) 7 | 8 | 9 | @PathField.register_lookup 10 | class NLevel(Transform): 11 | lookup_name = "depth" 12 | function = "nlevel" 13 | 14 | @property 15 | def output_field(self): 16 | return fields.IntegerField() 17 | -------------------------------------------------------------------------------- /docs/manager.md: -------------------------------------------------------------------------------- 1 | # Manager 2 | 3 | ```{eval-rst} 4 | 5 | .. currentmodule:: django_ltree.managers 6 | 7 | .. class:: TreeManager 8 | 9 | This manager augments the django model, allowing it to be queried with tree specific queries 10 | 11 | .. automethod:: get_queryset 12 | 13 | .. automethod:: roots 14 | 15 | .. automethod:: children 16 | 17 | .. automethod:: create_child 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /tests/test_label.py: -------------------------------------------------------------------------------- 1 | from django_ltree.paths import PathGenerator 2 | 3 | 4 | def test_label_generation(): 5 | assert PathGenerator.guess_the_label_size(62, 62) == 2 6 | assert PathGenerator.guess_the_label_size(0, 62) == 1 7 | 8 | 9 | def test_automatic_name_creation(): 10 | from taxonomy.models import Taxonomy 11 | 12 | for i in range(0, 1000): 13 | Taxonomy.objects.create_child(name=i) 14 | -------------------------------------------------------------------------------- /src/django_ltree/querysets.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from .models import TreeModel 7 | 8 | 9 | class TreeQuerySet(models.QuerySet): 10 | def roots(self) -> models.QuerySet["TreeModel"]: 11 | return self.filter(path__depth=1) 12 | 13 | def children(self, path: str) -> models.QuerySet["TreeModel"]: 14 | return self.filter(path__descendants=path, path__depth=len(path) + 1) 15 | -------------------------------------------------------------------------------- /tests/test_path_value.py: -------------------------------------------------------------------------------- 1 | from django_ltree.fields import PathValue 2 | 3 | 4 | def test_create(): 5 | assert str(PathValue([1, 2, 3, 4, 5])) == "1.2.3.4.5" 6 | assert str(PathValue((1, 3, 5, 7))) == "1.3.5.7" 7 | assert str(PathValue("hello.world")) == "hello.world" 8 | assert str(PathValue(5)) == "5" 9 | 10 | def generator(): 11 | yield "100" 12 | yield "bottles" 13 | yield "of" 14 | yield "beer" 15 | 16 | assert str(PathValue(generator())) == "100.bottles.of.beer" 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django_ltree documentation master file, created by 2 | sphinx-quickstart on Sat Feb 24 21:13:02 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django_ltree's documentation! 7 | ======================================== 8 | 9 | Augmenting `django` orm with postgres `ltree `_ functionalities 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | installation 16 | usage 17 | indexes 18 | manager 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | .. * :ref:`modindex` 25 | * :ref:`search` 26 | -------------------------------------------------------------------------------- /src/django_ltree/checks.py: -------------------------------------------------------------------------------- 1 | from django.core.checks import Warning, register 2 | 3 | 4 | @register 5 | def check_database_backend_is_postgres(app_configs, **kwargs): 6 | from django.conf import settings 7 | 8 | errors = [] 9 | valid_dbs = ["postgres", "postgis"] 10 | 11 | if "default" in settings.DATABASES and all( 12 | d not in settings.DATABASES["default"]["ENGINE"] for d in valid_dbs 13 | ): 14 | errors.append( 15 | Warning( 16 | "django_ltree needs postgres to support install the ltree extension.", 17 | hint="Use the postgres engine or ignore if you already use a custom engine for postgres", 18 | ) 19 | ) 20 | 21 | return errors 22 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Requirements 4 | 5 | - Python 3.9 to 3.13 supported. 6 | - PyPy 3.9 to 3.10 supported. 7 | - Django 4.2 to 5.1 supported. 8 | - Postgres 14 to 17 supported. 9 | 10 | ## Installation 11 | 12 | ```{attention} 13 | Please remember to uninstall `django-ltree` before installing `django-ltree-2`, since both uses `django_ltree` namespace. 14 | ``` 15 | 16 | 1. Install with **pip**: 17 | 18 | ```bash 19 | python -m pip install django-ltree-2 20 | ``` 21 | 22 | 2. Add django-ltree to your `INSTALLED_APPS`: 23 | 24 | ```{code-block} python 25 | :caption: settings.py 26 | 27 | INSTALLED_APPS = [ 28 | ..., 29 | "django_ltree", 30 | ..., 31 | ] 32 | ``` 33 | 34 | 3. Run migrations: 35 | 36 | ```sh 37 | ./manage.py migrate 38 | ``` 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | open-pull-requests-limit: 100 13 | 14 | - package-ecosystem: "github-actions" # See documentation for possible values 15 | directory: "/" # Location of package manifests 16 | schedule: 17 | interval: "daily" 18 | open-pull-requests-limit: 100 19 | -------------------------------------------------------------------------------- /docs/indexes.md: -------------------------------------------------------------------------------- 1 | # Indexes 2 | 3 | If you want to add index to your `TreeModel`, use [`GistIndex`](https://docs.djangoproject.com/en/5.1/ref/contrib/postgres/indexes/#gistindex) from [`postgres`](https://www.postgresql.org/docs/9.1/textsearch-indexes.html). 4 | 5 | ```{note} 6 | `GistIndex` was suggested based on [`@pauloxnet`](http://github.com/pauloxnet)'s code sample from this [microsoft Citus Con YouTube video](https://www.youtube.com/watch?v=u8F7bTJVe_4&t=1051s) 7 | 8 | ``` 9 | 10 | To implement the index in your model: 11 | 12 | ```{code-block} python 13 | :caption: models.py 14 | 15 | from django.contrib.postgres import indexes as idx 16 | from django_ltree import TreeModel 17 | 18 | class CustomTree(TreeModel): 19 | ... 20 | 21 | class Meta: 22 | indexes = [ 23 | idx.GistIndex(fields=["path"]), 24 | ] 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/test_register.py: -------------------------------------------------------------------------------- 1 | from django_ltree.fields import PathField 2 | from django_ltree import lookups, functions 3 | 4 | 5 | def test_registered_lookups(): 6 | registered_lookups = PathField.get_lookups() 7 | 8 | assert "ancestors" in registered_lookups, "Missing 'ancestors' in lookups" 9 | assert registered_lookups["ancestors"] is lookups.AncestorLookup 10 | 11 | assert "descendants" in registered_lookups, "Missing 'descendants' in lookups" 12 | assert registered_lookups["descendants"] is lookups.DescendantLookup 13 | 14 | assert "match" in registered_lookups, "Missing 'match' in lookups" 15 | assert registered_lookups["match"] is lookups.MatchLookup 16 | 17 | assert "depth" in registered_lookups, "Missing 'depth' in lookups" 18 | assert registered_lookups["depth"] is functions.NLevel 19 | 20 | assert "contains" in registered_lookups, "Missing 'contains' in lookups" 21 | assert registered_lookups["contains"] is lookups.ContainsLookup 22 | -------------------------------------------------------------------------------- /tests/test_path_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django.core.exceptions import ValidationError 4 | from django_ltree.fields import PathValue 5 | 6 | from taxonomy.models import Taxonomy 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ["path", "valid"], 11 | [ 12 | ("00_00a", True), 13 | ("00$00", False), 14 | ("00000a.00000b", True), 15 | ("00000a+00000b", False), 16 | ], 17 | ) 18 | def test_path_field_validation(path, valid): 19 | """Validating that the path field is valid.""" 20 | taxonomy = Taxonomy() 21 | taxonomy.name = "test" 22 | taxonomy.path = PathValue(path) 23 | if valid: 24 | taxonomy.full_clean() 25 | else: 26 | with pytest.raises(ValidationError) as excinfo: 27 | taxonomy.full_clean() 28 | 29 | assert excinfo.value.message_dict == { 30 | "path": [ 31 | "A label is a sequence of alphanumeric characters and underscores separated by dots." 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tests/taxonomy/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.24 on 2019-09-11 12:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django_ltree.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | initial = True 11 | 12 | dependencies = [] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Taxonomy", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("path", django_ltree.fields.PathField(unique=True)), 28 | ("name", models.TextField()), 29 | ], 30 | options={ 31 | "ordering": ("path",), 32 | "abstract": False, 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | from django.conf import settings 4 | 5 | 6 | def pytest_sessionstart(session): 7 | settings.configure( 8 | DEBUG=False, 9 | USE_TZ=True, 10 | DATABASES={ 11 | "default": { 12 | "ENGINE": "django.db.backends.postgresql", 13 | "NAME": "ltree_test", 14 | "HOST": os.environ.get("DJANGO_DATABASE_HOST", "database"), 15 | "USER": os.environ.get("DJANGO_DATABASE_USER", "postgres"), 16 | "PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD", ""), 17 | } 18 | }, 19 | ROOT_URLCONF="tests.urls", 20 | INSTALLED_APPS=[ 21 | "django.contrib.auth", 22 | "django.contrib.contenttypes", 23 | "django.contrib.messages", 24 | "django.contrib.sessions", 25 | "django.contrib.sites", 26 | "django_ltree", 27 | "taxonomy", 28 | ], 29 | SITE_ID=1, 30 | SILENCED_SYSTEM_CHECKS=["RemovedInDjango30Warning"], 31 | ) 32 | django.setup() 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mario César 4 | Copyright (c) 2019 Borys Szefczyk 5 | Copyright (c) 2023 baseplate-admin 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: '3.12' 13 | # You can also specify other tool versions: 14 | jobs: 15 | post_create_environment: 16 | # Install poetry 17 | # https://python-poetry.org/docs/#installing-manually 18 | - pip install poetry 19 | # Tell poetry to update itself 20 | - poetry lock 21 | # Tell poetry to not use a virtual environment 22 | - poetry config virtualenvs.create false 23 | post_install: 24 | # Install dependencies with 'docs' dependency group 25 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 26 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs 27 | 28 | # Build documentation in the "docs/" directory with Sphinx 29 | sphinx: 30 | configuration: docs/conf.py 31 | # Optionally build your docs in additional formats such as PDF and ePub 32 | # formats: 33 | # - pdf 34 | # - epub 35 | 36 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "django_ltree_2" 10 | copyright = "2024, baseplate-admin" 11 | author = "baseplate-admin" 12 | release = "0.1.10" 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | "sphinx.ext.autodoc", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.viewcode", 21 | "myst_parser", 22 | ] 23 | 24 | templates_path = ["_templates"] 25 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 26 | 27 | # List of patterns, relative to source directory, that match files and 28 | # directories to ignore when looking for source files. 29 | 30 | 31 | autodoc_typehints = "description" 32 | 33 | # -- Options for HTML output ------------------------------------------------- 34 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 35 | 36 | html_theme = "shibuya" 37 | html_static_path = ["_static"] 38 | -------------------------------------------------------------------------------- /src/django_ltree/lookups.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Lookup 2 | 3 | from .fields import PathField 4 | 5 | 6 | class SimpleLookup(Lookup): 7 | lookup_operator = "=" 8 | 9 | def as_sql(self, compiler, connection): 10 | lhs, lhs_params = self.process_lhs(compiler, connection) 11 | rhs, rhs_params = self.process_rhs(compiler, connection) 12 | return f"{lhs} {self.lookup_operator} {rhs}", [ 13 | *lhs_params, 14 | *rhs_params, 15 | ] 16 | 17 | 18 | @PathField.register_lookup 19 | class EqualLookup(Lookup): 20 | lookup_name = "exact" 21 | 22 | def as_sql(self, compiler, connection): 23 | lhs, lhs_params = self.process_lhs(compiler, connection) 24 | rhs, rhs_params = self.process_rhs(compiler, connection) 25 | return f"{lhs} = {rhs}", [*lhs_params, *rhs_params] 26 | 27 | 28 | @PathField.register_lookup 29 | class AncestorLookup(SimpleLookup): 30 | lookup_name = "ancestors" 31 | lookup_operator = "@>" 32 | 33 | 34 | @PathField.register_lookup 35 | class DescendantLookup(SimpleLookup): 36 | lookup_name = "descendants" 37 | lookup_operator = "<@" 38 | 39 | 40 | @PathField.register_lookup 41 | class MatchLookup(SimpleLookup): 42 | lookup_name = "match" 43 | lookup_operator = "~" 44 | 45 | 46 | @PathField.register_lookup 47 | class ContainsLookup(SimpleLookup): 48 | lookup_name = "contains" 49 | lookup_operator = "?" 50 | -------------------------------------------------------------------------------- /src/django_ltree/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from typing import Any 3 | 4 | from .fields import PathField, PathValue 5 | from .managers import TreeManager 6 | 7 | 8 | class TreeModel(models.Model): 9 | path = PathField(unique=True) 10 | objects = TreeManager() 11 | 12 | class Meta: 13 | abstract = True 14 | ordering = ("path",) 15 | 16 | def label(self): 17 | return self.path[-1] 18 | 19 | def get_ancestors_paths(self) -> list[list[str]]: 20 | return [PathValue(self.path[:n]) for n, p in enumerate(self.path) if n > 0] 21 | 22 | def ancestors(self): 23 | return type(self)._default_manager.filter(path__ancestors=self.path) 24 | 25 | def descendants(self): 26 | return type(self)._default_manager.filter(path__descendants=self.path) 27 | 28 | def parent(self): 29 | if len(self.path) > 1: 30 | return self.ancestors().exclude(id=self.id).last() 31 | 32 | def children(self): 33 | return self.descendants().filter(path__depth=len(self.path) + 1) 34 | 35 | def siblings(self): 36 | parent = self.path[:-1] 37 | return ( 38 | type(self) 39 | ._default_manager.filter(path__descendants=".".join(parent)) 40 | .filter(path__depth=len(self.path)) 41 | .exclude(path=self.path) 42 | ) 43 | 44 | def add_child(self, slug: str, **kwargs) -> Any: 45 | assert "path" not in kwargs 46 | kwargs["path"] = self.path[:] 47 | kwargs["path"].append(slug) 48 | kwargs["slug"] = slug 49 | return type(self)._default_manager.create(**kwargs) 50 | -------------------------------------------------------------------------------- /src/django_ltree/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from typing import TYPE_CHECKING 3 | from django_ltree.paths import PathGenerator 4 | 5 | from .querysets import TreeQuerySet 6 | 7 | if TYPE_CHECKING: 8 | from django_ltree.models import TreeModel 9 | 10 | 11 | class TreeManager(models.Manager): 12 | def get_queryset(self) -> TreeQuerySet["TreeModel"]: 13 | """Returns a queryset with the models ordered by `path`""" 14 | return TreeQuerySet(model=self.model, using=self._db).order_by("path") 15 | 16 | def roots(self) -> TreeQuerySet["TreeModel"]: 17 | """Returns the roots of a given model""" 18 | return self.filter().roots() 19 | 20 | def children(self, path: str) -> TreeQuerySet["TreeModel"]: 21 | """Returns the childrens of a given object""" 22 | return self.filter().children(path) 23 | 24 | def create_child( 25 | self, parent: "TreeModel" = None, label: str = None, **kwargs 26 | ) -> TreeQuerySet["TreeModel"]: 27 | """Creates a tree child with or without parent""" 28 | prefix = parent.path if parent else None 29 | 30 | """If a label is not provided, we generate a new one, else we use it as suffix""" 31 | if label is None: 32 | paths_in_use = parent.children() if parent else self.roots() 33 | path_generator = PathGenerator( 34 | prefix, 35 | skip=paths_in_use.values_list("path", flat=True), 36 | ) 37 | path = next(path_generator) 38 | else: 39 | if prefix is None: 40 | path = label 41 | else: 42 | path = str(prefix) + "." + label 43 | 44 | kwargs["path"] = path 45 | return self.create(**kwargs) 46 | -------------------------------------------------------------------------------- /src/django_ltree/paths.py: -------------------------------------------------------------------------------- 1 | import string 2 | import math 3 | from itertools import product 4 | 5 | from .fields import PathValue 6 | 7 | 8 | class PathGenerator: 9 | def __init__(self, prefix=None, skip=None): 10 | combinations = string.digits + string.ascii_letters 11 | 12 | self.skip_paths = [] if skip is None else skip[:] 13 | self.path_prefix = prefix if prefix else [] 14 | self.product_iterator = product( 15 | combinations, 16 | repeat=self.guess_the_label_size( 17 | path_size=len(self.skip_paths), combination_size=len(combinations) 18 | ), 19 | ) 20 | 21 | def __iter__(self): 22 | return self 23 | 24 | def __next__(self): 25 | for val in self.product_iterator: 26 | label = "".join(val) 27 | path = PathValue(self.path_prefix + [label]) 28 | if path not in self.skip_paths: 29 | return path 30 | 31 | @staticmethod 32 | def guess_the_label_size(path_size: int, combination_size: int) -> int: 33 | calculated_path_size = -1 # -1 is here for 0th index items 34 | # The theoritical limit for this at the time of writing is 32 (python 3.12.2) 35 | label_size = 0 36 | 37 | last = 0 38 | 39 | while True: 40 | possible_cominations = math.perm(combination_size, label_size) 41 | if last > possible_cominations: 42 | raise ValueError("There is error in the input value") 43 | 44 | last = possible_cominations 45 | calculated_path_size += possible_cominations 46 | 47 | if calculated_path_size > path_size and label_size != 0: 48 | break 49 | 50 | label_size += 1 51 | 52 | return label_size 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # OS Stuff 107 | .DS_Store 108 | *.swp 109 | 110 | .idea 111 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-ltree-2" 3 | version = "0.1.12" 4 | description = "Continual of django-ltree with additional goodies" 5 | authors = [ 6 | "baseplate-admin <61817579+baseplate-admin@users.noreply.github.com>", 7 | "Mario César Señoranis Ayala ", 8 | "Kimsia Sim", 9 | ] 10 | maintainers = [ 11 | "baseplate-admin <61817579+baseplate-admin@users.noreply.github.com>", 12 | ] 13 | license = "MIT" 14 | readme = "README.md" 15 | packages = [{ include = "django_ltree", from = "src" }] 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Framework :: Django", 19 | "Framework :: Django :: 4.2", 20 | "Framework :: Django :: 5.0", 21 | "Framework :: Django :: 5.1", 22 | "Intended Audience :: Developers", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python :: Implementation :: PyPy", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3.13", 31 | "Programming Language :: Python :: 3.14", 32 | ] 33 | 34 | [tool.poetry.urls] 35 | "Bug Tracker" = "https://github.com/baseplate-admin/django-ltree-2/issues" 36 | "homepage" = "https://github.com/baseplate-admin/django-ltree-2" 37 | "repository" = "https://github.com/baseplate-admin/django-ltree-2" 38 | 39 | 40 | [tool.poetry.dependencies] 41 | python = ">=3.9" 42 | django = ">=3.2" 43 | psycopg = ">=3" 44 | 45 | [tool.poetry.group.test.dependencies] 46 | pytest = "^8.0.1" 47 | pytest-django = "^4.8.0" 48 | 49 | [tool.poetry.group.docs.dependencies] 50 | sphinx = "^7.2.6" 51 | myst-parser = ">=2" 52 | sphinx-reload = "^0.2.0" 53 | shibuya = ">=2024.6.1,<2026.0.0" 54 | 55 | [build-system] 56 | requires = ["poetry-core"] 57 | build-backend = "poetry.core.masonry.api" 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-ltree-2 2 | 3 | [![Downloads](https://static.pepy.tech/badge/django-ltree-2)](https://pepy.tech/project/django-ltree-2) [![Documentation Status](https://readthedocs.org/projects/django-ltree-2/badge/?version=latest)](https://django-ltree-2.readthedocs.io/en/latest/?badge=latest) [![CI](https://github.com/baseplate-admin/django-ltree-2/actions/workflows/CI.yml/badge.svg)](https://github.com/baseplate-admin/django-ltree-2/actions/workflows/test.yml) [![Pypi Badge](https://img.shields.io/pypi/v/django-ltree-2.svg)](https://pypi.org/project/django-ltree-2/) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/baseplate-admin/django-ltree-2/master.svg)](https://results.pre-commit.ci/latest/github/baseplate-admin/django-ltree-2/master) 4 | 5 | A tree extension implementation to support hierarchical tree-like data in Django models, 6 | using the native Postgres extension `ltree`. 7 | 8 | Postgresql has already a optimized and very useful tree implementation for data. 9 | The extension is [ltree](https://www.postgresql.org/docs/9.6/static/ltree.html) 10 | 11 | This fork contains is a continuation of the work done by [`mariocesar`](https://github.com/mariocesar/) on [`django-ltree`](https://github.com/mariocesar/django-ltree) and merges the work done by [`simkimsia`](https://github.com/simkimsia) on [`greendeploy-django-ltree`](https://github.com/GreenDeploy-io/greendeploy-django-ltree) 12 | 13 | ## Install 14 | 15 | Please remember to uninstall `django-ltree` before installing `django-ltree-2`, since both uses `django_ltree` namespace. 16 | 17 | --- 18 | 19 | ``` 20 | pip install django-ltree-2 21 | ``` 22 | 23 | Then add `django_ltree` to `INSTALLED_APPS` in your Django project settings. 24 | 25 | ```python 26 | INSTALLED_APPS = [ 27 | ..., 28 | 'django_ltree', 29 | ... 30 | ] 31 | ``` 32 | 33 | Then use it like this: 34 | 35 | ```python 36 | 37 | from django_ltree.models import TreeModel 38 | 39 | 40 | class CustomTree(TreeModel): 41 | ... 42 | 43 | ``` 44 | 45 | ## Requires 46 | 47 | - Django 3.2 or superior 48 | - Python 3.9 or higher 49 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Lets assume that our model looks like this. 4 | 5 | ```{code-block} python 6 | :caption: models.py 7 | 8 | from django import models 9 | from django_ltree import TreeModel 10 | 11 | class CustomTree(TreeModel): 12 | text = models.TextField() 13 | ``` 14 | 15 | ## Create a child without parent 16 | 17 | ```{code-block} python 18 | :caption: views.py 19 | 20 | from .models import CustomTree 21 | 22 | CustomTree.objects.create_child(text='Hello world') 23 | ``` 24 | 25 | The following code with create a child with no parent (ie:roots of a 26 | tree) 27 | 28 | ## Create a child with parent 29 | 30 | Let's assume we want to add a child to the CustomTree object of `pk=1` 31 | 32 | ```{code-block} python 33 | :caption: views.py 34 | 35 | from .models import CustomTree 36 | 37 | # This must return a single object 38 | parent: CustomTree = CustomTree.objects.get(pk=1) 39 | CustomTree.objects.create_child(text='Hello world', parent=parent) 40 | ``` 41 | 42 | ## Get all the roots of the model 43 | 44 | A root means the the object that childrens anchor to. 45 | 46 | ```{code-block} python 47 | :caption: views.py 48 | 49 | from .models import CustomTree 50 | 51 | roots: list[CustomTree] = CustomTree.objects.roots() 52 | ``` 53 | 54 | ## Get all the childrens of a object 55 | 56 | To get the childrens of a object, we can first get the object then call 57 | the QuerySet specific children function. 58 | 59 | ```{code-block} python 60 | :caption: views.py 61 | 62 | from .models import CustomTree 63 | 64 | instance = CustomTree.objects.get(pk=1) 65 | 66 | childrens: list[CustomTree] = instance.children() 67 | ``` 68 | 69 | ## Get the length of childrens of a object 70 | 71 | `django` specific database functions still work when we call 72 | the filter method. 73 | 74 | Lets assume we want to get the childrens of `CustomTree` 75 | object whose pk is 1. 76 | 77 | ```{code-block} python 78 | :caption: views.py 79 | 80 | from .models import CustomTree 81 | 82 | instance = CustomTree.objects.get(pk=1) 83 | 84 | childrens: int = instance.children().count() 85 | ``` 86 | -------------------------------------------------------------------------------- /src/django_ltree/fields.py: -------------------------------------------------------------------------------- 1 | from collections import UserList 2 | from django import forms 3 | from django.core.validators import RegexValidator 4 | from django.db.models.fields import TextField 5 | from django.forms.widgets import TextInput 6 | 7 | from collections.abc import Iterable 8 | 9 | path_label_validator = RegexValidator( 10 | r"^(?P[a-zA-Z0-9_-]+)(?:\.[a-zA-Z0-9_-]+)*$", 11 | "A label is a sequence of alphanumeric characters and underscores separated by dots.", 12 | "invalid", 13 | ) 14 | 15 | 16 | class PathValue(UserList): 17 | def __init__(self, value): 18 | if isinstance(value, str): 19 | split_by = "/" if "/" in value else "." 20 | value = value.strip().split(split_by) if value else [] 21 | elif isinstance(value, int): 22 | value = [str(value)] 23 | elif isinstance(value, Iterable): 24 | value = [str(v) for v in value] 25 | else: 26 | raise ValueError("Invalid value: {!r} for path".format(value)) 27 | 28 | super().__init__(initlist=value) 29 | 30 | def __repr__(self): 31 | return str(self) 32 | 33 | def __str__(self): 34 | return ".".join(self) 35 | 36 | def __hash__(self): 37 | return hash(tuple(self)) 38 | 39 | 40 | class PathValueProxy: 41 | def __init__(self, field_name): 42 | self.field_name = field_name 43 | 44 | def __get__(self, instance, owner): 45 | if instance is None: 46 | return self 47 | 48 | value = instance.__dict__[self.field_name] 49 | 50 | if value is None: 51 | return value 52 | 53 | return PathValue(instance.__dict__[self.field_name]) 54 | 55 | def __set__(self, instance, value): 56 | if instance is None: 57 | return self 58 | 59 | instance.__dict__[self.field_name] = value 60 | 61 | 62 | class PathFormField(forms.CharField): 63 | default_validators = [path_label_validator] 64 | 65 | 66 | class PathField(TextField): 67 | default_validators = [path_label_validator] 68 | 69 | def db_type(self, connection): 70 | return "ltree" 71 | 72 | def formfield(self, **kwargs): 73 | kwargs["form_class"] = PathFormField 74 | kwargs["widget"] = TextInput(attrs={"class": "vTextField"}) 75 | return super().formfield(**kwargs) 76 | 77 | def contribute_to_class(self, cls, name, private_only=False): 78 | super().contribute_to_class(cls, name) 79 | setattr(cls, self.name, PathValueProxy(self.name)) 80 | 81 | def from_db_value(self, value, expression, connection, *args): 82 | if value is None: 83 | return value 84 | return PathValue(value) 85 | 86 | def get_prep_value(self, value): 87 | if value is None: 88 | return value 89 | return str(PathValue(value)) 90 | 91 | def to_python(self, value): 92 | if value is None: 93 | return value 94 | elif isinstance(value, PathValue): 95 | return value 96 | 97 | return PathValue(value) 98 | 99 | def get_db_prep_value(self, value, connection, prepared=False): 100 | if value is None: 101 | return value 102 | elif isinstance(value, PathValue): 103 | return str(value) 104 | elif isinstance(value, (list, str)): 105 | return str(PathValue(value)) 106 | 107 | raise ValueError(f"Unknown value type {type(value)}") 108 | -------------------------------------------------------------------------------- /tests/test_path_creation.py: -------------------------------------------------------------------------------- 1 | from taxonomy.models import Taxonomy 2 | 3 | # ----- Automatic Path Creation Tests ----- 4 | 5 | 6 | def test_create_root_node_automatic(db): 7 | """Test creating a root node with automatic labeling.""" 8 | root = Taxonomy.objects.create_child(name="test_auto_root") 9 | assert root is not None 10 | assert root.children().count() == 0 11 | 12 | 13 | def test_create_children_automatic(db): 14 | """Test creating children with automatic labels.""" 15 | root = Taxonomy.objects.create_child(name="test_auto_root") 16 | 17 | # Create children 18 | for i in range(3): 19 | Taxonomy.objects.create_child(name=f"test_auto_child_{i}", parent=root) 20 | 21 | assert root.children().count() == 3 22 | 23 | 24 | def test_children_have_automatic_labels(db): 25 | """Test that automatically created children get labels.""" 26 | root = Taxonomy.objects.create_child(name="test_auto_root") 27 | child = Taxonomy.objects.create_child(name="test_auto_child", parent=root) 28 | 29 | assert child.label() is not None 30 | 31 | 32 | def test_multilevel_hierarchy_automatic(db): 33 | """Test a multi-level hierarchy with automatic labels.""" 34 | root = Taxonomy.objects.create_child(name="test_auto_root") 35 | child = Taxonomy.objects.create_child(name="test_auto_child", parent=root) 36 | grandchild = Taxonomy.objects.create_child( 37 | name="test_auto_grandchild", parent=child 38 | ) 39 | 40 | assert root.children().count() == 1 41 | assert child.children().count() == 1 42 | assert grandchild.children().count() == 0 43 | 44 | 45 | def test_deep_tree_automatic(db): 46 | """Test creating a deep tree with automatic labels.""" 47 | root = Taxonomy.objects.create_child(name="test_auto_root") 48 | 49 | # Create a chain of nodes 50 | current = root 51 | for i in range(5): 52 | current = Taxonomy.objects.create_child( 53 | name=f"test_auto_level_{i}", parent=current 54 | ) 55 | 56 | # Leaf node should have no children 57 | assert current.children().count() == 0 58 | 59 | 60 | # ----- Manual Path Creation Tests ----- 61 | 62 | 63 | def test_create_root_with_custom_label(db): 64 | """Test creating a root node with a custom label.""" 65 | root = Taxonomy.objects.create_child(name="test_manual_root", label="ROOT") 66 | assert root.label() == "ROOT" 67 | 68 | 69 | def test_create_children_with_custom_labels(db): 70 | """Test creating children with custom labels.""" 71 | root = Taxonomy.objects.create_child(name="test_manual_root", label="ROOT") 72 | 73 | labels = ["A", "B", "C"] 74 | for label in labels: 75 | Taxonomy.objects.create_child( 76 | name=f"test_manual_child_{label}", label=label, parent=root 77 | ) 78 | 79 | assert root.children().count() == len(labels) 80 | 81 | child_labels = [child.label() for child in root.children()] 82 | for label in labels: 83 | assert label in child_labels 84 | 85 | 86 | def test_hierarchy_with_custom_labels(db): 87 | """Test a hierarchy with custom labels.""" 88 | root = Taxonomy.objects.create_child(name="test_manual_root", label="ROOT") 89 | child = Taxonomy.objects.create_child( 90 | name="test_manual_child", label="CHILD", parent=root 91 | ) 92 | 93 | assert child.label() == "CHILD" 94 | assert root.children().count() == 1 95 | 96 | 97 | def test_path_construction_with_custom_labels(db): 98 | """Test path construction with custom labels.""" 99 | root = Taxonomy.objects.create_child(name="test_manual_root", label="R") 100 | child = Taxonomy.objects.create_child( 101 | name="test_manual_child", label="C", parent=root 102 | ) 103 | grandchild = Taxonomy.objects.create_child( 104 | name="test_manual_grandchild", label="G", parent=child 105 | ) 106 | 107 | # Path should be concatenated with periods 108 | assert str(grandchild.path) == "R.C.G" 109 | 110 | 111 | def test_retrieve_node_by_path(db): 112 | """Test retrieving a node by its path.""" 113 | root = Taxonomy.objects.create_child(name="test_manual_root", label="R") 114 | child = Taxonomy.objects.create_child( 115 | name="test_manual_child", label="C", parent=root 116 | ) 117 | Taxonomy.objects.create_child( 118 | name="test_manual_grandchild", label="G", parent=child 119 | ) 120 | 121 | # Retrieve by path 122 | node = Taxonomy.objects.filter(path="R.C.G").first() 123 | assert node is not None 124 | assert node.name == "test_manual_grandchild" 125 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10000 # Very high timeout for label name testing 13 | 14 | name: Python ${{ matrix.python-version }} sample with Postgres ${{ matrix.postgres-version }} 15 | strategy: 16 | matrix: 17 | python-version: [ 18 | '3.9', 19 | '3.10', 20 | '3.11', 21 | '3.12', 22 | '3.13', 23 | '3.14-dev', 24 | 'pypy-3.9', 25 | 'pypy-3.10', 26 | #'pypy-3.11', 27 | ] 28 | postgres-version: ['14', '15', '16', '17'] 29 | continue-on-error: true 30 | services: 31 | database: 32 | image: postgres:${{ matrix.postgres-version }} 33 | env: 34 | POSTGRES_USER: postgres 35 | POSTGRES_PASSWORD: postgres 36 | POSTGRES_DB: postgres 37 | ports: 38 | - 5432:5432 39 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 40 | env: 41 | DJANGO_DATABASE_HOST: localhost 42 | DJANGO_DATABASE_USER: postgres 43 | DJANGO_DATABASE_PASSWORD: postgres 44 | DJANGO_DATABASE_NAME: postgres 45 | 46 | steps: 47 | - uses: actions/checkout@v6 48 | - name: Set up Python 49 | uses: actions/setup-python@v6 50 | with: 51 | python-version: ${{ matrix.python-version }} 52 | - uses: actions/checkout@v6 53 | - name: Install Poetry 54 | uses: snok/install-poetry@v1 55 | with: 56 | virtualenvs-create: true 57 | virtualenvs-in-project: true 58 | - name: Install python dependencies 59 | run: | 60 | poetry run pip install django psycopg pytest pytest-django 61 | poetry run pip install -e . 62 | 63 | - name: Run tests 64 | run: | 65 | poetry run pytest -v -x tests/ 66 | 67 | # - name: Upload pytest test results 68 | # uses: actions/upload-artifact@v2 69 | # with: 70 | # name: pytest-results-${{ matrix.python-version }} 71 | # path: junit/test-results-${{ matrix.python-version }}.xml 72 | # if: ${{ always() }} 73 | 74 | merge: 75 | runs-on: ubuntu-latest 76 | needs: [test] 77 | # Only run if the PR author is Dependabot or pre-commit-ci 78 | if: github.actor == 'dependabot[bot]' 79 | 80 | steps: 81 | - uses: actions/checkout@v6 82 | - name: Enable auto-merge for Dependabot PRs 83 | run: gh pr merge --auto --merge "$PR_URL" # Use Github CLI to merge automatically the PR 84 | env: 85 | PR_URL: ${{github.event.pull_request.html_url}} 86 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 87 | 88 | release: 89 | runs-on: ubuntu-latest 90 | if: "startsWith(github.ref, 'refs/tags/')" 91 | needs: [test] 92 | 93 | environment: 94 | name: pypi 95 | url: https://pypi.org/project/django-ltree-2 96 | 97 | permissions: 98 | contents: write 99 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 100 | steps: 101 | - uses: actions/checkout@v6 102 | - name: Install Poetry 103 | uses: snok/install-poetry@v1 104 | with: 105 | virtualenvs-create: true 106 | virtualenvs-in-project: true 107 | 108 | - name: Build the dependency 109 | run: poetry build 110 | 111 | - name: Upload package to release 112 | uses: svenstaro/upload-release-action@v2 113 | with: 114 | repo_token: ${{ secrets.GITHUB_TOKEN }} 115 | file: dist/*.whl 116 | tag: ${{ github.ref }} 117 | overwrite: true 118 | file_glob: true 119 | 120 | - name: Publish package distributions to PyPI 121 | uses: pypa/gh-action-pypi-publish@release/v1 122 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from taxonomy.models import Taxonomy 4 | 5 | 6 | TEST_DATA = [ 7 | {"name": "Bacteria"}, 8 | {"name": "Plantae"}, 9 | { 10 | "name": "Animalia", 11 | "sub": [ 12 | { 13 | "name": "Chordata", 14 | "sub": [ 15 | { 16 | "name": "Mammalia", 17 | "sub": [ 18 | { 19 | "name": "Carnivora", 20 | "sub": [ 21 | { 22 | "name": "Canidae", 23 | "sub": [ 24 | { 25 | "name": "Canis", 26 | "sub": [ 27 | {"name": "Canis lupus"}, 28 | {"name": "Canis rufus"}, 29 | ], 30 | }, 31 | { 32 | "name": "Urocyon", 33 | "sub": [ 34 | {"name": "Urocyon cinereoargenteus"} 35 | ], 36 | }, 37 | ], 38 | }, 39 | { 40 | "name": "Feliformia", 41 | "sub": [ 42 | { 43 | "name": "Felidae", 44 | "sub": [ 45 | { 46 | "name": "Felinae", 47 | "sub": [ 48 | { 49 | "name": "Lynx", 50 | "sub": [ 51 | { 52 | "name": "Lynx lynx" 53 | }, 54 | { 55 | "name": "Lynx rufus" 56 | }, 57 | ], 58 | }, 59 | { 60 | "name": "Puma", 61 | "sub": [ 62 | { 63 | "name": "Puma concolor" 64 | } 65 | ], 66 | }, 67 | ], 68 | } 69 | ], 70 | } 71 | ], 72 | }, 73 | ], 74 | }, 75 | { 76 | "name": "Pilosa", 77 | "sub": [ 78 | { 79 | "name": "Folivora", 80 | "sub": [ 81 | { 82 | "name": "Bradypodidae", 83 | "sub": [ 84 | { 85 | "name": "Bradypus", 86 | "sub": [ 87 | { 88 | "name": "Bradypus tridactylus" 89 | } 90 | ], 91 | } 92 | ], 93 | } 94 | ], 95 | } 96 | ], 97 | }, 98 | ], 99 | }, 100 | { 101 | "name": "Reptilia", 102 | "sub": [ 103 | { 104 | "name": "Squamata", 105 | "sub": [ 106 | { 107 | "name": "Iguania", 108 | "sub": [ 109 | { 110 | "name": "Agamidae", 111 | "sub": [ 112 | { 113 | "name": "Pogona", 114 | "sub": [ 115 | {"name": "Pogona barbata"}, 116 | {"name": "Pogona minor"}, 117 | { 118 | "name": "Pogona vitticeps" 119 | }, 120 | ], 121 | } 122 | ], 123 | } 124 | ], 125 | } 126 | ], 127 | } 128 | ], 129 | }, 130 | ], 131 | } 132 | ], 133 | }, 134 | ] 135 | 136 | 137 | def create_objects(objects, parent): 138 | for obj in objects: 139 | created = Taxonomy.objects.create_child(parent, name=obj["name"]) 140 | if "sub" in obj: 141 | create_objects(obj["sub"], created) 142 | 143 | 144 | def create_test_data(): 145 | create_objects(TEST_DATA, parent=None) 146 | 147 | 148 | def test_create(db): 149 | create_test_data() 150 | assert Taxonomy.objects.count() != 0 151 | 152 | 153 | def test_roots(db): 154 | create_test_data() 155 | roots = Taxonomy.objects.roots().values_list("name", flat=True) 156 | assert set(roots) == set(["Bacteria", "Plantae", "Animalia"]) 157 | 158 | 159 | @pytest.mark.parametrize( 160 | "name, expected", 161 | [ 162 | ("Animalia", ["Chordata"]), 163 | ("Mammalia", ["Carnivora", "Pilosa"]), 164 | ("Reptilia", ["Squamata"]), 165 | ("Pogona", ["Pogona barbata", "Pogona minor", "Pogona vitticeps"]), 166 | ], 167 | ) 168 | def test_children(db, name, expected): 169 | create_test_data() 170 | children = Taxonomy.objects.get(name=name).children().values_list("name", flat=True) 171 | assert set(children) == set(expected) 172 | 173 | 174 | def test_label(db): 175 | create_test_data() 176 | for item in Taxonomy.objects.all(): 177 | label = item.label() 178 | assert label.isalnum() 179 | assert str(item.path).endswith(label) 180 | 181 | 182 | @pytest.mark.parametrize( 183 | "name, expected", 184 | [ 185 | ( 186 | "Canis lupus", 187 | [ 188 | "Animalia", 189 | "Chordata", 190 | "Mammalia", 191 | "Carnivora", 192 | "Canidae", 193 | "Canis", 194 | "Canis lupus", 195 | ], 196 | ), 197 | ("Bacteria", ["Bacteria"]), 198 | ("Chordata", ["Animalia", "Chordata"]), 199 | ], 200 | ) 201 | def test_ancestors(db, name, expected): 202 | create_test_data() 203 | ancestors = ( 204 | Taxonomy.objects.get(name=name).ancestors().values_list("name", flat=True) 205 | ) 206 | assert list(ancestors) == expected 207 | 208 | 209 | @pytest.mark.parametrize( 210 | "name, expected", 211 | [ 212 | ( 213 | "Canidae", 214 | [ 215 | "Canidae", 216 | "Canis", 217 | "Canis lupus", 218 | "Canis rufus", 219 | "Urocyon", 220 | "Urocyon cinereoargenteus", 221 | ], 222 | ), 223 | ("Bradypus tridactylus", ["Bradypus tridactylus"]), 224 | ("Pogona", ["Pogona", "Pogona barbata", "Pogona minor", "Pogona vitticeps"]), 225 | ], 226 | ) 227 | def test_descendants(db, name, expected): 228 | create_test_data() 229 | descendants = ( 230 | Taxonomy.objects.get(name=name).descendants().values_list("name", flat=True) 231 | ) 232 | assert set(descendants) == set(expected) 233 | 234 | 235 | @pytest.mark.parametrize( 236 | "name, expected", 237 | [("Feliformia", "Carnivora"), ("Plantae", None), ("Pogona minor", "Pogona")], 238 | ) 239 | def test_parent(db, name, expected): 240 | create_test_data() 241 | parent = Taxonomy.objects.get(name=name).parent() 242 | assert getattr(parent, "name", None) == expected 243 | 244 | 245 | @pytest.mark.parametrize( 246 | "name, expected", 247 | [ 248 | ("Carnivora", ["Pilosa"]), 249 | ("Pogona vitticeps", ["Pogona minor", "Pogona barbata"]), 250 | ], 251 | ) 252 | def test_siblings(db, name, expected): 253 | create_test_data() 254 | siblings = Taxonomy.objects.get(name=name).siblings().values_list("name", flat=True) 255 | assert set(siblings) == set(expected) 256 | 257 | 258 | def test_slicing(db): 259 | create_test_data() 260 | qs = Taxonomy.objects.all() 261 | assert qs[:3].count() == 3 262 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alabaster" 5 | version = "0.7.16" 6 | description = "A light, configurable Sphinx theme" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["docs"] 10 | files = [ 11 | {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, 12 | {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, 13 | ] 14 | 15 | [[package]] 16 | name = "asgiref" 17 | version = "3.8.1" 18 | description = "ASGI specs, helper code, and adapters" 19 | optional = false 20 | python-versions = ">=3.8" 21 | groups = ["main"] 22 | files = [ 23 | {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, 24 | {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, 25 | ] 26 | 27 | [package.dependencies] 28 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 29 | 30 | [package.extras] 31 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 32 | 33 | [[package]] 34 | name = "babel" 35 | version = "2.15.0" 36 | description = "Internationalization utilities" 37 | optional = false 38 | python-versions = ">=3.8" 39 | groups = ["docs"] 40 | files = [ 41 | {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, 42 | {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, 43 | ] 44 | 45 | [package.extras] 46 | dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] 47 | 48 | [[package]] 49 | name = "certifi" 50 | version = "2024.6.2" 51 | description = "Python package for providing Mozilla's CA Bundle." 52 | optional = false 53 | python-versions = ">=3.6" 54 | groups = ["docs"] 55 | files = [ 56 | {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, 57 | {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, 58 | ] 59 | 60 | [[package]] 61 | name = "charset-normalizer" 62 | version = "3.3.2" 63 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 64 | optional = false 65 | python-versions = ">=3.7.0" 66 | groups = ["docs"] 67 | files = [ 68 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 69 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 70 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 71 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 72 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 73 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 74 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 75 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 76 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 77 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 78 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 79 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 80 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 81 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 82 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 83 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 84 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 85 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 86 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 87 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 88 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 89 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 90 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 91 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 92 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 93 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 94 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 95 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 96 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 97 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 98 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 99 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 100 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 101 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 102 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 103 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 104 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 105 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 106 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 107 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 108 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 109 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 110 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 111 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 112 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 113 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 114 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 115 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 116 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 117 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 118 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 119 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 120 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 121 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 122 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 123 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 124 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 125 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 126 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 127 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 128 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 129 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 130 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 131 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 132 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 133 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 134 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 135 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 136 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 137 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 138 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 139 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 140 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 141 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 142 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 143 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 144 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 145 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 146 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 147 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 148 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 149 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 150 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 151 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 152 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 153 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 154 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 155 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 156 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 157 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 158 | ] 159 | 160 | [[package]] 161 | name = "colorama" 162 | version = "0.4.6" 163 | description = "Cross-platform colored terminal text." 164 | optional = false 165 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 166 | groups = ["docs", "test"] 167 | markers = "sys_platform == \"win32\"" 168 | files = [ 169 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 170 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 171 | ] 172 | 173 | [[package]] 174 | name = "django" 175 | version = "4.2.27" 176 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 177 | optional = false 178 | python-versions = ">=3.8" 179 | groups = ["main"] 180 | files = [ 181 | {file = "django-4.2.27-py3-none-any.whl", hash = "sha256:f393a394053713e7d213984555c5b7d3caeee78b2ccb729888a0774dff6c11a8"}, 182 | {file = "django-4.2.27.tar.gz", hash = "sha256:b865fbe0f4a3d1ee36594c5efa42b20db3c8bbb10dff0736face1c6e4bda5b92"}, 183 | ] 184 | 185 | [package.dependencies] 186 | asgiref = ">=3.6.0,<4" 187 | sqlparse = ">=0.3.1" 188 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 189 | 190 | [package.extras] 191 | argon2 = ["argon2-cffi (>=19.1.0)"] 192 | bcrypt = ["bcrypt"] 193 | 194 | [[package]] 195 | name = "docutils" 196 | version = "0.21.2" 197 | description = "Docutils -- Python Documentation Utilities" 198 | optional = false 199 | python-versions = ">=3.9" 200 | groups = ["docs"] 201 | files = [ 202 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 203 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 204 | ] 205 | 206 | [[package]] 207 | name = "exceptiongroup" 208 | version = "1.2.1" 209 | description = "Backport of PEP 654 (exception groups)" 210 | optional = false 211 | python-versions = ">=3.7" 212 | groups = ["test"] 213 | markers = "python_version < \"3.11\"" 214 | files = [ 215 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 216 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 217 | ] 218 | 219 | [package.extras] 220 | test = ["pytest (>=6)"] 221 | 222 | [[package]] 223 | name = "idna" 224 | version = "3.7" 225 | description = "Internationalized Domain Names in Applications (IDNA)" 226 | optional = false 227 | python-versions = ">=3.5" 228 | groups = ["docs"] 229 | files = [ 230 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 231 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 232 | ] 233 | 234 | [[package]] 235 | name = "imagesize" 236 | version = "1.4.1" 237 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 238 | optional = false 239 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 240 | groups = ["docs"] 241 | files = [ 242 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 243 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 244 | ] 245 | 246 | [[package]] 247 | name = "importlib-metadata" 248 | version = "7.1.0" 249 | description = "Read metadata from Python packages" 250 | optional = false 251 | python-versions = ">=3.8" 252 | groups = ["docs"] 253 | markers = "python_version == \"3.9\"" 254 | files = [ 255 | {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, 256 | {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, 257 | ] 258 | 259 | [package.dependencies] 260 | zipp = ">=0.5" 261 | 262 | [package.extras] 263 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 264 | perf = ["ipython"] 265 | testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] 266 | 267 | [[package]] 268 | name = "iniconfig" 269 | version = "2.0.0" 270 | description = "brain-dead simple config-ini parsing" 271 | optional = false 272 | python-versions = ">=3.7" 273 | groups = ["test"] 274 | files = [ 275 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 276 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 277 | ] 278 | 279 | [[package]] 280 | name = "jinja2" 281 | version = "3.1.4" 282 | description = "A very fast and expressive template engine." 283 | optional = false 284 | python-versions = ">=3.7" 285 | groups = ["docs"] 286 | files = [ 287 | {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, 288 | {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, 289 | ] 290 | 291 | [package.dependencies] 292 | MarkupSafe = ">=2.0" 293 | 294 | [package.extras] 295 | i18n = ["Babel (>=2.7)"] 296 | 297 | [[package]] 298 | name = "livereload" 299 | version = "2.6.3" 300 | description = "Python LiveReload is an awesome tool for web developers" 301 | optional = false 302 | python-versions = "*" 303 | groups = ["docs"] 304 | files = [ 305 | {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, 306 | {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, 307 | ] 308 | 309 | [package.dependencies] 310 | six = "*" 311 | tornado = {version = "*", markers = "python_version > \"2.7\""} 312 | 313 | [[package]] 314 | name = "markdown-it-py" 315 | version = "3.0.0" 316 | description = "Python port of markdown-it. Markdown parsing, done right!" 317 | optional = false 318 | python-versions = ">=3.8" 319 | groups = ["docs"] 320 | files = [ 321 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 322 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 323 | ] 324 | 325 | [package.dependencies] 326 | mdurl = ">=0.1,<1.0" 327 | 328 | [package.extras] 329 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 330 | code-style = ["pre-commit (>=3.0,<4.0)"] 331 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 332 | linkify = ["linkify-it-py (>=1,<3)"] 333 | plugins = ["mdit-py-plugins"] 334 | profiling = ["gprof2dot"] 335 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 336 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 337 | 338 | [[package]] 339 | name = "markupsafe" 340 | version = "2.1.5" 341 | description = "Safely add untrusted strings to HTML/XML markup." 342 | optional = false 343 | python-versions = ">=3.7" 344 | groups = ["docs"] 345 | files = [ 346 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 347 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 348 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 349 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 350 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 351 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 352 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 353 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 354 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 355 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 356 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 357 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 358 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 359 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 360 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 361 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 362 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 363 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 364 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 365 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 366 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 367 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 368 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 369 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 370 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 371 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 372 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 373 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 374 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 375 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 376 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 377 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 378 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 379 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 380 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 381 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 382 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 383 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 384 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 385 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 386 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 387 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 388 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 389 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 390 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 391 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 392 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 393 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 394 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 395 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 396 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 397 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 398 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 399 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 400 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 401 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 402 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 403 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 404 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 405 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 406 | ] 407 | 408 | [[package]] 409 | name = "mdit-py-plugins" 410 | version = "0.4.1" 411 | description = "Collection of plugins for markdown-it-py" 412 | optional = false 413 | python-versions = ">=3.8" 414 | groups = ["docs"] 415 | files = [ 416 | {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"}, 417 | {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"}, 418 | ] 419 | 420 | [package.dependencies] 421 | markdown-it-py = ">=1.0.0,<4.0.0" 422 | 423 | [package.extras] 424 | code-style = ["pre-commit"] 425 | rtd = ["myst-parser", "sphinx-book-theme"] 426 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 427 | 428 | [[package]] 429 | name = "mdurl" 430 | version = "0.1.2" 431 | description = "Markdown URL utilities" 432 | optional = false 433 | python-versions = ">=3.7" 434 | groups = ["docs"] 435 | files = [ 436 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 437 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 438 | ] 439 | 440 | [[package]] 441 | name = "myst-parser" 442 | version = "3.0.1" 443 | description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," 444 | optional = false 445 | python-versions = ">=3.8" 446 | groups = ["docs"] 447 | files = [ 448 | {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, 449 | {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, 450 | ] 451 | 452 | [package.dependencies] 453 | docutils = ">=0.18,<0.22" 454 | jinja2 = "*" 455 | markdown-it-py = ">=3.0,<4.0" 456 | mdit-py-plugins = ">=0.4,<1.0" 457 | pyyaml = "*" 458 | sphinx = ">=6,<8" 459 | 460 | [package.extras] 461 | code-style = ["pre-commit (>=3.0,<4.0)"] 462 | linkify = ["linkify-it-py (>=2.0,<3.0)"] 463 | rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] 464 | testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] 465 | testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] 466 | 467 | [[package]] 468 | name = "packaging" 469 | version = "24.1" 470 | description = "Core utilities for Python packages" 471 | optional = false 472 | python-versions = ">=3.8" 473 | groups = ["docs", "test"] 474 | files = [ 475 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 476 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 477 | ] 478 | 479 | [[package]] 480 | name = "pluggy" 481 | version = "1.5.0" 482 | description = "plugin and hook calling mechanisms for python" 483 | optional = false 484 | python-versions = ">=3.8" 485 | groups = ["test"] 486 | files = [ 487 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 488 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 489 | ] 490 | 491 | [package.extras] 492 | dev = ["pre-commit", "tox"] 493 | testing = ["pytest", "pytest-benchmark"] 494 | 495 | [[package]] 496 | name = "psycopg" 497 | version = "3.2.13" 498 | description = "PostgreSQL database adapter for Python" 499 | optional = false 500 | python-versions = ">=3.8" 501 | groups = ["main"] 502 | files = [ 503 | {file = "psycopg-3.2.13-py3-none-any.whl", hash = "sha256:a481374514f2da627157f767a9336705ebefe93ea7a0522a6cbacba165da179a"}, 504 | {file = "psycopg-3.2.13.tar.gz", hash = "sha256:309adaeda61d44556046ec9a83a93f42bbe5310120b1995f3af49ab6d9f13c1d"}, 505 | ] 506 | 507 | [package.dependencies] 508 | typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} 509 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 510 | 511 | [package.extras] 512 | binary = ["psycopg-binary (==3.2.13) ; implementation_name != \"pypy\""] 513 | c = ["psycopg-c (==3.2.13) ; implementation_name != \"pypy\""] 514 | dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.14)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "types-shapely (>=2.0)", "wheel (>=0.37)"] 515 | docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] 516 | pool = ["psycopg-pool"] 517 | test = ["anyio (>=4.0)", "mypy (>=1.14)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] 518 | 519 | [[package]] 520 | name = "pygments" 521 | version = "2.18.0" 522 | description = "Pygments is a syntax highlighting package written in Python." 523 | optional = false 524 | python-versions = ">=3.8" 525 | groups = ["docs", "test"] 526 | files = [ 527 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 528 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 529 | ] 530 | 531 | [package.extras] 532 | windows-terminal = ["colorama (>=0.4.6)"] 533 | 534 | [[package]] 535 | name = "pygments-styles" 536 | version = "0.1.0" 537 | description = "A curated collection of Pygments styles based on VS Code themes." 538 | optional = false 539 | python-versions = ">=3.9" 540 | groups = ["docs"] 541 | files = [ 542 | {file = "pygments_styles-0.1.0-py3-none-any.whl", hash = "sha256:feb21772cecd976164298056649f09cf01a109ec240f05989d6ffd3f4a638d9e"}, 543 | {file = "pygments_styles-0.1.0.tar.gz", hash = "sha256:f1c502518d87af0fa07520acb689ae9aaee8dabec1c536c295b9b6e5ccefa15d"}, 544 | ] 545 | 546 | [package.dependencies] 547 | pygments = "*" 548 | 549 | [[package]] 550 | name = "pytest" 551 | version = "8.4.2" 552 | description = "pytest: simple powerful testing with Python" 553 | optional = false 554 | python-versions = ">=3.9" 555 | groups = ["test"] 556 | files = [ 557 | {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, 558 | {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, 559 | ] 560 | 561 | [package.dependencies] 562 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 563 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 564 | iniconfig = ">=1" 565 | packaging = ">=20" 566 | pluggy = ">=1.5,<2" 567 | pygments = ">=2.7.2" 568 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 569 | 570 | [package.extras] 571 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 572 | 573 | [[package]] 574 | name = "pytest-django" 575 | version = "4.11.1" 576 | description = "A Django plugin for pytest." 577 | optional = false 578 | python-versions = ">=3.8" 579 | groups = ["test"] 580 | files = [ 581 | {file = "pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10"}, 582 | {file = "pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991"}, 583 | ] 584 | 585 | [package.dependencies] 586 | pytest = ">=7.0.0" 587 | 588 | [package.extras] 589 | docs = ["sphinx", "sphinx_rtd_theme"] 590 | testing = ["Django", "django-configurations (>=2.0)"] 591 | 592 | [[package]] 593 | name = "pyyaml" 594 | version = "6.0.1" 595 | description = "YAML parser and emitter for Python" 596 | optional = false 597 | python-versions = ">=3.6" 598 | groups = ["docs"] 599 | files = [ 600 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 601 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 602 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 603 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 604 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 605 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 606 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 607 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 608 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 609 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 610 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 611 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 612 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 613 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 614 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 615 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 616 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 617 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 618 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 619 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 620 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 621 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 622 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 623 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 624 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 625 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 626 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 627 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 628 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 629 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 630 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 631 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 632 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 633 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 634 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 635 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 636 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 637 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 638 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 639 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 640 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 641 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 642 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 643 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 644 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 645 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 646 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 647 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 648 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 649 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 650 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 651 | ] 652 | 653 | [[package]] 654 | name = "requests" 655 | version = "2.32.3" 656 | description = "Python HTTP for Humans." 657 | optional = false 658 | python-versions = ">=3.8" 659 | groups = ["docs"] 660 | files = [ 661 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 662 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 663 | ] 664 | 665 | [package.dependencies] 666 | certifi = ">=2017.4.17" 667 | charset-normalizer = ">=2,<4" 668 | idna = ">=2.5,<4" 669 | urllib3 = ">=1.21.1,<3" 670 | 671 | [package.extras] 672 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 673 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 674 | 675 | [[package]] 676 | name = "shibuya" 677 | version = "2025.10.21" 678 | description = "A clean, responsive, and customizable Sphinx documentation theme with light/dark mode." 679 | optional = false 680 | python-versions = ">=3.9" 681 | groups = ["docs"] 682 | files = [ 683 | {file = "shibuya-2025.10.21-py3-none-any.whl", hash = "sha256:916e48c97ba5cc4b1949742beac19cf48923e12965a0d1689fda460d363f4d82"}, 684 | {file = "shibuya-2025.10.21.tar.gz", hash = "sha256:a668f2a33c8b57d33d78bd6edf723eece5734d01f129f973333e7096ad6b3690"}, 685 | ] 686 | 687 | [package.dependencies] 688 | pygments-styles = "*" 689 | Sphinx = "*" 690 | 691 | [[package]] 692 | name = "six" 693 | version = "1.16.0" 694 | description = "Python 2 and 3 compatibility utilities" 695 | optional = false 696 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 697 | groups = ["docs"] 698 | files = [ 699 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 700 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 701 | ] 702 | 703 | [[package]] 704 | name = "snowballstemmer" 705 | version = "2.2.0" 706 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 707 | optional = false 708 | python-versions = "*" 709 | groups = ["docs"] 710 | files = [ 711 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 712 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 713 | ] 714 | 715 | [[package]] 716 | name = "sphinx" 717 | version = "7.4.7" 718 | description = "Python documentation generator" 719 | optional = false 720 | python-versions = ">=3.9" 721 | groups = ["docs"] 722 | files = [ 723 | {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, 724 | {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, 725 | ] 726 | 727 | [package.dependencies] 728 | alabaster = ">=0.7.14,<0.8.0" 729 | babel = ">=2.13" 730 | colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} 731 | docutils = ">=0.20,<0.22" 732 | imagesize = ">=1.3" 733 | importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} 734 | Jinja2 = ">=3.1" 735 | packaging = ">=23.0" 736 | Pygments = ">=2.17" 737 | requests = ">=2.30.0" 738 | snowballstemmer = ">=2.2" 739 | sphinxcontrib-applehelp = "*" 740 | sphinxcontrib-devhelp = "*" 741 | sphinxcontrib-htmlhelp = ">=2.0.0" 742 | sphinxcontrib-jsmath = "*" 743 | sphinxcontrib-qthelp = "*" 744 | sphinxcontrib-serializinghtml = ">=1.1.9" 745 | tomli = {version = ">=2", markers = "python_version < \"3.11\""} 746 | 747 | [package.extras] 748 | docs = ["sphinxcontrib-websupport"] 749 | lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] 750 | test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] 751 | 752 | [[package]] 753 | name = "sphinx-reload" 754 | version = "0.2.0" 755 | description = "Live preview your Sphinx documentation" 756 | optional = false 757 | python-versions = "*" 758 | groups = ["docs"] 759 | files = [ 760 | {file = "sphinx-reload-0.2.0.tar.gz", hash = "sha256:800e07bffea6de0e4ee5f9c14ef565ba1d0343c4a516d028e978bbcaf712fa07"}, 761 | {file = "sphinx_reload-0.2.0-py3-none-any.whl", hash = "sha256:0c184b990d4bc50cf14b2e6b6cf89ce4f88a2baa9b22e36ae4e2abcb8f3f44b6"}, 762 | ] 763 | 764 | [package.dependencies] 765 | livereload = ">=2.5.1" 766 | 767 | [[package]] 768 | name = "sphinxcontrib-applehelp" 769 | version = "1.0.8" 770 | description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" 771 | optional = false 772 | python-versions = ">=3.9" 773 | groups = ["docs"] 774 | files = [ 775 | {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, 776 | {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, 777 | ] 778 | 779 | [package.extras] 780 | lint = ["docutils-stubs", "flake8", "mypy"] 781 | standalone = ["Sphinx (>=5)"] 782 | test = ["pytest"] 783 | 784 | [[package]] 785 | name = "sphinxcontrib-devhelp" 786 | version = "1.0.6" 787 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" 788 | optional = false 789 | python-versions = ">=3.9" 790 | groups = ["docs"] 791 | files = [ 792 | {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, 793 | {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, 794 | ] 795 | 796 | [package.extras] 797 | lint = ["docutils-stubs", "flake8", "mypy"] 798 | standalone = ["Sphinx (>=5)"] 799 | test = ["pytest"] 800 | 801 | [[package]] 802 | name = "sphinxcontrib-htmlhelp" 803 | version = "2.0.5" 804 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 805 | optional = false 806 | python-versions = ">=3.9" 807 | groups = ["docs"] 808 | files = [ 809 | {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, 810 | {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, 811 | ] 812 | 813 | [package.extras] 814 | lint = ["docutils-stubs", "flake8", "mypy"] 815 | standalone = ["Sphinx (>=5)"] 816 | test = ["html5lib", "pytest"] 817 | 818 | [[package]] 819 | name = "sphinxcontrib-jsmath" 820 | version = "1.0.1" 821 | description = "A sphinx extension which renders display math in HTML via JavaScript" 822 | optional = false 823 | python-versions = ">=3.5" 824 | groups = ["docs"] 825 | files = [ 826 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 827 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 828 | ] 829 | 830 | [package.extras] 831 | test = ["flake8", "mypy", "pytest"] 832 | 833 | [[package]] 834 | name = "sphinxcontrib-qthelp" 835 | version = "1.0.7" 836 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" 837 | optional = false 838 | python-versions = ">=3.9" 839 | groups = ["docs"] 840 | files = [ 841 | {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, 842 | {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, 843 | ] 844 | 845 | [package.extras] 846 | lint = ["docutils-stubs", "flake8", "mypy"] 847 | standalone = ["Sphinx (>=5)"] 848 | test = ["pytest"] 849 | 850 | [[package]] 851 | name = "sphinxcontrib-serializinghtml" 852 | version = "1.1.10" 853 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" 854 | optional = false 855 | python-versions = ">=3.9" 856 | groups = ["docs"] 857 | files = [ 858 | {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, 859 | {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, 860 | ] 861 | 862 | [package.extras] 863 | lint = ["docutils-stubs", "flake8", "mypy"] 864 | standalone = ["Sphinx (>=5)"] 865 | test = ["pytest"] 866 | 867 | [[package]] 868 | name = "sqlparse" 869 | version = "0.5.0" 870 | description = "A non-validating SQL parser." 871 | optional = false 872 | python-versions = ">=3.8" 873 | groups = ["main"] 874 | files = [ 875 | {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, 876 | {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, 877 | ] 878 | 879 | [package.extras] 880 | dev = ["build", "hatch"] 881 | doc = ["sphinx"] 882 | 883 | [[package]] 884 | name = "tomli" 885 | version = "2.0.1" 886 | description = "A lil' TOML parser" 887 | optional = false 888 | python-versions = ">=3.7" 889 | groups = ["docs", "test"] 890 | markers = "python_version < \"3.11\"" 891 | files = [ 892 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 893 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 894 | ] 895 | 896 | [[package]] 897 | name = "tornado" 898 | version = "6.4.1" 899 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 900 | optional = false 901 | python-versions = ">=3.8" 902 | groups = ["docs"] 903 | files = [ 904 | {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, 905 | {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, 906 | {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, 907 | {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, 908 | {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, 909 | {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, 910 | {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, 911 | {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, 912 | {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, 913 | {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, 914 | {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, 915 | ] 916 | 917 | [[package]] 918 | name = "typing-extensions" 919 | version = "4.12.2" 920 | description = "Backported and Experimental Type Hints for Python 3.8+" 921 | optional = false 922 | python-versions = ">=3.8" 923 | groups = ["main"] 924 | markers = "python_version < \"3.13\"" 925 | files = [ 926 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 927 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 928 | ] 929 | 930 | [[package]] 931 | name = "tzdata" 932 | version = "2024.1" 933 | description = "Provider of IANA time zone data" 934 | optional = false 935 | python-versions = ">=2" 936 | groups = ["main"] 937 | markers = "sys_platform == \"win32\"" 938 | files = [ 939 | {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, 940 | {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, 941 | ] 942 | 943 | [[package]] 944 | name = "urllib3" 945 | version = "2.2.2" 946 | description = "HTTP library with thread-safe connection pooling, file post, and more." 947 | optional = false 948 | python-versions = ">=3.8" 949 | groups = ["docs"] 950 | files = [ 951 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 952 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 953 | ] 954 | 955 | [package.extras] 956 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 957 | h2 = ["h2 (>=4,<5)"] 958 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 959 | zstd = ["zstandard (>=0.18.0)"] 960 | 961 | [[package]] 962 | name = "zipp" 963 | version = "3.19.2" 964 | description = "Backport of pathlib-compatible object wrapper for zip files" 965 | optional = false 966 | python-versions = ">=3.8" 967 | groups = ["docs"] 968 | markers = "python_version == \"3.9\"" 969 | files = [ 970 | {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, 971 | {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, 972 | ] 973 | 974 | [package.extras] 975 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 976 | test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 977 | 978 | [metadata] 979 | lock-version = "2.1" 980 | python-versions = ">=3.9" 981 | content-hash = "4ec964ab5ccd2dc81e5762ef0c774825c938a9a4d3b609e8da830bd31e1dc554" 982 | --------------------------------------------------------------------------------