├── tests ├── __init__.py ├── settings.py ├── test_utils.py └── test_models.py ├── .python-version ├── languages_plus ├── migrations │ ├── __init__.py │ ├── 0002_auto_20141008_0947.py │ ├── 0003_auto_20150803_1140.py │ ├── 0004_auto_20171214_0004.py │ └── 0001_initial.py ├── views.py ├── data │ └── data.zip ├── __init__.py ├── fixtures │ └── languages_data.json.gz ├── apps.py ├── admin.py ├── utils.py └── models.py ├── .coveragerc ├── MANIFEST.in ├── AUTHORS.rst ├── manage.py ├── .editorconfig ├── setup.cfg ├── tox.ini ├── .gitignore ├── pytest.ini ├── LICENSE ├── pyproject.toml ├── Makefile ├── README.rst ├── CHANGELOG.md ├── .github └── workflows │ └── tox.yml ├── CONTRIBUTING.rst └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | 3.11 3 | -------------------------------------------------------------------------------- /languages_plus/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /languages_plus/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /languages_plus/data/data.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordery/django-languages-plus/HEAD/languages_plus/data/data.zip -------------------------------------------------------------------------------- /languages_plus/__init__.py: -------------------------------------------------------------------------------- 1 | APP_NAME = "languages_plus" 2 | default_app_config = "languages_plus.apps.LanguagesPlusConfig" 3 | __version__ = "2.1.1" 4 | -------------------------------------------------------------------------------- /languages_plus/fixtures/languages_data.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordery/django-languages-plus/HEAD/languages_plus/fixtures/languages_data.json.gz -------------------------------------------------------------------------------- /languages_plus/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from . import APP_NAME 4 | 5 | 6 | class LanguagesPlusConfig(AppConfig): 7 | name = APP_NAME 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | 4 | [report] 5 | omit = 6 | *site-packages* 7 | *tests* 8 | *.tox* 9 | show_missing = True 10 | exclude_lines = 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include CHANGELOG.md 4 | include LICENSE 5 | include README.rst 6 | recursive-include languages_plus *.html *.png *.gif *js *.css *jpg *jpeg *svg *py *gz 7 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Andrew Cordery 9 | 10 | Contributors 11 | ------------ 12 | 13 | decibyte (Django 2.0 support, bug fixes) 14 | sklirg (bug fixes) 15 | blueyed (improved admin) 16 | luiscberrocal (Python 3 support) 17 | -------------------------------------------------------------------------------- /languages_plus/migrations/0002_auto_20141008_0947.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("languages_plus", "0001_initial"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import os 6 | import sys 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.1.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:languages_plus/__init__.py] 7 | 8 | [bumpversion:file:pyproject.toml] 9 | search = version = "{current_version}" 10 | replace = version = "{new_version}" 11 | 12 | [wheel] 13 | universal = 1 14 | 15 | [flake8] 16 | ignore = D203 17 | exclude = 18 | languages_plus/migrations, 19 | .git, 20 | .tox, 21 | docs/conf.py, 22 | build, 23 | dist 24 | max-line-length = 119 25 | -------------------------------------------------------------------------------- /languages_plus/migrations/0003_auto_20150803_1140.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("languages_plus", "0002_auto_20141008_0947"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="language", 16 | name="countries_spoken", 17 | field=models.ManyToManyField(blank=True, to="countries_plus.Country"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | envlist = 4 | {py39,py310,py311}-django-32 5 | {py39,py310,py311}-django-42 6 | {py310,py311,py312}-django-50 7 | 8 | [gh-actions] 9 | python = 10 | 3.9: py39 11 | 3.10: py310 12 | 3.11: py311 13 | 3.12: py312 14 | 15 | [testenv] 16 | deps = 17 | django-32: Django>=3.2,<4 18 | django-42: Django>=4.2,<5 19 | django-50: Django>=5,<6 20 | pytest>=7 21 | pytest-sugar 22 | pytest-django 23 | commands = 24 | python --version 25 | django-admin --version 26 | pytest tests/ --import-mode importlib 27 | 28 | -------------------------------------------------------------------------------- /languages_plus/migrations/0004_auto_20171214_0004.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-12-14 06:04 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("languages_plus", "0003_auto_20150803_1140"), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name="language", 17 | name="iso_639_6", 18 | ), 19 | migrations.RemoveField( 20 | model_name="language", 21 | name="name_other", 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Pycharm/Intellij 40 | .idea 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | 49 | # virtualenv 50 | venv 51 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.settings 3 | python_files = tests.py test_*.py *_tests.py 4 | testpaths = tests/ 5 | addopts = 6 | --nomigrations 7 | --capture=no 8 | ; pytest-django prioritizes the env variable DJANGO_SETTINGS_MODULE over pytest.ini, 9 | ; so in order to make sure pytest still uses tests.settings, we need to specify it 10 | ; using the --ds option which overrides the env variable. Otherwise cases like 11 | ; pipenv run -- which automatically loads environment variables from the .env file -- will fail. 12 | ; See https://pytest-django.readthedocs.io/en/latest/configuring_django.html#pytest-ini-settings 13 | --ds=tests.settings 14 | log_cli = true 15 | log_level = WARN 16 | -------------------------------------------------------------------------------- /languages_plus/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Language, CultureCode 4 | 5 | 6 | @admin.register(Language) 7 | class LanguageAdmin(admin.ModelAdmin): 8 | list_display = ( 9 | "name_en", 10 | "name_native", 11 | "iso_639_1", 12 | "iso_639_2T", 13 | "iso_639_2B", 14 | "iso_639_2T", 15 | "iso_639_3", 16 | "notes", 17 | ) 18 | list_display_links = ("name_en",) 19 | search_fields = ("name_en", "name_native") 20 | 21 | 22 | @admin.register(CultureCode) 23 | class CultureCodeAdmin(admin.ModelAdmin): 24 | list_display = ("code", "language", "country") 25 | list_display_links = ("code",) 26 | search_fields = ("code", "language", "country") 27 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | import django 5 | 6 | DEBUG = True 7 | USE_TZ = True 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww" 11 | 12 | DATABASES = { 13 | "default": { 14 | "ENGINE": "django.db.backends.sqlite3", 15 | "NAME": ":memory:", 16 | } 17 | } 18 | 19 | INSTALLED_APPS = [ 20 | "django.contrib.auth", 21 | "django.contrib.contenttypes", 22 | "django.contrib.sites", 23 | "languages_plus", 24 | "countries_plus", 25 | ] 26 | 27 | SITE_ID = 1 28 | 29 | if django.VERSION >= (1, 10): 30 | MIDDLEWARE = () 31 | else: 32 | MIDDLEWARE_CLASSES = () 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2017, Andrew Cordery 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.test import TestCase 4 | 5 | from languages_plus.models import CultureCode 6 | from languages_plus.utils import associate_countries_and_languages 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class TestAssociation(TestCase): 12 | fixtures = ["countries_data.json.gz", "languages_data.json.gz"] 13 | 14 | @classmethod 15 | def setUpTestData(cls): 16 | associate_countries_and_languages() 17 | 18 | def test_count(self): 19 | culture_code_count = CultureCode.objects.count() 20 | self.assertEqual(culture_code_count, 235) 21 | 22 | def test_get_culture_code(self): 23 | culture_code = CultureCode.objects.get(pk="en-CA") 24 | self.assertEqual(culture_code.country.name, "Canada") 25 | self.assertEqual(culture_code.language.name, "English") 26 | 27 | def test_get_languages_for_country(self): 28 | culture_codes = CultureCode.objects.filter(country__name="Canada") 29 | results = {"en-CA": "English", "fr-CA": "French"} 30 | self.assertEqual(len(results), len(culture_codes)) 31 | for culture_code in culture_codes: 32 | self.assertEqual(results[culture_code.code], str(culture_code.language)) 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-languages-plus" 3 | version = "2.1.1" 4 | description = "A django model & fixture containing common languages and culture codes" 5 | authors = ["Andrew Cordery "] 6 | license = "MIT" 7 | readme = "README.rst" 8 | include = ["LICENSE", "*.rst", "*.md"] 9 | packages = [ 10 | { include = "languages_plus" } 11 | ] 12 | homepage = "https://github.com/cordery/django-languages-plus" 13 | repository = "https://github.com/cordery/django-languages-plus" 14 | keywords = ["django-languages-plus"] 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Framework :: Django", 18 | "Framework :: Django :: 3.2", 19 | "Framework :: Django :: 4.2", 20 | "Framework :: Django :: 5.0", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: MIT License", 23 | "Natural Language :: English", 24 | "Programming Language :: Python :: 3", 25 | ] 26 | 27 | [tool.poetry.dependencies] 28 | python = ">=3.8,<4" 29 | django = ">=3.2,<6" 30 | django-countries-plus = ">2" 31 | 32 | [tool.poetry.group.test.dependencies] 33 | coverage = "^7" 34 | pytest = "^7" 35 | pytest-cov = "^4" 36 | pytest-django = "^4.5" 37 | tox = "^4" 38 | 39 | 40 | [tool.poetry.group.dev.dependencies] 41 | bump2version = "^1.0.1" 42 | ruff = "^0.1.11" 43 | 44 | 45 | [tool.ruff.format] 46 | exclude = ["**/migrations/*"] 47 | 48 | 49 | [build-system] 50 | requires = ["poetry_core>=1.0.0"] 51 | build-backend = "poetry.core.masonry.api" 52 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 14 | 15 | help: 16 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | clean: clean-build clean-pyc 19 | 20 | clean-build: ## remove build artifacts 21 | rm -fr build/ 22 | rm -fr dist/ 23 | rm -fr *.egg-info 24 | 25 | clean-pyc: ## remove Python file artifacts 26 | find . -name '*.pyc' -exec rm -f {} + 27 | find . -name '*.pyo' -exec rm -f {} + 28 | find . -name '*~' -exec rm -f {} + 29 | 30 | lint: ## check style with flake8 31 | flake8 languages_plus tests 32 | 33 | test: ## run tests quickly with the default Python 34 | poetry run pytest 35 | 36 | test-all: ## run tests on every Python version with tox 37 | tox 38 | 39 | coverage: ## check code coverage quickly with the default Python 40 | coverage run --source languages_plus runtests.py tests 41 | coverage report -m 42 | coverage html 43 | open htmlcov/index.html 44 | 45 | docs: ## generate Sphinx HTML documentation, including API docs 46 | rm -f docs/django-languages-plus.rst 47 | rm -f docs/modules.rst 48 | sphinx-apidoc -o docs/ languages_plus 49 | $(MAKE) -C docs clean 50 | $(MAKE) -C docs html 51 | $(BROWSER) docs/_build/html/index.html 52 | 53 | release: clean ## package and upload a release 54 | python setup.py sdist upload 55 | python setup.py bdist_wheel upload 56 | 57 | sdist: clean ## package 58 | python setup.py sdist 59 | ls -l dist 60 | -------------------------------------------------------------------------------- /languages_plus/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from countries_plus.models import Country 4 | from django.core.exceptions import ObjectDoesNotExist 5 | 6 | from .models import Language, CultureCode 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def associate_countries_and_languages(): 12 | """Associate the Language and Country objects from django-countries-plus.""" 13 | languages = {x.iso_639_1: x for x in Language.objects.all()} 14 | countries = {x.iso: x for x in Country.objects.exclude(languages=None)} 15 | 16 | for country in countries.values(): 17 | codes = country.languages.strip(",").split(",") 18 | for code in codes: 19 | if "-" in code: 20 | lang_code, country_code = code.split("-") 21 | language = languages.get(lang_code) 22 | if not language: 23 | logger.info( 24 | "Cannot find language identified by code %s" % lang_code 25 | ) 26 | continue 27 | 28 | country = countries.get(country_code) 29 | if not country: 30 | logger.info( 31 | "Cannot find country identified by code %s" % country_code 32 | ) 33 | continue 34 | 35 | country.language_set.add(language) 36 | CultureCode.objects.get_or_create( 37 | code=code, language=language, country=country 38 | ) 39 | else: 40 | try: 41 | language = Language.objects.get_by_code(code) 42 | country.language_set.add(language) 43 | except ObjectDoesNotExist: 44 | logger.info("Cannot find language identified by code %s" % code) 45 | continue 46 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Django Languages Plus 3 | ============================= 4 | 5 | .. image:: https://badge.fury.io/py/django-languages-plus.svg 6 | :target: https://badge.fury.io/py/django-languages-plus 7 | 8 | .. image:: https://github.com/cordery/django-languages-plus/actions/workflows/tox.yml/badge.svg 9 | :target: https://github.com/cordery/django-languages-plus/actions/workflows/tox.yml 10 | 11 | 12 | django-languages-plus provides models and fixtures for working with both common languages and 'culture codes' or locale codes, like pt-BR. 13 | 14 | Note that this is only a small (but popular) subset of all living languages, and is not even a comprehensive set of the ISO 639 languages. It does however include the endonym/autonym/exonym. 15 | 16 | The Language model contains all ISO 639-1 languages and related information from http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 17 | 18 | The model provides the following fields (original wikipedia.org column name in parentheses). 19 | 20 | * name_en (ISO Language Name) 21 | * name_native (Native Name) 22 | * iso_639_1 (639-1) 23 | * iso_639_2T = (639-2/T) 24 | * iso_639_2B = (639-2/B) 25 | * iso_639_3 = (639-3) 26 | * family = (Language Family) 27 | * countries_spoken 28 | 29 | 30 | ------------ 31 | Installation 32 | ------------ 33 | 34 | :: 35 | 36 | pip install django-languages-plus 37 | 38 | 39 | ------------ 40 | Usage 41 | ------------ 42 | 43 | 1. Add ``languages_plus`` and ``countries_plus`` to your INSTALLED_APPS. 44 | 45 | 2. Migrate your database and load the language data fixture:: 46 | 47 | python manage.py migrate 48 | python manage.py loaddata languages_data.json.gz 49 | 50 | 3. In your code use:: 51 | 52 | from languages_plus.models import Language 53 | lang = Language.objects.get(iso_639_1='en') 54 | 55 | --------------------------------------- 56 | Generating Culture Codes (ex: pt_BR) 57 | --------------------------------------- 58 | django-countries-plus(https://pypi.python.org/pypi/django-countries-plus) is an explicit requirement, and should be installed automatically when installing languages-plus. When both packages are installed correctly you can run the following command once to associate the two datasets and generate a list of culture codes (pt_BR for example):: 59 | 60 | from languages_plus.utils import associate_countries_and_languages 61 | associate_countries_and_languages() 62 | 63 | --------------------------------------- 64 | Requirements 65 | --------------------------------------- 66 | django-countries-plus >= 1. 67 | 68 | Django: Tested against the LTS versions of 3, 4, and the latest of 5. 69 | Python 3 70 | 71 | 72 | Running Tests 73 | ------------- 74 | 75 | Does the code actually work? 76 | 77 | :: 78 | 79 | $ poetry install 80 | $ poetry run pytest 81 | 82 | Or for the full tox suite: 83 | 84 | :: 85 | 86 | $ poetry install 87 | $ pip install tox 88 | $ tox 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.1.1] 11 | 12 | ### Changed 13 | 14 | - Removed unnecessary codecov dependency 15 | 16 | ## [2.1] 17 | 18 | ### Added 19 | 20 | - Added support and tests for Django 5.0 (thanks Akay7) 21 | 22 | 23 | ## [2.0] 24 | 25 | ### Added 26 | 27 | - Added support and tests for Django 4.0 (thanks niSeRdiSeR, henrikek) 28 | 29 | ### Changed 30 | 31 | - Dropped support for Python 2, Django 1 & 2. 32 | - CultureCodeMixin has been removed in favor of the more typical way of declaring the manager using 33 | Manager.from_queryset. 34 | - General modernization of the code. 35 | - Formatted with black. 36 | 37 | ## [1.1.1] 38 | 39 | ### Fixed 40 | 41 | - Restored support for Django 1.11+ 42 | 43 | ## [1.1.0] 44 | 45 | ### Added 46 | 47 | - Added support and tests for Django 3.0 (thanks OskarPersson) 48 | 49 | ### Changed 50 | 51 | - Switched to poetry for dependency management & pytest for tests. 52 | 53 | ## [1.0.0] 54 | 55 | ### Added 56 | 57 | - Added support and tests for Django 2.0 (thanks decibyte) 58 | - Improved associate_countries_and_languages's handling of countries with no languages (thanks decibyte) 59 | 60 | ### Changed 61 | 62 | - Dropped Language.name_other (unused) 63 | - Dropped Language.iso_639_6 (Proposal was withdrawn [https://www.iso.org/standard/43380.html]) 64 | - The fixture is now called languages_data.json.gz 65 | - The fixture is no longer loaded by migration and must be manually loaded 66 | - Dropped test support for Django <1.11 67 | - Cleaned up project and documentation 68 | 69 | ### Fixed 70 | 71 | - Fixed issue with Country().get_all_languages 72 | - Fixed issue with CultureCode.objects.filter_by_codes 73 | 74 | ## [0.1.6] - 2015-02-22 75 | 76 | ### Added 77 | 78 | - Added django-countries-plus as an explicit requirement 79 | 80 | ## [0.1.5] - 2015-01-11 81 | 82 | ### Changed 83 | 84 | - Returned to the use of initial_data. 85 | 86 | ### Fixed 87 | 88 | Eliminated warning for renaming get_query_set to get_queryset. 89 | 90 | ## [0.1.4] - 2015-01-10 91 | 92 | ### Fixed 93 | 94 | - Corrected version number on setup.py 95 | 96 | ## [0.1.3] - 2015-01-10 97 | 98 | ### Added 99 | 100 | - Added Python 3 support. 101 | - Added basic tests for models. 102 | - Added countries plus as requirement. 103 | 104 | ### Changed 105 | 106 | - Changed the way fixtures auto loaded 107 | 108 | ## [0.1.2] - 2014-11-07 109 | 110 | ### Fixed 111 | 112 | - Fixed gzipped fixture loading. 113 | 114 | ## [0.1.1] - 2014-11-05 115 | 116 | ### Added 117 | 118 | - Added missing initial data fixture. 119 | 120 | ## [0.1.0] - 2014-05-21 121 | 122 | ### Added 123 | 124 | - Initial release. 125 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | python-version: [ '3.9', '3.10', '3.11', '3.12' ] 14 | django-version: [ ">=3,<4", ">=4,<5", ">=5,<6" ] 15 | exclude: 16 | - python-version: '3.9' 17 | django-version: '>=5,<6' 18 | 19 | steps: 20 | #---------------------------------------------- 21 | # check-out repo and set-up python 22 | #---------------------------------------------- 23 | - name: Check out repository 24 | uses: actions/checkout@v2 25 | - name: Set up python ${{ matrix.python-version }} 26 | id: setup-python 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | #---------------------------------------------- 31 | # ----- install & configure poetry ----- 32 | #---------------------------------------------- 33 | - name: Load cached Poetry installation 34 | uses: actions/cache@v2 35 | with: 36 | path: ~/.local # the path depends on the OS 37 | key: poetry-0-${{ matrix.python-version }} # increment to reset cache 38 | - name: Install Poetry 39 | uses: snok/install-poetry@v1 40 | with: 41 | virtualenvs-create: true 42 | virtualenvs-in-project: true 43 | installer-parallel: true 44 | #---------------------------------------------- 45 | # load cached venv if cache exists 46 | #---------------------------------------------- 47 | - name: Load cached venv 48 | id: cached-poetry-dependencies 49 | uses: actions/cache@v2 50 | with: 51 | path: .venv 52 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 53 | #---------------------------------------------- 54 | # install dependencies if cache does not exist 55 | #---------------------------------------------- 56 | - name: Install dependencies 57 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 58 | run: poetry install --no-interaction --no-root 59 | #---------------------------------------------- 60 | # install your root project, if required 61 | #---------------------------------------------- 62 | - name: Install library 63 | run: poetry install --no-interaction 64 | #---------------------------------------------- 65 | # add matrix specifics and run test suite 66 | #---------------------------------------------- 67 | - name: Install django ${{ matrix.django-version }} 68 | run: | 69 | source .venv/bin/activate 70 | pip install -U pip setuptools 71 | pip install "Django${{ matrix.django-version }}" 72 | - name: Run tests 73 | run: | 74 | source .venv/bin/activate 75 | pytest tests/ 76 | -------------------------------------------------------------------------------- /languages_plus/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("countries_plus", "__first__"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="CultureCode", 16 | fields=[ 17 | ( 18 | "code", 19 | models.CharField(max_length=10, serialize=False, primary_key=True), 20 | ), 21 | ( 22 | "country", 23 | models.ForeignKey( 24 | to="countries_plus.Country", on_delete=models.CASCADE 25 | ), 26 | ), 27 | ], 28 | options={ 29 | "ordering": ["code"], 30 | "verbose_name": "CultureCode", 31 | "verbose_name_plural": "CultureCodes", 32 | }, 33 | bases=(models.Model,), 34 | ), 35 | migrations.CreateModel( 36 | name="Language", 37 | fields=[ 38 | ( 39 | "iso_639_1", 40 | models.CharField(max_length=2, serialize=False, primary_key=True), 41 | ), 42 | ("iso_639_2T", models.CharField(unique=True, max_length=3, blank=True)), 43 | ("iso_639_2B", models.CharField(unique=True, max_length=3, blank=True)), 44 | ("iso_639_3", models.CharField(max_length=3, blank=True)), 45 | ("iso_639_6", models.CharField(max_length=4, blank=True)), 46 | ("name_en", models.CharField(max_length=100)), 47 | ("name_native", models.CharField(max_length=100)), 48 | ("name_other", models.CharField(max_length=50, blank=True)), 49 | ("family", models.CharField(max_length=50)), 50 | ("notes", models.CharField(max_length=100, blank=True)), 51 | ( 52 | "countries_spoken", 53 | models.ManyToManyField( 54 | to="countries_plus.Country", null=True, blank=True 55 | ), 56 | ), 57 | ], 58 | options={ 59 | "ordering": ["name_en"], 60 | "verbose_name": "Language", 61 | "verbose_name_plural": "Languages", 62 | }, 63 | bases=(models.Model,), 64 | ), 65 | migrations.AddField( 66 | model_name="culturecode", 67 | name="language", 68 | field=models.ForeignKey( 69 | to="languages_plus.Language", on_delete=models.CASCADE 70 | ), 71 | preserve_default=True, 72 | ), 73 | migrations.CreateModel( 74 | name="LangCountry", 75 | fields=[], 76 | options={ 77 | "proxy": True, 78 | }, 79 | bases=("countries_plus.country",), 80 | ), 81 | ] 82 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | from countries_plus.models import Country 2 | from django.test import TestCase 3 | 4 | from languages_plus.models import Language, CultureCode 5 | from languages_plus.utils import associate_countries_and_languages 6 | 7 | 8 | class TestLanguages(TestCase): 9 | fixtures = ["countries_data.json.gz", "languages_data.json.gz"] 10 | 11 | @classmethod 12 | def setUpTestData(cls): 13 | associate_countries_and_languages() 14 | 15 | def test_language_count(self): 16 | language_count = Language.objects.all().count() 17 | self.assertEqual(language_count, 184) 18 | 19 | def test_get_language_by_pk(self): 20 | language = Language.objects.get(pk="en") 21 | self.assertEqual(language.name_en, "English") 22 | 23 | def test_get_by_code(self): 24 | language = Language.objects.get_by_code("iu") 25 | self.assertEqual(language.name_en, "Inuktitut") 26 | 27 | def test_language__str__(self): 28 | language = Language.objects.get(pk="es") 29 | self.assertEqual(str(language), "Spanish; Castilian") 30 | 31 | def test_get_culture_pair(self): 32 | language, country = Language.objects.get_culture_pair("en-CA") 33 | self.assertEqual(str(language), "English") 34 | self.assertEqual(str(country), "Canada") 35 | 36 | def test_filter_by_codes(self): 37 | canada = Country.objects.get(name="Canada") 38 | codes = canada.languages.split(",") 39 | langs = Language.objects.filter_by_codes(codes) 40 | self.assertEqual( 41 | langs, [Language.objects.get(pk=x.split("-")[0]) for x in codes] 42 | ) 43 | langs = {x.pk: x for x in langs} 44 | self.assertEqual(langs["en"].country, canada) 45 | self.assertEqual(langs["en"].culturecode, "en-CA") 46 | 47 | 48 | class TestCultureCode(TestCase): 49 | fixtures = ["countries_data.json.gz", "languages_data.json.gz"] 50 | 51 | @classmethod 52 | def setUpTestData(cls): 53 | associate_countries_and_languages() 54 | 55 | def test_get_as_languages(self): 56 | languages = CultureCode.objects.filter(pk="en-CA").as_languages() 57 | self.assertIsInstance(languages[0], Language) 58 | self.assertEqual(languages[0].country, Country.objects.get(name="Canada")) 59 | self.assertEqual(languages[0].culturecode, "en-CA") 60 | 61 | 62 | class TestCountry(TestCase): 63 | fixtures = ["countries_data.json.gz", "languages_data.json.gz"] 64 | 65 | @classmethod 66 | def setUpTestData(cls): 67 | associate_countries_and_languages() 68 | 69 | def test_primary_language(self): 70 | uk = Country.objects.get(name="United Kingdom") 71 | self.assertEqual(uk.primary_language(), Language.objects.get(pk="en")) 72 | 73 | def test_get_all_languages(self): 74 | canada = Country.objects.get(pk="CA") 75 | languages = canada.get_all_languages() 76 | results = {"en": "English", "fr": "French", "iu": "Inuktitut"} 77 | self.assertEqual(len(results), len(languages)) 78 | for language in languages: 79 | self.assertEqual(results[language.iso], str(language.name_en)) 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/cordery/django-languages-plus/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | Django Languages Plus could always use more documentation, whether as part of the 40 | official Django Languages Plus docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/cordery/django-languages-plus/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `django-languages-plus` for local development. 59 | 60 | 1. Fork the `django-languages-plus` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone git@github.com:your_name_here/django-languages-plus.git 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv django-languages-plus 68 | $ cd django-languages-plus/ 69 | $ poetry install 70 | 71 | 4. Create a branch for local development:: 72 | 73 | $ git checkout -b name-of-your-bugfix-or-feature 74 | 75 | Now you can make your changes locally. 76 | 77 | 5. When you're done making changes, check that your changes are lint free, formatted, and pass the tests, 78 | including testing other Python versions with tox:: 79 | 80 | $ ruff check . 81 | # ruff format 82 | $ tox 83 | 84 | 85 | 6. Commit your changes and push your branch to GitHub:: 86 | 87 | $ git add . 88 | $ git commit -m "Your detailed description of your changes." 89 | $ git push origin name-of-your-bugfix-or-feature 90 | 91 | 7. Submit a pull request through the GitHub website. 92 | 93 | Pull Request Guidelines 94 | ----------------------- 95 | 96 | Before you submit a pull request, check that it meets these guidelines: 97 | 98 | 1. The pull request should include tests. 99 | 2. If the pull request adds functionality, the docs should be updated. Put 100 | your new functionality into a function with a docstring, and add the 101 | feature to the list in README.rst. 102 | 3. The pull request should work for the Python & Django versions specified in 103 | tox.ini. Run tox and make sure that the tests pass for all supported Python 104 | versions. 105 | -------------------------------------------------------------------------------- /languages_plus/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import logging 4 | from typing import List, Tuple, Optional 5 | 6 | from countries_plus.models import Country 7 | from django.db import models 8 | from django.db.models import Q 9 | from django.db.models.query import QuerySet 10 | from django.utils.translation import gettext as _ 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class LanguageManager(models.Manager): 16 | def get_by_code(self, code: str) -> "Language": 17 | """ 18 | Retrieve a language by a code. 19 | 20 | :param code: iso code (any of the three) or its culture code 21 | :return: a Language object 22 | """ 23 | if any(x in code for x in ("_", "-")): 24 | cc = CultureCode.objects.get(code=code.replace("_", "-")) 25 | return cc.language 26 | 27 | elif len(code) == 2: 28 | return self.get(iso_639_1=code) 29 | 30 | elif len(code) == 3: 31 | return self.get(Q(iso_639_2T=code) | Q(iso_639_2B=code) | Q(iso_639_3=code)) 32 | 33 | raise ValueError( 34 | 'Code must be either 2, or 3 characters: "%s" is %s' % (code, len(code)) 35 | ) 36 | 37 | @staticmethod 38 | def get_culture_pair(code: str) -> Tuple["Language", "Country"]: 39 | """ 40 | # Return a tuple of the language and country for a given culture code 41 | :param code: 42 | :return: 43 | """ 44 | if not any(x in code for x in ("_", "-")): 45 | raise ValueError("%s is not a valid culture code" % code) 46 | 47 | cc = CultureCode.objects.get(code=code.replace("_", "-")) 48 | return cc.language, cc.country 49 | 50 | def filter_by_codes(self, codes, sort: str = "name_en") -> List["Language"]: 51 | """ 52 | Retrieve a list (not queryset) of languages from a list of codes 53 | The language objects retrieved by culture code will be annotated with the country and 54 | culture code. 55 | :param codes: list of strings that can be either iso codes (any of the three) or culture 56 | codes. 57 | :param sort: str name_en|name_native|sort_code 58 | :return: a list of languages 59 | """ 60 | lang_codes = [] 61 | cc_codes = [] 62 | for code in codes: 63 | if any(x in code for x in ("_", "-")): 64 | cc_codes.append(code.replace("_", "-")) 65 | else: 66 | lang_codes.append(code) 67 | 68 | cc_langs = CultureCode.objects.filter(code__in=cc_codes).as_languages() 69 | 70 | qs = self.get_queryset().filter( 71 | Q(iso_639_1__in=lang_codes) 72 | | Q(iso_639_2T__in=lang_codes) 73 | | Q(iso_639_2B__in=lang_codes) 74 | | Q(iso_639_3__in=lang_codes) 75 | ) 76 | langs = list(qs) 77 | langs.extend(cc_langs) 78 | langs.sort(key=lambda x: getattr(x, sort)) 79 | return langs 80 | 81 | 82 | class Language(models.Model): 83 | class Meta: 84 | verbose_name = _("Language") 85 | verbose_name_plural = _("Languages") 86 | ordering = ["name_en"] 87 | 88 | # Added by CultureCode when returning language objects 89 | country = None 90 | culturecode = None 91 | 92 | iso_639_1 = models.CharField(max_length=2, primary_key=True) 93 | iso_639_2T = models.CharField(max_length=3, unique=True, blank=True) 94 | iso_639_2B = models.CharField(max_length=3, unique=True, blank=True) 95 | iso_639_3 = models.CharField(max_length=3, blank=True) 96 | name_en = models.CharField(max_length=100) 97 | name_native = models.CharField(max_length=100) 98 | family = models.CharField(max_length=50) 99 | notes = models.CharField(max_length=100, blank=True) 100 | countries_spoken = models.ManyToManyField(Country, blank=True) 101 | 102 | objects = LanguageManager() 103 | 104 | @property 105 | def iso(self) -> str: 106 | return self.iso_639_1 107 | 108 | @property 109 | def name(self) -> str: 110 | return self.name_native 111 | 112 | @property 113 | def sort_code(self) -> str: 114 | return self._get_sort_code() 115 | 116 | def _get_sort_code(self) -> str: 117 | return self.culturecode or self.iso_639_1 118 | 119 | def __str__(self) -> str: 120 | return self.name_en 121 | 122 | 123 | class CultureCodeQuerySet(QuerySet): 124 | def as_languages(self): 125 | """ 126 | Get the Language objects associated with this queryset of CultureCodes as a list. 127 | The Language objects will have country and culturecode set. 128 | :return: 129 | """ 130 | langs = [] 131 | for culture_code in self.select_related("language", "country").all(): 132 | lang = culture_code.language 133 | lang.country = culture_code.country 134 | lang.culturecode = culture_code.code 135 | langs.append(lang) 136 | return langs 137 | 138 | 139 | class CultureCodeManager(models.Manager.from_queryset(CultureCodeQuerySet)): 140 | pass 141 | 142 | 143 | class CultureCode(models.Model): 144 | class Meta: 145 | verbose_name = _("CultureCode") 146 | verbose_name_plural = _("CultureCodes") 147 | ordering = ["code"] 148 | 149 | code = models.CharField(max_length=10, primary_key=True) 150 | language = models.ForeignKey("Language", on_delete=models.CASCADE) 151 | country = models.ForeignKey(Country, on_delete=models.CASCADE) 152 | 153 | objects = CultureCodeManager() 154 | 155 | def __str__(self): 156 | return self.code 157 | 158 | 159 | # fix for django-csv-importer 160 | class LangCountry(Country): 161 | class Meta: 162 | proxy = True 163 | 164 | 165 | # Enhance Country class 166 | def primary_language(self) -> Optional["Language"]: 167 | try: 168 | lang = Language.objects.get_by_code(self.languages.split(",")[0]) 169 | except Language.DoesNotExist: 170 | return None 171 | return lang 172 | 173 | 174 | def get_all_languages(self, sort: str = "code") -> List["Language"]: 175 | """Retrieve the language objects for a country. 176 | 177 | Language objects will be annotated with the country and culturecode attributes when applicable. 178 | 179 | :param sort: code|name_en|name_native 180 | :return: A list of Language objects. 181 | """ 182 | langs = list(self.culturecode_set.all().as_languages()) 183 | langs.extend(self.language_set.exclude(iso_639_1__in=[x.iso_639_1 for x in langs])) 184 | if sort in ["name_en", "name_native"]: 185 | return sorted(langs, key=lambda x: getattr(x, sort)) 186 | elif sort == "code": 187 | codes = self.languages.split(",") 188 | 189 | return sorted(langs, key=lambda x: codes.index(x.sort_code)) 190 | raise ValueError( 191 | "Invalid code option %s. Valid options are: code, name_en, name_native" % sort 192 | ) 193 | 194 | 195 | Country.add_to_class("primary_language", primary_language) 196 | Country.add_to_class("get_all_languages", get_all_languages) 197 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.7.2" 6 | description = "ASGI specs, helper code, and adapters" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, 11 | {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 16 | 17 | [package.extras] 18 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 19 | 20 | [[package]] 21 | name = "backports-zoneinfo" 22 | version = "0.2.1" 23 | description = "Backport of the standard library zoneinfo module" 24 | optional = false 25 | python-versions = ">=3.6" 26 | files = [ 27 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, 28 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, 29 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, 30 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, 31 | {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, 32 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, 33 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, 34 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, 35 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, 36 | {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, 37 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, 38 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, 39 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, 40 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, 41 | {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, 42 | {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, 43 | ] 44 | 45 | [package.extras] 46 | tzdata = ["tzdata"] 47 | 48 | [[package]] 49 | name = "bump2version" 50 | version = "1.0.1" 51 | description = "Version-bump your software with a single command!" 52 | optional = false 53 | python-versions = ">=3.5" 54 | files = [ 55 | {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, 56 | {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, 57 | ] 58 | 59 | [[package]] 60 | name = "cachetools" 61 | version = "5.3.2" 62 | description = "Extensible memoizing collections and decorators" 63 | optional = false 64 | python-versions = ">=3.7" 65 | files = [ 66 | {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, 67 | {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, 68 | ] 69 | 70 | [[package]] 71 | name = "certifi" 72 | version = "2023.11.17" 73 | description = "Python package for providing Mozilla's CA Bundle." 74 | optional = false 75 | python-versions = ">=3.6" 76 | files = [ 77 | {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, 78 | {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, 79 | ] 80 | 81 | [[package]] 82 | name = "chardet" 83 | version = "5.2.0" 84 | description = "Universal encoding detector for Python 3" 85 | optional = false 86 | python-versions = ">=3.7" 87 | files = [ 88 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, 89 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, 90 | ] 91 | 92 | [[package]] 93 | name = "charset-normalizer" 94 | version = "3.3.2" 95 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 96 | optional = false 97 | python-versions = ">=3.7.0" 98 | files = [ 99 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 100 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 101 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 102 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 103 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 104 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 105 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 106 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 107 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 108 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 109 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 110 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 111 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 112 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 113 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 114 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 115 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 116 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 117 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 118 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 119 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 120 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 121 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 122 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 123 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 124 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 125 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 126 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 127 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 128 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 129 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 130 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 131 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 132 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 133 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 134 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 135 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 136 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 137 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 138 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 139 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 140 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 141 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 142 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 143 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 144 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 145 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 146 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 147 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 148 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 149 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 150 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 151 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 152 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 153 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 154 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 155 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 156 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 157 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 158 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 159 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 160 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 161 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 162 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 163 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 164 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 165 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 166 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 167 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 168 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 169 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 170 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 171 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 172 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 173 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 174 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 175 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 176 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 177 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 178 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 179 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 180 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 181 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 182 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 183 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 184 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 185 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 186 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 187 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 188 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 189 | ] 190 | 191 | [[package]] 192 | name = "colorama" 193 | version = "0.4.6" 194 | description = "Cross-platform colored terminal text." 195 | optional = false 196 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 197 | files = [ 198 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 199 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 200 | ] 201 | 202 | [[package]] 203 | name = "coverage" 204 | version = "7.4.0" 205 | description = "Code coverage measurement for Python" 206 | optional = false 207 | python-versions = ">=3.8" 208 | files = [ 209 | {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, 210 | {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, 211 | {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, 212 | {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, 213 | {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, 214 | {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, 215 | {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, 216 | {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, 217 | {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, 218 | {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, 219 | {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, 220 | {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, 221 | {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, 222 | {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, 223 | {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, 224 | {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, 225 | {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, 226 | {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, 227 | {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, 228 | {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, 229 | {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, 230 | {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, 231 | {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, 232 | {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, 233 | {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, 234 | {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, 235 | {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, 236 | {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, 237 | {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, 238 | {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, 239 | {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, 240 | {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, 241 | {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, 242 | {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, 243 | {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, 244 | {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, 245 | {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, 246 | {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, 247 | {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, 248 | {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, 249 | {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, 250 | {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, 251 | {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, 252 | {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, 253 | {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, 254 | {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, 255 | {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, 256 | {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, 257 | {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, 258 | {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, 259 | {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, 260 | {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, 261 | ] 262 | 263 | [package.dependencies] 264 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 265 | 266 | [package.extras] 267 | toml = ["tomli"] 268 | 269 | [[package]] 270 | name = "distlib" 271 | version = "0.3.8" 272 | description = "Distribution utilities" 273 | optional = false 274 | python-versions = "*" 275 | files = [ 276 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 277 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 278 | ] 279 | 280 | [[package]] 281 | name = "django" 282 | version = "4.2.9" 283 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 284 | optional = false 285 | python-versions = ">=3.8" 286 | files = [ 287 | {file = "Django-4.2.9-py3-none-any.whl", hash = "sha256:2cc2fc7d1708ada170ddd6c99f35cc25db664f165d3794bc7723f46b2f8c8984"}, 288 | {file = "Django-4.2.9.tar.gz", hash = "sha256:12498cc3cb8bc8038539fef9e90e95f507502436c1f0c3a673411324fa675d14"}, 289 | ] 290 | 291 | [package.dependencies] 292 | asgiref = ">=3.6.0,<4" 293 | "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} 294 | sqlparse = ">=0.3.1" 295 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 296 | 297 | [package.extras] 298 | argon2 = ["argon2-cffi (>=19.1.0)"] 299 | bcrypt = ["bcrypt"] 300 | 301 | [[package]] 302 | name = "django-countries-plus" 303 | version = "2.1.0" 304 | description = "A django model & fixture containing all data from the countries table of Geonames.org" 305 | optional = false 306 | python-versions = ">=3.7,<4" 307 | files = [ 308 | {file = "django-countries-plus-2.1.0.tar.gz", hash = "sha256:7502c01545a0ef0f833a031aa8aa5bc06d755d97d9d2ecfbbb6ca5aea1384c10"}, 309 | {file = "django_countries_plus-2.1.0-py3-none-any.whl", hash = "sha256:af63cc274ce06e1fd4165b48c90d88dcc6ba3adf85573733b30cb97e390b4102"}, 310 | ] 311 | 312 | [package.dependencies] 313 | django = ">=2.2" 314 | requests = ">=2,<3" 315 | urllib3 = {version = ">=1.26,<2.0", markers = "python_version >= \"3.10\" and python_version < \"4.0\""} 316 | 317 | [[package]] 318 | name = "exceptiongroup" 319 | version = "1.2.0" 320 | description = "Backport of PEP 654 (exception groups)" 321 | optional = false 322 | python-versions = ">=3.7" 323 | files = [ 324 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 325 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 326 | ] 327 | 328 | [package.extras] 329 | test = ["pytest (>=6)"] 330 | 331 | [[package]] 332 | name = "filelock" 333 | version = "3.13.1" 334 | description = "A platform independent file lock." 335 | optional = false 336 | python-versions = ">=3.8" 337 | files = [ 338 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 339 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 340 | ] 341 | 342 | [package.extras] 343 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 344 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 345 | typing = ["typing-extensions (>=4.8)"] 346 | 347 | [[package]] 348 | name = "idna" 349 | version = "3.6" 350 | description = "Internationalized Domain Names in Applications (IDNA)" 351 | optional = false 352 | python-versions = ">=3.5" 353 | files = [ 354 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 355 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 356 | ] 357 | 358 | [[package]] 359 | name = "iniconfig" 360 | version = "2.0.0" 361 | description = "brain-dead simple config-ini parsing" 362 | optional = false 363 | python-versions = ">=3.7" 364 | files = [ 365 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 366 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 367 | ] 368 | 369 | [[package]] 370 | name = "packaging" 371 | version = "23.2" 372 | description = "Core utilities for Python packages" 373 | optional = false 374 | python-versions = ">=3.7" 375 | files = [ 376 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 377 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 378 | ] 379 | 380 | [[package]] 381 | name = "platformdirs" 382 | version = "4.1.0" 383 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 384 | optional = false 385 | python-versions = ">=3.8" 386 | files = [ 387 | {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, 388 | {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, 389 | ] 390 | 391 | [package.extras] 392 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 393 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 394 | 395 | [[package]] 396 | name = "pluggy" 397 | version = "1.3.0" 398 | description = "plugin and hook calling mechanisms for python" 399 | optional = false 400 | python-versions = ">=3.8" 401 | files = [ 402 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, 403 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, 404 | ] 405 | 406 | [package.extras] 407 | dev = ["pre-commit", "tox"] 408 | testing = ["pytest", "pytest-benchmark"] 409 | 410 | [[package]] 411 | name = "pyproject-api" 412 | version = "1.6.1" 413 | description = "API to interact with the python pyproject.toml based projects" 414 | optional = false 415 | python-versions = ">=3.8" 416 | files = [ 417 | {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, 418 | {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, 419 | ] 420 | 421 | [package.dependencies] 422 | packaging = ">=23.1" 423 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 424 | 425 | [package.extras] 426 | docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] 427 | testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] 428 | 429 | [[package]] 430 | name = "pytest" 431 | version = "7.4.4" 432 | description = "pytest: simple powerful testing with Python" 433 | optional = false 434 | python-versions = ">=3.7" 435 | files = [ 436 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 437 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 438 | ] 439 | 440 | [package.dependencies] 441 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 442 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 443 | iniconfig = "*" 444 | packaging = "*" 445 | pluggy = ">=0.12,<2.0" 446 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 447 | 448 | [package.extras] 449 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 450 | 451 | [[package]] 452 | name = "pytest-cov" 453 | version = "4.1.0" 454 | description = "Pytest plugin for measuring coverage." 455 | optional = false 456 | python-versions = ">=3.7" 457 | files = [ 458 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 459 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 460 | ] 461 | 462 | [package.dependencies] 463 | coverage = {version = ">=5.2.1", extras = ["toml"]} 464 | pytest = ">=4.6" 465 | 466 | [package.extras] 467 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 468 | 469 | [[package]] 470 | name = "pytest-django" 471 | version = "4.7.0" 472 | description = "A Django plugin for pytest." 473 | optional = false 474 | python-versions = ">=3.8" 475 | files = [ 476 | {file = "pytest-django-4.7.0.tar.gz", hash = "sha256:92d6fd46b1d79b54fb6b060bbb39428073396cec717d5f2e122a990d4b6aa5e8"}, 477 | {file = "pytest_django-4.7.0-py3-none-any.whl", hash = "sha256:4e1c79d5261ade2dd58d91208017cd8f62cb4710b56e012ecd361d15d5d662a2"}, 478 | ] 479 | 480 | [package.dependencies] 481 | pytest = ">=7.0.0" 482 | 483 | [package.extras] 484 | docs = ["sphinx", "sphinx-rtd-theme"] 485 | testing = ["Django", "django-configurations (>=2.0)"] 486 | 487 | [[package]] 488 | name = "requests" 489 | version = "2.31.0" 490 | description = "Python HTTP for Humans." 491 | optional = false 492 | python-versions = ">=3.7" 493 | files = [ 494 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 495 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 496 | ] 497 | 498 | [package.dependencies] 499 | certifi = ">=2017.4.17" 500 | charset-normalizer = ">=2,<4" 501 | idna = ">=2.5,<4" 502 | urllib3 = ">=1.21.1,<3" 503 | 504 | [package.extras] 505 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 506 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 507 | 508 | [[package]] 509 | name = "ruff" 510 | version = "0.1.11" 511 | description = "An extremely fast Python linter and code formatter, written in Rust." 512 | optional = false 513 | python-versions = ">=3.7" 514 | files = [ 515 | {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, 516 | {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, 517 | {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, 518 | {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, 519 | {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, 520 | {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, 521 | {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, 522 | {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, 523 | {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, 524 | {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, 525 | {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, 526 | {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, 527 | {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, 528 | {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, 529 | {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, 530 | {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, 531 | {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, 532 | ] 533 | 534 | [[package]] 535 | name = "sqlparse" 536 | version = "0.4.4" 537 | description = "A non-validating SQL parser." 538 | optional = false 539 | python-versions = ">=3.5" 540 | files = [ 541 | {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, 542 | {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, 543 | ] 544 | 545 | [package.extras] 546 | dev = ["build", "flake8"] 547 | doc = ["sphinx"] 548 | test = ["pytest", "pytest-cov"] 549 | 550 | [[package]] 551 | name = "tomli" 552 | version = "2.0.1" 553 | description = "A lil' TOML parser" 554 | optional = false 555 | python-versions = ">=3.7" 556 | files = [ 557 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 558 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 559 | ] 560 | 561 | [[package]] 562 | name = "tox" 563 | version = "4.11.4" 564 | description = "tox is a generic virtualenv management and test command line tool" 565 | optional = false 566 | python-versions = ">=3.8" 567 | files = [ 568 | {file = "tox-4.11.4-py3-none-any.whl", hash = "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229"}, 569 | {file = "tox-4.11.4.tar.gz", hash = "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1"}, 570 | ] 571 | 572 | [package.dependencies] 573 | cachetools = ">=5.3.1" 574 | chardet = ">=5.2" 575 | colorama = ">=0.4.6" 576 | filelock = ">=3.12.3" 577 | packaging = ">=23.1" 578 | platformdirs = ">=3.10" 579 | pluggy = ">=1.3" 580 | pyproject-api = ">=1.6.1" 581 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 582 | virtualenv = ">=20.24.3" 583 | 584 | [package.extras] 585 | docs = ["furo (>=2023.8.19)", "sphinx (>=7.2.4)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.24)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 586 | testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.18)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.12)", "wheel (>=0.41.2)"] 587 | 588 | [[package]] 589 | name = "typing-extensions" 590 | version = "4.9.0" 591 | description = "Backported and Experimental Type Hints for Python 3.8+" 592 | optional = false 593 | python-versions = ">=3.8" 594 | files = [ 595 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 596 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 597 | ] 598 | 599 | [[package]] 600 | name = "tzdata" 601 | version = "2023.4" 602 | description = "Provider of IANA time zone data" 603 | optional = false 604 | python-versions = ">=2" 605 | files = [ 606 | {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, 607 | {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, 608 | ] 609 | 610 | [[package]] 611 | name = "urllib3" 612 | version = "1.26.18" 613 | description = "HTTP library with thread-safe connection pooling, file post, and more." 614 | optional = false 615 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 616 | files = [ 617 | {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, 618 | {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, 619 | ] 620 | 621 | [package.extras] 622 | brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 623 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 624 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 625 | 626 | [[package]] 627 | name = "virtualenv" 628 | version = "20.25.0" 629 | description = "Virtual Python Environment builder" 630 | optional = false 631 | python-versions = ">=3.7" 632 | files = [ 633 | {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, 634 | {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, 635 | ] 636 | 637 | [package.dependencies] 638 | distlib = ">=0.3.7,<1" 639 | filelock = ">=3.12.2,<4" 640 | platformdirs = ">=3.9.1,<5" 641 | 642 | [package.extras] 643 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 644 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 645 | 646 | [metadata] 647 | lock-version = "2.0" 648 | python-versions = ">=3.8,<4" 649 | content-hash = "6fc5ef0aa61d261bbe544334052d0d4ca7965c6938fd8037a138faf6695c6766" 650 | --------------------------------------------------------------------------------