pax_global_header00006660000000000000000000000064146721217600014520gustar00rootroot0000000000000052 comment=22be88703beecf0bf9af2d1515ee940caffb4ce3 django-dbconn-retry-0.1.8/000077500000000000000000000000001467212176000153745ustar00rootroot00000000000000django-dbconn-retry-0.1.8/.github/000077500000000000000000000000001467212176000167345ustar00rootroot00000000000000django-dbconn-retry-0.1.8/.github/dependabot.yml000066400000000000000000000006561467212176000215730ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" django-dbconn-retry-0.1.8/.github/workflows/000077500000000000000000000000001467212176000207715ustar00rootroot00000000000000django-dbconn-retry-0.1.8/.github/workflows/django.yml000066400000000000000000000030631467212176000227600ustar00rootroot00000000000000name: Django CI on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies run: | python -m pip install --upgrade pip wheel pip install -r requirements-test.txt -e . - name: Start PostgreSQL run: | sudo systemctl start postgresql.service pg_isready - name: Set up PostgreSQL run: | sudo -u postgres psql --command="CREATE USER dbconntest WITH CREATEDB LOGIN PASSWORD 'dbconntest'" --command="\du" sudo -u postgres createdb -O dbconntest -p 5432 dbconntest - name: Run Tests run: | coverage run --source=django_dbconn_retry test_project/manage.py test coverage report -m coverage lcov - name: Run Coveralls uses: coverallsapp/github-action@main with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coverage.lcov parallel: true - name: Run Flake8 run: flake8 --max-line-length=120 django_dbconn_retry setup.py finish: needs: build if: ${{ always() }} runs-on: ubuntu-latest steps: - name: Close parallel build uses: coverallsapp/github-action@master with: parallel-finished: true django-dbconn-retry-0.1.8/.gitignore000066400000000000000000000000731467212176000173640ustar00rootroot00000000000000.coverage *.iml *.pyc *.pyd build/ dist/ test/ *.egg-info/ django-dbconn-retry-0.1.8/LICENSE000066400000000000000000000027321467212176000164050ustar00rootroot00000000000000Copyright (c) 2018, Jonas Maurus All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-dbconn-retry-0.1.8/MANIFEST.in000066400000000000000000000001251467212176000171300ustar00rootroot00000000000000recursive-include django_dbconn_retry/tests *.py recursive-include test_project *.py django-dbconn-retry-0.1.8/README.rst000066400000000000000000000123111467212176000170610ustar00rootroot00000000000000Django Database Connection Autoreconnect ======================================== .. image:: https://coveralls.io/repos/github/jdelic/django-dbconn-retry/badge.svg?branch=master :target: https://coveralls.io/github/jdelic/django-dbconn-retry?branch=master .. image:: https://github.com/jdelic/django-dbconn-retry/actions/workflows/django.yml/badge.svg :target: https://github.com/jdelic/django-dbconn-retry/actions This library monkeypatches ``django.db.backends.base.BaseDatabaseWrapper`` so that when a database operation fails because the underlying TCP connection was already closed, it first tried to reconnect, instead of immediately raising an ``OperationException``. Why is this useful? ------------------- I use `HAProxy`_ as a load-balancer in front of my PostgreSQL databases all the time, sometimes in addition to ``pgbouncer``. Even though you can mostly prevent surprises by enabling TCP keep-alive packets through `tcpka`_, `clitcpka`_ and `srvtcpka`_, I still encounter situations where the underlying TCP connection has been closed through the load-balancer. Most often this results in .. code-block:: django.db.utils.OperationalError: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. This library patches Django such that it try to reconnect once before failing. Another application of this is when using `Hashicorp Vault`_, where credentials for a database connection can expire at any time and then need to be refreshed from Vault. How to install? --------------- Just pull the library in using ``pip install django-dbconn-retry``. Then add ``django_dbconn_retry`` to ``INSTALLED_APPS`` in your ``settings.py``. Signals ------- The library provides an interface for other code to plug into the process to, for example, allow `12factor-vault`_ to refresh the database credentials before the code tries to reestablish the database connection. These are implemented using `Django Signals`_. =========================== ================================================== Signal Description =========================== ================================================== ``pre_reconnect`` Installs a hook of the type ``Callable[[type, BaseDatabaseWrapper], None]`` that will be called before the library tries to reestablish a connection. 12factor-vault uses this to refresh the database credentials from Vault. ``post_reconnect`` Installs a hook of the type ``Callable[[type, BaseDatabaseWrapper], None]`` that will be called after the library tried to reestablish the connection. Success or failure has not been tested at this point. So the connection may be in any state. =========================== ================================================== Both signals send a parameter ``dbwrapper`` which points to the current instance of ``django.db.backends.base.BaseDatabaseWrapper`` which allows the signal receiver to act on the database connection. License ======= Copyright (c) 2018, Jonas Maurus All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .. _12factor-vault: https://github.com/jdelic/12factor-vault/ .. _Django Signals: https://docs.djangoproject.com/en/dev/topics/signals/ .. _HAProxy: http://www.haproxy.org/ .. _tcpka: https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#option%20tcpka .. _clitcpka: https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4-option%20clitcpka .. _srvtcpka: https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#option%20srvtcpka .. _Hashicorp Vault: https://vaultproject.io/ django-dbconn-retry-0.1.8/django_dbconn_retry/000077500000000000000000000000001467212176000214065ustar00rootroot00000000000000django-dbconn-retry-0.1.8/django_dbconn_retry/__init__.py000066400000000000000000000005031467212176000235150ustar00rootroot00000000000000# -* encoding: utf-8 *- import django from django_dbconn_retry.apps import pre_reconnect, post_reconnect, monkeypatch_django, DjangoIntegration __all__ = [pre_reconnect, post_reconnect, monkeypatch_django, DjangoIntegration] if django.VERSION < (3, 2): default_app_config = 'django_dbconn_retry.DjangoIntegration' django-dbconn-retry-0.1.8/django_dbconn_retry/apps.py000066400000000000000000000064061467212176000227310ustar00rootroot00000000000000import logging from django.apps.config import AppConfig from django.db.backends.base import base as django_db_base from django.dispatch import Signal from typing import Union, Tuple, Callable, List # noqa. flake8 #118 _log = logging.getLogger(__name__) pre_reconnect = Signal() post_reconnect = Signal() _operror_types = () # type: Union[Tuple[type], Tuple] database_modules = [ ("django.db.utils", "OperationalError"), ("psycopg2", "OperationalError"), ("psycopg", "OperationalError"), ("sqlite3", "OperationalError"), ("MySQLdb", "OperationalError"), ("pyodbc", "InterfaceError"), ] for module_name, error_name in database_modules: try: module = __import__(module_name, fromlist=[error_name]) error_type = getattr(module, error_name) _operror_types += (error_type,) except ImportError: pass def monkeypatch_django() -> None: def ensure_connection_with_retries(self: django_db_base.BaseDatabaseWrapper) -> None: if self.connection is not None and hasattr(self.connection, 'closed') and self.connection.closed: _log.debug("failed connection detected") self.connection = None if self.connection is None and not hasattr(self, '_in_connecting'): with self.wrap_database_errors: try: self._in_connecting = True self.connect() except Exception as e: if isinstance(e, _operror_types): if hasattr(self, "_connection_retries") and self._connection_retries >= 1: _log.error("Reconnecting to the database didn't help %s", str(e)) del self._in_connecting post_reconnect.send(self.__class__, dbwrapper=self) raise else: _log.info("Database connection failed. Refreshing...") # mark the retry self._connection_retries = 1 # ensure that we retry the connection. Sometimes .closed isn't set correctly. self.connection = None del self._in_connecting # give libraries like 12factor-vault the chance to update the credentials pre_reconnect.send(self.__class__, dbwrapper=self) self.ensure_connection() post_reconnect.send(self.__class__, dbwrapper=self) else: _log.debug("Database connection failed, but not due to a known error for dbconn_retry %s", str(e)) del self._in_connecting raise else: # connection successful, reset the flag self._connection_retries = 0 del self._in_connecting _log.debug("django_dbconn_retry: monkeypatching BaseDatabaseWrapper") django_db_base.BaseDatabaseWrapper.ensure_connection = ensure_connection_with_retries class DjangoIntegration(AppConfig): name = "django_dbconn_retry" def ready(self) -> None: monkeypatch_django() django-dbconn-retry-0.1.8/django_dbconn_retry/tests/000077500000000000000000000000001467212176000225505ustar00rootroot00000000000000django-dbconn-retry-0.1.8/django_dbconn_retry/tests/__init__.py000066400000000000000000000066571467212176000246770ustar00rootroot00000000000000# -* encoding: utf-8 *- import sys import logging from unittest.mock import Mock from typing import Any import django_dbconn_retry as ddr from django.db.backends.base.base import BaseDatabaseWrapper from django.db import connection, OperationalError, transaction from django.test import TestCase logging.basicConfig(stream=sys.stderr) logging.getLogger("django_dbconn_retry").setLevel(logging.DEBUG) _log = logging.getLogger(__name__) class FullErrorTests(TestCase): """ This is SUPERHACKY. I couldn't find a better way to ensure that the database connections reliably fail. If I had been able to think of a better way, I'd have used it. """ def test_getting_root(self) -> None: self.client.get('/') def setUp(self) -> None: _log.debug("[FullErrorTests] patching for setup") self.s_connect = BaseDatabaseWrapper.connect BaseDatabaseWrapper.connect = Mock(side_effect=OperationalError('fail testing')) BaseDatabaseWrapper.connection = property(lambda x: None, lambda x, y: None) # type: ignore def tearDown(self) -> None: _log.debug("[FullErrorTests] restoring") BaseDatabaseWrapper.connect = self.s_connect del BaseDatabaseWrapper.connection def test_prehook(self) -> None: cb = Mock(name='pre_reconnect_hook') ddr.pre_reconnect.connect(cb) self.assertRaises(OperationalError, connection.ensure_connection) self.assertTrue(cb.called) del connection._connection_retries def test_posthook(self) -> None: cb = Mock(name='post_reconnect_hook') ddr.post_reconnect.connect(cb) self.assertRaises(OperationalError, connection.ensure_connection) self.assertTrue(cb.called) del connection._connection_retries def fix_connection(sender: type, *, dbwrapper: BaseDatabaseWrapper, **kwargs: Any) -> None: dbwrapper.connect = dbwrapper.s_connect class ReconnectTests(TestCase): @classmethod def tearDownClass(cls) -> None: return def test_ensure_closed(self) -> None: from django.db import connection connection.close() self.assertFalse(connection.is_usable()) # should be true after setUp def test_prehook(self) -> None: cb = Mock(name='pre_reconnect_hook') ddr.pre_reconnect.connect(fix_connection) ddr.pre_reconnect.connect(cb) from django.db import connection connection.close() connection.s_connect = connection.connect connection.connect = Mock(side_effect=OperationalError('reconnect testing')) connection.ensure_connection() ReconnectTests.cls_atomics['default'] = transaction.atomic(using='default') ReconnectTests.cls_atomics['default'].__enter__() self.assertTrue(cb.called) self.assertTrue(connection.is_usable()) def test_posthook(self) -> None: cb = Mock(name='post_reconnect_hook') ddr.pre_reconnect.connect(fix_connection) ddr.post_reconnect.connect(cb) from django.db import connection connection.close() connection.s_connect = connection.connect connection.connect = Mock(side_effect=OperationalError('reconnect testing')) connection.ensure_connection() ReconnectTests.cls_atomics['default'] = transaction.atomic(using='default') ReconnectTests.cls_atomics['default'].__enter__() self.assertTrue(cb.called) self.assertTrue(connection.is_usable()) django-dbconn-retry-0.1.8/requirements-test.txt000066400000000000000000000002101467212176000216260ustar00rootroot00000000000000coverage==7.6.1 flake8==7.1.0 doc8==1.1.1 mypy==1.11.2 psycopg2-binary==2.9.9 Django==5.1.1 django-stubs==5.0.4 django-stubs-ext==5.0.4 django-dbconn-retry-0.1.8/setup.py000066400000000000000000000021411467212176000171040ustar00rootroot00000000000000#!/usr/bin/env python # -* encoding: utf-8 *- import os from setuptools import setup HERE = os.path.dirname(__file__) try: long_description = open(os.path.join(HERE, 'README.rst')).read() except IOError: long_description = None setup( name="django-dbconn-retry", version="0.1.8", packages=[ 'django_dbconn_retry', 'django_dbconn_retry.tests', ], package_dir={ '': '.', }, classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: BSD License", "Operating System :: POSIX", ], url="https://github.com/jdelic/django-dbconn-retry/", author="Jonas Maurus (@jdelic)", author_email="jonas-dbconn-retry@gopythongo.com", maintainer="GoPythonGo.com", maintainer_email="info@gopythongo.com", description="Patch Django to retry a database connection first before failing.", long_description=long_description, install_requires=[ ], ) django-dbconn-retry-0.1.8/test_project/000077500000000000000000000000001467212176000201015ustar00rootroot00000000000000django-dbconn-retry-0.1.8/test_project/manage.py000066400000000000000000000012341467212176000217030ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main() django-dbconn-retry-0.1.8/test_project/test_project/000077500000000000000000000000001467212176000226065ustar00rootroot00000000000000django-dbconn-retry-0.1.8/test_project/test_project/__init__.py000066400000000000000000000000001467212176000247050ustar00rootroot00000000000000django-dbconn-retry-0.1.8/test_project/test_project/asgi.py000066400000000000000000000006211467212176000241020ustar00rootroot00000000000000""" ASGI config for test_project project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') application = get_asgi_application() django-dbconn-retry-0.1.8/test_project/test_project/settings.py000066400000000000000000000100131467212176000250130ustar00rootroot00000000000000""" Django settings for test_project project. Generated by 'django-admin startproject' using Django 4.0.2. For more information on this file, see https://docs.djangoproject.com/en/4.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.0/ref/settings/ """ from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'django-insecure-=#1bt1w$67$t9d80$z9v8yf*4=#o1a85s74#vmux46&9rz3)wv' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django_dbconn_retry', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'test_project.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'test_project.wsgi.application' # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'dbconntest', 'USER': 'dbconntest', 'HOST': 'localhost', 'PASSWORD': 'dbconntest', } } # Password validation # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ STATIC_URL = 'static/' # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "simple": { "format": "%(asctime)s %(levelname)s %(message)s", }, }, "handlers": { "application_logs": { "level": "DEBUG", "formatter": "simple", "class": 'logging.StreamHandler', "stream": 'ext://sys.stderr', }, "server_logs": { "level": "INFO", "formatter": "simple", "class": 'logging.StreamHandler', "stream": 'ext://sys.stdout', }, }, "loggers": { "": { "handlers": ["application_logs"], "level": "DEBUG", "propagate": True, }, }, } django-dbconn-retry-0.1.8/test_project/test_project/urls.py000066400000000000000000000015741467212176000241540ustar00rootroot00000000000000"""test_project URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/4.0/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.urls import path from django.http import HttpRequest, HttpResponse def testview(request: HttpRequest) -> HttpResponse: return HttpResponse("nothing here", content_type='text/plain', status=200) urlpatterns = [ path('', testview), ] django-dbconn-retry-0.1.8/test_project/test_project/wsgi.py000066400000000000000000000006211467212176000241300ustar00rootroot00000000000000""" WSGI config for test_project project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') application = get_wsgi_application() django-dbconn-retry-0.1.8/tox.ini000066400000000000000000000010671467212176000167130ustar00rootroot00000000000000[tox] envlist = flake8, doc8, mypy, django, [testenv] envdir = {toxinidir}/.toxenv usedevelop = true deps = -rrequirements-test.txt django: Django setenv = MYPYPATH=. passenv = COVERALLS_REPO_TOKEN commands = django: coverage run --source=django_dbconn_retry test_project/manage.py test django: coverage report -m django: coveralls flake8: flake8 --max-line-length=120 django_dbconn_retry setup.py doc8: doc8 README.rst mypy: mypy --ignore-missing-imports --follow-imports=skip --disallow-untyped-calls --disallow-untyped-defs -p django_dbconn_retry