From d60104dfdcef22816252d42b126764931d4b44da Mon Sep 17 00:00:00 2001 From: Lino Helms Date: Sun, 10 Jul 2022 11:16:06 +0200 Subject: [PATCH] Added support for Timescale and Timescale (GIS) Timescale is a Postgres based database for time series data and it's supported via the django-timescaledb package. [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.rst | 62 ++++++++++--------- dj_database_url.py | 6 ++ test_dj_database_url.py | 130 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index 47665d2..a83f246 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Supported Databases ------------------- Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS), -Oracle, Oracle (GIS), Redshift, CockroachDB, and SQLite. +Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and SQLite. Installation ------------ @@ -85,33 +85,37 @@ and should instead be passed as:: URL schema ---------- -+-------------+-----------------------------------------------+--------------------------------------------------+ -| Engine | Django Backend | URL | -+=============+===============================================+==================================================+ -| PostgreSQL | ``django.db.backends.postgresql`` [1]_ | ``postgres://USER:PASSWORD@HOST:PORT/NAME`` [2]_ | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| PostGIS | ``django.contrib.gis.db.backends.postgis`` | ``postgis://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| MSSQL | ``sql_server.pyodbc`` | ``mssql://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| MSSQL [5]_ | ``mssql`` | ``mssqlms://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| MySQL | ``django.db.backends.mysql`` | ``mysql://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| MySQL (GIS) | ``django.contrib.gis.db.backends.mysql`` | ``mysqlgis://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| SQLite | ``django.db.backends.sqlite3`` | ``sqlite:///PATH`` [3]_ | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| SpatiaLite | ``django.contrib.gis.db.backends.spatialite`` | ``spatialite:///PATH`` [3]_ | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| Oracle | ``django.db.backends.oracle`` | ``oracle://USER:PASSWORD@HOST:PORT/NAME`` [4]_ | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| Oracle (GIS)| ``django.contrib.gis.db.backends.oracle`` | ``oraclegis://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| Redshift | ``django_redshift_backend`` | ``redshift://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ -| CockroachDB | ``django_cockroachdb`` | ``cockroach://USER:PASSWORD@HOST:PORT/NAME`` | -+-------------+-----------------------------------------------+--------------------------------------------------+ ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| Engine | Django Backend | URL | ++======================+===============================================+==================================================+ +| PostgreSQL | ``django.db.backends.postgresql`` [1]_ | ``postgres://USER:PASSWORD@HOST:PORT/NAME`` [2]_ | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| PostGIS | ``django.contrib.gis.db.backends.postgis`` | ``postgis://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| MSSQL | ``sql_server.pyodbc`` | ``mssql://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| MSSQL [5]_ | ``mssql`` | ``mssqlms://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| MySQL | ``django.db.backends.mysql`` | ``mysql://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| MySQL (GIS) | ``django.contrib.gis.db.backends.mysql`` | ``mysqlgis://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| SQLite | ``django.db.backends.sqlite3`` | ``sqlite:///PATH`` [3]_ | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| SpatiaLite | ``django.contrib.gis.db.backends.spatialite`` | ``spatialite:///PATH`` [3]_ | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| Oracle | ``django.db.backends.oracle`` | ``oracle://USER:PASSWORD@HOST:PORT/NAME`` [4]_ | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| Oracle (GIS) | ``django.contrib.gis.db.backends.oracle`` | ``oraclegis://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| Redshift | ``django_redshift_backend`` | ``redshift://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| CockroachDB | ``django_cockroachdb`` | ``cockroach://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| Timescale [6]_ | ``timescale.db.backends.postgresql`` | ``timescale://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ +| Timescale (GIS) [6]_ | ``timescale.db.backend.postgis`` | ``timescalegis://USER:PASSWORD@HOST:PORT/NAME`` | ++----------------------+-----------------------------------------------+--------------------------------------------------+ .. [1] The django.db.backends.postgresql backend is named django.db.backends.postgresql_psycopg2 in older releases. For backwards compatibility, the old name still works in newer versions. (The new name does not work in older versions). @@ -127,6 +131,8 @@ URL schema by ``:`` not by ``/``. Also you can omit ``HOST`` and ``PORT`` and provide a full DSN string or TNS name in ``NAME`` part. .. [5] Microsoft official `mssql-django `_ adapter. +.. [6] Using the django-timescaledb Package which must be installed. + Contributing ------------ diff --git a/dj_database_url.py b/dj_database_url.py index d4de026..59e8506 100644 --- a/dj_database_url.py +++ b/dj_database_url.py @@ -26,6 +26,8 @@ urlparse.uses_netloc.append("oraclegis") urlparse.uses_netloc.append("redshift") urlparse.uses_netloc.append("cockroach") +urlparse.uses_netloc.append("timescale") +urlparse.uses_netloc.append("timescalegis") DEFAULT_ENV = "DATABASE_URL" @@ -43,6 +45,8 @@ "oraclegis": "django.contrib.gis.db.backends.oracle", "redshift": "django_redshift_backend", "cockroach": "django_cockroachdb", + "timescale": "timescale.db.backends.postgresql", + "timescalegis": "timescale.db.backends.postgis", } # https://docs.djangoproject.com/en/2.0/releases/2.0/#id1 @@ -147,6 +151,8 @@ def parse(url, engine=None, conn_max_age=0, ssl_require=False): "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")) diff --git a/test_dj_database_url.py b/test_dj_database_url.py index 2998729..09918da 100644 --- a/test_dj_database_url.py +++ b/test_dj_database_url.py @@ -371,6 +371,136 @@ def test_mssqlms_parsing(self): assert url["OPTIONS"]["driver"] == "ODBC Driver 13 for SQL Server" assert "currentSchema" not in url["OPTIONS"] + 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" + assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com" + assert url["USER"] == "uf07k1i6d8ia0v" + assert url["PASSWORD"] == "wegauwhgeuioweg" + assert url["PORT"] == 5431 + + 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" + assert url["HOST"] == "/var/run/postgresql" + assert url["USER"] == "" + assert url["PASSWORD"] == "" + assert url["PORT"] == "" + + 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" + assert url["USER"] == "" + assert url["PASSWORD"] == "" + assert url["PORT"] == "" + + 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" + assert url["HOST"] == "2001:db8:1234::1234:5678:90af" + assert url["USER"] == "ieRaekei9wilaim7" + assert url["PASSWORD"] == "wegauwhgeuioweg" + assert url["PORT"] == 5431 + + 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" + assert url["USER"] == "uf07k1i6d8ia0v" + assert url["PASSWORD"] == "wegauwhgeuioweg" + assert url["PORT"] == 5431 + assert url["OPTIONS"]["options"] == "-c search_path=otherschema" + assert "currentSchema" not in url["OPTIONS"] + + 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" + assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com" + assert url["USER"] == "#user" + assert url["PASSWORD"] == "#password" + assert url["PORT"] == 5431 + + 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" + assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com" + assert url["USER"] == "uf07k1i6d8ia0v" + assert url["PASSWORD"] == "wegauwhgeuioweg" + assert url["PORT"] == 5431 + + 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" + assert url["HOST"] == "/var/run/postgresql" + assert url["USER"] == "" + assert url["PASSWORD"] == "" + assert url["PORT"] == "" + + 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" + assert url["USER"] == "" + assert url["PASSWORD"] == "" + assert url["PORT"] == "" + + 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" + assert url["HOST"] == "2001:db8:1234::1234:5678:90af" + assert url["USER"] == "ieRaekei9wilaim7" + assert url["PASSWORD"] == "wegauwhgeuioweg" + assert url["PORT"] == 5431 + + 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" + assert url["USER"] == "uf07k1i6d8ia0v" + assert url["PASSWORD"] == "wegauwhgeuioweg" + assert url["PORT"] == 5431 + assert url["OPTIONS"]["options"] == "-c search_path=otherschema" + assert "currentSchema" not in url["OPTIONS"] + + 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" + assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com" + assert url["USER"] == "#user" + assert url["PASSWORD"] == "#password" + assert url["PORT"] == 5431 + if __name__ == "__main__": unittest.main()