├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.rst ├── dj_database_url.py ├── manifest.in ├── setup.py └── test_dj_database_url.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8] 9 | django-version: [1.11, 2.0, 2.1, 2.2, 3.0] 10 | exclude: 11 | # Python 2.7 is only compatible with Django 1.11 12 | - python-version: 2.7 13 | django-version: 2.0 14 | - python-version: 2.7 15 | django-version: 2.1 16 | - python-version: 2.7 17 | django-version: 2.2 18 | - python-version: 2.7 19 | django-version: 3.0 20 | # Python 3.5 is compatible with Django 1.11 to 2.2 but not 3.0 21 | - python-version: 3.5 22 | django-version: 3.0 23 | fail-fast: false 24 | steps: 25 | - uses: actions/checkout@v1 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v1 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install dependencies 33 | run: | 34 | pip install -U pip 35 | pip install "Django~=${{ matrix.django-version }}.0" . 36 | 37 | - name: Run Tests 38 | run: | 39 | echo "$(python --version) / Django $(django-admin --version)" 40 | python -m unittest discover 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | .tox/ 31 | .coverage 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | 44 | # Rope 45 | .ropeproject 46 | 47 | # Django stuff: 48 | *.log 49 | *.pot 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # Virtualenv 55 | env/ 56 | .vscode/ 57 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - 2.7 5 | - 3.4 6 | - 3.5 7 | - 3.6 8 | - 3.7 9 | 10 | env: 11 | matrix: 12 | - DJANGO="Django>=1.11,<2.0" 13 | - DJANGO="Django>=2.0,<2.1" 14 | - DJANGO="Django>=2.1,<2.2" 15 | - DJANGO="Django>=2.2a1,<3.0" 16 | - DJANGO="https://github.com/django/django/archive/master.tar.gz" 17 | 18 | matrix: 19 | fast_finish: true 20 | exclude: 21 | # Django 2 dropped support for Python 2. 22 | - python: 2.7 23 | env: DJANGO="Django>=2.0,<2.1" 24 | - python: 2.7 25 | env: DJANGO="Django>=2.1,<2.2" 26 | - python: 2.7 27 | env: DJANGO="Django>=2.2a1,<3.0" 28 | - python: 2.7 29 | env: DJANGO="https://github.com/django/django/archive/master.tar.gz" 30 | # Django 2.1 dropped support for Python 3.4. 31 | - python: 3.4 32 | env: DJANGO="Django>=2.1,<2.2" 33 | - python: 3.4 34 | env: DJANGO="Django>=2.2a1,<3.0" 35 | - python: 3.4 36 | env: DJANGO="https://github.com/django/django/archive/master.tar.gz" 37 | allow_failures: 38 | - env: DJANGO="https://github.com/django/django/archive/master.tar.gz" 39 | 40 | install: pip install . $DJANGO 41 | script: make test 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## master 4 | 5 | - Add Django trove classifiers 6 | - Add testing for Django 2.1 7 | - Add EXPECTED_POSTGRES_ENGINE 8 | - Fix #96 deprecated postgres backend strings 9 | 10 | ## v0.5.0 (2018-03-01) 11 | 12 | - Use str port for mssql 13 | - Added license 14 | - Add mssql to readme 15 | - Add mssql support using pyodbc 16 | - Fix RST schemas 17 | - Django expects Oracle Ports as strings 18 | - Fix IPv6 address parsing 19 | - Add testing for Python 3.6 20 | - Revert "Add setup.cfg for wheel support" 21 | - added option of postgis backend to also add path parsing. (test added also) 22 | - Support schema definition for redshift 23 | - add redshift support 24 | - Add testing for Python 3.5 25 | - Drop testing for Python 2.6 26 | - Fixes issue with unix file paths being turned to lower case 27 | - add Redis support 28 | - Added SpatiaLite in README.rst 29 | 30 | ## v0.4.1 (2016-04-06) 31 | 32 | - Enable CA providing for MySQL URIs 33 | - Update Readme 34 | - Update trove classifiers 35 | - Updated setup.py description 36 | 37 | ## v0.4.0 (2016-02-04) 38 | 39 | - Update readme 40 | - Fix for python3 41 | - Handle search path config in connect url for postgres 42 | - Add tox config to ease testing against multiple Python versions 43 | - Simplified the querystring parse logic 44 | - Cleaned up querystring parsing 45 | - supports database options 46 | - Added tests for CONN_MAX_AGE 47 | - Added documentation for conn_max_age 48 | - Add in optional support for CONN_MAX_AGE 49 | - Support special characters in user, password and name fields 50 | - Add oracle support 51 | - Added support for percent-encoded postgres paths 52 | - Fixed test_cleardb_parsing test 53 | - Enable automated testing with Python 3.4 54 | - Add URL schema examples to README 55 | - Added support for python mysql-connector 56 | 57 | ## v0.3.0 (2014-03-10) 58 | 59 | - Add .gitignore file 60 | - Remove .pyc file 61 | - Remove travis-ci unsupported python version Per docs http://docs.travis-ci.com/user/languages/python/ "Travis CI support Python versions 2.6, 2.7, 3.2 and 3.3" 62 | - Fix cleardb test 63 | - Add setup.cfg for wheel support 64 | - Add trove classifiers for python versions 65 | - Replace Python 3.1 with Python 3.3 66 | - Add MySQL (GIS) support 67 | - Ability to set different engine 68 | 69 | ## v0.2.2 (2013-07-17) 70 | 71 | - Added spatialite to uses_netloc too 72 | - Added spatialite backend 73 | - Replacing tab with spaces 74 | - Handling special case of sqlite://:memory: 75 | - Empty sqlite path will now use a :memory: database 76 | - Fixing test to actually use the result of the parse 77 | - Adding in tests to ensure sqlite in-memory databases work 78 | - Fixed too-short title underline 79 | - Added :target: attribute to Travis status image in README 80 | - Added docs for default argument to config 81 | - Add "pgsql" as a PostgreSQL URL scheme. 82 | - Add support for blank fields (Django expects '' not None) 83 | - fixed url 84 | 85 | ## v0.2.1 (2012-06-19) 86 | 87 | - Add python3 support 88 | - Adding travis status and tests 89 | - Adding test environment variables 90 | - Adding test for cleardb 91 | - Remove query strings from name 92 | - Adding postgres tests 93 | - Adding tests 94 | - refactor scheme lookup 95 | - RedHat's OpenShift platform uses the 'postgresql' scheme 96 | - Registered postgis URL scheme 97 | - Added `postgis://` url scheme 98 | - Use get() on os.environ instead of an if 99 | 100 | ## v0.2.0 (2012-05-30) 101 | 102 | - Fix parse(s) 103 | 104 | ## v0.1.4 (2012-05-30) 105 | 106 | - Add defaults for env 107 | - Set the DATABASES dict rather than assigning to it 108 | 109 | ## v0.1.3 (2012-05-01) 110 | 111 | - Add note to README on supported databases 112 | - Add support for SQLite 113 | - Clean dependencies 114 | 115 | ## v0.1.2 (2012-04-30) 116 | 117 | - Update readme 118 | - Refactor config and use new parse function 119 | 120 | ## v0.1.1 (2012-04-30) First release 121 | 122 | 🐍 ✨ 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Kenneth Reitz & individual contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 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. 8 | 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. 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DJ-Database-URL 2 | ~~~~~~~~~~~~~~~ 3 | 4 | .. image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fjacobian%2Fdj-database-url%2Fbadge&style=for-the-badge 5 | :target: https://actions-badge.atrox.dev/jacobian/dj-database-url/goto 6 | 7 | This simple Django utility allows you to utilize the 8 | `12factor `_ inspired 9 | ``DATABASE_URL`` environment variable to configure your Django application. 10 | 11 | The ``dj_database_url.config`` method returns a Django database connection 12 | dictionary, populated with all the data specified in your URL. There is 13 | also a `conn_max_age` argument to easily enable Django's connection pool. 14 | 15 | If you'd rather not use an environment variable, you can pass a URL in directly 16 | instead to ``dj_database_url.parse``. 17 | 18 | Supported Databases 19 | ------------------- 20 | 21 | Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS), 22 | Oracle, Oracle (GIS), Redshift, and SQLite. 23 | 24 | Installation 25 | ------------ 26 | 27 | Installation is simple:: 28 | 29 | $ pip install dj-database-url 30 | 31 | Usage 32 | ----- 33 | 34 | Configure your database in ``settings.py`` from ``DATABASE_URL``:: 35 | 36 | import dj_database_url 37 | 38 | DATABASES['default'] = dj_database_url.config(conn_max_age=600) 39 | 40 | Provide a default:: 41 | 42 | DATABASES['default'] = dj_database_url.config(default='postgres://...') 43 | 44 | Parse an arbitrary Database URL:: 45 | 46 | DATABASES['default'] = dj_database_url.parse('postgres://...', conn_max_age=600) 47 | 48 | The ``conn_max_age`` attribute is the lifetime of a database connection in seconds 49 | and is available in Django 1.6+. If you do not set a value, it will default to ``0`` 50 | which is Django's historical behavior of using a new database connection on each 51 | request. Use ``None`` for unlimited persistent connections. 52 | 53 | URL schema 54 | ---------- 55 | 56 | +-------------+-----------------------------------------------+--------------------------------------------------+ 57 | | Engine | Django Backend | URL | 58 | +=============+===============================================+==================================================+ 59 | | PostgreSQL | ``django.db.backends.postgresql`` [1]_ | ``postgres://USER:PASSWORD@HOST:PORT/NAME`` [2]_ | 60 | +-------------+-----------------------------------------------+--------------------------------------------------+ 61 | | PostGIS | ``django.contrib.gis.db.backends.postgis`` | ``postgis://USER:PASSWORD@HOST:PORT/NAME`` | 62 | +-------------+-----------------------------------------------+--------------------------------------------------+ 63 | | MSSQL | ``sql_server.pyodbc`` | ``mssql://USER:PASSWORD@HOST:PORT/NAME`` | 64 | +-------------+-----------------------------------------------+--------------------------------------------------+ 65 | | MySQL | ``django.db.backends.mysql`` | ``mysql://USER:PASSWORD@HOST:PORT/NAME`` | 66 | +-------------+-----------------------------------------------+--------------------------------------------------+ 67 | | MySQL (GIS) | ``django.contrib.gis.db.backends.mysql`` | ``mysqlgis://USER:PASSWORD@HOST:PORT/NAME`` | 68 | +-------------+-----------------------------------------------+--------------------------------------------------+ 69 | | SQLite | ``django.db.backends.sqlite3`` | ``sqlite:///PATH`` [3]_ | 70 | +-------------+-----------------------------------------------+--------------------------------------------------+ 71 | | SpatiaLite | ``django.contrib.gis.db.backends.spatialite`` | ``spatialite:///PATH`` [3]_ | 72 | +-------------+-----------------------------------------------+--------------------------------------------------+ 73 | | Oracle | ``django.db.backends.oracle`` | ``oracle://USER:PASSWORD@HOST:PORT/NAME`` [4]_ | 74 | +-------------+-----------------------------------------------+--------------------------------------------------+ 75 | | Oracle (GIS)| ``django.contrib.gis.db.backends.oracle`` | ``oraclegis://USER:PASSWORD@HOST:PORT/NAME`` | 76 | +-------------+-----------------------------------------------+--------------------------------------------------+ 77 | | Redshift | ``django_redshift_backend`` | ``redshift://USER:PASSWORD@HOST:PORT/NAME`` | 78 | +-------------+-----------------------------------------------+--------------------------------------------------+ 79 | 80 | .. [1] The django.db.backends.postgresql backend is named django.db.backends.postgresql_psycopg2 in older releases. For 81 | backwards compatibility, the old name still works in newer versions. (The new name does not work in older versions). 82 | .. [2] With PostgreSQL, you can also use unix domain socket paths with 83 | `percent encoding `_: 84 | ``postgres://%2Fvar%2Flib%2Fpostgresql/dbname``. 85 | .. [3] SQLite connects to file based databases. The same URL format is used, omitting 86 | the hostname, and using the "file" portion as the filename of the database. 87 | This has the effect of four slashes being present for an absolute file path: 88 | ``sqlite:////full/path/to/your/database/file.sqlite``. 89 | .. [4] Note that when connecting to Oracle the URL isn't in the form you may know 90 | from using other Oracle tools (like SQLPlus) i.e. user and password are separated 91 | by ``:`` not by ``/``. Also you can omit ``HOST`` and ``PORT`` 92 | and provide a full DSN string or TNS name in ``NAME`` part. 93 | -------------------------------------------------------------------------------- /dj_database_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | try: 6 | import urlparse 7 | except ImportError: 8 | import urllib.parse as urlparse 9 | 10 | try: 11 | from django import VERSION as DJANGO_VERSION 12 | except ImportError: 13 | DJANGO_VERSION = None 14 | 15 | 16 | # Register database schemes in URLs. 17 | urlparse.uses_netloc.append('postgres') 18 | urlparse.uses_netloc.append('postgresql') 19 | urlparse.uses_netloc.append('pgsql') 20 | urlparse.uses_netloc.append('postgis') 21 | urlparse.uses_netloc.append('mysql') 22 | urlparse.uses_netloc.append('mysql2') 23 | urlparse.uses_netloc.append('mysqlgis') 24 | urlparse.uses_netloc.append('mysql-connector') 25 | urlparse.uses_netloc.append('mssql') 26 | urlparse.uses_netloc.append('spatialite') 27 | urlparse.uses_netloc.append('sqlite') 28 | urlparse.uses_netloc.append('oracle') 29 | urlparse.uses_netloc.append('oraclegis') 30 | urlparse.uses_netloc.append('redshift') 31 | 32 | DEFAULT_ENV = 'DATABASE_URL' 33 | 34 | SCHEMES = { 35 | 'postgis': 'django.contrib.gis.db.backends.postgis', 36 | 'mysql': 'django.db.backends.mysql', 37 | 'mysql2': 'django.db.backends.mysql', 38 | 'mysqlgis': 'django.contrib.gis.db.backends.mysql', 39 | 'mysql-connector': 'mysql.connector.django', 40 | 'mssql': 'sql_server.pyodbc', 41 | 'spatialite': 'django.contrib.gis.db.backends.spatialite', 42 | 'sqlite': 'django.db.backends.sqlite3', 43 | 'oracle': 'django.db.backends.oracle', 44 | 'oraclegis': 'django.contrib.gis.db.backends.oracle', 45 | 'redshift': 'django_redshift_backend', 46 | } 47 | 48 | # https://docs.djangoproject.com/en/2.0/releases/2.0/#id1 49 | if DJANGO_VERSION and DJANGO_VERSION < (2, 0): 50 | SCHEMES['postgres'] = 'django.db.backends.postgresql_psycopg2' 51 | SCHEMES['postgresql'] = 'django.db.backends.postgresql_psycopg2' 52 | SCHEMES['pgsql'] = 'django.db.backends.postgresql_psycopg2' 53 | else: 54 | SCHEMES['postgres'] = 'django.db.backends.postgresql' 55 | SCHEMES['postgresql'] = 'django.db.backends.postgresql' 56 | SCHEMES['pgsql'] = 'django.db.backends.postgresql' 57 | 58 | 59 | def config(env=DEFAULT_ENV, default=None, engine=None, conn_max_age=0, ssl_require=False): 60 | """Returns configured DATABASE dictionary from DATABASE_URL.""" 61 | 62 | config = {} 63 | 64 | s = os.environ.get(env, default) 65 | 66 | if s: 67 | config = parse(s, engine, conn_max_age, ssl_require) 68 | 69 | return config 70 | 71 | 72 | def parse(url, engine=None, conn_max_age=0, ssl_require=False): 73 | """Parses a database URL.""" 74 | 75 | if url == 'sqlite://:memory:': 76 | # this is a special case, because if we pass this URL into 77 | # urlparse, urlparse will choke trying to interpret "memory" 78 | # as a port number 79 | return { 80 | 'ENGINE': SCHEMES['sqlite'], 81 | 'NAME': ':memory:' 82 | } 83 | # note: no other settings are required for sqlite 84 | 85 | # otherwise parse the url as normal 86 | config = {} 87 | 88 | url = urlparse.urlparse(url) 89 | 90 | # Split query strings from path. 91 | path = url.path[1:] 92 | if '?' in path and not url.query: 93 | path, query = path.split('?', 2) 94 | else: 95 | path, query = path, url.query 96 | query = urlparse.parse_qs(query) 97 | 98 | # If we are using sqlite and we have no path, then assume we 99 | # want an in-memory database (this is the behaviour of sqlalchemy) 100 | if url.scheme == 'sqlite' and path == '': 101 | path = ':memory:' 102 | 103 | # Handle postgres percent-encoded paths. 104 | hostname = url.hostname or '' 105 | if '%2f' in hostname.lower(): 106 | # Switch to url.netloc to avoid lower cased paths 107 | hostname = url.netloc 108 | if "@" in hostname: 109 | hostname = hostname.rsplit("@", 1)[1] 110 | if ":" in hostname: 111 | hostname = hostname.split(":", 1)[0] 112 | hostname = hostname.replace('%2f', '/').replace('%2F', '/') 113 | 114 | # Lookup specified engine. 115 | engine = SCHEMES[url.scheme] if engine is None else engine 116 | 117 | port = (str(url.port) if url.port and engine in [SCHEMES['oracle'], SCHEMES['mssql']] 118 | else url.port) 119 | 120 | # Update with environment configuration. 121 | config.update({ 122 | 'NAME': urlparse.unquote(path or ''), 123 | 'USER': urlparse.unquote(url.username or ''), 124 | 'PASSWORD': urlparse.unquote(url.password or ''), 125 | 'HOST': hostname, 126 | 'PORT': port or '', 127 | 'CONN_MAX_AGE': conn_max_age, 128 | }) 129 | 130 | # Pass the query string into OPTIONS. 131 | options = {} 132 | for key, values in query.items(): 133 | if url.scheme == 'mysql' and key == 'ssl-ca': 134 | options['ssl'] = {'ca': values[-1]} 135 | continue 136 | 137 | options[key] = values[-1] 138 | 139 | if ssl_require: 140 | options['sslmode'] = 'require' 141 | 142 | # Support for Postgres Schema URLs 143 | if 'currentSchema' in options and engine in ( 144 | 'django.contrib.gis.db.backends.postgis', 145 | 'django.db.backends.postgresql_psycopg2', 146 | 'django.db.backends.postgresql', 147 | 'django_redshift_backend', 148 | ): 149 | options['options'] = '-c search_path={0}'.format(options.pop('currentSchema')) 150 | 151 | if options: 152 | config['OPTIONS'] = options 153 | 154 | if engine: 155 | config['ENGINE'] = engine 156 | 157 | return config 158 | -------------------------------------------------------------------------------- /manifest.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | with open('README.rst') as readme_rst: 5 | readme = readme_rst.read() 6 | 7 | setup( 8 | name="dj-database-url", 9 | version="0.5.0", 10 | url="https://github.com/jacobian/dj-database-url", 11 | license="BSD", 12 | author="Kenneth Reitz", 13 | author_email="me@kennethreitz.com", 14 | description="Use Database URLs in your Django Application.", 15 | long_description=readme, 16 | py_modules=["dj_database_url"], 17 | install_requires=["Django>1.11"], 18 | zip_safe=False, 19 | include_package_data=True, 20 | platforms="any", 21 | classifiers=[ 22 | "Environment :: Web Environment", 23 | "Framework :: Django", 24 | "Framework :: Django :: 1.11", 25 | "Framework :: Django :: 2.0", 26 | "Framework :: Django :: 2.1", 27 | "Framework :: Django :: 2.2", 28 | "Framework :: Django :: 3.0", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: BSD License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python", 33 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | "Programming Language :: Python", 36 | "Programming Language :: Python :: 2.7", 37 | "Programming Language :: Python :: 3", 38 | "Programming Language :: Python :: 3.4", 39 | "Programming Language :: Python :: 3.5", 40 | "Programming Language :: Python :: 3.6", 41 | "Programming Language :: Python :: 3.7", 42 | "Programming Language :: Python :: 3.8", 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /test_dj_database_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #!/usr/bin/env python 3 | 4 | import os 5 | import unittest 6 | 7 | from django import VERSION as DJANGO_VERSION 8 | 9 | import dj_database_url 10 | 11 | 12 | POSTGIS_URL = 'postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' 13 | 14 | # Django deprecated the `django.db.backends.postgresql_psycopg2` in 2.0. 15 | # https://docs.djangoproject.com/en/2.0/releases/2.0/#id1 16 | EXPECTED_POSTGRES_ENGINE = 'django.db.backends.postgresql' 17 | if DJANGO_VERSION < (2, 0): 18 | EXPECTED_POSTGRES_ENGINE = 'django.db.backends.postgresql_psycopg2' 19 | 20 | 21 | class DatabaseTestSuite(unittest.TestCase): 22 | 23 | def test_postgres_parsing(self): 24 | url = 'postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' 25 | url = dj_database_url.parse(url) 26 | 27 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 28 | assert url['NAME'] == 'd8r82722r2kuvn' 29 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 30 | assert url['USER'] == 'uf07k1i6d8ia0v' 31 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 32 | assert url['PORT'] == 5431 33 | 34 | def test_postgres_unix_socket_parsing(self): 35 | url = 'postgres://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn' 36 | url = dj_database_url.parse(url) 37 | 38 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 39 | assert url['NAME'] == 'd8r82722r2kuvn' 40 | assert url['HOST'] == '/var/run/postgresql' 41 | assert url['USER'] == '' 42 | assert url['PASSWORD'] == '' 43 | assert url['PORT'] == '' 44 | 45 | url = 'postgres://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn' 46 | url = dj_database_url.parse(url) 47 | 48 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 49 | assert url['HOST'] == '/Users/postgres/RuN' 50 | assert url['USER'] == '' 51 | assert url['PASSWORD'] == '' 52 | assert url['PORT'] == '' 53 | 54 | def test_ipv6_parsing(self): 55 | url = 'postgres://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn' 56 | url = dj_database_url.parse(url) 57 | 58 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 59 | assert url['NAME'] == 'd8r82722r2kuvn' 60 | assert url['HOST'] == '2001:db8:1234::1234:5678:90af' 61 | assert url['USER'] == 'ieRaekei9wilaim7' 62 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 63 | assert url['PORT'] == 5431 64 | 65 | def test_postgres_search_path_parsing(self): 66 | url = 'postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema' 67 | url = dj_database_url.parse(url) 68 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 69 | assert url['NAME'] == 'd8r82722r2kuvn' 70 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 71 | assert url['USER'] == 'uf07k1i6d8ia0v' 72 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 73 | assert url['PORT'] == 5431 74 | assert url['OPTIONS']['options'] == '-c search_path=otherschema' 75 | assert 'currentSchema' not in url['OPTIONS'] 76 | 77 | def test_postgres_parsing_with_special_characters(self): 78 | url = 'postgres://%23user:%23password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database' 79 | url = dj_database_url.parse(url) 80 | 81 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 82 | assert url['NAME'] == '#database' 83 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 84 | assert url['USER'] == '#user' 85 | assert url['PASSWORD'] == '#password' 86 | assert url['PORT'] == 5431 87 | 88 | def test_postgis_parsing(self): 89 | url = 'postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' 90 | url = dj_database_url.parse(url) 91 | 92 | assert url['ENGINE'] == 'django.contrib.gis.db.backends.postgis' 93 | assert url['NAME'] == 'd8r82722r2kuvn' 94 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 95 | assert url['USER'] == 'uf07k1i6d8ia0v' 96 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 97 | assert url['PORT'] == 5431 98 | 99 | def test_postgis_search_path_parsing(self): 100 | url = 'postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema' 101 | url = dj_database_url.parse(url) 102 | assert url['ENGINE'] == 'django.contrib.gis.db.backends.postgis' 103 | assert url['NAME'] == 'd8r82722r2kuvn' 104 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 105 | assert url['USER'] == 'uf07k1i6d8ia0v' 106 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 107 | assert url['PORT'] == 5431 108 | assert url['OPTIONS']['options'] == '-c search_path=otherschema' 109 | assert 'currentSchema' not in url['OPTIONS'] 110 | 111 | def test_mysql_gis_parsing(self): 112 | url = 'mysqlgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' 113 | url = dj_database_url.parse(url) 114 | 115 | assert url['ENGINE'] == 'django.contrib.gis.db.backends.mysql' 116 | assert url['NAME'] == 'd8r82722r2kuvn' 117 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 118 | assert url['USER'] == 'uf07k1i6d8ia0v' 119 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 120 | assert url['PORT'] == 5431 121 | 122 | def test_mysql_connector_parsing(self): 123 | url = 'mysql-connector://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' 124 | url = dj_database_url.parse(url) 125 | 126 | assert url['ENGINE'] == 'mysql.connector.django' 127 | assert url['NAME'] == 'd8r82722r2kuvn' 128 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 129 | assert url['USER'] == 'uf07k1i6d8ia0v' 130 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 131 | assert url['PORT'] == 5431 132 | 133 | def test_cleardb_parsing(self): 134 | url = 'mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true' 135 | url = dj_database_url.parse(url) 136 | 137 | assert url['ENGINE'] == 'django.db.backends.mysql' 138 | assert url['NAME'] == 'heroku_97681db3eff7580' 139 | assert url['HOST'] == 'us-cdbr-east.cleardb.com' 140 | assert url['USER'] == 'bea6eb025ca0d8' 141 | assert url['PASSWORD'] == '69772142' 142 | assert url['PORT'] == '' 143 | 144 | def test_database_url(self): 145 | del os.environ['DATABASE_URL'] 146 | a = dj_database_url.config() 147 | assert not a 148 | 149 | os.environ['DATABASE_URL'] = 'postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn' 150 | 151 | url = dj_database_url.config() 152 | 153 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 154 | assert url['NAME'] == 'd8r82722r2kuvn' 155 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 156 | assert url['USER'] == 'uf07k1i6d8ia0v' 157 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 158 | assert url['PORT'] == 5431 159 | 160 | def test_empty_sqlite_url(self): 161 | url = 'sqlite://' 162 | url = dj_database_url.parse(url) 163 | 164 | assert url['ENGINE'] == 'django.db.backends.sqlite3' 165 | assert url['NAME'] == ':memory:' 166 | 167 | def test_memory_sqlite_url(self): 168 | url = 'sqlite://:memory:' 169 | url = dj_database_url.parse(url) 170 | 171 | assert url['ENGINE'] == 'django.db.backends.sqlite3' 172 | assert url['NAME'] == ':memory:' 173 | 174 | def test_parse_engine_setting(self): 175 | engine = 'django_mysqlpool.backends.mysqlpool' 176 | url = 'mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true' 177 | url = dj_database_url.parse(url, engine) 178 | 179 | assert url['ENGINE'] == engine 180 | 181 | def test_config_engine_setting(self): 182 | engine = 'django_mysqlpool.backends.mysqlpool' 183 | os.environ['DATABASE_URL'] = 'mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true' 184 | url = dj_database_url.config(engine=engine) 185 | 186 | assert url['ENGINE'] == engine 187 | 188 | def test_parse_conn_max_age_setting(self): 189 | conn_max_age = 600 190 | url = 'mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true' 191 | url = dj_database_url.parse(url, conn_max_age=conn_max_age) 192 | 193 | assert url['CONN_MAX_AGE'] == conn_max_age 194 | 195 | def test_config_conn_max_age_setting(self): 196 | conn_max_age = 600 197 | os.environ['DATABASE_URL'] = 'mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true' 198 | url = dj_database_url.config(conn_max_age=conn_max_age) 199 | 200 | assert url['CONN_MAX_AGE'] == conn_max_age 201 | 202 | def test_database_url_with_options(self): 203 | # Test full options 204 | os.environ['DATABASE_URL'] = 'postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?sslrootcert=rds-combined-ca-bundle.pem&sslmode=verify-full' 205 | url = dj_database_url.config() 206 | 207 | assert url['ENGINE'] == EXPECTED_POSTGRES_ENGINE 208 | assert url['NAME'] == 'd8r82722r2kuvn' 209 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 210 | assert url['USER'] == 'uf07k1i6d8ia0v' 211 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 212 | assert url['PORT'] == 5431 213 | assert url['OPTIONS'] == { 214 | 'sslrootcert': 'rds-combined-ca-bundle.pem', 215 | 'sslmode': 'verify-full' 216 | } 217 | 218 | # Test empty options 219 | os.environ['DATABASE_URL'] = 'postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?' 220 | url = dj_database_url.config() 221 | assert 'OPTIONS' not in url 222 | 223 | def test_mysql_database_url_with_sslca_options(self): 224 | os.environ['DATABASE_URL'] = 'mysql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:3306/d8r82722r2kuvn?ssl-ca=rds-combined-ca-bundle.pem' 225 | url = dj_database_url.config() 226 | 227 | assert url['ENGINE'] == 'django.db.backends.mysql' 228 | assert url['NAME'] == 'd8r82722r2kuvn' 229 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 230 | assert url['USER'] == 'uf07k1i6d8ia0v' 231 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 232 | assert url['PORT'] == 3306 233 | assert url['OPTIONS'] == { 234 | 'ssl': { 235 | 'ca': 'rds-combined-ca-bundle.pem' 236 | } 237 | } 238 | 239 | # Test empty options 240 | os.environ['DATABASE_URL'] = 'mysql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:3306/d8r82722r2kuvn?' 241 | url = dj_database_url.config() 242 | assert 'OPTIONS' not in url 243 | 244 | def test_oracle_parsing(self): 245 | url = 'oracle://scott:tiger@oraclehost:1521/hr' 246 | url = dj_database_url.parse(url) 247 | 248 | assert url['ENGINE'] == 'django.db.backends.oracle' 249 | assert url['NAME'] == 'hr' 250 | assert url['HOST'] == 'oraclehost' 251 | assert url['USER'] == 'scott' 252 | assert url['PASSWORD'] == 'tiger' 253 | assert url['PORT'] == '1521' 254 | 255 | def test_oracle_gis_parsing(self): 256 | url = 'oraclegis://scott:tiger@oraclehost:1521/hr' 257 | url = dj_database_url.parse(url) 258 | 259 | assert url['ENGINE'] == 'django.contrib.gis.db.backends.oracle' 260 | assert url['NAME'] == 'hr' 261 | assert url['HOST'] == 'oraclehost' 262 | assert url['USER'] == 'scott' 263 | assert url['PASSWORD'] == 'tiger' 264 | assert url['PORT'] == 1521 265 | 266 | def test_oracle_dsn_parsing(self): 267 | url = ( 268 | 'oracle://scott:tiger@/' 269 | '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' 270 | '(HOST=oraclehost)(PORT=1521)))' 271 | '(CONNECT_DATA=(SID=hr)))' 272 | ) 273 | url = dj_database_url.parse(url) 274 | 275 | assert url['ENGINE'] == 'django.db.backends.oracle' 276 | assert url['USER'] == 'scott' 277 | assert url['PASSWORD'] == 'tiger' 278 | assert url['HOST'] == '' 279 | assert url['PORT'] == '' 280 | 281 | dsn = ( 282 | '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' 283 | '(HOST=oraclehost)(PORT=1521)))' 284 | '(CONNECT_DATA=(SID=hr)))' 285 | ) 286 | 287 | assert url['NAME'] == dsn 288 | 289 | def test_oracle_tns_parsing(self): 290 | url = 'oracle://scott:tiger@/tnsname' 291 | url = dj_database_url.parse(url) 292 | 293 | assert url['ENGINE'] == 'django.db.backends.oracle' 294 | assert url['USER'] == 'scott' 295 | assert url['PASSWORD'] == 'tiger' 296 | assert url['NAME'] == 'tnsname' 297 | assert url['HOST'] == '' 298 | assert url['PORT'] == '' 299 | 300 | def test_redshift_parsing(self): 301 | url = 'redshift://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5439/d8r82722r2kuvn?currentSchema=otherschema' 302 | url = dj_database_url.parse(url) 303 | 304 | assert url['ENGINE'] == 'django_redshift_backend' 305 | assert url['NAME'] == 'd8r82722r2kuvn' 306 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 307 | assert url['USER'] == 'uf07k1i6d8ia0v' 308 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 309 | assert url['PORT'] == 5439 310 | assert url['OPTIONS']['options'] == '-c search_path=otherschema' 311 | assert 'currentSchema' not in url['OPTIONS'] 312 | 313 | def test_mssql_parsing(self): 314 | url = 'mssql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server' 315 | url = dj_database_url.parse(url) 316 | 317 | assert url['ENGINE'] == 'sql_server.pyodbc' 318 | assert url['NAME'] == 'd8r82722r2kuvn' 319 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com' 320 | assert url['USER'] == 'uf07k1i6d8ia0v' 321 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 322 | assert url['PORT'] == '' 323 | assert url['OPTIONS']['driver'] == 'ODBC Driver 13 for SQL Server' 324 | assert 'currentSchema' not in url['OPTIONS'] 325 | 326 | def test_mssql_instance_port_parsing(self): 327 | url = 'mssql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com\\insnsnss:12345/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server' 328 | url = dj_database_url.parse(url) 329 | 330 | assert url['ENGINE'] == 'sql_server.pyodbc' 331 | assert url['NAME'] == 'd8r82722r2kuvn' 332 | assert url['HOST'] == 'ec2-107-21-253-135.compute-1.amazonaws.com\\insnsnss' 333 | assert url['USER'] == 'uf07k1i6d8ia0v' 334 | assert url['PASSWORD'] == 'wegauwhgeuioweg' 335 | assert url['PORT'] == '12345' 336 | assert url['OPTIONS']['driver'] == 'ODBC Driver 13 for SQL Server' 337 | assert 'currentSchema' not in url['OPTIONS'] 338 | 339 | 340 | if __name__ == '__main__': 341 | unittest.main() 342 | --------------------------------------------------------------------------------