├── .gitignore ├── LICENSE ├── README.md ├── demo └── blog │ ├── blog │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── views.py │ └── wsgi.py │ ├── manage.py │ └── requirements.txt ├── djdbpool ├── __init__.py └── db │ ├── __init__.py │ ├── backends │ ├── __init__.py │ ├── mysql │ │ ├── __init__.py │ │ └── base.py │ ├── postgresql │ │ ├── __init__.py │ │ └── base.py │ └── postgresql_psycopg2 │ │ ├── __init__.py │ │ └── base.py │ └── pool.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | .idea/ 128 | *.pyc 129 | .DS_Store/ 130 | *.DS_Store/ 131 | *.DS_Store 132 | .DS_Store 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 彬文 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 通过DBUtils库实现Django数据库连接池,目前支持mysql、postgresql 数据库. 2 | 3 | 下面的配置参考: 4 | ----------- 5 | ```python 6 | DATABASES = { 7 | 'default':{ 8 | 'ENGINE': 'djdbpool.db.backends.mysql', 9 | "HOST": "127.0.0.1", 10 | "NAME": "test_db", 11 | "PASSWORD": "", 12 | "USER": "django", 13 | "PORT": 3306, 14 | 'OPTIONS': {'charset': 'utf8mb4'}, 15 | 'POOL': { # 更多的配置请参考DBUtils的配置 16 | 'minsize': 5, # 初始化时,连接池中至少创建的空闲的链接,0表示不创建,不填默认为5 17 | 'maxsize': 10, # 连接池中最多闲置的链接,0不限制,不填默认为0 18 | 'maxconnections': 60, # 连接池允许的最大连接数,0表示不限制连接数, 默认为0 19 | 'blocking': True, # 连接池中如果没有可用连接后,是否阻塞等待。True:等待;False:不等待然后报错, 默认False 20 | 'ping':0 # 检查服务是否可用 1检查0不检查 21 | } 22 | } 23 | } 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /demo/blog/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binwen/django-db-pool/3c0255464be675cdd76ad1fa815437df3772fe0d/demo/blog/blog/__init__.py -------------------------------------------------------------------------------- /demo/blog/blog/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for blog project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.9. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = ')d*m8zz-1&un%@wxrpjf6d9(y%3zz10xt05ff-3-z2^ef98l)q' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE_CLASSES = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'blog.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'blog.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'djdbpool.db.backends.mysql', 80 | "HOST": "192.168.9.104", 81 | "NAME": "sql", 82 | "PASSWORD": "", 83 | "USER": "root", 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 103 | }, 104 | ] 105 | 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 109 | 110 | LANGUAGE_CODE = 'en-us' 111 | 112 | TIME_ZONE = 'UTC' 113 | 114 | USE_I18N = True 115 | 116 | USE_L10N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 123 | 124 | STATIC_URL = '/static/' 125 | 126 | LOGGING = { 127 | 'version': 1, 128 | 'disable_existing_loggers': False, 129 | 'handlers': { 130 | 'console':{ 131 | 'level':'DEBUG', 132 | 'class':'logging.StreamHandler', 133 | }, 134 | }, 135 | 'loggers': { 136 | 'django.db.backends': { 137 | 'handlers': ['console'], 138 | 'propagate': True, 139 | 'level':'DEBUG', 140 | }, 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /demo/blog/blog/urls.py: -------------------------------------------------------------------------------- 1 | """blog URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from . import views 19 | urlpatterns = [ 20 | url(r'^admin/', admin.site.urls), 21 | url(r'^user/create/', views.create_user), 22 | url(r'^user/info/', views.get_user), 23 | ] 24 | -------------------------------------------------------------------------------- /demo/blog/blog/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | from django.contrib.auth.hashers import make_password 4 | from django.contrib.auth.models import User 5 | from django.http import HttpResponse 6 | 7 | 8 | def create_user(request, *args, **kwargs): 9 | for i in range(10000): 10 | User.objects.create( 11 | username="test{}".format(i), 12 | password=make_password('1qaz1qaz'), 13 | is_superuser=0, 14 | email="test{}@xuetangx.com".format(i), 15 | is_staff=1, 16 | is_active=1 17 | ) 18 | 19 | return HttpResponse(json.dumps({"status": "ok"}), content_type="text/json") 20 | 21 | 22 | def get_user(request, *args, **kwargs): 23 | user = User.objects.filter(username="test{}".format(random.randint(0, 100))).first() 24 | 25 | return HttpResponse(json.dumps({"status": "ok", "email": user.email}), content_type="text/json") 26 | -------------------------------------------------------------------------------- /demo/blog/blog/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for blog project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /demo/blog/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /demo/blog/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.9 2 | django-database-pool==0.0.2 3 | -------------------------------------------------------------------------------- /djdbpool/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binwen/django-db-pool/3c0255464be675cdd76ad1fa815437df3772fe0d/djdbpool/__init__.py -------------------------------------------------------------------------------- /djdbpool/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binwen/django-db-pool/3c0255464be675cdd76ad1fa815437df3772fe0d/djdbpool/db/__init__.py -------------------------------------------------------------------------------- /djdbpool/db/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binwen/django-db-pool/3c0255464be675cdd76ad1fa815437df3772fe0d/djdbpool/db/backends/__init__.py -------------------------------------------------------------------------------- /djdbpool/db/backends/mysql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binwen/django-db-pool/3c0255464be675cdd76ad1fa815437df3772fe0d/djdbpool/db/backends/mysql/__init__.py -------------------------------------------------------------------------------- /djdbpool/db/backends/mysql/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.db.backends.mysql.base import DatabaseWrapper as MysqlDatabaseWrapper 5 | from django.utils.safestring import SafeText,SafeData 6 | 7 | class SafeBytes(bytes, SafeData): 8 | """ 9 | A bytes subclass that has been specifically marked as "safe" (requires no 10 | further escaping) for HTML output purposes. 11 | 12 | Kept in Django 2.0 for usage by apps supporting Python 2. Shouldn't be used 13 | in Django anymore. 14 | """ 15 | 16 | def __add__(self, rhs): 17 | """ 18 | Concatenating a safe byte string with another safe byte string or safe 19 | string is safe. Otherwise, the result is no longer safe. 20 | """ 21 | t = super().__add__(rhs) 22 | if isinstance(rhs, SafeText): 23 | return SafeText(t) 24 | elif isinstance(rhs, SafeBytes): 25 | return SafeBytes(t) 26 | return t 27 | 28 | 29 | try: 30 | import MySQLdb as Database 31 | except ImportError as err: 32 | try: 33 | import pymysql as Database 34 | except ImportError: 35 | raise ImproperlyConfigured("Error loading MySQLdb or pymsql module.\n Did you install mysqlclient or pymysql") 36 | 37 | from djdbpool.db.pool import DBPoolWrapper 38 | 39 | 40 | Database = DBPoolWrapper(Database) 41 | 42 | 43 | class DatabaseWrapper(MysqlDatabaseWrapper): 44 | 45 | Database = Database 46 | 47 | def get_connection_params(self): 48 | params = super(DatabaseWrapper, self).get_connection_params() 49 | pool_config = self.settings_dict.get('POOL', {}) 50 | minsize = pool_config.pop("minsize", 5) 51 | maxsize = pool_config.pop("maxsize", 0) 52 | params["mincached"] = int(minsize) 53 | params["maxcached"] = int(maxsize) 54 | params.update(pool_config) 55 | 56 | return params 57 | 58 | def get_new_connection(self, conn_params): 59 | conn = Database.connect(**conn_params) 60 | conn.encoders[SafeText] = conn.encoders[six.text_type] 61 | conn.encoders[SafeBytes] = conn.encoders[bytes] 62 | return conn 63 | -------------------------------------------------------------------------------- /djdbpool/db/backends/postgresql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binwen/django-db-pool/3c0255464be675cdd76ad1fa815437df3772fe0d/djdbpool/db/backends/postgresql/__init__.py -------------------------------------------------------------------------------- /djdbpool/db/backends/postgresql/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | try: 5 | import psycopg2 as Database 6 | import psycopg2.extensions 7 | import psycopg2.extras 8 | except ImportError as e: 9 | raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e) 10 | 11 | from django.db.backends.postgresql.base import DatabaseWrapper as PostgresDatabaseWrapper 12 | from djdbpool.db.pool import DBPoolWrapper 13 | 14 | 15 | Database = DBPoolWrapper(Database) 16 | 17 | 18 | class DatabaseWrapper(PostgresDatabaseWrapper): 19 | Database = Database 20 | 21 | def get_connection_params(self): 22 | params = super(DatabaseWrapper, self).get_connection_params() 23 | pool_config = self.settings_dict.get('POOL', {}) 24 | minsize = pool_config.pop("minsize", 5) 25 | maxsize = pool_config.pop("maxsize", 0) 26 | params["mincached"] = int(minsize) 27 | params["maxcached"] = int(maxsize) 28 | params.update(pool_config) 29 | 30 | return params 31 | 32 | def get_new_connection(self, conn_params): 33 | connection = Database.connect(**conn_params) 34 | options = self.settings_dict['OPTIONS'] 35 | try: 36 | self.isolation_level = options['isolation_level'] 37 | except KeyError: 38 | self.isolation_level = connection.isolation_level 39 | else: 40 | # Set the isolation level to the value from OPTIONS. 41 | if self.isolation_level != connection.isolation_level: 42 | connection.set_session(isolation_level=self.isolation_level) 43 | return connection 44 | -------------------------------------------------------------------------------- /djdbpool/db/backends/postgresql_psycopg2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binwen/django-db-pool/3c0255464be675cdd76ad1fa815437df3772fe0d/djdbpool/db/backends/postgresql_psycopg2/__init__.py -------------------------------------------------------------------------------- /djdbpool/db/backends/postgresql_psycopg2/base.py: -------------------------------------------------------------------------------- 1 | from ..postgresql.base import * # NOQA 2 | -------------------------------------------------------------------------------- /djdbpool/db/pool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | from DBUtils.PooledDB import PooledDB 4 | from DBUtils.SteadyDB import SteadyDBConnection 5 | except ImportError: 6 | from dbutils.pooled_db import PooledDB 7 | from dbutils.steady_db import SteadyDBConnection 8 | 9 | 10 | class DBPoolWrapper(object): 11 | def __init__(self, db_module): 12 | self._connection = None 13 | self._db_module = db_module 14 | self._pool = {} 15 | 16 | def __getattr__(self, item): 17 | return getattr(self._db_module, item) 18 | 19 | def connect(self, **kwargs): 20 | db = kwargs.get("db", "") 21 | if db not in self._pool: 22 | self._pool[db] = PooledDB(creator=self._db_module, **kwargs) 23 | self._connection = self._pool[db].connection() 24 | return self._connection 25 | 26 | 27 | def autocommit(self, *args, **kwargs): 28 | self._con.autocommit(*args, **kwargs) 29 | 30 | 31 | def get_server_info(self): 32 | return self._con.get_server_info() 33 | 34 | 35 | @property 36 | def encoders(self): 37 | return self._con.encoders 38 | 39 | 40 | # postgresql 41 | @property 42 | def isolation_level(self): 43 | return self._con.isolation_level 44 | 45 | 46 | def set_client_encoding(self, encoding): 47 | return self._con.set_client_encoding(encoding) 48 | 49 | 50 | def get_parameter_status(self, parameter): 51 | return self._con.get_parameter_status(parameter) 52 | 53 | 54 | setattr(SteadyDBConnection, "autocommit", autocommit) 55 | setattr(SteadyDBConnection, "get_server_info", get_server_info) 56 | setattr(SteadyDBConnection, "encoders", encoders) 57 | setattr(SteadyDBConnection, "isolation_level", isolation_level) 58 | setattr(SteadyDBConnection, "set_client_encoding", set_client_encoding) 59 | setattr(SteadyDBConnection, "get_parameter_status", get_parameter_status) 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from distutils.core import setup 8 | 9 | from setuptools import find_packages 10 | 11 | __version__ = '0.0.3' 12 | 13 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f: 14 | readme = f.read() 15 | 16 | setup( 17 | name='django-database-pool', 18 | version=__version__, 19 | packages=find_packages(), 20 | url='https://github.com/binwen/django-db-pool', 21 | license='MIT License', 22 | author='binwen', 23 | author_email='cwb201314@qq.com', 24 | description='通过DBUtils库实现Django数据库连接池,目前支持mysql、postgresql 数据库.', 25 | long_description=readme, 26 | long_description_content_type="text/markdown", 27 | classifiers=[ 28 | 'Topic :: Database', 29 | 'Development Status :: 3 - Alpha', 30 | 'Environment :: Web Environment', 31 | 'Framework :: Django', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Programming Language :: Python', 35 | ], 36 | install_requires=[ 37 | 'Django>=1.9.9', 38 | 'DBUtils>=1.3', 39 | 'six' 40 | ] 41 | ) 42 | --------------------------------------------------------------------------------