Compare commits

..

No commits in common. "master" and "v2.1.0" have entirely different histories.

13 changed files with 427 additions and 1719 deletions

5
.flake8 Normal file
View file

@ -0,0 +1,5 @@
[flake8]
max-line-length = 88
extend-ignore = E203
per-file-ignores=
tests/test_dj_database_url.py: E501, E265

View file

@ -1,8 +1,9 @@
name: Release
on:
release:
types: [published]
push:
tags:
- '*'
jobs:
build:
@ -10,29 +11,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set version from release/tag
id: version
run: |
VERSION=${GITHUB_REF#refs/*/}
VERSION=${VERSION#v}
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
- uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.12
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools wheel twine
- name: Build package
run: |
uv version ${{ env.VERSION }}
uv build
uvx twine check dist/*
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: jazzband

View file

@ -1,74 +1,55 @@
name: test
on: [push, pull_request]
jobs:
formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- name: Run ruff
run: uvx ruff check
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- name: Run mypy
run: uvx mypy dj_database_url
- name: Run pyright
run: uvx pyright dj_database_url
test:
runs-on: ubuntu-latest
needs: [formatting, typecheck]
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
django-version: ["4.2", "5.2", "6.0"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
django-version: ["3.2", "4.0", "4.1"]
exclude:
# django 4.x is not compatible with python 3.13 or higher
- python-version: "3.13"
django-version: "4.2"
- python-version: "3.14"
django-version: "4.2"
# django 6.x is not compatible with python 3.11 or lower
- python-version: "3.10"
django-version: "6.0"
# Python 3.7 is not compatible with 4.0
- python-version: "3.7"
django-version: "4.0"
# Python 3.7 is not compatible with 4.1
- python-version: "3.7"
django-version: "4.1"
# Python 3.11 is not compatible with 3.2
- python-version: "3.11"
django-version: "6.0"
django-version: "3.2"
# Python 3.11 is not compatible with 4.0
- python-version: "3.11"
django-version: "4.0"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install uv and set the Python version
uses: astral-sh/setup-uv@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
activate-environment: 'true'
cache: 'pip'
- name: Install dependencies
run: |
uv pip install "Django~=${{ matrix.django-version }}.0"
pip install -r requirements.txt
pip install "Django~=${{ matrix.django-version }}.0" .
- name: Run mypy
run: |
python -m mypy dj_database_url
- name: Run Tests
run: |
echo "$(python --version) / Django $(django-admin --version)"
uvx coverage run --source=dj_database_url --branch -m unittest discover -v
uvx coverage report
uvx coverage xml
coverage run --source=dj_database_url --branch -m unittest discover -v
coverage report
coverage xml
- uses: codecov/codecov-action@v4
- uses: codecov/codecov-action@v3
- name: Check mypy types installation
run: |
pip install .
cd tests
python -m mypy .

2
.isort.cfg Normal file
View file

@ -0,0 +1,2 @@
[settings]
profile = black

View file

@ -1,21 +1,23 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.4
- repo: https://github.com/pycqa/isort
rev: "5.12.0"
hooks:
- id: ruff-check
args:
- --fix
- id: ruff-format
args:
- --quiet
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/crate-ci/typos
rev: v1
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: typos
- id: black
args: [--target-version=py38]
- repo: https://github.com/pycqa/flake8
rev: '6.0.0'
hooks:
- id: flake8

View file

@ -1,69 +1,49 @@
# CHANGELOG
## v3.1.0 (2026-01-03)
* Add support for Django 6.0
* Update CI structure.
* Migrate to UV for dependency management and builds.
* Python >3.10 support.
## v3.0.1 (2025-07-01)
* Drop dependency on `typing_extensions`.
## v3.0.0 (2025-05-18)
> Bumping to version 3; changes to code do break some API compatibility.
* Implement a new decorator registry pattern to implement checks on database connection string.
* You can now support and implement your own database strings by extending the @register functionality.
* Update supported python versions and django versions.
## v2.3.0 (2024-10-23)
* Remove Python 3.8 support.
* Remove Django 3 support.
* Add python 3.13 support.
* Add Django 5.1 to the testing library.
## v2.2.0 (2024-05-28)
* Add disable_server_side_cursors parameter
* Enhance Query String Parsing for Server-Side Binding in Django 4.2 with psycopg 3.1.8+
* Update django 5.0 python compatibility by @mattseymour in #239
* Improved internals
* Improved documentation
## v2.1.0 (2023-08-15)
* Add value to int parsing when deconstructing url string.
## v2.0.0 (2023-04-27)
* Update project setup such that we now install it as a package.
* Update project setup such that we now install as a package.
_Notes_: while this does not alter the underlying application code, we are bumping to
2.0 incase there are unforeseen knock-on use-case issues.
2.0 incase there are unforeseen knock on use-case issues.
## v1.3.0 (2023-03-27)
* Cosmetic changes to the generation of schemes.
* Bump isort version - 5.11.5.
* raise a warning message if `database_url` is not set.
* `CONN_MAX_AGE` fix type - `Optional[int]`.
* raise warning message if database_url is not set.
* CONN_MAX_AGE fix type - Optional[int].
## v1.2.0 (2022-12-13)
* Add the ability to add test databases.
* Improve url parsing and encoding.
* Fix missing parameter `conn_health_check` in check function.
* Fix missing parameter conn_health_check in check function.
## v1.1.0 (2022-12-12)
* Option for connection health checks parameter.
* Update supported version python 3.11.
* Code changes, various improvements.
* Add project links to `setup.py`.
* Code changes, various improvments.
* Add project links to setup.py
## v1.0.0 (2022-06-18)
Initial release of code, dj-database-urls is now part of [Jazzband](https://jazzband.co/).
Initial release of code now dj-database-urls is part of jazzband.
* Add support for cockroachdb.
* Add support for the official MSSQL connector.
* Add support for the offical MSSQL connector.
* Update License to be compatible with Jazzband.
* Remove support for Python < 3.5 including Python 2.7
* Update source code to Black format.
* Update CI using pre-commit
## v0.5.0 (2018-03-01)
- Use str port for mssql
- Added license
- Add mssql to readme
@ -83,15 +63,17 @@ Initial release of code, dj-database-urls is now part of [Jazzband](https://jazz
- Added SpatiaLite in README.rst
## v0.4.1 (2016-04-06)
- Enable CA providing for MySQL URIs
- Update Readme
- Update trove classifiers
- Updated setup.py description
## v0.4.0 (2016-02-04)
- Update readme
- Fix for python3
- Handle search path config in connection url for postgres
- Handle search path config in connect url for postgres
- Add tox config to ease testing against multiple Python versions
- Simplified the querystring parse logic
- Cleaned up querystring parsing
@ -108,25 +90,27 @@ Initial release of code, dj-database-urls is now part of [Jazzband](https://jazz
- Added support for python mysql-connector
## v0.3.0 (2014-03-10)
- Add `.gitignore` file
- Remove `.pyc` file
- Remove travis-ci unsupported python versions (Travis CI supports Python versions 2.6, 2.7, 3.2 and 3.3)
- Add .gitignore file
- Remove .pyc file
- 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"
- Fix cleardb test
- Add `setup.cfg` for wheel support
- Add setup.cfg for wheel support
- Add trove classifiers for python versions
- Replace Python 3.1 with Python 3.3
- Add MySQL (GIS) support
- Ability to set different engine
## v0.2.2 (2013-07-17)
- Added spatialite to uses_netloc too
- Added spatialite backend
- Replacing tab with spaces
- Handling special case of `sqlite://:memory:`
- Handling special case of sqlite://:memory:
- Empty sqlite path will now use a :memory: database
- Fixing test to actually use the result of the parse
- Adding in tests to ensure sqlite in-memory databases work
- Fixed a too-short title underline
- Fixed too-short title underline
- Added :target: attribute to Travis status image in README
- Added docs for default argument to config
- Add "pgsql" as a PostgreSQL URL scheme.
@ -134,6 +118,7 @@ Initial release of code, dj-database-urls is now part of [Jazzband](https://jazz
- fixed url
## v0.2.1 (2012-06-19)
- Add python3 support
- Adding travis status and tests
- Adding test environment variables
@ -145,23 +130,28 @@ Initial release of code, dj-database-urls is now part of [Jazzband](https://jazz
- RedHat's OpenShift platform uses the 'postgresql' scheme
- Registered postgis URL scheme
- Added `postgis://` url scheme
- Use `get()` on `os.environ` instead of an `if`.
- Use get() on os.environ instead of an if
## v0.2.0 (2012-05-30)
- Fix parse(s)
## v0.1.4 (2012-05-30)
- Add defaults for env
- Set the DATABASES dict rather than assigning to it
## v0.1.3 (2012-05-01)
- Add a note to README on supported databases
- Add note to README on supported databases
- Add support for SQLite
- Clean dependencies
## v0.1.2 (2012-04-30)
- Update readme
- Refactor config and use a new parse function
- Refactor config and use new parse function
## v0.1.1 (2012-04-30) First release
🐍 ✨

View file

@ -22,6 +22,12 @@ 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
------------
@ -142,63 +148,6 @@ and should instead be passed as:
DATABASES['default'] = dj_database_url.config(default='postgres://...', test_options={'NAME': 'mytestdatabase'})
Supported Databases
-------------------
Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and SQLite.
If you want to use
some non-default backends, you need to register them first:
.. code-block:: python
import dj_database_url
# registration should be performed only once
dj_database_url.register("mysql-connector", "mysql.connector.django")
assert dj_database_url.parse("mysql-connector://user:password@host:port/db-name") == {
"ENGINE": "mysql.connector.django",
# ...other connection params
}
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):
.. code-block:: python
import dj_database_url
@dj_database_url.register("mssql", "sql_server.pyodbc")
def stringify_port(config):
config["PORT"] = str(config["PORT"])
@dj_database_url.register("redshift", "django_redshift_backend")
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("snowflake", "django_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
----------
@ -206,7 +155,6 @@ URL schema
| Engine | Django Backend | URL |
+======================+===============================================+==================================================+
| PostgreSQL | ``django.db.backends.postgresql`` [1]_ | ``postgres://USER:PASSWORD@HOST:PORT/NAME`` [2]_ |
| | | ``postgresql://USER:PASSWORD@HOST:PORT/NAME`` |
+----------------------+-----------------------------------------------+--------------------------------------------------+
| PostGIS | ``django.contrib.gis.db.backends.postgis`` | ``postgis://USER:PASSWORD@HOST:PORT/NAME`` |
+----------------------+-----------------------------------------------+--------------------------------------------------+

View file

@ -1,156 +1,76 @@
import logging
import os
import urllib.parse as urlparse
from collections.abc import Callable
from typing import Any, TypedDict
from typing import Any, Dict, Optional, Union
from typing_extensions import TypedDict
DEFAULT_ENV = "DATABASE_URL"
ENGINE_SCHEMES: dict[str, "Engine"] = {}
SCHEMES = {
"postgres": "django.db.backends.postgresql",
"postgresql": "django.db.backends.postgresql",
"pgsql": "django.db.backends.postgresql",
"postgis": "django.contrib.gis.db.backends.postgis",
"mysql": "django.db.backends.mysql",
"mysql2": "django.db.backends.mysql",
"mysqlgis": "django.contrib.gis.db.backends.mysql",
"mysql-connector": "mysql.connector.django",
"mssql": "sql_server.pyodbc",
"mssqlms": "mssql",
"spatialite": "django.contrib.gis.db.backends.spatialite",
"sqlite": "django.db.backends.sqlite3",
"oracle": "django.db.backends.oracle",
"oraclegis": "django.contrib.gis.db.backends.oracle",
"redshift": "django_redshift_backend",
"cockroach": "django_cockroachdb",
"timescale": "timescale.db.backends.postgresql",
"timescalegis": "timescale.db.backends.postgis",
}
# Register database schemes in URLs.
for key in SCHEMES.keys():
urlparse.uses_netloc.append(key)
# From https://docs.djangoproject.com/en/stable/ref/settings/#databases
# From https://docs.djangoproject.com/en/4.0/ref/settings/#databases
class DBConfig(TypedDict, total=False):
ATOMIC_REQUESTS: bool
AUTOCOMMIT: bool
CONN_MAX_AGE: int | None
CONN_MAX_AGE: Optional[int]
CONN_HEALTH_CHECKS: bool
DISABLE_SERVER_SIDE_CURSORS: bool
ENGINE: str
HOST: str
NAME: str
OPTIONS: dict[str, Any]
OPTIONS: Optional[Dict[str, Any]]
PASSWORD: str
PORT: str | int
TEST: dict[str, Any]
PORT: Union[str, int]
TEST: Dict[str, Any]
TIME_ZONE: str
USER: str
PostprocessCallable = Callable[[DBConfig], None]
OptionType = int | str | bool
class ParseError(ValueError):
def __str__(self) -> str:
return (
"This string is not a valid url, possibly because some of its parts"
" is not properly urllib.parse.quote()'ed."
)
class UnknownSchemeError(ValueError):
def __init__(self, scheme: str):
self.scheme = scheme
def __str__(self) -> str:
schemes = ", ".join(sorted(ENGINE_SCHEMES.keys()))
return (
f"Scheme '{self.scheme}://' is unknown."
" Did you forget to register custom backend?"
f" Following schemes have registered backends: {schemes}."
)
def default_postprocess(parsed_config: DBConfig) -> None:
pass
class Engine:
def __init__(
self,
backend: str,
postprocess: PostprocessCallable = default_postprocess,
):
self.backend = backend
self.postprocess = postprocess
def register(
scheme: str, backend: str
) -> Callable[[PostprocessCallable], PostprocessCallable]:
engine = Engine(backend)
if scheme not in ENGINE_SCHEMES:
urlparse.uses_netloc.append(scheme)
ENGINE_SCHEMES[scheme] = engine
def inner(func: PostprocessCallable) -> PostprocessCallable:
engine.postprocess = func
return func
return inner
register("spatialite", "django.contrib.gis.db.backends.spatialite")
register("mysql-connector", "mysql.connector.django")
register("mysqlgis", "django.contrib.gis.db.backends.mysql")
register("oraclegis", "django.contrib.gis.db.backends.oracle")
register("cockroach", "django_cockroachdb")
@register("sqlite", "django.db.backends.sqlite3")
def default_to_in_memory_db(parsed_config: DBConfig) -> None:
# mimic sqlalchemy behaviour
if not parsed_config.get("NAME"):
parsed_config["NAME"] = ":memory:"
@register("oracle", "django.db.backends.oracle")
@register("mssqlms", "mssql")
@register("mssql", "sql_server.pyodbc")
def stringify_port(parsed_config: DBConfig) -> None:
parsed_config["PORT"] = str(parsed_config.get("PORT", ""))
@register("mysql", "django.db.backends.mysql")
@register("mysql2", "django.db.backends.mysql")
def apply_ssl_ca(parsed_config: DBConfig) -> None:
options = parsed_config.get("OPTIONS", {})
ca = options.pop("ssl-ca", None)
if ca:
options["ssl"] = {"ca": ca}
@register("postgres", "django.db.backends.postgresql")
@register("postgresql", "django.db.backends.postgresql")
@register("pgsql", "django.db.backends.postgresql")
@register("postgis", "django.contrib.gis.db.backends.postgis")
@register("redshift", "django_redshift_backend")
@register("timescale", "timescale.db.backends.postgresql")
@register("timescalegis", "timescale.db.backends.postgis")
def apply_current_schema(parsed_config: DBConfig) -> None:
options = parsed_config.get("OPTIONS", {})
schema = options.pop("currentSchema", None)
if schema:
options["options"] = f"-c search_path={schema}"
def config(
env: str = DEFAULT_ENV,
default: str | None = None,
engine: str | None = None,
conn_max_age: int | None = 0,
default: Optional[str] = None,
engine: Optional[str] = None,
conn_max_age: Optional[int] = 0,
conn_health_checks: bool = False,
disable_server_side_cursors: bool = False,
ssl_require: bool = False,
test_options: dict[str, Any] | None = None,
test_options: Optional[Dict] = None,
) -> DBConfig:
"""Returns configured DATABASE dictionary from DATABASE_URL."""
s = os.environ.get(env, default)
if s is None:
logging.warning(
"No %s environment variable set, and so no databases setup", env
"No %s environment variable set, and so no databases setup" % env
)
if s:
return parse(
s,
engine,
conn_max_age,
conn_health_checks,
disable_server_side_cursors,
ssl_require,
test_options,
s, engine, conn_max_age, conn_health_checks, ssl_require, test_options
)
return {}
@ -158,96 +78,110 @@ def config(
def parse(
url: str,
engine: str | None = None,
conn_max_age: int | None = 0,
engine: Optional[str] = None,
conn_max_age: Optional[int] = 0,
conn_health_checks: bool = False,
disable_server_side_cursors: bool = False,
ssl_require: bool = False,
test_options: dict[str, Any] | None = None,
test_options: Optional[dict] = None,
) -> DBConfig:
"""Parses a database URL and returns configured DATABASE dictionary."""
settings = _convert_to_settings(
engine,
conn_max_age,
conn_health_checks,
disable_server_side_cursors,
ssl_require,
test_options,
)
"""Parses a database URL."""
if url == "sqlite://:memory:":
# this is a special case, because if we pass this URL into
# urlparse, urlparse will choke trying to interpret "memory"
# as a port number
return {"ENGINE": ENGINE_SCHEMES["sqlite"].backend, "NAME": ":memory:"}
return {"ENGINE": SCHEMES["sqlite"], "NAME": ":memory:"}
# note: no other settings are required for sqlite
try:
split_result = urlparse.urlsplit(url)
engine_obj = ENGINE_SCHEMES.get(split_result.scheme)
if engine_obj is None:
raise UnknownSchemeError(split_result.scheme)
path = split_result.path[1:]
query = urlparse.parse_qs(split_result.query)
options = {k: _parse_option_values(v) for k, v in query.items()}
parsed_config: DBConfig = {
"ENGINE": engine_obj.backend,
"USER": urlparse.unquote(split_result.username or ""),
"PASSWORD": urlparse.unquote(split_result.password or ""),
"HOST": urlparse.unquote(split_result.hostname or ""),
"PORT": split_result.port or "",
"NAME": urlparse.unquote(path),
"OPTIONS": options,
# otherwise parse the url as normal
parsed_config: DBConfig = {}
if test_options is None:
test_options = {}
spliturl = urlparse.urlsplit(url)
# Split query strings from path.
path = spliturl.path[1:]
query = urlparse.parse_qs(spliturl.query)
# If we are using sqlite and we have no path, then assume we
# want an in-memory database (this is the behaviour of sqlalchemy)
if spliturl.scheme == "sqlite" and path == "":
path = ":memory:"
# Handle postgres percent-encoded paths.
hostname = spliturl.hostname or ""
if "%" in hostname:
# Switch to url.netloc to avoid lower cased paths
hostname = spliturl.netloc
if "@" in hostname:
hostname = hostname.rsplit("@", 1)[1]
# Use URL Parse library to decode % encodes
hostname = urlparse.unquote(hostname)
# Lookup specified engine.
if engine is None:
engine = SCHEMES.get(spliturl.scheme)
if engine is None:
raise ValueError(
"No support for '%s'. We support: %s"
% (spliturl.scheme, ", ".join(sorted(SCHEMES.keys())))
)
port = (
str(spliturl.port)
if spliturl.port
and engine in (SCHEMES["oracle"], SCHEMES["mssql"], SCHEMES["mssqlms"])
else spliturl.port
)
# Update with environment configuration.
parsed_config.update(
{
"NAME": urlparse.unquote(path or ""),
"USER": urlparse.unquote(spliturl.username or ""),
"PASSWORD": urlparse.unquote(spliturl.password or ""),
"HOST": hostname,
"PORT": port or "",
"CONN_MAX_AGE": conn_max_age,
"CONN_HEALTH_CHECKS": conn_health_checks,
"ENGINE": engine,
}
except UnknownSchemeError:
raise
except ValueError:
raise ParseError() from None
# Guarantee that config has options, possibly empty, when postprocess() is called
assert isinstance(parsed_config["OPTIONS"], dict)
engine_obj.postprocess(parsed_config)
# Update the final config with any settings passed in explicitly.
parsed_config["OPTIONS"].update(settings.pop("OPTIONS", {}))
parsed_config.update(settings)
if not parsed_config["OPTIONS"]:
parsed_config.pop("OPTIONS")
return parsed_config
def _parse_option_values(values: list[str]) -> OptionType | list[OptionType]:
parsed_values = [_parse_value(v) for v in values]
return parsed_values[0] if len(parsed_values) == 1 else parsed_values
def _parse_value(value: str) -> OptionType:
if value.isdigit():
return int(value)
if value.lower() in ("true", "false"):
return value.lower() == "true"
return value
def _convert_to_settings(
engine: str | None,
conn_max_age: int | None,
conn_health_checks: bool,
disable_server_side_cursors: bool,
ssl_require: bool,
test_options: dict[str, Any] | None,
) -> DBConfig:
settings: DBConfig = {
"CONN_MAX_AGE": conn_max_age,
"CONN_HEALTH_CHECKS": conn_health_checks,
"DISABLE_SERVER_SIDE_CURSORS": disable_server_side_cursors,
}
if engine:
settings["ENGINE"] = engine
if ssl_require:
settings["OPTIONS"] = {}
settings["OPTIONS"]["sslmode"] = "require"
)
if test_options:
settings["TEST"] = test_options
return settings
parsed_config.update(
{
'TEST': test_options,
}
)
# Pass the query string into OPTIONS.
options: Dict[str, Any] = {}
for key, values in query.items():
if spliturl.scheme == "mysql" and key == "ssl-ca":
options["ssl"] = {"ca": values[-1]}
continue
try:
options[key] = int(values[-1])
except (TypeError, ValueError):
options[key] = values[-1]
if ssl_require:
options["sslmode"] = "require"
# Support for Postgres Schema URLs
if "currentSchema" in options and engine in (
"django.contrib.gis.db.backends.postgis",
"django.db.backends.postgresql_psycopg2",
"django.db.backends.postgresql",
"django_redshift_backend",
"timescale.db.backends.postgresql",
"timescale.db.backends.postgis",
):
options["options"] = "-c search_path={0}".format(options.pop("currentSchema"))
if options:
parsed_config["OPTIONS"] = options
return parsed_config

View file

@ -1,79 +1,8 @@
[project]
name = "dj-database-url"
version = "0.0.0"
description = "Use Database URLs in your Django Application."
authors = [
{ name = "Jazzband community" }
]
readme = "README.rst"
requires-python = ">=3.10"
license = "BSD-3-Clause"
license-files = ["LICENSE"]
dependencies = [
"django>=4.2",
]
classifiers = [
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
[project.urls]
Homepage = "https://jazzband.co/projects/dj-database-url"
Changelog = "https://github.com/jazzband/dj-database-url/blob/master/CHANGELOG.md"
Funding = "https://psfmember.org/civicrm/contribute/transact/?reset=1&id=34"
Bug = "https://github.com/jazzband/dj-database-url/issues"
[build-system]
requires = ["uv_build>=0.9.17,<0.10.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "dj_database_url"
module-root = ""
source-include = ["dj_database_url/py.typed", "tests/**"]
[tool.ruff]
line-length = 88
[tool.ruff.lint]
extend-select = ["B", "I"]
[tool.ruff.format]
quote-style = "preserve"
[tool.black]
skip-string-normalization = 1
[tool.mypy]
show_error_codes=true
disallow_untyped_defs=true
disallow_untyped_calls=true
warn_redundant_casts=true
[tool.pyright]
typeCheckingMode = "strict"
[dependency-groups]
dev = [
"coverage>=7.13.0",
"mypy>=1.19.1",
"pyright>=1.1.407",
"pytest>=9.0.2",
"ruff>=0.14.10",
"setuptools>=80.9.0",
"twine>=6.2.0",
"wheel>=0.45.1",
]

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
coverage
mypy

49
setup.py Normal file
View file

@ -0,0 +1,49 @@
from pathlib import Path
from setuptools import setup
readme = Path("README.rst").read_text()
setup(
name="dj-database-url",
version="2.1.0",
url="https://github.com/jazzband/dj-database-url",
license="BSD",
author="Original Author: Kenneth Reitz, Maintained by: JazzBand Community",
description="Use Database URLs in your Django Application.",
long_description=readme,
long_description_content_type="text/x-rst",
packages=["dj_database_url"],
install_requires=["Django>=3.2", "typing_extensions >= 3.10.0.0"],
include_package_data=True,
package_data={
"dj_database_url": ["py.typed"],
},
platforms="any",
project_urls={
"GitHub": "https://github.com/jazzband/dj-database-url/",
"Release log": (
"https://github.com/jazzband/dj-database-url/blob/master/CHANGELOG.md"
),
},
classifiers=[
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
],
)

View file

@ -1,10 +1,6 @@
# pyright: reportTypedDictNotRequiredAccess=false
import os
import re
import unittest
from unittest import mock
from urllib.parse import uses_netloc
import dj_database_url
@ -12,10 +8,9 @@ POSTGIS_URL = "postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compu
class DatabaseTestSuite(unittest.TestCase):
def test_postgres_parsing(self) -> None:
url = dj_database_url.parse(
"postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
)
def test_postgres_parsing(self):
url = "postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -24,10 +19,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_postgres_unix_socket_parsing(self) -> None:
url = dj_database_url.parse(
"postgres://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
)
def test_postgres_unix_socket_parsing(self):
url = "postgres://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -36,9 +30,8 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == ""
assert url["PORT"] == ""
url = dj_database_url.parse(
"postgres://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
)
url = "postgres://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["HOST"] == "/Users/postgres/RuN"
@ -46,10 +39,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == ""
assert url["PORT"] == ""
def test_postgres_google_cloud_parsing(self) -> None:
url = dj_database_url.parse(
"postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@%2Fcloudsql%2Fproject_id%3Aregion%3Ainstance_id/d8r82722r2kuvn"
)
def test_postgres_google_cloud_parsing(self):
url = "postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@%2Fcloudsql%2Fproject_id%3Aregion%3Ainstance_id/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -58,10 +50,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == ""
def test_ipv6_parsing(self) -> None:
url = dj_database_url.parse(
"postgres://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
)
def test_ipv6_parsing(self):
url = "postgres://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -70,10 +61,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_postgres_search_path_parsing(self) -> None:
url = dj_database_url.parse(
"postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
)
def test_postgres_search_path_parsing(self):
url = "postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@ -83,10 +73,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
assert "currentSchema" not in url["OPTIONS"]
def test_postgres_parsing_with_special_characters(self) -> None:
url = dj_database_url.parse(
"postgres://%23user:%23password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
)
def test_postgres_parsing_with_special_characters(self):
url = "postgres://%23user:%23password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["NAME"] == "#database"
@ -95,26 +84,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "#password"
assert url["PORT"] == 5431
def test_postgres_parsing_with_int_bool_str_query_string(self) -> None:
url = dj_database_url.parse(
"postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?server_side_binding=true&timeout=20&service=my_service&passfile=.my_pgpass"
)
assert url["ENGINE"] == "django.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
assert url["USER"] == "uf07k1i6d8ia0v"
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
assert url["OPTIONS"]["server_side_binding"] is True
assert url["OPTIONS"]["timeout"] == 20
assert url["OPTIONS"]["service"] == "my_service"
assert url["OPTIONS"]["passfile"] == ".my_pgpass"
def test_postgis_parsing(self) -> None:
url = dj_database_url.parse(
"postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
)
def test_postgis_parsing(self):
url = "postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.contrib.gis.db.backends.postgis"
assert url["NAME"] == "d8r82722r2kuvn"
@ -123,10 +95,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_postgis_search_path_parsing(self) -> None:
url = dj_database_url.parse(
"postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
)
def test_postgis_search_path_parsing(self):
url = "postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.contrib.gis.db.backends.postgis"
assert url["NAME"] == "d8r82722r2kuvn"
assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@ -136,10 +107,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
assert "currentSchema" not in url["OPTIONS"]
def test_mysql_gis_parsing(self) -> None:
url = dj_database_url.parse(
"mysqlgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
)
def test_mysql_gis_parsing(self):
url = "mysqlgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.contrib.gis.db.backends.mysql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -148,10 +118,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_mysql_connector_parsing(self) -> None:
url = dj_database_url.parse(
"mysql-connector://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
)
def test_mysql_connector_parsing(self):
url = "mysql-connector://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "mysql.connector.django"
assert url["NAME"] == "d8r82722r2kuvn"
@ -160,7 +129,7 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_config_test_options(self) -> None:
def test_config_test_options(self):
with mock.patch.dict(
os.environ,
{
@ -174,10 +143,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url['TEST']['NAME'] == 'mytestdatabase'
def test_cleardb_parsing(self) -> None:
url = dj_database_url.parse(
"mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
)
def test_cleardb_parsing(self):
url = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.mysql"
assert url["NAME"] == "heroku_97681db3eff7580"
@ -186,7 +154,7 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "69772142"
assert url["PORT"] == ""
def test_database_url(self) -> None:
def test_database_url(self):
with mock.patch.dict(os.environ, clear=True):
a = dj_database_url.config()
assert not a
@ -206,46 +174,28 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_empty_sqlite_url(self) -> None:
url = dj_database_url.parse("sqlite://")
def test_empty_sqlite_url(self):
url = "sqlite://"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.sqlite3"
assert url["NAME"] == ":memory:"
def test_memory_sqlite_url(self) -> None:
url = dj_database_url.parse("sqlite://:memory:")
def test_memory_sqlite_url(self):
url = "sqlite://:memory:"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.sqlite3"
assert url["NAME"] == ":memory:"
def test_sqlite_relative_url(self) -> None:
url = "sqlite:///db.sqlite3"
config = dj_database_url.parse(url)
assert config["ENGINE"] == "django.db.backends.sqlite3"
assert config["NAME"] == "db.sqlite3"
def test_sqlite_absolute_url(self) -> None:
# 4 slashes are needed:
# two are part of scheme
# one separates host:port from path
# and the fourth goes to "NAME" value
url = "sqlite:////db.sqlite3"
config = dj_database_url.parse(url)
assert config["ENGINE"] == "django.db.backends.sqlite3"
assert config["NAME"] == "/db.sqlite3"
def test_parse_engine_setting(self) -> None:
def test_parse_engine_setting(self):
engine = "django_mysqlpool.backends.mysqlpool"
url = dj_database_url.parse(
"mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true",
engine,
)
url = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
url = dj_database_url.parse(url, engine)
assert url["ENGINE"] == engine
def test_config_engine_setting(self) -> None:
def test_config_engine_setting(self):
engine = "django_mysqlpool.backends.mysqlpool"
with mock.patch.dict(
os.environ,
@ -257,17 +207,15 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["ENGINE"] == engine
def test_parse_conn_max_age_setting(self) -> None:
def test_parse_conn_max_age_setting(self):
conn_max_age = 600
url = dj_database_url.parse(
"mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true",
conn_max_age=conn_max_age,
)
url = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
url = dj_database_url.parse(url, conn_max_age=conn_max_age)
assert url["CONN_MAX_AGE"] == conn_max_age
def test_config_conn_max_age_setting_none(self) -> None:
conn_max_age = None
def test_config_conn_max_age_setting(self):
conn_max_age = 600
with mock.patch.dict(
os.environ,
{
@ -278,7 +226,7 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["CONN_MAX_AGE"] == conn_max_age
def test_database_url_with_options(self) -> None:
def test_database_url_with_options(self):
# Test full options
with mock.patch.dict(
os.environ,
@ -309,7 +257,7 @@ class DatabaseTestSuite(unittest.TestCase):
url = dj_database_url.config()
assert "OPTIONS" not in url
def test_mysql_database_url_with_sslca_options(self) -> None:
def test_mysql_database_url_with_sslca_options(self):
with mock.patch.dict(
os.environ,
{
@ -336,8 +284,9 @@ class DatabaseTestSuite(unittest.TestCase):
url = dj_database_url.config()
assert "OPTIONS" not in url
def test_oracle_parsing(self) -> None:
url = dj_database_url.parse("oracle://scott:tiger@oraclehost:1521/hr")
def test_oracle_parsing(self):
url = "oracle://scott:tiger@oraclehost:1521/hr"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.oracle"
assert url["NAME"] == "hr"
@ -346,8 +295,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "tiger"
assert url["PORT"] == "1521"
def test_oracle_gis_parsing(self) -> None:
url = dj_database_url.parse("oraclegis://scott:tiger@oraclehost:1521/hr")
def test_oracle_gis_parsing(self):
url = "oraclegis://scott:tiger@oraclehost:1521/hr"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.contrib.gis.db.backends.oracle"
assert url["NAME"] == "hr"
@ -356,13 +306,14 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "tiger"
assert url["PORT"] == 1521
def test_oracle_dsn_parsing(self) -> None:
url = dj_database_url.parse(
def test_oracle_dsn_parsing(self):
url = (
"oracle://scott:tiger@/"
"(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)"
"(HOST=oraclehost)(PORT=1521)))"
"(CONNECT_DATA=(SID=hr)))"
)
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.oracle"
assert url["USER"] == "scott"
@ -378,8 +329,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["NAME"] == dsn
def test_oracle_tns_parsing(self) -> None:
url = dj_database_url.parse("oracle://scott:tiger@/tnsname")
def test_oracle_tns_parsing(self):
url = "oracle://scott:tiger@/tnsname"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django.db.backends.oracle"
assert url["USER"] == "scott"
@ -388,10 +340,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["HOST"] == ""
assert url["PORT"] == ""
def test_redshift_parsing(self) -> None:
url = dj_database_url.parse(
"redshift://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5439/d8r82722r2kuvn?currentSchema=otherschema"
)
def test_redshift_parsing(self):
url = "redshift://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5439/d8r82722r2kuvn?currentSchema=otherschema"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "django_redshift_backend"
assert url["NAME"] == "d8r82722r2kuvn"
@ -402,10 +353,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
assert "currentSchema" not in url["OPTIONS"]
def test_mssql_parsing(self) -> None:
url = dj_database_url.parse(
"mssql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server"
)
def test_mssql_parsing(self):
url = "mssql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "sql_server.pyodbc"
assert url["NAME"] == "d8r82722r2kuvn"
@ -416,10 +366,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["driver"] == "ODBC Driver 13 for SQL Server"
assert "currentSchema" not in url["OPTIONS"]
def test_mssql_instance_port_parsing(self) -> None:
url = dj_database_url.parse(
"mssql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com\\insnsnss:12345/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server"
)
def test_mssql_instance_port_parsing(self):
url = "mssql://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com\\insnsnss:12345/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "sql_server.pyodbc"
assert url["NAME"] == "d8r82722r2kuvn"
@ -430,10 +379,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["driver"] == "ODBC Driver 13 for SQL Server"
assert "currentSchema" not in url["OPTIONS"]
def test_cockroach(self) -> None:
url = dj_database_url.parse(
"cockroach://testuser:testpass@testhost:26257/cockroach?sslmode=verify-full&sslrootcert=/certs/ca.crt&sslcert=/certs/client.myprojectuser.crt&sslkey=/certs/client.myprojectuser.key"
)
def test_cockroach(self):
url = "cockroach://testuser:testpass@testhost:26257/cockroach?sslmode=verify-full&sslrootcert=/certs/ca.crt&sslcert=/certs/client.myprojectuser.crt&sslkey=/certs/client.myprojectuser.key"
url = dj_database_url.parse(url)
assert url['ENGINE'] == 'django_cockroachdb'
assert url['NAME'] == 'cockroach'
assert url['HOST'] == 'testhost'
@ -445,10 +393,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url['OPTIONS']['sslcert'] == '/certs/client.myprojectuser.crt'
assert url['OPTIONS']['sslkey'] == '/certs/client.myprojectuser.key'
def test_mssqlms_parsing(self) -> None:
url = dj_database_url.parse(
"mssqlms://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server"
)
def test_mssqlms_parsing(self):
url = "mssqlms://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC Driver 13 for SQL Server"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "mssql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -459,10 +406,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["driver"] == "ODBC Driver 13 for SQL Server"
assert "currentSchema" not in url["OPTIONS"]
def test_timescale_parsing(self) -> None:
url = dj_database_url.parse(
"timescale://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
)
def test_timescale_parsing(self):
url = "timescale://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -471,10 +417,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_timescale_unix_socket_parsing(self) -> None:
url = dj_database_url.parse(
"timescale://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
)
def test_timescale_unix_socket_parsing(self):
url = "timescale://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -483,9 +428,8 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == ""
assert url["PORT"] == ""
url = dj_database_url.parse(
"timescale://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
)
url = "timescale://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgresql"
assert url["HOST"] == "/Users/postgres/RuN"
@ -493,10 +437,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == ""
assert url["PORT"] == ""
def test_timescale_ipv6_parsing(self) -> None:
url = dj_database_url.parse(
"timescale://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
)
def test_timescale_ipv6_parsing(self):
url = "timescale://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
@ -505,10 +448,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_timescale_search_path_parsing(self) -> None:
url = dj_database_url.parse(
"timescale://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
)
def test_timescale_search_path_parsing(self):
url = "timescale://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgresql"
assert url["NAME"] == "d8r82722r2kuvn"
assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@ -518,10 +460,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
assert "currentSchema" not in url["OPTIONS"]
def test_timescale_parsing_with_special_characters(self) -> None:
url = dj_database_url.parse(
"timescale://%23user:%23password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
)
def test_timescale_parsing_with_special_characters(self):
url = "timescale://%23user:%23password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgresql"
assert url["NAME"] == "#database"
@ -530,10 +471,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "#password"
assert url["PORT"] == 5431
def test_timescalegis_parsing(self) -> None:
url = dj_database_url.parse(
"timescalegis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
)
def test_timescalegis_parsing(self):
url = "timescalegis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgis"
assert url["NAME"] == "d8r82722r2kuvn"
@ -542,10 +482,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_timescalegis_unix_socket_parsing(self) -> None:
url = dj_database_url.parse(
"timescalegis://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
)
def test_timescalegis_unix_socket_parsing(self):
url = "timescalegis://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgis"
assert url["NAME"] == "d8r82722r2kuvn"
@ -554,9 +493,8 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == ""
assert url["PORT"] == ""
url = dj_database_url.parse(
"timescalegis://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
)
url = "timescalegis://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgis"
assert url["HOST"] == "/Users/postgres/RuN"
@ -564,10 +502,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == ""
assert url["PORT"] == ""
def test_timescalegis_ipv6_parsing(self) -> None:
url = dj_database_url.parse(
"timescalegis://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
)
def test_timescalegis_ipv6_parsing(self):
url = "timescalegis://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgis"
assert url["NAME"] == "d8r82722r2kuvn"
@ -576,10 +513,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "wegauwhgeuioweg"
assert url["PORT"] == 5431
def test_timescalegis_search_path_parsing(self) -> None:
url = dj_database_url.parse(
"timescalegis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
)
def test_timescalegis_search_path_parsing(self):
url = "timescalegis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgis"
assert url["NAME"] == "d8r82722r2kuvn"
assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@ -589,10 +525,9 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
assert "currentSchema" not in url["OPTIONS"]
def test_timescalegis_parsing_with_special_characters(self) -> None:
url = dj_database_url.parse(
"timescalegis://%23user:%23password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
)
def test_timescalegis_parsing_with_special_characters(self):
url = "timescalegis://%23user:%23password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
url = dj_database_url.parse(url)
assert url["ENGINE"] == "timescale.db.backends.postgis"
assert url["NAME"] == "#database"
@ -601,7 +536,7 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["PASSWORD"] == "#password"
assert url["PORT"] == 5431
def test_persistent_connection_variables(self) -> None:
def test_persistent_connection_variables(self):
url = dj_database_url.parse(
"sqlite://myfile.db", conn_max_age=600, conn_health_checks=True
)
@ -609,7 +544,7 @@ class DatabaseTestSuite(unittest.TestCase):
assert url["CONN_MAX_AGE"] == 600
assert url["CONN_HEALTH_CHECKS"] is True
def test_sqlite_memory_persistent_connection_variables(self) -> None:
def test_sqlite_memory_persistent_connection_variables(self):
# memory sqlite ignores connection.close(), so persistent connection
# variables arent required
url = dj_database_url.parse(
@ -623,13 +558,13 @@ class DatabaseTestSuite(unittest.TestCase):
os.environ,
{"DATABASE_URL": "postgres://user:password@instance.amazonaws.com:5431/d8r8?"},
)
def test_persistent_connection_variables_config(self) -> None:
def test_persistent_connection_variables_config(self):
url = dj_database_url.config(conn_max_age=600, conn_health_checks=True)
assert url["CONN_MAX_AGE"] == 600
assert url["CONN_HEALTH_CHECKS"] is True
def test_no_env_variable(self) -> None:
def test_no_env_variable(self):
with self.assertLogs() as cm:
with mock.patch.dict(os.environ, clear=True):
url = dj_database_url.config()
@ -638,60 +573,24 @@ class DatabaseTestSuite(unittest.TestCase):
'WARNING:root:No DATABASE_URL environment variable set, and so no databases setup'
], cm.output
def test_credentials_unquoted__raise_value_error(self) -> None:
expected_message = (
"This string is not a valid url, possibly because some of its parts "
r"is not properly urllib.parse.quote()'ed."
)
with self.assertRaisesRegex(ValueError, re.escape(expected_message)):
dj_database_url.parse("postgres://user:passw#ord!@localhost/foobar")
def test_credentials_quoted__ok(self) -> None:
url = "postgres://user%40domain:p%23ssword!@localhost/foobar"
config = dj_database_url.parse(url)
assert config["USER"] == "user@domain"
assert config["PASSWORD"] == "p#ssword!"
def test_unknown_scheme__raise_value_error(self) -> None:
expected_message = (
"Scheme 'unknown-scheme://' is unknown. "
"Did you forget to register custom backend? Following schemes have registered backends:"
)
with self.assertRaisesRegex(ValueError, re.escape(expected_message)):
dj_database_url.parse("unknown-scheme://user:password@localhost/foobar")
def test_register_multiple_times__no_duplicates_in_uses_netloc(self) -> None:
# make sure that when register() function is misused,
# it won't pollute urllib.parse.uses_netloc list with duplicates.
# Otherwise, it might cause performance issue if some code assumes that
# that list is short and performs linear search on it.
dj_database_url.register("django.contrib.db.backends.bag_end", "bag-end")
dj_database_url.register("django.contrib.db.backends.bag_end", "bag-end")
assert len(uses_netloc) == len(set(uses_netloc))
def test_bad_url_parsing(self):
with self.assertRaisesRegex(ValueError, "No support for 'foo'. We support: "):
dj_database_url.parse("foo://bar")
@mock.patch.dict(
os.environ,
{"DATABASE_URL": "postgres://user:password@instance.amazonaws.com:5431/d8r8?"},
)
def test_ssl_require(self) -> None:
def test_ssl_require(self):
url = dj_database_url.config(ssl_require=True)
assert url["OPTIONS"] == {'sslmode': 'require'}
def test_options_int_values(self) -> None:
def test_options_int_values(self):
"""Ensure that options with integer values are parsed correctly."""
url = dj_database_url.parse(
"mysql://user:pw@127.0.0.1:15036/db?connect_timeout=3"
"mysql://user:pw@127.0.0.1:15036/db?connect_timout=3"
)
assert url["OPTIONS"] == {'connect_timeout': 3}
@mock.patch.dict(
os.environ,
{"DATABASE_URL": "postgres://user:password@instance.amazonaws.com:5431/d8r8?"},
)
def test_server_side_cursors__config(self) -> None:
url = dj_database_url.config(disable_server_side_cursors=True)
assert url["DISABLE_SERVER_SIDE_CURSORS"] is True
assert url["OPTIONS"] == {'connect_timout': 3}
if __name__ == "__main__":

1033
uv.lock

File diff suppressed because it is too large Load diff