├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.rst ├── Makefile ├── README.rst ├── poetry.lock ├── pyproject.toml ├── setup.cfg ├── src └── custom_user │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ ├── 0001_initial_django17.py │ ├── 0002_initial_django18.py │ └── __init__.py │ ├── models.py │ └── tests.py ├── test_custom_user_subclass ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial_django17.py │ ├── 0002_initial_django18.py │ └── __init__.py └── models.py ├── test_settings ├── __init__.py ├── settings.py ├── settings_subclass.py └── urls.py └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | # Also used as inspiration: https://hynek.me/articles/python-github-actions/ 4 | 5 | name: CI 6 | 7 | on: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | env: 14 | # Make tools pretty. 15 | FORCE_COLOR: "1" 16 | TOX_TESTENV_PASSENV: FORCE_COLOR 17 | 18 | jobs: 19 | build: 20 | 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 26 | 27 | services: 28 | mysql: 29 | image: mysql 30 | env: 31 | MYSQL_ALLOW_EMPTY_PASSWORD: true 32 | options: >- 33 | --health-cmd="mysqladmin ping" 34 | --health-interval 10s 35 | --health-timeout 5s 36 | --health-retries 5 37 | ports: 38 | - 3306:3306 39 | postgres: 40 | image: postgres 41 | env: 42 | POSTGRES_PASSWORD: postgres 43 | options: >- 44 | --health-cmd pg_isready 45 | --health-interval 10s 46 | --health-timeout 5s 47 | --health-retries 5 48 | ports: 49 | - 5432:5432 50 | 51 | steps: 52 | - uses: actions/checkout@v2 53 | - name: Set up Python ${{ matrix.python-version }} 54 | uses: actions/setup-python@v2 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | - name: Install dependencies 58 | run: | 59 | python -m pip install --upgrade pip poetry setuptools wheel 60 | poetry install -v 61 | - name: Run tox targets for ${{ matrix.python-version }} 62 | run: | 63 | poetry run tox 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python bytecode 2 | *.py[co] 3 | 4 | # Packaging files 5 | *.egg* 6 | build 7 | dist 8 | 9 | # Unit test / coverage reports 10 | /.coverage 11 | /.coverage.* 12 | /.tox 13 | 14 | # Editor temp files 15 | *.swp 16 | *~ 17 | 18 | # Sublime Text 19 | *.sublime-workspace 20 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) Josep Cugat and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django Custom User nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: format 2 | format: 3 | autoflake --recursive --in-place --remove-all-unused-imports . 4 | isort . 5 | black . 6 | flake8 7 | 8 | .PHONY: lint 9 | lint: 10 | isort --check . 11 | black --check . 12 | flake8 13 | 14 | .PHONY: publish 15 | publish: 16 | @echo "Remember to update the Changelog's version and date in README.rst and stage the changes" 17 | @echo "" 18 | @read -p "Press Enter to continue..."; 19 | @echo "" 20 | @echo "Afterwards, run the following commands:" 21 | @echo "poetry run bumpversion --allow-dirty major/minor" 22 | @echo "poetry build" 23 | @echo "poetry publish" 24 | @echo "" 25 | @read -p "Press Enter to continue..."; 26 | @echo "" 27 | @echo "Remove branch protection for Administrators." 28 | @echo "You probably want to update the repo now:" 29 | @echo "git push origin main" 30 | @echo "git push --tags" 31 | @echo "" 32 | @echo "Remember to enable back the branch protection." 33 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Custom User 2 | ================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-custom-user.svg 5 | :target: https://pypi.org/project/django-custom-user/ 6 | :alt: PyPI version 7 | 8 | .. image:: https://github.com/jcugat/django-custom-user/actions/workflows/ci.yml/badge.svg 9 | :target: https://github.com/jcugat/django-custom-user/actions/workflows/ci.yml 10 | :alt: GitHub Actions Workflow Status (main branch) 11 | 12 | .. image:: https://img.shields.io/pypi/dm/django-custom-user.svg 13 | :target: https://pypi.python.org/pypi/django-custom-user 14 | 15 | 16 | Custom user model for Django with the same behaviour as the default User class but without a username field. Uses email as the USERNAME_FIELD for authentication. 17 | 18 | 19 | Quick start 20 | ----------- 21 | 22 | 1. Install django-custom-user with your favorite Python package manager: 23 | 24 | .. code-block:: 25 | 26 | pip install django-custom-user 27 | 28 | 29 | 2. Add ``'custom_user'`` to your ``INSTALLED_APPS`` setting: 30 | 31 | .. code-block:: python 32 | 33 | INSTALLED_APPS = ( 34 | # other apps 35 | 'custom_user', 36 | ) 37 | 38 | 39 | 3. Set your ``AUTH_USER_MODEL`` setting to use ``EmailUser``: 40 | 41 | .. code-block:: python 42 | 43 | AUTH_USER_MODEL = 'custom_user.EmailUser' 44 | 45 | 46 | 4. Create the database tables: 47 | 48 | .. code-block:: 49 | 50 | python manage.py migrate 51 | 52 | 53 | Usage 54 | ----- 55 | 56 | Instead of referring to ``EmailUser`` directly, you should reference the user model using ``get_user_model()`` as explained in the `Django documentation`_. For example: 57 | 58 | .. _Django documentation: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#referencing-the-user-model 59 | 60 | .. code-block:: python 61 | 62 | from django.contrib.auth import get_user_model 63 | 64 | user = get_user_model().objects.get(email="user@example.com") 65 | 66 | 67 | When you define a foreign key or many-to-many relations to the ``EmailUser`` model, you should specify the custom model using the ``AUTH_USER_MODEL`` setting. For example: 68 | 69 | .. code-block:: python 70 | 71 | from django.conf import settings 72 | from django.db import models 73 | 74 | class Article(models.Model): 75 | author = models.ForeignKey(settings.AUTH_USER_MODEL) 76 | 77 | 78 | Extending EmailUser model 79 | ------------------------- 80 | 81 | You can easily extend ``EmailUser`` by inheriting from ``AbstractEmailUser``. For example: 82 | 83 | .. code-block:: python 84 | 85 | from custom_user.models import AbstractEmailUser 86 | 87 | class MyCustomEmailUser(AbstractEmailUser): 88 | """ 89 | Example of an EmailUser with a new field date_of_birth 90 | """ 91 | date_of_birth = models.DateField() 92 | 93 | Remember to change the ``AUTH_USER_MODEL`` setting to your new class: 94 | 95 | .. code-block:: python 96 | 97 | AUTH_USER_MODEL = 'my_app.MyCustomEmailUser' 98 | 99 | If you use the AdminSite, add the following code to your ``my_app/admin.py`` file: 100 | 101 | .. code-block:: python 102 | 103 | from django.contrib import admin 104 | from custom_user.admin import EmailUserAdmin 105 | from .models import MyCustomEmailUser 106 | 107 | 108 | class MyCustomEmailUserAdmin(EmailUserAdmin): 109 | """ 110 | You can customize the interface of your model here. 111 | """ 112 | pass 113 | 114 | # Register your models here. 115 | admin.site.register(MyCustomEmailUser, MyCustomEmailUserAdmin) 116 | 117 | 118 | Supported versions 119 | ------------------ 120 | 121 | Django: 122 | 123 | - 4.1 124 | - 4.0 125 | - 3.2 LTS 126 | 127 | Python: 128 | 129 | - 3.11 130 | - 3.10 131 | - 3.9 132 | - 3.8 133 | - 3.7 134 | 135 | 136 | Changelog 137 | --------- 138 | 139 | Version 1.1 (2022-12-10) 140 | ~~~~~~~~~~~~~~~~~~~~~~~~ 141 | 142 | Added support for Django 4.1 and Python 3.11. 143 | 144 | Version 1.0 (2022-03-29) 145 | ~~~~~~~~~~~~~~~~~~~~~~~~ 146 | 147 | After a long hiatus, this new version brings compatibility with the latest Django and Python versions, among lots of small improvements and cleanups. 148 | 149 | - Supported versions: 150 | 151 | - Django: 3.2 LTS, 4.0 152 | 153 | - Python: 3.7, 3.8, 3.9, 3.10 154 | 155 | - Import latest code changes from Django 4.0 (`#65 `_): 156 | 157 | - ``EmailUserCreationForm`` does not strip whitespaces in the password fields, to match Django's behavior. 158 | 159 | - ``EmailUserCreationForm`` supports custom password validators configured by ``AUTH_PASSWORD_VALIDATORS``. 160 | 161 | - ``EmailUser.objects.create_superuser()`` allows empty passwords. It will also check that both ``is_staff`` and ``is_superuser`` parameters are ``True`` (if passed). Otherwise, it would create an invalid superuser. 162 | 163 | - Internal changes: 164 | 165 | - Moved away from Travis CI to Github Actions. 166 | 167 | - Build system and dependencies managed with `Poetry `_. 168 | 169 | - Code formatted with `black `_ and `isort `_. 170 | 171 | Note that older versions of Django are not supported, but you can use the previous version 0.7 if you need it. 172 | 173 | Version 0.7 (2017-01-12) 174 | ~~~~~~~~~~~~~~~~~~~~~~~~ 175 | 176 | - Fixed change password link in EmailUserChangeForm (thanks to Igor Gai and rubengrill) 177 | 178 | Version 0.6 (2016-04-03) 179 | ~~~~~~~~~~~~~~~~~~~~~~~~ 180 | 181 | - Added migrations (thanks to everybody for the help). 182 | 183 | How to apply the migrations after upgrading: 184 | 185 | Django 1.7 186 | ++++++++++ 187 | 188 | For this version just run the following commands. 189 | 190 | .. code-block:: 191 | 192 | python manage.py migrate custom_user 0001_initial_django17 --fake 193 | python manage.py migrate custom_user 194 | 195 | Django 1.8 196 | ++++++++++ 197 | 198 | This version didn't work without migrations, which means that your migrations will conflict with the new ones included in this version. 199 | 200 | If you added the migrations with Django's `MIGRATION_MODULES `_ setting, delete the folder containing the migration modules and remove the setting from your config. 201 | 202 | If you just ran ``python manage.py makemigrations``, the migrations are located inside your system's or virtualenv's ``site-packages`` folder. You can check the location running this command, and then delete the folder ``migrations`` that is inside: 203 | 204 | .. code-block:: 205 | 206 | python -c "import os; import custom_user; print(os.path.dirname(custom_user.__file__))" 207 | 208 | You can check if you have removed the migrations successfully running this command, you shouldn't see the section ``custom_user`` anymore: 209 | 210 | .. code-block:: 211 | 212 | python manage.py migrate --list 213 | 214 | Once the old migrations are gone, run the following command to finish: 215 | 216 | .. code-block:: 217 | 218 | python manage.py migrate custom_user 0002_initial_django18 --fake 219 | 220 | Version 0.5 (2014-09-20) 221 | ~~~~~~~~~~~~~~~~~~~~~~~~ 222 | 223 | - Django 1.7 compatible (thanks to j0hnsmith). 224 | - Custom application verbose_name in AdminSite with AppConfig. 225 | 226 | Version 0.4 (2014-03-06) 227 | ~~~~~~~~~~~~~~~~~~~~~~~~ 228 | 229 | - The create_user() and create_superuser() manager methods now accept is_active and is_staff as parameters (thanks to Edil Kratskih). 230 | 231 | Version 0.3 (2014-01-17) 232 | ~~~~~~~~~~~~~~~~~~~~~~~~ 233 | 234 | - AdminSite now works when subclassing AbstractEmailUser (thanks to Ivan Virabyan). 235 | - Updated model changes from Django 1.6.1. 236 | 237 | Version 0.2 (2013-11-24) 238 | ~~~~~~~~~~~~~~~~~~~~~~~~ 239 | 240 | - Django 1.6 compatible (thanks to Simon Luijk). 241 | 242 | Version 0.1 (2013-04-09) 243 | ~~~~~~~~~~~~~~~~~~~~~~~~ 244 | 245 | - Initial release. 246 | 247 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "asgiref" 3 | version = "3.5.2" 4 | description = "ASGI specs, helper code, and adapters" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.7" 8 | 9 | [package.dependencies] 10 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 11 | 12 | [package.extras] 13 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 14 | 15 | [[package]] 16 | name = "autoflake" 17 | version = "1.7.8" 18 | description = "Removes unused imports and unused variables" 19 | category = "dev" 20 | optional = false 21 | python-versions = ">=3.7" 22 | 23 | [package.dependencies] 24 | pyflakes = ">=1.1.0,<3" 25 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 26 | 27 | [[package]] 28 | name = "black" 29 | version = "22.12.0" 30 | description = "The uncompromising code formatter." 31 | category = "dev" 32 | optional = false 33 | python-versions = ">=3.7" 34 | 35 | [package.dependencies] 36 | click = ">=8.0.0" 37 | mypy-extensions = ">=0.4.3" 38 | pathspec = ">=0.9.0" 39 | platformdirs = ">=2" 40 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 41 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 42 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 43 | 44 | [package.extras] 45 | colorama = ["colorama (>=0.4.3)"] 46 | d = ["aiohttp (>=3.7.4)"] 47 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 48 | uvloop = ["uvloop (>=0.15.2)"] 49 | 50 | [[package]] 51 | name = "bump2version" 52 | version = "1.0.1" 53 | description = "Version-bump your software with a single command!" 54 | category = "dev" 55 | optional = false 56 | python-versions = ">=3.5" 57 | 58 | [[package]] 59 | name = "cachecontrol" 60 | version = "0.12.11" 61 | description = "httplib2 caching for requests" 62 | category = "dev" 63 | optional = false 64 | python-versions = ">=3.6" 65 | 66 | [package.dependencies] 67 | lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""} 68 | msgpack = ">=0.5.2" 69 | requests = "*" 70 | 71 | [package.extras] 72 | filecache = ["lockfile (>=0.9)"] 73 | redis = ["redis (>=2.10.5)"] 74 | 75 | [[package]] 76 | name = "cachy" 77 | version = "0.3.0" 78 | description = "Cachy provides a simple yet effective caching library." 79 | category = "dev" 80 | optional = false 81 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 82 | 83 | [package.extras] 84 | memcached = ["python-memcached (>=1.59,<2.0)"] 85 | msgpack = ["msgpack-python (>=0.5,<0.6)"] 86 | redis = ["redis (>=3.3.6,<4.0.0)"] 87 | 88 | [[package]] 89 | name = "certifi" 90 | version = "2022.12.7" 91 | description = "Python package for providing Mozilla's CA Bundle." 92 | category = "dev" 93 | optional = false 94 | python-versions = ">=3.6" 95 | 96 | [[package]] 97 | name = "cffi" 98 | version = "1.15.1" 99 | description = "Foreign Function Interface for Python calling C code." 100 | category = "dev" 101 | optional = false 102 | python-versions = "*" 103 | 104 | [package.dependencies] 105 | pycparser = "*" 106 | 107 | [[package]] 108 | name = "charset-normalizer" 109 | version = "2.1.1" 110 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 111 | category = "dev" 112 | optional = false 113 | python-versions = ">=3.6.0" 114 | 115 | [package.extras] 116 | unicode-backport = ["unicodedata2"] 117 | 118 | [[package]] 119 | name = "cleo" 120 | version = "0.8.1" 121 | description = "Cleo allows you to create beautiful and testable command-line interfaces." 122 | category = "dev" 123 | optional = false 124 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 125 | 126 | [package.dependencies] 127 | clikit = ">=0.6.0,<0.7.0" 128 | 129 | [[package]] 130 | name = "click" 131 | version = "8.1.3" 132 | description = "Composable command line interface toolkit" 133 | category = "dev" 134 | optional = false 135 | python-versions = ">=3.7" 136 | 137 | [package.dependencies] 138 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 139 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 140 | 141 | [[package]] 142 | name = "clikit" 143 | version = "0.6.2" 144 | description = "CliKit is a group of utilities to build beautiful and testable command line interfaces." 145 | category = "dev" 146 | optional = false 147 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 148 | 149 | [package.dependencies] 150 | crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 151 | pastel = ">=0.2.0,<0.3.0" 152 | pylev = ">=1.3,<2.0" 153 | 154 | [[package]] 155 | name = "colorama" 156 | version = "0.4.6" 157 | description = "Cross-platform colored terminal text." 158 | category = "dev" 159 | optional = false 160 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 161 | 162 | [[package]] 163 | name = "coverage" 164 | version = "6.5.0" 165 | description = "Code coverage measurement for Python" 166 | category = "dev" 167 | optional = false 168 | python-versions = ">=3.7" 169 | 170 | [package.dependencies] 171 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 172 | 173 | [package.extras] 174 | toml = ["tomli"] 175 | 176 | [[package]] 177 | name = "crashtest" 178 | version = "0.3.1" 179 | description = "Manage Python errors with ease" 180 | category = "dev" 181 | optional = false 182 | python-versions = ">=3.6,<4.0" 183 | 184 | [[package]] 185 | name = "cryptography" 186 | version = "38.0.4" 187 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 188 | category = "dev" 189 | optional = false 190 | python-versions = ">=3.6" 191 | 192 | [package.dependencies] 193 | cffi = ">=1.12" 194 | 195 | [package.extras] 196 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 197 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 198 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 199 | sdist = ["setuptools-rust (>=0.11.4)"] 200 | ssh = ["bcrypt (>=3.1.5)"] 201 | test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] 202 | 203 | [[package]] 204 | name = "distlib" 205 | version = "0.3.6" 206 | description = "Distribution utilities" 207 | category = "dev" 208 | optional = false 209 | python-versions = "*" 210 | 211 | [[package]] 212 | name = "django" 213 | version = "3.2.16" 214 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 215 | category = "main" 216 | optional = false 217 | python-versions = ">=3.6" 218 | 219 | [package.dependencies] 220 | asgiref = ">=3.3.2,<4" 221 | pytz = "*" 222 | sqlparse = ">=0.2.2" 223 | 224 | [package.extras] 225 | argon2 = ["argon2-cffi (>=19.1.0)"] 226 | bcrypt = ["bcrypt"] 227 | 228 | [[package]] 229 | name = "django-environ" 230 | version = "0.8.1" 231 | description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." 232 | category = "dev" 233 | optional = false 234 | python-versions = ">=3.4,<4" 235 | 236 | [package.extras] 237 | develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] 238 | docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] 239 | testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] 240 | 241 | [[package]] 242 | name = "filelock" 243 | version = "3.8.2" 244 | description = "A platform independent file lock." 245 | category = "dev" 246 | optional = false 247 | python-versions = ">=3.7" 248 | 249 | [package.extras] 250 | docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] 251 | testing = ["covdefaults (>=2.2.2)", "coverage (>=6.5)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] 252 | 253 | [[package]] 254 | name = "flake8" 255 | version = "5.0.4" 256 | description = "the modular source code checker: pep8 pyflakes and co" 257 | category = "dev" 258 | optional = false 259 | python-versions = ">=3.6.1" 260 | 261 | [package.dependencies] 262 | importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} 263 | mccabe = ">=0.7.0,<0.8.0" 264 | pycodestyle = ">=2.9.0,<2.10.0" 265 | pyflakes = ">=2.5.0,<2.6.0" 266 | 267 | [[package]] 268 | name = "html5lib" 269 | version = "1.1" 270 | description = "HTML parser based on the WHATWG HTML specification" 271 | category = "dev" 272 | optional = false 273 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 274 | 275 | [package.dependencies] 276 | six = ">=1.9" 277 | webencodings = "*" 278 | 279 | [package.extras] 280 | all = ["chardet (>=2.2)", "genshi", "lxml"] 281 | chardet = ["chardet (>=2.2)"] 282 | genshi = ["genshi"] 283 | lxml = ["lxml"] 284 | 285 | [[package]] 286 | name = "idna" 287 | version = "3.4" 288 | description = "Internationalized Domain Names in Applications (IDNA)" 289 | category = "dev" 290 | optional = false 291 | python-versions = ">=3.5" 292 | 293 | [[package]] 294 | name = "importlib-metadata" 295 | version = "1.7.0" 296 | description = "Read metadata from Python packages" 297 | category = "dev" 298 | optional = false 299 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 300 | 301 | [package.dependencies] 302 | zipp = ">=0.5" 303 | 304 | [package.extras] 305 | docs = ["rst.linker", "sphinx"] 306 | testing = ["importlib-resources (>=1.3)", "packaging", "pep517"] 307 | 308 | [[package]] 309 | name = "importlib-resources" 310 | version = "5.10.1" 311 | description = "Read resources from Python packages" 312 | category = "dev" 313 | optional = false 314 | python-versions = ">=3.7" 315 | 316 | [package.dependencies] 317 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 318 | 319 | [package.extras] 320 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] 321 | testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 322 | 323 | [[package]] 324 | name = "isort" 325 | version = "5.10.1" 326 | description = "A Python utility / library to sort Python imports." 327 | category = "dev" 328 | optional = false 329 | python-versions = ">=3.6.1,<4.0" 330 | 331 | [package.extras] 332 | colors = ["colorama (>=0.4.3,<0.5.0)"] 333 | pipfile-deprecated-finder = ["pipreqs", "requirementslib"] 334 | plugins = ["setuptools"] 335 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 336 | 337 | [[package]] 338 | name = "jeepney" 339 | version = "0.8.0" 340 | description = "Low-level, pure Python DBus protocol wrapper." 341 | category = "dev" 342 | optional = false 343 | python-versions = ">=3.7" 344 | 345 | [package.extras] 346 | test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] 347 | trio = ["async_generator", "trio"] 348 | 349 | [[package]] 350 | name = "keyring" 351 | version = "22.3.0" 352 | description = "Store and access your passwords safely." 353 | category = "dev" 354 | optional = false 355 | python-versions = ">=3.6" 356 | 357 | [package.dependencies] 358 | importlib-metadata = {version = ">=1", markers = "python_version < \"3.8\""} 359 | jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} 360 | pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} 361 | SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} 362 | 363 | [package.extras] 364 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 365 | testing = ["pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy"] 366 | 367 | [[package]] 368 | name = "lockfile" 369 | version = "0.12.2" 370 | description = "Platform-independent file locking module" 371 | category = "dev" 372 | optional = false 373 | python-versions = "*" 374 | 375 | [[package]] 376 | name = "mccabe" 377 | version = "0.7.0" 378 | description = "McCabe checker, plugin for flake8" 379 | category = "dev" 380 | optional = false 381 | python-versions = ">=3.6" 382 | 383 | [[package]] 384 | name = "msgpack" 385 | version = "1.0.4" 386 | description = "MessagePack serializer" 387 | category = "dev" 388 | optional = false 389 | python-versions = "*" 390 | 391 | [[package]] 392 | name = "mypy-extensions" 393 | version = "0.4.3" 394 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 395 | category = "dev" 396 | optional = false 397 | python-versions = "*" 398 | 399 | [[package]] 400 | name = "mysqlclient" 401 | version = "2.1.1" 402 | description = "Python interface to MySQL" 403 | category = "main" 404 | optional = true 405 | python-versions = ">=3.5" 406 | 407 | [[package]] 408 | name = "packaging" 409 | version = "20.9" 410 | description = "Core utilities for Python packages" 411 | category = "dev" 412 | optional = false 413 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 414 | 415 | [package.dependencies] 416 | pyparsing = ">=2.0.2" 417 | 418 | [[package]] 419 | name = "pastel" 420 | version = "0.2.1" 421 | description = "Bring colors to your terminal." 422 | category = "dev" 423 | optional = false 424 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 425 | 426 | [[package]] 427 | name = "pathspec" 428 | version = "0.10.2" 429 | description = "Utility library for gitignore style pattern matching of file paths." 430 | category = "dev" 431 | optional = false 432 | python-versions = ">=3.7" 433 | 434 | [[package]] 435 | name = "pexpect" 436 | version = "4.8.0" 437 | description = "Pexpect allows easy control of interactive console applications." 438 | category = "dev" 439 | optional = false 440 | python-versions = "*" 441 | 442 | [package.dependencies] 443 | ptyprocess = ">=0.5" 444 | 445 | [[package]] 446 | name = "pkginfo" 447 | version = "1.9.2" 448 | description = "Query metadatdata from sdists / bdists / installed packages." 449 | category = "dev" 450 | optional = false 451 | python-versions = ">=3.6" 452 | 453 | [package.extras] 454 | testing = ["pytest", "pytest-cov"] 455 | 456 | [[package]] 457 | name = "platformdirs" 458 | version = "2.6.0" 459 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 460 | category = "dev" 461 | optional = false 462 | python-versions = ">=3.7" 463 | 464 | [package.extras] 465 | docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] 466 | test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 467 | 468 | [[package]] 469 | name = "pluggy" 470 | version = "1.0.0" 471 | description = "plugin and hook calling mechanisms for python" 472 | category = "dev" 473 | optional = false 474 | python-versions = ">=3.6" 475 | 476 | [package.dependencies] 477 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 478 | 479 | [package.extras] 480 | dev = ["pre-commit", "tox"] 481 | testing = ["pytest", "pytest-benchmark"] 482 | 483 | [[package]] 484 | name = "poetry" 485 | version = "1.1.15" 486 | description = "Python dependency management and packaging made easy." 487 | category = "dev" 488 | optional = false 489 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 490 | 491 | [package.dependencies] 492 | cachecontrol = {version = ">=0.12.9,<0.13.0", extras = ["filecache"], markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 493 | cachy = ">=0.3.0,<0.4.0" 494 | cleo = ">=0.8.1,<0.9.0" 495 | clikit = ">=0.6.2,<0.7.0" 496 | crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 497 | html5lib = ">=1.0,<2.0" 498 | importlib-metadata = {version = ">=1.6.0,<2.0.0", markers = "python_version < \"3.8\""} 499 | keyring = {version = ">=21.2.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 500 | packaging = ">=20.4,<21.0" 501 | pexpect = ">=4.7.0,<5.0.0" 502 | pkginfo = ">=1.4,<2.0" 503 | poetry-core = ">=1.0.7,<1.1.0" 504 | requests = ">=2.18,<3.0" 505 | requests-toolbelt = ">=0.9.1,<0.10.0" 506 | shellingham = ">=1.1,<2.0" 507 | tomlkit = ">=0.7.0,<1.0.0" 508 | virtualenv = ">=20.0.26,<21.0.0" 509 | 510 | [[package]] 511 | name = "poetry-core" 512 | version = "1.0.8" 513 | description = "Poetry PEP 517 Build Backend" 514 | category = "dev" 515 | optional = false 516 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 517 | 518 | [package.dependencies] 519 | importlib-metadata = {version = ">=1.7.0,<2.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.5\" and python_version < \"3.8\""} 520 | 521 | [[package]] 522 | name = "psycopg2" 523 | version = "2.9.5" 524 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 525 | category = "main" 526 | optional = true 527 | python-versions = ">=3.6" 528 | 529 | [[package]] 530 | name = "ptyprocess" 531 | version = "0.7.0" 532 | description = "Run a subprocess in a pseudo terminal" 533 | category = "dev" 534 | optional = false 535 | python-versions = "*" 536 | 537 | [[package]] 538 | name = "py" 539 | version = "1.11.0" 540 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 541 | category = "dev" 542 | optional = false 543 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 544 | 545 | [[package]] 546 | name = "pycodestyle" 547 | version = "2.9.1" 548 | description = "Python style guide checker" 549 | category = "dev" 550 | optional = false 551 | python-versions = ">=3.6" 552 | 553 | [[package]] 554 | name = "pycparser" 555 | version = "2.21" 556 | description = "C parser in Python" 557 | category = "dev" 558 | optional = false 559 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 560 | 561 | [[package]] 562 | name = "pyflakes" 563 | version = "2.5.0" 564 | description = "passive checker of Python programs" 565 | category = "dev" 566 | optional = false 567 | python-versions = ">=3.6" 568 | 569 | [[package]] 570 | name = "pylev" 571 | version = "1.4.0" 572 | description = "A pure Python Levenshtein implementation that's not freaking GPL'd." 573 | category = "dev" 574 | optional = false 575 | python-versions = "*" 576 | 577 | [[package]] 578 | name = "pyparsing" 579 | version = "3.0.9" 580 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 581 | category = "dev" 582 | optional = false 583 | python-versions = ">=3.6.8" 584 | 585 | [package.extras] 586 | diagrams = ["jinja2", "railroad-diagrams"] 587 | 588 | [[package]] 589 | name = "pytz" 590 | version = "2022.6" 591 | description = "World timezone definitions, modern and historical" 592 | category = "main" 593 | optional = false 594 | python-versions = "*" 595 | 596 | [[package]] 597 | name = "pywin32-ctypes" 598 | version = "0.2.0" 599 | description = "" 600 | category = "dev" 601 | optional = false 602 | python-versions = "*" 603 | 604 | [[package]] 605 | name = "requests" 606 | version = "2.28.1" 607 | description = "Python HTTP for Humans." 608 | category = "dev" 609 | optional = false 610 | python-versions = ">=3.7, <4" 611 | 612 | [package.dependencies] 613 | certifi = ">=2017.4.17" 614 | charset-normalizer = ">=2,<3" 615 | idna = ">=2.5,<4" 616 | urllib3 = ">=1.21.1,<1.27" 617 | 618 | [package.extras] 619 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 620 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 621 | 622 | [[package]] 623 | name = "requests-toolbelt" 624 | version = "0.9.1" 625 | description = "A utility belt for advanced users of python-requests" 626 | category = "dev" 627 | optional = false 628 | python-versions = "*" 629 | 630 | [package.dependencies] 631 | requests = ">=2.0.1,<3.0.0" 632 | 633 | [[package]] 634 | name = "secretstorage" 635 | version = "3.3.3" 636 | description = "Python bindings to FreeDesktop.org Secret Service API" 637 | category = "dev" 638 | optional = false 639 | python-versions = ">=3.6" 640 | 641 | [package.dependencies] 642 | cryptography = ">=2.0" 643 | jeepney = ">=0.6" 644 | 645 | [[package]] 646 | name = "shellingham" 647 | version = "1.5.0" 648 | description = "Tool to Detect Surrounding Shell" 649 | category = "dev" 650 | optional = false 651 | python-versions = ">=3.4" 652 | 653 | [[package]] 654 | name = "six" 655 | version = "1.16.0" 656 | description = "Python 2 and 3 compatibility utilities" 657 | category = "dev" 658 | optional = false 659 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 660 | 661 | [[package]] 662 | name = "sqlparse" 663 | version = "0.4.3" 664 | description = "A non-validating SQL parser." 665 | category = "main" 666 | optional = false 667 | python-versions = ">=3.5" 668 | 669 | [[package]] 670 | name = "tomli" 671 | version = "2.0.1" 672 | description = "A lil' TOML parser" 673 | category = "dev" 674 | optional = false 675 | python-versions = ">=3.7" 676 | 677 | [[package]] 678 | name = "tomlkit" 679 | version = "0.11.6" 680 | description = "Style preserving TOML library" 681 | category = "dev" 682 | optional = false 683 | python-versions = ">=3.6" 684 | 685 | [[package]] 686 | name = "tox" 687 | version = "3.27.1" 688 | description = "tox is a generic virtualenv management and test command line tool" 689 | category = "dev" 690 | optional = false 691 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 692 | 693 | [package.dependencies] 694 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 695 | filelock = ">=3.0.0" 696 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 697 | packaging = ">=14" 698 | pluggy = ">=0.12.0" 699 | py = ">=1.4.17" 700 | six = ">=1.14.0" 701 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 702 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 703 | 704 | [package.extras] 705 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 706 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] 707 | 708 | [[package]] 709 | name = "tox-gh-actions" 710 | version = "2.11.0" 711 | description = "Seamless integration of tox into GitHub Actions" 712 | category = "dev" 713 | optional = false 714 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 715 | 716 | [package.dependencies] 717 | importlib-resources = "*" 718 | tox = ">=3.12,<4" 719 | 720 | [package.extras] 721 | testing = ["black", "coverage (<6)", "flake8 (>=3,<4)", "pytest (>=4,<7)", "pytest (>=6.2.5,<7)", "pytest-cov (>=2,<3)", "pytest-mock (>=2,<3)", "pytest-randomly (>=3)"] 722 | 723 | [[package]] 724 | name = "tox-poetry-installer" 725 | version = "0.8.5" 726 | description = "A plugin for Tox that lets you install test environment dependencies from the Poetry lockfile" 727 | category = "dev" 728 | optional = false 729 | python-versions = ">=3.6.1,<4.0.0" 730 | 731 | [package.dependencies] 732 | poetry = {version = ">=1.0.0,<1.2", optional = true, markers = "extra == \"poetry\""} 733 | poetry-core = ">=1.0.0,<2.0.0" 734 | tox = ">=3.8.0,<4.0.0" 735 | 736 | [package.extras] 737 | poetry = ["poetry (>=1.0.0,<1.2)"] 738 | 739 | [[package]] 740 | name = "typed-ast" 741 | version = "1.5.4" 742 | description = "a fork of Python 2 and 3 ast modules with type comment support" 743 | category = "dev" 744 | optional = false 745 | python-versions = ">=3.6" 746 | 747 | [[package]] 748 | name = "typing-extensions" 749 | version = "4.4.0" 750 | description = "Backported and Experimental Type Hints for Python 3.7+" 751 | category = "main" 752 | optional = false 753 | python-versions = ">=3.7" 754 | 755 | [[package]] 756 | name = "urllib3" 757 | version = "1.26.13" 758 | description = "HTTP library with thread-safe connection pooling, file post, and more." 759 | category = "dev" 760 | optional = false 761 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 762 | 763 | [package.extras] 764 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 765 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 766 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 767 | 768 | [[package]] 769 | name = "virtualenv" 770 | version = "20.16.2" 771 | description = "Virtual Python Environment builder" 772 | category = "dev" 773 | optional = false 774 | python-versions = ">=3.6" 775 | 776 | [package.dependencies] 777 | distlib = ">=0.3.1,<1" 778 | filelock = ">=3.2,<4" 779 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 780 | platformdirs = ">=2,<3" 781 | 782 | [package.extras] 783 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 784 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] 785 | 786 | [[package]] 787 | name = "webencodings" 788 | version = "0.5.1" 789 | description = "Character encoding aliases for legacy web content" 790 | category = "dev" 791 | optional = false 792 | python-versions = "*" 793 | 794 | [[package]] 795 | name = "zipp" 796 | version = "3.11.0" 797 | description = "Backport of pathlib-compatible object wrapper for zip files" 798 | category = "dev" 799 | optional = false 800 | python-versions = ">=3.7" 801 | 802 | [package.extras] 803 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] 804 | testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 805 | 806 | [extras] 807 | mysql = ["mysqlclient"] 808 | postgres = ["psycopg2"] 809 | 810 | [metadata] 811 | lock-version = "1.1" 812 | python-versions = "^3.7" 813 | content-hash = "aa2af97e01132941420a1c66f1c6379be2cfed8b1bc7aa92d056df24e05ef5f7" 814 | 815 | [metadata.files] 816 | asgiref = [ 817 | {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, 818 | {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, 819 | ] 820 | autoflake = [ 821 | {file = "autoflake-1.7.8-py3-none-any.whl", hash = "sha256:46373ef69b6714f5064c923bb28bd797c4f8a9497f557d87fc36665c6d956b39"}, 822 | {file = "autoflake-1.7.8.tar.gz", hash = "sha256:e7e46372dee46fa1c97acf310d99d922b63d369718a270809d7c278d34a194cf"}, 823 | ] 824 | black = [ 825 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 826 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 827 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 828 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 829 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 830 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 831 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 832 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 833 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 834 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 835 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 836 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 837 | ] 838 | bump2version = [ 839 | {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, 840 | {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, 841 | ] 842 | cachecontrol = [ 843 | {file = "CacheControl-0.12.11-py2.py3-none-any.whl", hash = "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b"}, 844 | {file = "CacheControl-0.12.11.tar.gz", hash = "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144"}, 845 | ] 846 | cachy = [ 847 | {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"}, 848 | {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"}, 849 | ] 850 | certifi = [ 851 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 852 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 853 | ] 854 | cffi = [ 855 | {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, 856 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, 857 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, 858 | {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, 859 | {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, 860 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, 861 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, 862 | {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, 863 | {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, 864 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, 865 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, 866 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, 867 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, 868 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, 869 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, 870 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, 871 | {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, 872 | {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, 873 | {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, 874 | {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, 875 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, 876 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, 877 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, 878 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, 879 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, 880 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, 881 | {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, 882 | {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, 883 | {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, 884 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, 885 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, 886 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, 887 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, 888 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, 889 | {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, 890 | {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, 891 | {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, 892 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, 893 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, 894 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, 895 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, 896 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, 897 | {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, 898 | {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, 899 | {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, 900 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, 901 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, 902 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, 903 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, 904 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, 905 | {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, 906 | {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, 907 | {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, 908 | {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, 909 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, 910 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, 911 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, 912 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, 913 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, 914 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, 915 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, 916 | {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, 917 | {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, 918 | {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, 919 | ] 920 | charset-normalizer = [ 921 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 922 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 923 | ] 924 | cleo = [ 925 | {file = "cleo-0.8.1-py2.py3-none-any.whl", hash = "sha256:141cda6dc94a92343be626bb87a0b6c86ae291dfc732a57bf04310d4b4201753"}, 926 | {file = "cleo-0.8.1.tar.gz", hash = "sha256:3d0e22d30117851b45970b6c14aca4ab0b18b1b53c8af57bed13208147e4069f"}, 927 | ] 928 | click = [ 929 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 930 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 931 | ] 932 | clikit = [ 933 | {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"}, 934 | {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"}, 935 | ] 936 | colorama = [ 937 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 938 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 939 | ] 940 | coverage = [ 941 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 942 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 943 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 944 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 945 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, 946 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 947 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 948 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 949 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 950 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 951 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 952 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 953 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 954 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, 955 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 956 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 957 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 958 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 959 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 960 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 961 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 962 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 963 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, 964 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 965 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 966 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 967 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 968 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 969 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 970 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 971 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 972 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 973 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, 974 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 975 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 976 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 977 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 978 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 979 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 980 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 981 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 982 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 983 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, 984 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 985 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 986 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 987 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 988 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 989 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 990 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 991 | ] 992 | crashtest = [ 993 | {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"}, 994 | {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"}, 995 | ] 996 | cryptography = [ 997 | {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"}, 998 | {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"}, 999 | {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"}, 1000 | {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"}, 1001 | {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"}, 1002 | {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"}, 1003 | {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"}, 1004 | {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"}, 1005 | {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"}, 1006 | {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"}, 1007 | {file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"}, 1008 | {file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"}, 1009 | {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"}, 1010 | {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"}, 1011 | {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"}, 1012 | {file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"}, 1013 | {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"}, 1014 | {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"}, 1015 | {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"}, 1016 | {file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"}, 1017 | {file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"}, 1018 | {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"}, 1019 | {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"}, 1020 | {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"}, 1021 | {file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"}, 1022 | {file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"}, 1023 | ] 1024 | distlib = [ 1025 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 1026 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 1027 | ] 1028 | django = [ 1029 | {file = "Django-3.2.16-py3-none-any.whl", hash = "sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121"}, 1030 | {file = "Django-3.2.16.tar.gz", hash = "sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"}, 1031 | ] 1032 | django-environ = [ 1033 | {file = "django-environ-0.8.1.tar.gz", hash = "sha256:6f0bc902b43891656b20486938cba0861dc62892784a44919170719572a534cb"}, 1034 | {file = "django_environ-0.8.1-py2.py3-none-any.whl", hash = "sha256:42593bee519a527602a467c7b682aee1a051c2597f98c45f4f4f44169ecdb6e5"}, 1035 | ] 1036 | filelock = [ 1037 | {file = "filelock-3.8.2-py3-none-any.whl", hash = "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c"}, 1038 | {file = "filelock-3.8.2.tar.gz", hash = "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2"}, 1039 | ] 1040 | flake8 = [ 1041 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 1042 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 1043 | ] 1044 | html5lib = [ 1045 | {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, 1046 | {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, 1047 | ] 1048 | idna = [ 1049 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 1050 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 1051 | ] 1052 | importlib-metadata = [ 1053 | {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, 1054 | {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, 1055 | ] 1056 | importlib-resources = [ 1057 | {file = "importlib_resources-5.10.1-py3-none-any.whl", hash = "sha256:c09b067d82e72c66f4f8eb12332f5efbebc9b007c0b6c40818108c9870adc363"}, 1058 | {file = "importlib_resources-5.10.1.tar.gz", hash = "sha256:32bb095bda29741f6ef0e5278c42df98d135391bee5f932841efc0041f748dc3"}, 1059 | ] 1060 | isort = [ 1061 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 1062 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 1063 | ] 1064 | jeepney = [ 1065 | {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, 1066 | {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, 1067 | ] 1068 | keyring = [ 1069 | {file = "keyring-22.3.0-py3-none-any.whl", hash = "sha256:2bc8363ebdd63886126a012057a85c8cb6e143877afa02619ac7dbc9f38a207b"}, 1070 | {file = "keyring-22.3.0.tar.gz", hash = "sha256:16927a444b2c73f983520a48dec79ddab49fe76429ea05b8d528d778c8339522"}, 1071 | ] 1072 | lockfile = [ 1073 | {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, 1074 | {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, 1075 | ] 1076 | mccabe = [ 1077 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 1078 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 1079 | ] 1080 | msgpack = [ 1081 | {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"}, 1082 | {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"}, 1083 | {file = "msgpack-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467"}, 1084 | {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6"}, 1085 | {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa"}, 1086 | {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6"}, 1087 | {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba"}, 1088 | {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e"}, 1089 | {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db"}, 1090 | {file = "msgpack-1.0.4-cp310-cp310-win32.whl", hash = "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef"}, 1091 | {file = "msgpack-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075"}, 1092 | {file = "msgpack-1.0.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52"}, 1093 | {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9"}, 1094 | {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9"}, 1095 | {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08"}, 1096 | {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"}, 1097 | {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6"}, 1098 | {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae"}, 1099 | {file = "msgpack-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6"}, 1100 | {file = "msgpack-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661"}, 1101 | {file = "msgpack-1.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c"}, 1102 | {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0"}, 1103 | {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227"}, 1104 | {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff"}, 1105 | {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd"}, 1106 | {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e"}, 1107 | {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236"}, 1108 | {file = "msgpack-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44"}, 1109 | {file = "msgpack-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1"}, 1110 | {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d"}, 1111 | {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab"}, 1112 | {file = "msgpack-1.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb"}, 1113 | {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9"}, 1114 | {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e"}, 1115 | {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1"}, 1116 | {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e"}, 1117 | {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43"}, 1118 | {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243"}, 1119 | {file = "msgpack-1.0.4-cp38-cp38-win32.whl", hash = "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2"}, 1120 | {file = "msgpack-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6"}, 1121 | {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae"}, 1122 | {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55"}, 1123 | {file = "msgpack-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da"}, 1124 | {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f"}, 1125 | {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92"}, 1126 | {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f"}, 1127 | {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624"}, 1128 | {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8"}, 1129 | {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae"}, 1130 | {file = "msgpack-1.0.4-cp39-cp39-win32.whl", hash = "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c"}, 1131 | {file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"}, 1132 | {file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"}, 1133 | ] 1134 | mypy-extensions = [ 1135 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1136 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1137 | ] 1138 | mysqlclient = [ 1139 | {file = "mysqlclient-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37"}, 1140 | {file = "mysqlclient-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b"}, 1141 | {file = "mysqlclient-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c"}, 1142 | {file = "mysqlclient-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994"}, 1143 | {file = "mysqlclient-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855"}, 1144 | {file = "mysqlclient-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"}, 1145 | {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, 1146 | ] 1147 | packaging = [ 1148 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 1149 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 1150 | ] 1151 | pastel = [ 1152 | {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, 1153 | {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, 1154 | ] 1155 | pathspec = [ 1156 | {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"}, 1157 | {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"}, 1158 | ] 1159 | pexpect = [ 1160 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 1161 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 1162 | ] 1163 | pkginfo = [ 1164 | {file = "pkginfo-1.9.2-py3-none-any.whl", hash = "sha256:d580059503f2f4549ad6e4c106d7437356dbd430e2c7df99ee1efe03d75f691e"}, 1165 | {file = "pkginfo-1.9.2.tar.gz", hash = "sha256:ac03e37e4d601aaee40f8087f63fc4a2a6c9814dda2c8fa6aab1b1829653bdfa"}, 1166 | ] 1167 | platformdirs = [ 1168 | {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, 1169 | {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, 1170 | ] 1171 | pluggy = [ 1172 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1173 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1174 | ] 1175 | poetry = [ 1176 | {file = "poetry-1.1.15-py2.py3-none-any.whl", hash = "sha256:2f8f68bc02006386dd640d08e0ae483231501c6e727842a8a799c9ee376b98c2"}, 1177 | {file = "poetry-1.1.15.tar.gz", hash = "sha256:a373848fd205f31b2f6bee6b87a201ea1e09ca573a2f40d0991539f564cedffd"}, 1178 | ] 1179 | poetry-core = [ 1180 | {file = "poetry-core-1.0.8.tar.gz", hash = "sha256:951fc7c1f8d710a94cb49019ee3742125039fc659675912ea614ac2aa405b118"}, 1181 | {file = "poetry_core-1.0.8-py2.py3-none-any.whl", hash = "sha256:54b0fab6f7b313886e547a52f8bf52b8cf43e65b2633c65117f8755289061924"}, 1182 | ] 1183 | psycopg2 = [ 1184 | {file = "psycopg2-2.9.5-cp310-cp310-win32.whl", hash = "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f"}, 1185 | {file = "psycopg2-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee"}, 1186 | {file = "psycopg2-2.9.5-cp311-cp311-win32.whl", hash = "sha256:093e3894d2d3c592ab0945d9eba9d139c139664dcf83a1c440b8a7aa9bb21955"}, 1187 | {file = "psycopg2-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:920bf418000dd17669d2904472efeab2b20546efd0548139618f8fa305d1d7ad"}, 1188 | {file = "psycopg2-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d"}, 1189 | {file = "psycopg2-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5"}, 1190 | {file = "psycopg2-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0"}, 1191 | {file = "psycopg2-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a"}, 1192 | {file = "psycopg2-2.9.5-cp38-cp38-win32.whl", hash = "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2"}, 1193 | {file = "psycopg2-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e"}, 1194 | {file = "psycopg2-2.9.5-cp39-cp39-win32.whl", hash = "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5"}, 1195 | {file = "psycopg2-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa"}, 1196 | {file = "psycopg2-2.9.5.tar.gz", hash = "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a"}, 1197 | ] 1198 | ptyprocess = [ 1199 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 1200 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 1201 | ] 1202 | py = [ 1203 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 1204 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 1205 | ] 1206 | pycodestyle = [ 1207 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 1208 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 1209 | ] 1210 | pycparser = [ 1211 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 1212 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 1213 | ] 1214 | pyflakes = [ 1215 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 1216 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 1217 | ] 1218 | pylev = [ 1219 | {file = "pylev-1.4.0-py2.py3-none-any.whl", hash = "sha256:7b2e2aa7b00e05bb3f7650eb506fc89f474f70493271a35c242d9a92188ad3dd"}, 1220 | {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"}, 1221 | ] 1222 | pyparsing = [ 1223 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 1224 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 1225 | ] 1226 | pytz = [ 1227 | {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, 1228 | {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, 1229 | ] 1230 | pywin32-ctypes = [ 1231 | {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, 1232 | {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, 1233 | ] 1234 | requests = [ 1235 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, 1236 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, 1237 | ] 1238 | requests-toolbelt = [ 1239 | {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, 1240 | {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, 1241 | ] 1242 | secretstorage = [ 1243 | {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, 1244 | {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, 1245 | ] 1246 | shellingham = [ 1247 | {file = "shellingham-1.5.0-py2.py3-none-any.whl", hash = "sha256:a8f02ba61b69baaa13facdba62908ca8690a94b8119b69f5ec5873ea85f7391b"}, 1248 | {file = "shellingham-1.5.0.tar.gz", hash = "sha256:72fb7f5c63103ca2cb91b23dee0c71fe8ad6fbfd46418ef17dbe40db51592dad"}, 1249 | ] 1250 | six = [ 1251 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1252 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1253 | ] 1254 | sqlparse = [ 1255 | {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, 1256 | {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, 1257 | ] 1258 | tomli = [ 1259 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1260 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1261 | ] 1262 | tomlkit = [ 1263 | {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, 1264 | {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, 1265 | ] 1266 | tox = [ 1267 | {file = "tox-3.27.1-py2.py3-none-any.whl", hash = "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84"}, 1268 | {file = "tox-3.27.1.tar.gz", hash = "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04"}, 1269 | ] 1270 | tox-gh-actions = [ 1271 | {file = "tox-gh-actions-2.11.0.tar.gz", hash = "sha256:07779eedc7ba4a1749f9fad23fdc7e4760a9663c4a80c9e1adffc54858917f60"}, 1272 | {file = "tox_gh_actions-2.11.0-py2.py3-none-any.whl", hash = "sha256:1aff5a36549c383adf133f4d1432c2f5206c7665f7756d6397704aa4c8880c6b"}, 1273 | ] 1274 | tox-poetry-installer = [ 1275 | {file = "tox-poetry-installer-0.8.5.tar.gz", hash = "sha256:2107c7265afb9d704ca9f3eeff6e4d4ee99f874d0bfafad10815e7f3d6854240"}, 1276 | {file = "tox_poetry_installer-0.8.5-py3-none-any.whl", hash = "sha256:04d2c12964e61d33399597533b8428b453e694919431873be120817599493505"}, 1277 | ] 1278 | typed-ast = [ 1279 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, 1280 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, 1281 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, 1282 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, 1283 | {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, 1284 | {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, 1285 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, 1286 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, 1287 | {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, 1288 | {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, 1289 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, 1290 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, 1291 | {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, 1292 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, 1293 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, 1294 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, 1295 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, 1296 | {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, 1297 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, 1298 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, 1299 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, 1300 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, 1301 | {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, 1302 | {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, 1303 | ] 1304 | typing-extensions = [ 1305 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 1306 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 1307 | ] 1308 | urllib3 = [ 1309 | {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, 1310 | {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, 1311 | ] 1312 | virtualenv = [ 1313 | {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, 1314 | {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, 1315 | ] 1316 | webencodings = [ 1317 | {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, 1318 | {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, 1319 | ] 1320 | zipp = [ 1321 | {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, 1322 | {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, 1323 | ] 1324 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-custom-user" 3 | version = "1.1" 4 | description = "Custom user model for Django with the same behaviour as the default User class but with email instead of username." 5 | authors = ["Josep Cugat "] 6 | license = "BSD-3-Clause" 7 | readme = "README.rst" 8 | homepage = "https://github.com/jcugat/django-custom-user" 9 | repository = "https://github.com/jcugat/django-custom-user" 10 | documentation = "https://github.com/jcugat/django-custom-user#django-custom-user" 11 | keywords = ["django", "custom", "user", "auth", "model", "email", "without", "username"] 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Environment :: Web Environment", 15 | "Framework :: Django", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3 :: Only", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Topic :: Internet :: WWW/HTTP", 27 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 28 | "Topic :: Internet :: WWW/HTTP :: WSGI", 29 | "Topic :: Software Development :: Libraries :: Application Frameworks", 30 | "Topic :: Software Development :: Libraries :: Python Modules", 31 | ] 32 | packages = [ 33 | { include = "custom_user", from = "src" }, 34 | ] 35 | 36 | [tool.poetry.urls] 37 | "Changelog" = "https://github.com/jcugat/django-custom-user#changelog" 38 | 39 | [tool.poetry.dependencies] 40 | python = "^3.7" 41 | Django = ">=3.2" 42 | mysqlclient = {version = "^2.1.0", optional = true} 43 | psycopg2 = {version = "^2.9.0", optional = true} 44 | 45 | [tool.poetry.dev-dependencies] 46 | autoflake = "^1.4" 47 | black = "^22.1.0" 48 | bump2version = "^1.0.1" 49 | coverage = {version = "^6.5.0", extras = ["toml"]} 50 | django-environ = "^0.8.0" 51 | flake8 = "^5.0.0" 52 | isort = "^5.10.0" 53 | tox = "^3.24.0" 54 | tox-gh-actions = "^2.9.0" 55 | tox-poetry-installer = {extras = ["poetry"], version = "^0.8.3"} 56 | # This is needed because newer versions of poetry-core are not compatible with 57 | # tox-poetry-installer, and if we upgrade tox-poetry-installer then we are forced 58 | # to bump the lowest required version of Python, which we still want to test against 59 | poetry-core = "<1.1" 60 | 61 | [tool.poetry.extras] 62 | mysql = ["mysqlclient"] 63 | postgres = ["psycopg2"] 64 | 65 | [build-system] 66 | requires = ["poetry-core>=1.0.0"] 67 | build-backend = "poetry.core.masonry.api" 68 | 69 | [tool.black] 70 | exclude = ''' 71 | /( 72 | .tox 73 | | src/custom_user/migrations/ 74 | | test_custom_user_subclass/migrations/ 75 | )/ 76 | ''' 77 | 78 | [tool.coverage.run] 79 | branch = true 80 | parallel = true 81 | source = [ 82 | "custom_user", 83 | "test_custom_user_subclass", 84 | "test_settings", 85 | ] 86 | 87 | [tool.coverage.paths] 88 | source = [ 89 | "src/custom_user", 90 | ".tox/*/lib/python*/site-packages/custom_user", 91 | ] 92 | 93 | [tool.coverage.report] 94 | fail_under = 100 95 | show_missing = true 96 | skip_covered = true 97 | 98 | [tool.isort] 99 | profile = "black" 100 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.1 3 | commit = True 4 | message = Version {new_version} 5 | tag = True 6 | tag_name = {new_version} 7 | tag_message = django-custom-user {new_version} 8 | parse = (?P\d+)\.(?P\d+)(\.(?P\d+))? 9 | serialize = 10 | {major}.{minor}.{patch} 11 | {major}.{minor} 12 | 13 | [bumpversion:file:src/custom_user/__init__.py] 14 | search = __version__ = "{current_version}" 15 | replace = __version__ = "{new_version}" 16 | 17 | [bumpversion:file:pyproject.toml] 18 | search = version = "{current_version}" 19 | replace = version = "{new_version}" 20 | 21 | [flake8] 22 | max-line-length = 88 23 | extend-ignore = E203 24 | exclude = .tox,src/custom_user/migrations/,test_custom_user_subclass/migrations/ 25 | -------------------------------------------------------------------------------- /src/custom_user/__init__.py: -------------------------------------------------------------------------------- 1 | """Custom user model for Django with email instead of username.""" 2 | 3 | __version__ = "1.1" 4 | -------------------------------------------------------------------------------- /src/custom_user/admin.py: -------------------------------------------------------------------------------- 1 | """Admin definition for EmailUser.""" 2 | from django.contrib import admin 3 | from django.contrib.auth.admin import UserAdmin 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from .forms import EmailUserChangeForm, EmailUserCreationForm 7 | from .models import EmailUser 8 | 9 | 10 | @admin.register(EmailUser) 11 | class EmailUserAdmin(UserAdmin): 12 | """ 13 | EmailUser Admin model. 14 | """ 15 | 16 | fieldsets = ( 17 | (None, {"fields": ("email", "password")}), 18 | ( 19 | _("Permissions"), 20 | { 21 | "fields": ( 22 | "is_active", 23 | "is_staff", 24 | "is_superuser", 25 | "groups", 26 | "user_permissions", 27 | ), 28 | }, 29 | ), 30 | (_("Important dates"), {"fields": ("last_login", "date_joined")}), 31 | ) 32 | add_fieldsets = ( 33 | ( 34 | None, 35 | { 36 | "classes": ("wide",), 37 | "fields": ("email", "password1", "password2"), 38 | }, 39 | ), 40 | ) 41 | 42 | # The forms to add and change user instances 43 | form = EmailUserChangeForm 44 | add_form = EmailUserCreationForm 45 | 46 | # The fields to be used in displaying the User model. 47 | # These override the definitions on the base UserAdmin 48 | # that reference specific fields on auth.User. 49 | list_display = ("email", "is_staff") 50 | list_filter = ("is_staff", "is_superuser", "is_active", "groups") 51 | search_fields = ("email",) 52 | ordering = ("email",) 53 | filter_horizontal = ( 54 | "groups", 55 | "user_permissions", 56 | ) 57 | -------------------------------------------------------------------------------- /src/custom_user/apps.py: -------------------------------------------------------------------------------- 1 | """App configuration for custom_user.""" 2 | from django.apps import AppConfig 3 | 4 | 5 | class CustomUserConfig(AppConfig): 6 | """ 7 | Default configuration for custom_user. 8 | """ 9 | 10 | name = "custom_user" 11 | verbose_name = "Custom User" 12 | 13 | # https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys 14 | default_auto_field = "django.db.models.AutoField" 15 | -------------------------------------------------------------------------------- /src/custom_user/forms.py: -------------------------------------------------------------------------------- 1 | """EmailUser forms.""" 2 | from django import forms 3 | from django.contrib.auth import get_user_model, password_validation 4 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 5 | from django.core.exceptions import ValidationError 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | 9 | class EmailUserCreationForm(forms.ModelForm): 10 | """ 11 | A form for creating new users. 12 | 13 | Includes all the required fields, plus a repeated password. 14 | """ 15 | 16 | error_messages = { 17 | "duplicate_email": _("A user with that email already exists."), 18 | "password_mismatch": _("The two password fields didn't match."), 19 | } 20 | 21 | password1 = forms.CharField( 22 | label=_("Password"), 23 | strip=False, 24 | widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), 25 | help_text=password_validation.password_validators_help_text_html(), 26 | ) 27 | password2 = forms.CharField( 28 | label=_("Password confirmation"), 29 | widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), 30 | strip=False, 31 | help_text=_("Enter the same password as before, for verification."), 32 | ) 33 | 34 | class Meta: 35 | model = get_user_model() 36 | fields = ("email",) 37 | 38 | def clean_email(self): 39 | """ 40 | Clean form email. 41 | 42 | :return str email: cleaned email 43 | :raise ValidationError: Email is duplicated 44 | """ 45 | # Since EmailUser.email is unique, this check is redundant, 46 | # but it sets a nicer error message than the ORM. See #13147. 47 | email = self.cleaned_data["email"] 48 | try: 49 | get_user_model()._default_manager.get(email=email) 50 | except get_user_model().DoesNotExist: 51 | return email 52 | raise ValidationError( 53 | self.error_messages["duplicate_email"], 54 | code="duplicate_email", 55 | ) 56 | 57 | def clean_password2(self): 58 | """ 59 | Check that the two password entries match. 60 | 61 | :return str password2: cleaned password2 62 | :raise ValidationError: password2 != password1 63 | """ 64 | password1 = self.cleaned_data.get("password1") 65 | password2 = self.cleaned_data.get("password2") 66 | if password1 and password2 and password1 != password2: 67 | raise ValidationError( 68 | self.error_messages["password_mismatch"], 69 | code="password_mismatch", 70 | ) 71 | return password2 72 | 73 | def _post_clean(self): 74 | super()._post_clean() 75 | # Validate the password after self.instance is updated with form data 76 | # by super(). 77 | password = self.cleaned_data.get("password2") 78 | if password: 79 | try: 80 | password_validation.validate_password(password, self.instance) 81 | except ValidationError as error: 82 | self.add_error("password2", error) 83 | 84 | def save(self, commit=True): 85 | """ 86 | Save user. 87 | 88 | Save the provided password in hashed format. 89 | 90 | :return custom_user.models.EmailUser: user 91 | """ 92 | user = super().save(commit=False) 93 | user.set_password(self.cleaned_data["password1"]) 94 | if commit: 95 | user.save() 96 | return user 97 | 98 | 99 | class EmailUserChangeForm(forms.ModelForm): 100 | 101 | """ 102 | A form for updating users. 103 | 104 | Includes all the fields on the user, but replaces the password field 105 | with admin's password hash display field. 106 | """ 107 | 108 | password = ReadOnlyPasswordHashField( 109 | label=_("Password"), 110 | help_text=_( 111 | "Raw passwords are not stored, so there is no way to see this " 112 | "user's password, but you can change the password using " 113 | 'this form.' 114 | ), 115 | ) 116 | 117 | class Meta: 118 | model = get_user_model() 119 | exclude = () 120 | 121 | def __init__(self, *args, **kwargs): 122 | super().__init__(*args, **kwargs) 123 | password = self.fields.get("password") 124 | if password: # pragma: no cover 125 | password.help_text = password.help_text.format("../password/") 126 | user_permissions = self.fields.get("user_permissions") 127 | if user_permissions: 128 | user_permissions.queryset = user_permissions.queryset.select_related( 129 | "content_type" 130 | ) 131 | -------------------------------------------------------------------------------- /src/custom_user/migrations/0001_initial_django17.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import django.utils.timezone 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("auth", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="EmailUser", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | verbose_name="ID", 22 | serialize=False, 23 | auto_created=True, 24 | primary_key=True, 25 | ), 26 | ), 27 | ("password", models.CharField(max_length=128, verbose_name="password")), 28 | ( 29 | "last_login", 30 | models.DateTimeField( 31 | default=django.utils.timezone.now, verbose_name="last login" 32 | ), 33 | ), 34 | ( 35 | "is_superuser", 36 | models.BooleanField( 37 | default=False, 38 | help_text="Designates that this user has all permissions without explicitly assigning them.", 39 | verbose_name="superuser status", 40 | ), 41 | ), 42 | ( 43 | "email", 44 | models.EmailField( 45 | verbose_name="email address", 46 | db_index=True, 47 | max_length=255, 48 | unique=True, 49 | ), 50 | ), 51 | ( 52 | "is_staff", 53 | models.BooleanField( 54 | default=False, 55 | help_text="Designates whether the user can log into this admin site.", 56 | verbose_name="staff status", 57 | ), 58 | ), 59 | ( 60 | "is_active", 61 | models.BooleanField( 62 | default=True, 63 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 64 | verbose_name="active", 65 | ), 66 | ), 67 | ( 68 | "date_joined", 69 | models.DateTimeField( 70 | default=django.utils.timezone.now, verbose_name="date joined" 71 | ), 72 | ), 73 | ( 74 | "groups", 75 | models.ManyToManyField( 76 | to="auth.Group", 77 | verbose_name="groups", 78 | blank=True, 79 | help_text="The groups this user belongs to. A user will get all permissions granted to each of his/her group.", 80 | related_name="user_set", 81 | related_query_name="user", 82 | ), 83 | ), 84 | ( 85 | "user_permissions", 86 | models.ManyToManyField( 87 | to="auth.Permission", 88 | verbose_name="user permissions", 89 | blank=True, 90 | help_text="Specific permissions for this user.", 91 | related_name="user_set", 92 | related_query_name="user", 93 | ), 94 | ), 95 | ], 96 | options={ 97 | "swappable": "AUTH_USER_MODEL", 98 | "verbose_name": "user", 99 | "verbose_name_plural": "users", 100 | "abstract": False, 101 | }, 102 | bases=(models.Model,), 103 | ), 104 | ] 105 | -------------------------------------------------------------------------------- /src/custom_user/migrations/0002_initial_django18.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("custom_user", "0001_initial_django17"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="emailuser", 16 | name="groups", 17 | field=models.ManyToManyField( 18 | to="auth.Group", 19 | verbose_name="groups", 20 | blank=True, 21 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 22 | related_name="user_set", 23 | related_query_name="user", 24 | ), 25 | ), 26 | migrations.AlterField( 27 | model_name="emailuser", 28 | name="last_login", 29 | field=models.DateTimeField( 30 | null=True, verbose_name="last login", blank=True 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /src/custom_user/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcugat/django-custom-user/309d36c681d622bcdbc4648368cb13faab9cce95/src/custom_user/migrations/__init__.py -------------------------------------------------------------------------------- /src/custom_user/models.py: -------------------------------------------------------------------------------- 1 | """User models.""" 2 | from django.contrib.auth.models import ( 3 | AbstractBaseUser, 4 | BaseUserManager, 5 | PermissionsMixin, 6 | ) 7 | from django.core.mail import send_mail 8 | from django.db import models 9 | from django.utils import timezone 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | 13 | class EmailUserManager(BaseUserManager): 14 | """ 15 | Custom manager for EmailUser. 16 | """ 17 | 18 | def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 19 | """ 20 | Create and save an EmailUser with the given email and password. 21 | 22 | :param str email: user email 23 | :param str password: user password 24 | :param bool is_staff: whether user staff or not 25 | :param bool is_superuser: whether user admin or not 26 | :return custom_user.models.EmailUser user: user 27 | :raise ValueError: email is not set 28 | """ 29 | now = timezone.now() 30 | if not email: 31 | raise ValueError("The given email must be set") 32 | email = self.normalize_email(email) 33 | is_active = extra_fields.pop("is_active", True) 34 | user = self.model( 35 | email=email, 36 | is_staff=is_staff, 37 | is_active=is_active, 38 | is_superuser=is_superuser, 39 | last_login=now, 40 | date_joined=now, 41 | **extra_fields 42 | ) 43 | user.set_password(password) 44 | user.save(using=self._db) 45 | return user 46 | 47 | def create_user(self, email, password=None, **extra_fields): 48 | """ 49 | Create and save an EmailUser with the given email and password. 50 | 51 | :param str email: user email 52 | :param str password: user password 53 | :return custom_user.models.EmailUser user: regular user 54 | """ 55 | extra_fields.setdefault("is_staff", False) 56 | extra_fields.setdefault("is_superuser", False) 57 | return self._create_user(email, password, **extra_fields) 58 | 59 | def create_superuser(self, email, password=None, **extra_fields): 60 | """ 61 | Create and save an EmailUser with the given email and password. 62 | 63 | :param str email: user email 64 | :param str password: user password 65 | :return custom_user.models.EmailUser user: admin user 66 | """ 67 | extra_fields.setdefault("is_staff", True) 68 | extra_fields.setdefault("is_superuser", True) 69 | 70 | if extra_fields.get("is_staff") is not True: 71 | raise ValueError("Superuser must have is_staff=True.") 72 | if extra_fields.get("is_superuser") is not True: 73 | raise ValueError("Superuser must have is_superuser=True.") 74 | 75 | return self._create_user(email, password, **extra_fields) 76 | 77 | 78 | class AbstractEmailUser(AbstractBaseUser, PermissionsMixin): 79 | """ 80 | Abstract User with the same behaviour as Django's default User. 81 | 82 | AbstractEmailUser does not have username field. Uses email as the 83 | USERNAME_FIELD for authentication. 84 | 85 | Use this if you need to extend EmailUser. 86 | 87 | Inherits from both the AbstractBaseUser and PermissionMixin. 88 | 89 | The following attributes are inherited from the superclasses: 90 | * password 91 | * last_login 92 | * is_superuser 93 | """ 94 | 95 | email = models.EmailField( 96 | _("email address"), max_length=255, unique=True, db_index=True 97 | ) 98 | is_staff = models.BooleanField( 99 | _("staff status"), 100 | default=False, 101 | help_text=_("Designates whether the user can log into this admin site."), 102 | ) 103 | is_active = models.BooleanField( 104 | _("active"), 105 | default=True, 106 | help_text=_( 107 | "Designates whether this user should be treated as " 108 | "active. Unselect this instead of deleting accounts." 109 | ), 110 | ) 111 | date_joined = models.DateTimeField(_("date joined"), default=timezone.now) 112 | 113 | objects = EmailUserManager() 114 | 115 | USERNAME_FIELD = "email" 116 | REQUIRED_FIELDS = [] 117 | 118 | class Meta: 119 | verbose_name = _("user") 120 | verbose_name_plural = _("users") 121 | abstract = True 122 | 123 | def get_full_name(self): 124 | """Return the email.""" 125 | return self.email 126 | 127 | def get_short_name(self): 128 | """Return the email.""" 129 | return self.email 130 | 131 | def email_user(self, subject, message, from_email=None, **kwargs): 132 | """Send an email to this User.""" 133 | send_mail(subject, message, from_email, [self.email], **kwargs) 134 | 135 | 136 | class EmailUser(AbstractEmailUser): 137 | """ 138 | Concrete class of AbstractEmailUser. 139 | 140 | Use this if you don't need to extend EmailUser. 141 | """ 142 | 143 | class Meta(AbstractEmailUser.Meta): 144 | swappable = "AUTH_USER_MODEL" 145 | -------------------------------------------------------------------------------- /src/custom_user/tests.py: -------------------------------------------------------------------------------- 1 | """EmailUser tests.""" 2 | import os 3 | import re 4 | from io import StringIO 5 | from unittest import mock 6 | 7 | import django 8 | from django.conf import settings 9 | from django.contrib.auth import get_user_model 10 | from django.contrib.auth.middleware import AuthenticationMiddleware 11 | from django.core import mail, management 12 | from django.forms.fields import Field 13 | from django.http import HttpRequest, HttpResponse 14 | from django.test import TestCase 15 | from django.test.utils import override_settings 16 | from django.urls import reverse 17 | from django.utils import timezone 18 | from django.utils.translation import gettext as _ 19 | 20 | from .forms import EmailUserChangeForm, EmailUserCreationForm 21 | 22 | 23 | class UserTest(TestCase): 24 | 25 | user_email = "newuser@localhost.local" 26 | user_password = "1234" 27 | 28 | def create_user(self): 29 | """ 30 | Create and return a new user with self.user_email as login and 31 | self.user_password as password. 32 | """ 33 | return get_user_model().objects.create_user(self.user_email, self.user_password) 34 | 35 | def test_user_creation(self): 36 | # Create a new user saving the time frame 37 | right_now = timezone.now().replace( 38 | microsecond=0 39 | ) # MySQL doesn't store microseconds 40 | with mock.patch.object(timezone, "now", return_value=right_now): 41 | self.create_user() 42 | 43 | # Check user exists and email is correct 44 | self.assertEqual(get_user_model().objects.all().count(), 1) 45 | self.assertEqual(get_user_model().objects.all()[0].email, self.user_email) 46 | 47 | # Check date_joined and last_login dates 48 | self.assertEqual(get_user_model().objects.all()[0].date_joined, right_now) 49 | self.assertEqual(get_user_model().objects.all()[0].last_login, right_now) 50 | 51 | # Check flags 52 | self.assertTrue(get_user_model().objects.all()[0].is_active) 53 | self.assertFalse(get_user_model().objects.all()[0].is_staff) 54 | self.assertFalse(get_user_model().objects.all()[0].is_superuser) 55 | 56 | def test_user_get_full_name(self): 57 | user = self.create_user() 58 | self.assertEqual(user.get_full_name(), self.user_email) 59 | 60 | def test_user_get_short_name(self): 61 | user = self.create_user() 62 | self.assertEqual(user.get_short_name(), self.user_email) 63 | 64 | def test_email_user(self): 65 | # Email definition 66 | subject = "Email Subject" 67 | message = "Email Message" 68 | from_email = "from@normal.com" 69 | 70 | user = self.create_user() 71 | 72 | # Test that no message exists 73 | self.assertEqual(len(mail.outbox), 0) 74 | 75 | # Send test email 76 | user.email_user(subject, message, from_email) 77 | 78 | # Test that one message has been sent 79 | self.assertEqual(len(mail.outbox), 1) 80 | 81 | # Verify that the email is correct 82 | self.assertEqual(mail.outbox[0].subject, subject) 83 | self.assertEqual(mail.outbox[0].body, message) 84 | self.assertEqual(mail.outbox[0].from_email, from_email) 85 | self.assertEqual(mail.outbox[0].to, [user.email]) 86 | 87 | def test_email_user_kwargs(self): 88 | # valid send_mail parameters 89 | kwargs = { 90 | "fail_silently": False, 91 | "auth_user": None, 92 | "auth_password": None, 93 | "connection": None, 94 | } 95 | user = get_user_model()(email="foo@bar.com") 96 | user.email_user( 97 | subject="Subject here", 98 | message="This is a message", 99 | from_email="from@domain.com", 100 | **kwargs 101 | ) 102 | # Test that one message has been sent. 103 | self.assertEqual(len(mail.outbox), 1) 104 | # Verify that test email contains the correct attributes: 105 | message = mail.outbox[0] 106 | self.assertEqual(message.subject, "Subject here") 107 | self.assertEqual(message.body, "This is a message") 108 | self.assertEqual(message.from_email, "from@domain.com") 109 | self.assertEqual(message.to, [user.email]) 110 | 111 | 112 | class UserManagerTest(TestCase): 113 | def test_create_user(self): 114 | email_lowercase = "normal@normal.com" 115 | user = get_user_model().objects.create_user(email_lowercase) 116 | self.assertEqual(user.email, email_lowercase) 117 | self.assertFalse(user.has_usable_password()) 118 | self.assertTrue(user.is_active) 119 | self.assertFalse(user.is_staff) 120 | self.assertFalse(user.is_superuser) 121 | 122 | def test_create_user_is_staff(self): 123 | email_lowercase = "normal@normal.com" 124 | user = get_user_model().objects.create_user(email_lowercase, is_staff=True) 125 | self.assertEqual(user.email, email_lowercase) 126 | self.assertFalse(user.has_usable_password()) 127 | self.assertTrue(user.is_active) 128 | self.assertTrue(user.is_staff) 129 | self.assertFalse(user.is_superuser) 130 | 131 | def test_create_superuser(self): 132 | email_lowercase = "normal@normal.com" 133 | password = "password1234$%&/" 134 | user = get_user_model().objects.create_superuser(email_lowercase, password) 135 | self.assertEqual(user.email, email_lowercase) 136 | self.assertTrue(user.check_password, password) 137 | self.assertTrue(user.is_active) 138 | self.assertTrue(user.is_staff) 139 | self.assertTrue(user.is_superuser) 140 | 141 | def test_create_super_user_raises_error_on_false_is_superuser(self): 142 | with self.assertRaisesMessage( 143 | ValueError, "Superuser must have is_superuser=True." 144 | ): 145 | get_user_model().objects.create_superuser( 146 | email="test@test.com", 147 | is_superuser=False, 148 | ) 149 | 150 | def test_create_superuser_raises_error_on_false_is_staff(self): 151 | with self.assertRaisesMessage(ValueError, "Superuser must have is_staff=True."): 152 | get_user_model().objects.create_superuser( 153 | email="test@test.com", 154 | is_staff=False, 155 | ) 156 | 157 | def test_user_creation_is_active(self): 158 | # Create deactivated user 159 | email_lowercase = "normal@normal.com" 160 | password = "password1234$%&/" 161 | user = get_user_model().objects.create_user( 162 | email_lowercase, password, is_active=False 163 | ) 164 | self.assertFalse(user.is_active) 165 | 166 | def test_user_creation_is_staff(self): 167 | # Create staff user 168 | email_lowercase = "normal@normal.com" 169 | password = "password1234$%&/" 170 | user = get_user_model().objects.create_user( 171 | email_lowercase, password, is_staff=True 172 | ) 173 | self.assertTrue(user.is_staff) 174 | 175 | def test_create_user_email_domain_normalize_rfc3696(self): 176 | # According to https://tools.ietf.org/html/rfc3696#section-3 177 | # the "@" symbol can be part of the local part of an email address 178 | returned = get_user_model().objects.normalize_email(r"Abc\@DEF@EXAMPLE.com") 179 | self.assertEqual(returned, r"Abc\@DEF@example.com") 180 | 181 | def test_create_user_email_domain_normalize(self): 182 | returned = get_user_model().objects.normalize_email("normal@DOMAIN.COM") 183 | self.assertEqual(returned, "normal@domain.com") 184 | 185 | def test_create_user_email_domain_normalize_with_whitespace(self): 186 | returned = get_user_model().objects.normalize_email( 187 | r"email\ with_whitespace@D.COM" 188 | ) 189 | self.assertEqual(returned, r"email\ with_whitespace@d.com") 190 | 191 | def test_empty_username(self): 192 | self.assertRaisesMessage( 193 | ValueError, 194 | "The given email must be set", 195 | get_user_model().objects.create_user, 196 | email="", 197 | ) 198 | 199 | 200 | class MigrationsTest(TestCase): 201 | def test_makemigrations_no_changes(self): 202 | with mock.patch("sys.stdout", new_callable=StringIO) as mocked: 203 | management.call_command("makemigrations", "custom_user", dry_run=True) 204 | self.assertEqual( 205 | mocked.getvalue(), "No changes detected in app 'custom_user'\n" 206 | ) 207 | 208 | 209 | class TestAuthenticationMiddleware(TestCase): 210 | @classmethod 211 | def setUpTestData(cls): 212 | cls.user_email = "test@example.com" 213 | cls.user_password = "test_password" 214 | cls.user = get_user_model().objects.create_user( 215 | cls.user_email, cls.user_password 216 | ) 217 | 218 | def setUp(self): 219 | self.middleware = AuthenticationMiddleware(lambda req: HttpResponse()) 220 | self.client.force_login(self.user) 221 | self.request = HttpRequest() 222 | self.request.session = self.client.session 223 | 224 | def test_changed_password_doesnt_invalidate_session(self): 225 | # Changing a user's password shouldn't invalidate the session if session 226 | # verification isn't activated. 227 | session_key = self.request.session.session_key 228 | self.middleware(self.request) 229 | self.assertIsNotNone(self.request.user) 230 | self.assertFalse(self.request.user.is_anonymous) 231 | 232 | # After password change, user should remain logged in. 233 | self.user.set_password("new_password") 234 | self.user.save() 235 | self.middleware(self.request) 236 | self.assertIsNotNone(self.request.user) 237 | self.assertFalse(self.request.user.is_anonymous) 238 | self.assertEqual(session_key, self.request.session.session_key) 239 | 240 | def test_no_password_change_doesnt_invalidate_session(self): 241 | self.request.session = self.client.session 242 | self.middleware(self.request) 243 | self.assertIsNotNone(self.request.user) 244 | self.assertFalse(self.request.user.is_anonymous) 245 | 246 | def test_changed_password_invalidates_session(self): 247 | # After password change, user should be anonymous 248 | self.user.set_password("new_password") 249 | self.user.save() 250 | self.middleware(self.request) 251 | self.assertIsNotNone(self.request.user) 252 | self.assertTrue(self.request.user.is_anonymous) 253 | # session should be flushed 254 | self.assertIsNone(self.request.session.session_key) 255 | 256 | 257 | class TestDataMixin: 258 | @classmethod 259 | def setUpTestData(cls): 260 | cls.email = "testclient@example.com" 261 | cls.password = "test123" 262 | get_user_model().objects.create_user(cls.email, cls.password) 263 | 264 | get_user_model().objects.create(email="empty_password@example.com", password="") 265 | get_user_model().objects.create( 266 | email="unmanageable_password@example.com", password="$" 267 | ) 268 | get_user_model().objects.create( 269 | email="unknown_password@example.com", password="foo$bar" 270 | ) 271 | 272 | 273 | class EmailUserCreationFormTest(TestDataMixin, TestCase): 274 | def test_user_already_exists(self): 275 | data = { 276 | "email": self.email, 277 | "password1": self.password, 278 | "password2": self.password, 279 | } 280 | form = EmailUserCreationForm(data) 281 | self.assertFalse(form.is_valid()) 282 | self.assertEqual( 283 | form["email"].errors, 284 | [str(form.error_messages["duplicate_email"])], 285 | ) 286 | 287 | def test_invalid_data(self): 288 | data = { 289 | "email": "testclient", 290 | "password1": self.password, 291 | "password2": self.password, 292 | } 293 | form = EmailUserCreationForm(data) 294 | self.assertFalse(form.is_valid()) 295 | validator = next( # pragma: no cover 296 | v 297 | for v in get_user_model()._meta.get_field("email").validators 298 | if v.code == "invalid" 299 | ) 300 | self.assertEqual(form["email"].errors, [str(validator.message)]) 301 | 302 | def test_password_verification(self): 303 | # The verification password is incorrect. 304 | data = { 305 | "email": self.email, 306 | "password1": "test123", 307 | "password2": "test", 308 | } 309 | form = EmailUserCreationForm(data) 310 | self.assertFalse(form.is_valid()) 311 | self.assertEqual( 312 | form["password2"].errors, [str(form.error_messages["password_mismatch"])] 313 | ) 314 | 315 | def test_both_passwords(self): 316 | # One (or both) passwords weren't given 317 | data = {"email": self.email} 318 | form = EmailUserCreationForm(data) 319 | required_error = [str(Field.default_error_messages["required"])] 320 | self.assertFalse(form.is_valid()) 321 | self.assertEqual(form["password1"].errors, required_error) 322 | self.assertEqual(form["password2"].errors, required_error) 323 | 324 | data["password2"] = self.password 325 | form = EmailUserCreationForm(data) 326 | self.assertFalse(form.is_valid()) 327 | self.assertEqual(form["password1"].errors, required_error) 328 | self.assertEqual(form["password2"].errors, []) 329 | 330 | @mock.patch("django.contrib.auth.password_validation.password_changed") 331 | def test_success(self, password_changed): 332 | # The success case. 333 | data = { 334 | "email": "jsmith@example.com", 335 | "password1": self.password, 336 | "password2": self.password, 337 | } 338 | form = EmailUserCreationForm(data) 339 | self.assertTrue(form.is_valid()) 340 | form.save(commit=False) 341 | self.assertEqual(password_changed.call_count, 0) 342 | u = form.save() 343 | self.assertEqual(password_changed.call_count, 1) 344 | self.assertEqual( 345 | repr(u), "<{}: jsmith@example.com>".format(get_user_model().__name__) 346 | ) 347 | 348 | @override_settings( 349 | AUTH_PASSWORD_VALIDATORS=[ 350 | { 351 | "NAME": ( 352 | "django.contrib.auth.password_validation." 353 | "UserAttributeSimilarityValidator" 354 | ) 355 | }, 356 | { 357 | "NAME": ( 358 | "django.contrib.auth.password_validation.MinimumLengthValidator" 359 | ), 360 | "OPTIONS": { 361 | "min_length": 12, 362 | }, 363 | }, 364 | ] 365 | ) 366 | def test_validates_password(self): 367 | data = { 368 | "email": "jsmith@example.com", 369 | "password1": "jsmith", 370 | "password2": "jsmith", 371 | } 372 | form = EmailUserCreationForm(data) 373 | self.assertFalse(form.is_valid()) 374 | self.assertEqual(len(form["password2"].errors), 2) 375 | self.assertIn( 376 | "The password is too similar to the email address.", 377 | form["password2"].errors, 378 | ) 379 | self.assertIn( 380 | "This password is too short. It must contain at least 12 characters.", 381 | form["password2"].errors, 382 | ) 383 | 384 | def test_password_whitespace_not_stripped(self): 385 | data = { 386 | "email": "jsmith@example.com", 387 | "password1": " testpassword ", 388 | "password2": " testpassword ", 389 | } 390 | form = EmailUserCreationForm(data) 391 | self.assertTrue(form.is_valid()) 392 | self.assertEqual(form.cleaned_data["password1"], data["password1"]) 393 | self.assertEqual(form.cleaned_data["password2"], data["password2"]) 394 | 395 | @override_settings( 396 | AUTH_PASSWORD_VALIDATORS=[ 397 | { 398 | "NAME": ( 399 | "django.contrib.auth.password_validation." 400 | "UserAttributeSimilarityValidator" 401 | ) 402 | }, 403 | ] 404 | ) 405 | def test_password_help_text(self): 406 | form = EmailUserCreationForm() 407 | self.assertEqual( 408 | form.fields["password1"].help_text, 409 | "
  • " 410 | "Your password can’t be too similar to your other personal information." 411 | "
