Skip to content

Commit

Permalink
Generic backend registration and arbitrary django settings support
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Gerbik committed Oct 17, 2022
1 parent 9a4a6f3 commit a823de4
Show file tree
Hide file tree
Showing 3 changed files with 426 additions and 209 deletions.
173 changes: 127 additions & 46 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,6 @@ This simple Django utility allows you to utilize the
`12factor <http://www.12factor.net/backing-services>`_ inspired
``DATABASE_URL`` environment variable to configure your Django application.

The ``dj_database_url.config`` method returns a Django database connection
dictionary, populated with all the data specified in your URL. There is
also a `conn_max_age` argument to easily enable Django's connection pool.

If you'd rather not use an environment variable, you can pass a URL in directly
instead to ``dj_database_url.parse``.

Supported Databases
-------------------

Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and SQLite.

Installation
------------

Expand All @@ -38,52 +25,146 @@ Installation is simple::
Usage
-----

1. If ``DATABASES`` is already defined:

- Configure your database in ``settings.py`` from ``DATABASE_URL``::
The ``dj_database_url.config()`` method returns a Django database
connection dictionary, populated with all the data specified in your
``DATABASE_URL`` environment variable::

import dj_database_url

DATABASES['default'] = dj_database_url.config(conn_max_age=600)

- Provide a default::

DATABASES['default'] = dj_database_url.config(default='postgres://...')
DATABASES = {
"default": dj_database_url.config(),
# arbitrary environment variable can be used
"replica": dj_database_url.config("REPLICA_URL"),
}

Given the following environment variables are defined::

$ export DATABASE_URL="postgres://user:[email protected]:5431/db-name"
# All the characters which are reserved in URL as per RFC 3986 should be urllib.parse.quote()'ed.
$ export REPLICA_URL="postgres://%23user:%[email protected]/db-name?timeout=20"

The above-mentioned code will result in::

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"USER": "user",
"PASSWORD": "password",
"HOST": "ec2-107-21-253-135.compute-1.amazonaws.com",
"PORT": 5431,
"NAME": "db-name",
},
"replica": {
"ENGINE": "django.db.backends.postgresql",
"USER": "#user",
"PASSWORD": "#password",
"HOST": "replica-host.com",
"PORT": "",
"NAME": "db-name",
# Any querystring parameters are automatically parsed and added to `OPTIONS`.
"OPTIONS": {
"timeout": "20",
},
},
}

A default value can be provided which will be used when the environment
variable is not set::

DATABASES["default"] = dj_database_url.config(default="sqlite://")

If you'd rather not use an environment variable, you can pass a URL
directly into ``dj_database_url.parse()``::

DATABASES["default"] = dj_database_url.parse("postgres://...")

You can also pass in any keyword argument that Django's |databases hyperlink|_ setting accepts,
such as |max age hyperlink|_ or |options hyperlink|_::

dj_database_url.config(CONN_MAX_AGE=600, TEST={"NAME": "mytestdatabase"})
# results in:
{
"ENGINE": "django.db.backends.postgresql",
# ... other values coming from DATABASE_URL
"CONN_MAX_AGE": 600,
"TEST": {
"NAME": "mytestdatabase",
},
}

# such usage is also possible:
dj_database_url.parse("postgres://...", **{
"CONN_MAX_AGE": 600,
"TEST": {
"NAME": "mytestdatabase",
},
"OPTIONS": {
"isolation_level": psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE,
},
})

``OPTIONS`` will be properly merged with the parameters coming from
querystring (keyword argument has higher priority than querystring).

.. |databases hyperlink| replace:: ``DATABASES``
.. _databases hyperlink: https://docs.djangoproject.com/en/stable/ref/settings/#databases
.. |max age hyperlink| replace:: ``CONN_MAX_AGE``
.. _max age hyperlink: https://docs.djangoproject.com/en/stable/ref/settings/#conn-max-age
.. |options hyperlink| replace:: ``OPTIONS``
.. _options hyperlink: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-OPTIONS

- Parse an arbitrary Database URL::

DATABASES['default'] = dj_database_url.parse('postgres://...', conn_max_age=600)
Supported Databases
-------------------

2. If ``DATABASES`` is not defined:
Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and SQLite.

- Configure your database in ``settings.py`` from ``DATABASE_URL``::
If you want to use
some non-default backends, you need to register them first::

import dj_database_url

DATABASES = {'default': dj_database_url.config(conn_max_age=600)}

- Provide a default::
# registration should be performed only once
dj_database_url.register("mysql.connector.django", "mysql-connector")

DATABASES = {'default': dj_database_url.config(default='postgres://...')}
assert dj_database_url.parse("mysql-connector://user:password@host:port/db-name") == {
"ENGINE": "mysql.connector.django",
# ...other connection params
}

- Parse an arbitrary Database URL::
Some backends need further config adjustments (e.g. oracle and mssql
expect ``PORT`` to be a string). For such cases you can provide a
post-processing function to ``register()`` (note that ``register()`` is
used as a **decorator(!)** in this case)::

DATABASES = {'default': dj_database_url.parse('postgres://...', conn_max_age=600)}

The ``conn_max_age`` attribute is the lifetime of a database connection in seconds
and is available in Django 1.6+. If you do not set a value, it will default to ``0``
which is Django's historical behavior of using a new database connection on each
request. Use ``None`` for unlimited persistent connections.

Strings passed to `dj_database_url` must be valid URLs; in
particular, special characters must be url-encoded. The following url will raise
a `ValueError`::

postgres://user:p#ssword!@localhost/foobar

and should instead be passed as::
import dj_database_url

postgres://user:p%23ssword!@localhost/foobar
@dj_database_url.register("sql_server.pyodbc", "mssql")
def stringify_port(config):
config["PORT"] = str(config["PORT"])

@dj_database_url.register("django_redshift_backend", "redshift")
def apply_current_schema(config):
options = config["OPTIONS"]
schema = options.pop("currentSchema", None)
if schema:
options["options"] = f"-c search_path={schema}"

@dj_database_url.register("django_snowflake", "snowflake")
def adjust_snowflake_config(config):
config.pop("PORT", None)
config["ACCOUNT"] = config.pop("HOST")
name, _, schema = config["NAME"].partition("/")
if schema:
config["SCHEMA"] = schema
config["NAME"] = name
options = config.get("OPTIONS", {})
warehouse = options.pop("warehouse", None)
if warehouse:
config["WAREHOUSE"] = warehouse
role = options.pop("role", None)
if role:
config["ROLE"] = role

URL schema
----------
Expand Down
Loading

0 comments on commit a823de4

Please sign in to comment.