", 412 | ) 413 | 414 | def test_html_autocomplete_attributes(self): 415 | form = EmailUserCreationForm() 416 | tests = ( 417 | ("password1", "new-password"), 418 | ("password2", "new-password"), 419 | ) 420 | for field_name, autocomplete in tests: 421 | with self.subTest(field_name=field_name, autocomplete=autocomplete): 422 | self.assertEqual( 423 | form.fields[field_name].widget.attrs["autocomplete"], autocomplete 424 | ) 425 | 426 | 427 | class EmailUserChangeFormTest(TestDataMixin, TestCase): 428 | def test_username_validity(self): 429 | user = get_user_model().objects.get(email=self.email) 430 | data = {"email": "not valid"} 431 | form = EmailUserChangeForm(data, instance=user) 432 | self.assertFalse(form.is_valid()) 433 | validator = next( # pragma: no cover 434 | v 435 | for v in get_user_model()._meta.get_field("email").validators 436 | if v.code == "invalid" 437 | ) 438 | self.assertEqual(form["email"].errors, [str(validator.message)]) 439 | 440 | def test_bug_14242(self): 441 | # A regression test, introduce by adding an optimization for the 442 | # EmailUserChangeForm. 443 | 444 | class MyUserForm(EmailUserChangeForm): 445 | def __init__(self, *args, **kwargs): 446 | super().__init__(*args, **kwargs) 447 | self.fields[ 448 | "groups" 449 | ].help_text = "These groups give users different permissions" 450 | 451 | class Meta(EmailUserChangeForm.Meta): 452 | fields = ("groups",) 453 | 454 | # Just check we can create it 455 | MyUserForm({}) 456 | 457 | def test_unusable_password(self): 458 | user = get_user_model().objects.get(email="empty_password@example.com") 459 | user.set_unusable_password() 460 | user.save() 461 | form = EmailUserChangeForm(instance=user) 462 | self.assertIn(_("No password set."), form.as_table()) 463 | 464 | def test_bug_17944_empty_password(self): 465 | user = get_user_model().objects.get(email="empty_password@example.com") 466 | form = EmailUserChangeForm(instance=user) 467 | self.assertIn(_("No password set."), form.as_table()) 468 | 469 | def test_bug_17944_unmanageable_password(self): 470 | user = get_user_model().objects.get(email="unmanageable_password@example.com") 471 | form = EmailUserChangeForm(instance=user) 472 | self.assertIn( 473 | _("Invalid password format or unknown hashing algorithm."), form.as_table() 474 | ) 475 | 476 | def test_bug_17944_unknown_password_algorithm(self): 477 | user = get_user_model().objects.get(email="unknown_password@example.com") 478 | form = EmailUserChangeForm(instance=user) 479 | self.assertIn( 480 | _("Invalid password format or unknown hashing algorithm."), form.as_table() 481 | ) 482 | 483 | def test_bug_19133(self): 484 | "The change form does not return the password value" 485 | # Use the form to construct the POST data 486 | user = get_user_model().objects.get(email=self.email) 487 | form_for_data = EmailUserChangeForm(instance=user) 488 | post_data = form_for_data.initial 489 | 490 | # The password field should be readonly, so anything 491 | # posted here should be ignored; the form will be 492 | # valid, and give back the 'initial' value for the 493 | # password field. 494 | post_data["password"] = "new password" 495 | form = EmailUserChangeForm(instance=user, data=post_data) 496 | 497 | self.assertTrue(form.is_valid()) 498 | # original hashed password contains $ 499 | self.assertIn("$", form.cleaned_data["password"]) 500 | 501 | def test_bug_19349_bound_password_field(self): 502 | user = get_user_model().objects.get(email=self.email) 503 | form = EmailUserChangeForm(data={}, instance=user) 504 | # When rendering the bound password field, 505 | # ReadOnlyPasswordHashWidget needs the initial 506 | # value to render correctly 507 | self.assertEqual(form.initial["password"], form["password"].value()) 508 | 509 | 510 | class EmailUserAdminTest(TestCase): 511 | def setUp(self): 512 | self.user_email = "test@example.com" 513 | self.user_password = "test_password" 514 | self.user = get_user_model().objects.create_superuser( 515 | self.user_email, self.user_password 516 | ) 517 | 518 | if settings.AUTH_USER_MODEL == "custom_user.EmailUser": 519 | self.app_name = "custom_user" 520 | self.model_name = "emailuser" 521 | self.model_verbose_name = "user" 522 | self.model_verbose_name_plural = "Users" 523 | self.app_verbose_name = "Custom User" 524 | if settings.AUTH_USER_MODEL == "test_custom_user_subclass.MyCustomEmailUser": 525 | self.app_name = "test_custom_user_subclass" 526 | self.model_name = "mycustomemailuser" 527 | self.model_verbose_name = "MyCustomEmailUserVerboseName" 528 | self.model_verbose_name_plural = "MyCustomEmailUserVerboseNamePlural" 529 | if django.VERSION[:2] < (4, 1): 530 | self.app_verbose_name = "Test Custom User Subclass" # pragma: no cover 531 | else: 532 | self.app_verbose_name = "Test_Custom_User_Subclass" # pragma: no cover 533 | 534 | def test_url(self): 535 | self.assertTrue( 536 | self.client.login( 537 | username=self.user_email, 538 | password=self.user_password, 539 | ) 540 | ) 541 | response = self.client.get(reverse("admin:app_list", args=(self.app_name,))) 542 | self.assertEqual(response.status_code, 200) 543 | 544 | def test_app_name(self): 545 | self.assertTrue( 546 | self.client.login( 547 | username=self.user_email, 548 | password=self.user_password, 549 | ) 550 | ) 551 | 552 | response = self.client.get(reverse("admin:app_list", args=(self.app_name,))) 553 | self.assertEqual(response.context["app_list"][0]["name"], self.app_verbose_name) 554 | 555 | def test_model_name(self): 556 | self.assertTrue( 557 | self.client.login( 558 | username=self.user_email, 559 | password=self.user_password, 560 | ) 561 | ) 562 | 563 | response = self.client.get( 564 | reverse("admin:%s_%s_changelist" % (self.app_name, self.model_name)) 565 | ) 566 | self.assertEqual( 567 | str(response.context["title"]), 568 | "Select %s to change" % self.model_verbose_name, 569 | ) 570 | 571 | def test_model_name_plural(self): 572 | self.assertTrue( 573 | self.client.login( 574 | username=self.user_email, 575 | password=self.user_password, 576 | ) 577 | ) 578 | 579 | response = self.client.get(reverse("admin:app_list", args=(self.app_name,))) 580 | self.assertEqual( 581 | str(response.context["app_list"][0]["models"][0]["name"]), 582 | self.model_verbose_name_plural, 583 | ) 584 | 585 | def test_user_change_password(self): 586 | self.assertTrue( 587 | self.client.login( 588 | username=self.user_email, 589 | password=self.user_password, 590 | ) 591 | ) 592 | 593 | user_change_url = reverse( 594 | "admin:%s_%s_change" % (self.app_name, self.model_name), 595 | args=(self.user.pk,), 596 | ) 597 | password_change_url = reverse( 598 | "admin:auth_user_password_change", args=(self.user.pk,) 599 | ) 600 | 601 | response = self.client.get(user_change_url) 602 | # Test the link inside password field help_text. 603 | rel_link = re.search( 604 | r'you can change the password using this form', 605 | str(response.content), 606 | ).groups()[0] 607 | self.assertEqual( 608 | os.path.normpath(user_change_url + rel_link), 609 | os.path.normpath(password_change_url), 610 | ) 611 | 612 | # Test url is correct. 613 | self.assertEqual( 614 | self.client.get(password_change_url).status_code, 615 | 200, 616 | ) 617 | -------------------------------------------------------------------------------- /test_custom_user_subclass/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "test_custom_user_subclass.apps.CustomUserSubclassConfig" 2 | -------------------------------------------------------------------------------- /test_custom_user_subclass/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from custom_user.admin import EmailUserAdmin 4 | 5 | from .models import MyCustomEmailUser 6 | 7 | 8 | class MyCustomEmailUserAdmin(EmailUserAdmin): 9 | pass 10 | 11 | 12 | # Register your models here. 13 | admin.site.register(MyCustomEmailUser, MyCustomEmailUserAdmin) 14 | -------------------------------------------------------------------------------- /test_custom_user_subclass/apps.py: -------------------------------------------------------------------------------- 1 | from custom_user.apps import CustomUserConfig 2 | 3 | 4 | class CustomUserSubclassConfig(CustomUserConfig): 5 | name = "test_custom_user_subclass" 6 | verbose_name = "Test Custom User Subclass" 7 | -------------------------------------------------------------------------------- /test_custom_user_subclass/migrations/0001_initial_django17.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import django.utils.timezone 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("auth", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="MyCustomEmailUser", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | verbose_name="ID", 22 | serialize=False, 23 | auto_created=True, 24 | primary_key=True, 25 | ), 26 | ), 27 | ("password", models.CharField(max_length=128, verbose_name="password")), 28 | ( 29 | "last_login", 30 | models.DateTimeField( 31 | default=django.utils.timezone.now, verbose_name="last login" 32 | ), 33 | ), 34 | ( 35 | "is_superuser", 36 | models.BooleanField( 37 | default=False, 38 | help_text="Designates that this user has all permissions without explicitly assigning them.", 39 | verbose_name="superuser status", 40 | ), 41 | ), 42 | ( 43 | "email", 44 | models.EmailField( 45 | verbose_name="email address", 46 | db_index=True, 47 | max_length=255, 48 | unique=True, 49 | ), 50 | ), 51 | ( 52 | "is_staff", 53 | models.BooleanField( 54 | default=False, 55 | help_text="Designates whether the user can log into this admin site.", 56 | verbose_name="staff status", 57 | ), 58 | ), 59 | ( 60 | "is_active", 61 | models.BooleanField( 62 | default=True, 63 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 64 | verbose_name="active", 65 | ), 66 | ), 67 | ( 68 | "date_joined", 69 | models.DateTimeField( 70 | default=django.utils.timezone.now, verbose_name="date joined" 71 | ), 72 | ), 73 | ( 74 | "groups", 75 | models.ManyToManyField( 76 | to="auth.Group", 77 | verbose_name="groups", 78 | blank=True, 79 | help_text="The groups this user belongs to. A user will get all permissions granted to each of his/her group.", 80 | related_name="user_set", 81 | related_query_name="user", 82 | ), 83 | ), 84 | ( 85 | "user_permissions", 86 | models.ManyToManyField( 87 | to="auth.Permission", 88 | verbose_name="user permissions", 89 | blank=True, 90 | help_text="Specific permissions for this user.", 91 | related_name="user_set", 92 | related_query_name="user", 93 | ), 94 | ), 95 | ], 96 | options={ 97 | "verbose_name": "MyCustomEmailUserVerboseName", 98 | "verbose_name_plural": "MyCustomEmailUserVerboseNamePlural", 99 | "abstract": False, 100 | }, 101 | bases=(models.Model,), 102 | ), 103 | ] 104 | -------------------------------------------------------------------------------- /test_custom_user_subclass/migrations/0002_initial_django18.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("test_custom_user_subclass", "0001_initial_django17"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="mycustomemailuser", 16 | name="groups", 17 | field=models.ManyToManyField( 18 | to="auth.Group", 19 | verbose_name="groups", 20 | blank=True, 21 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 22 | related_name="user_set", 23 | related_query_name="user", 24 | ), 25 | ), 26 | migrations.AlterField( 27 | model_name="mycustomemailuser", 28 | name="last_login", 29 | field=models.DateTimeField( 30 | null=True, verbose_name="last login", blank=True 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /test_custom_user_subclass/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcugat/django-custom-user/309d36c681d622bcdbc4648368cb13faab9cce95/test_custom_user_subclass/migrations/__init__.py -------------------------------------------------------------------------------- /test_custom_user_subclass/models.py: -------------------------------------------------------------------------------- 1 | from custom_user.models import AbstractEmailUser 2 | 3 | 4 | class MyCustomEmailUser(AbstractEmailUser): 5 | class Meta(AbstractEmailUser.Meta): 6 | verbose_name = "MyCustomEmailUserVerboseName" 7 | verbose_name_plural = "MyCustomEmailUserVerboseNamePlural" 8 | -------------------------------------------------------------------------------- /test_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcugat/django-custom-user/309d36c681d622bcdbc4648368cb13faab9cce95/test_settings/__init__.py -------------------------------------------------------------------------------- /test_settings/settings.py: -------------------------------------------------------------------------------- 1 | import environ 2 | 3 | env = environ.Env() 4 | 5 | DEBUG = True 6 | USE_TZ = True 7 | DATABASES = { 8 | "default": env.db(), 9 | } 10 | INSTALLED_APPS = [ 11 | "django.contrib.admin", 12 | "django.contrib.auth", 13 | "django.contrib.contenttypes", 14 | "django.contrib.messages", 15 | "django.contrib.sessions", 16 | "custom_user", 17 | ] 18 | MIDDLEWARE = [ 19 | "django.middleware.security.SecurityMiddleware", 20 | "django.contrib.sessions.middleware.SessionMiddleware", 21 | "django.middleware.common.CommonMiddleware", 22 | "django.middleware.csrf.CsrfViewMiddleware", 23 | "django.contrib.auth.middleware.AuthenticationMiddleware", 24 | "django.contrib.messages.middleware.MessageMiddleware", 25 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 26 | ] 27 | TEMPLATES = [ 28 | { 29 | "BACKEND": "django.template.backends.django.DjangoTemplates", 30 | "DIRS": [], 31 | "APP_DIRS": True, 32 | "OPTIONS": { 33 | "context_processors": [ 34 | "django.template.context_processors.debug", 35 | "django.template.context_processors.request", 36 | "django.contrib.auth.context_processors.auth", 37 | "django.contrib.messages.context_processors.messages", 38 | ], 39 | }, 40 | }, 41 | ] 42 | SECRET_KEY = "not_random" 43 | ROOT_URLCONF = "test_settings.urls" 44 | AUTH_USER_MODEL = "custom_user.EmailUser" 45 | -------------------------------------------------------------------------------- /test_settings/settings_subclass.py: -------------------------------------------------------------------------------- 1 | from .settings import * # NOQA: F403 2 | 3 | INSTALLED_APPS += [ # NOQA: F405 4 | "test_custom_user_subclass", 5 | ] 6 | AUTH_USER_MODEL = "test_custom_user_subclass.MyCustomEmailUser" 7 | -------------------------------------------------------------------------------- /test_settings/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path("admin/", admin.site.urls), 6 | ] 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | envlist = py37-django32-{mysql,postgres,sqlite}, {py38,py39,py310}-django{32,40,41,master}-{mysql,postgres,sqlite}, py311-django41-{mysql,postgres,sqlite} 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39 10 | 3.10: py310 11 | 3.11: py311, lint 12 | 13 | [testenv] 14 | setenv = 15 | mysql: DATABASE_URL = mysql://root@127.0.0.1/mysql 16 | postgres: DATABASE_URL = postgres://postgres:postgres@localhost/postgres 17 | sqlite: DATABASE_URL = sqlite://:memory: 18 | commands = 19 | coverage run -m django test --noinput --settings=test_settings.settings {posargs:custom_user} 20 | coverage run -m django test --noinput --settings=test_settings.settings_subclass {posargs:custom_user} 21 | coverage combine 22 | coverage report 23 | install_dev_deps = true 24 | deps = 25 | django32: Django>=3.2,<3.3 26 | django40: Django>=4.0,<4.1 27 | django41: Django>=4.1,<4.2 28 | djangomaster: https://github.com/django/django/archive/master.tar.gz#egg=Django 29 | extras = 30 | mysql: mysql 31 | postgres: postgres 32 | 33 | [testenv:{py38,py39,py310,py311}-djangomaster-{mysql,postgres,sqlite}] 34 | ignore_outcome = true 35 | 36 | [testenv:lint] 37 | basepython = python3.11 38 | skip_install = true 39 | allowlist_externals = make 40 | commands = make lint 41 | --------------------------------------------------------------------------------