hdx-python-database 1.3.9__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/PKG-INFO +5 -4
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/documentation/index.md +9 -3
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/pyproject.toml +4 -4
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/_version.py +2 -2
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/database.py +9 -1
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/dburi.py +5 -5
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/no_timezone.py +4 -4
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/postgresql.py +6 -2
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/views.py +3 -2
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/test_database.py +3 -3
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/test_dburi.py +13 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/test_postgresql.py +12 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/test_ssh.py +9 -0
- hdx_python_database-1.4.0/tests/hdx/database/test_views.py +23 -0
- hdx_python_database-1.4.0/uv.lock +1505 -0
- hdx_python_database-1.3.9/uv.lock +0 -1513
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/.coveragerc +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/.github/workflows/publish.yaml +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/.github/workflows/run-python-tests.yaml +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/.gitignore +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/.pre-commit-config.yaml +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/CLAUDE.md +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/CONTRIBUTING.md +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/LICENSE +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/README.md +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/documentation/mkdocs.yaml +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/__init__.py +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/utils.py +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/src/hdx/database/with_timezone.py +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/fixtures/test.db +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/__init__.py +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/conftest.py +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/dbtestdate.py +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/test_drop_recreate.py +0 -0
- {hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/test_reflect.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hdx-python-database
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: HDX Python database utilities
|
|
5
5
|
Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-database
|
|
6
6
|
Author-email: Michael Rans <rans@un.org>
|
|
@@ -17,14 +17,15 @@ Classifier: Operating System :: POSIX :: Linux
|
|
|
17
17
|
Classifier: Operating System :: Unix
|
|
18
18
|
Classifier: Programming Language :: Python
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: sqlalchemy>=2
|
|
22
22
|
Provides-Extra: docs
|
|
23
23
|
Requires-Dist: mkapi; extra == 'docs'
|
|
24
24
|
Provides-Extra: postgresql
|
|
25
25
|
Requires-Dist: psycopg[binary]; extra == 'postgresql'
|
|
26
26
|
Provides-Extra: sshtunnel
|
|
27
|
-
Requires-Dist:
|
|
27
|
+
Requires-Dist: cryptography>=48.0.1; extra == 'sshtunnel'
|
|
28
|
+
Requires-Dist: sshtunnel>=0.5.1; extra == 'sshtunnel'
|
|
28
29
|
Description-Content-Type: text/markdown
|
|
29
30
|
|
|
30
31
|
[](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yaml)
|
|
@@ -79,20 +79,26 @@ tunnel (which requires installing `hdx-python-database[sshtunnel]`):
|
|
|
79
79
|
|
|
80
80
|
# Get SQLAlchemy session object given database parameters and
|
|
81
81
|
# if needed SSH parameters. If database is PostgreSQL, will poll
|
|
82
|
-
# till it is up.
|
|
82
|
+
# till it is up (or until pg_timeout seconds if supplied).
|
|
83
83
|
from hdx.database import Database
|
|
84
84
|
with Database(database="db", host="1.2.3.4", username="user",
|
|
85
85
|
password="pass", dialect="dialect", driver="driver",
|
|
86
86
|
|
|
87
87
|
ssh_host="5.6.7.8", ssh_port=2222, ssh_username="sshuser",
|
|
88
|
-
ssh_private_key="path_to_key",
|
|
89
|
-
reflect=True, prepare_fn=prepare_views
|
|
88
|
+
ssh_private_key="path_to_key", ssh_host_key="AAAAC3...",
|
|
89
|
+
db_has_tz=True, reflect=True, prepare_fn=prepare_views,
|
|
90
|
+
pg_timeout=60) as database:
|
|
90
91
|
engine = database.get_engine()
|
|
91
92
|
session = database.get_session()
|
|
92
93
|
reflected_classes = database.get_reflected_classes()
|
|
93
94
|
views = database.get_prepare_results()
|
|
94
95
|
session.query(...)
|
|
95
96
|
|
|
97
|
+
`ssh_host_key` is the SSH server's public host key, used to verify the server's
|
|
98
|
+
identity. Without it, the tunnel is open to man-in-the-middle attacks and a
|
|
99
|
+
warning is logged. It can be obtained with `ssh-keyscan -t ed25519 5.6.7.8`
|
|
100
|
+
(the base64 field of the output).
|
|
101
|
+
|
|
96
102
|
`db_has_tz` which defaults to `False` indicates whether database datetime
|
|
97
103
|
columns have timezones. If `db_has_tz` is `True`, use `Base` from
|
|
98
104
|
`hdx.database.with_timezone`, otherwise use `Base` from
|
|
@@ -26,20 +26,20 @@ classifiers = [
|
|
|
26
26
|
]
|
|
27
27
|
readme = "README.md"
|
|
28
28
|
dynamic = ["version"]
|
|
29
|
-
requires-python = ">=3.
|
|
29
|
+
requires-python = ">=3.11"
|
|
30
30
|
|
|
31
31
|
dependencies = [
|
|
32
32
|
"sqlalchemy>=2",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
|
-
sshtunnel = ["sshtunnel"]
|
|
36
|
+
sshtunnel = ["sshtunnel>=0.5.1", "cryptography>=48.0.1"]
|
|
37
37
|
postgresql = ["psycopg[binary]"]
|
|
38
38
|
docs = ["mkapi"]
|
|
39
39
|
|
|
40
40
|
[dependency-groups]
|
|
41
41
|
dev = [
|
|
42
|
-
"sshtunnel",
|
|
42
|
+
"sshtunnel>=0.5.1",
|
|
43
43
|
"psycopg[binary]",
|
|
44
44
|
"pytest",
|
|
45
45
|
"pytest-cov",
|
|
@@ -75,7 +75,7 @@ allow-direct-references = true
|
|
|
75
75
|
# ----------------------------------------------------------------------------
|
|
76
76
|
|
|
77
77
|
[tool.ruff]
|
|
78
|
-
target-version = "
|
|
78
|
+
target-version = "py311"
|
|
79
79
|
src = ["src"]
|
|
80
80
|
exclude = ["_version.py"]
|
|
81
81
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 4, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -65,6 +65,7 @@ class Database:
|
|
|
65
65
|
reflect (bool): Whether to reflect existing tables. Defaults to False.
|
|
66
66
|
**kwargs: See below
|
|
67
67
|
pg_restore_file (Path | str): Restore database from pg_restore file
|
|
68
|
+
pg_timeout (int): Seconds to wait for PostgreSQL. Defaults to None (wait forever).
|
|
68
69
|
recreate_schema (bool): Whether to recreate schema
|
|
69
70
|
schema_name (str): Database schema name. Defaults to "public".
|
|
70
71
|
prepare_fn (Callable[[], None]]): Function to call before Base.metadata.create_all.
|
|
@@ -73,6 +74,7 @@ class Database:
|
|
|
73
74
|
ssh_username (str): SSH username
|
|
74
75
|
ssh_password (str): SSH password
|
|
75
76
|
ssh_private_key: Location of SSH private key (instead of ssh_password)
|
|
77
|
+
ssh_host_key (str): SSH server host key to verify against. Strongly recommended.
|
|
76
78
|
For more advanced usage, see SSHTunnelForwarder documentation.
|
|
77
79
|
|
|
78
80
|
"""
|
|
@@ -98,10 +100,12 @@ class Database:
|
|
|
98
100
|
schema_name = None
|
|
99
101
|
if len(kwargs) == 0:
|
|
100
102
|
pg_restore_file = None
|
|
103
|
+
pg_timeout = None
|
|
101
104
|
recreate_schema = False
|
|
102
105
|
prepare_fn = do_nothing_fn
|
|
103
106
|
else:
|
|
104
107
|
pg_restore_file = kwargs.pop("pg_restore_file", None)
|
|
108
|
+
pg_timeout = kwargs.pop("pg_timeout", None)
|
|
105
109
|
recreate_schema = kwargs.pop("recreate_schema", False)
|
|
106
110
|
schema_name = kwargs.pop("schema", "public")
|
|
107
111
|
prepare_fn = kwargs.pop("prepare_fn", do_nothing_fn)
|
|
@@ -116,6 +120,10 @@ class Database:
|
|
|
116
120
|
raise
|
|
117
121
|
ssh_host = kwargs["ssh_host"]
|
|
118
122
|
del kwargs["ssh_host"]
|
|
123
|
+
if "ssh_host_key" not in kwargs:
|
|
124
|
+
logger.warning(
|
|
125
|
+
"ssh_host_key not supplied: SSH server identity will not be verified."
|
|
126
|
+
)
|
|
119
127
|
ssh_port = kwargs.get("ssh_port")
|
|
120
128
|
if ssh_port is not None:
|
|
121
129
|
ssh_port = int(ssh_port)
|
|
@@ -147,7 +155,7 @@ class Database:
|
|
|
147
155
|
"No engine, database uri or connection parameters provided!"
|
|
148
156
|
)
|
|
149
157
|
if not engine and dialect == "postgresql":
|
|
150
|
-
wait_for_postgresql(db_uri)
|
|
158
|
+
wait_for_postgresql(db_uri, pg_timeout)
|
|
151
159
|
if pg_restore_file and db_uri:
|
|
152
160
|
restore_from_pgfile(db_uri, pg_restore_file)
|
|
153
161
|
if not table_base:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Connection URI utilities"""
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
|
-
from urllib.parse import urlsplit
|
|
4
|
+
from urllib.parse import quote_plus, unquote, urlsplit
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def get_params_from_connection_uri(
|
|
@@ -28,8 +28,8 @@ def get_params_from_connection_uri(
|
|
|
28
28
|
"database": result.path[1:],
|
|
29
29
|
"host": result.hostname,
|
|
30
30
|
"port": result.port,
|
|
31
|
-
"username": result.username,
|
|
32
|
-
"password": result.password,
|
|
31
|
+
"username": unquote(result.username) if result.username else None,
|
|
32
|
+
"password": unquote(result.password) if result.password else None,
|
|
33
33
|
"dialect": dialect,
|
|
34
34
|
}
|
|
35
35
|
if include_driver:
|
|
@@ -72,9 +72,9 @@ def get_connection_uri(
|
|
|
72
72
|
strings.append(f"+{driver}")
|
|
73
73
|
strings.append("://")
|
|
74
74
|
if username:
|
|
75
|
-
strings.append(username)
|
|
75
|
+
strings.append(quote_plus(username))
|
|
76
76
|
if password:
|
|
77
|
-
strings.append(f":{password}@")
|
|
77
|
+
strings.append(f":{quote_plus(password)}@")
|
|
78
78
|
else:
|
|
79
79
|
strings.append("@")
|
|
80
80
|
if host:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Utilities for database datetime columns without timezone"""
|
|
2
2
|
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import DateTime, TypeDecorator
|
|
6
6
|
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
|
@@ -19,7 +19,7 @@ class ConversionNoTZ(TypeDecorator):
|
|
|
19
19
|
if value is None:
|
|
20
20
|
return value
|
|
21
21
|
if value.tzinfo:
|
|
22
|
-
value = value.astimezone(
|
|
22
|
+
value = value.astimezone(UTC)
|
|
23
23
|
|
|
24
24
|
return value.replace(tzinfo=None)
|
|
25
25
|
|
|
@@ -27,9 +27,9 @@ class ConversionNoTZ(TypeDecorator):
|
|
|
27
27
|
if value is None:
|
|
28
28
|
return value
|
|
29
29
|
if value.tzinfo is None:
|
|
30
|
-
return value.replace(tzinfo=
|
|
30
|
+
return value.replace(tzinfo=UTC)
|
|
31
31
|
|
|
32
|
-
return value.astimezone(
|
|
32
|
+
return value.astimezone(UTC)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class Base(DeclarativeBase):
|
|
@@ -26,17 +26,19 @@ class PostgresError(Exception):
|
|
|
26
26
|
pass
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def wait_for_postgresql(db_uri: str) -> None:
|
|
29
|
+
def wait_for_postgresql(db_uri: str, timeout: int | None = None) -> None:
|
|
30
30
|
"""Waits for PostgreSQL database to be up
|
|
31
31
|
|
|
32
32
|
Args:
|
|
33
33
|
db_uri (str): Connection URI
|
|
34
|
+
timeout (Optional[int]): Seconds to wait. Defaults to None (wait forever).
|
|
34
35
|
|
|
35
36
|
Returns:
|
|
36
37
|
None
|
|
37
38
|
"""
|
|
38
39
|
connecting_string = "Checking for PostgreSQL..."
|
|
39
40
|
db_uri_nd = remove_driver_from_uri(db_uri)
|
|
41
|
+
start = time.monotonic()
|
|
40
42
|
while True:
|
|
41
43
|
try:
|
|
42
44
|
logger.info(connecting_string)
|
|
@@ -45,6 +47,8 @@ def wait_for_postgresql(db_uri: str) -> None:
|
|
|
45
47
|
logger.info("PostgreSQL is running!")
|
|
46
48
|
break
|
|
47
49
|
except psycopg.OperationalError:
|
|
50
|
+
if timeout is not None and time.monotonic() - start > timeout:
|
|
51
|
+
raise PostgresError(f"PostgreSQL not available after {timeout}s")
|
|
48
52
|
time.sleep(1)
|
|
49
53
|
|
|
50
54
|
|
|
@@ -59,7 +63,7 @@ def restore_from_pgfile(db_uri: str, pg_restore_file: Path | str) -> str:
|
|
|
59
63
|
Returns:
|
|
60
64
|
str: Output from the pg_restore command
|
|
61
65
|
"""
|
|
62
|
-
pg_restore_file = Path(pg_restore_file)
|
|
66
|
+
pg_restore_file = Path(pg_restore_file).absolute()
|
|
63
67
|
db_params = get_params_from_connection_uri(db_uri)
|
|
64
68
|
if pg_restore_file.suffix == ".xz":
|
|
65
69
|
decompress = subprocess.Popen(
|
|
@@ -24,12 +24,13 @@ class DropView(DDLElement):
|
|
|
24
24
|
|
|
25
25
|
@compiler.compiles(CreateView)
|
|
26
26
|
def _create_view(element, compiler, **kw):
|
|
27
|
-
|
|
27
|
+
name = compiler.preparer.quote(element.name)
|
|
28
|
+
return f"CREATE VIEW {name} AS {compiler.sql_compiler.process(element.selectable, literal_binds=True)}"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@compiler.compiles(DropView)
|
|
31
32
|
def _drop_view(element, compiler, **kw):
|
|
32
|
-
return f"DROP VIEW {element.name}"
|
|
33
|
+
return f"DROP VIEW {compiler.preparer.quote(element.name)}"
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def view_exists(ddl, target, connection, **kw):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Database Utility Tests"""
|
|
2
2
|
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
4
|
from os import remove
|
|
5
5
|
from os.path import exists, join
|
|
6
6
|
from tempfile import gettempdir
|
|
@@ -32,7 +32,7 @@ class TestDatabase:
|
|
|
32
32
|
dbsession = dbdatabase.get_session()
|
|
33
33
|
assert str(dbsession.bind.engine.url) == f"sqlite:///{dbpath}"
|
|
34
34
|
assert dbdatabase._base == NoTZBase
|
|
35
|
-
now = datetime(2022, 10, 20, 22, 35, 55, tzinfo=
|
|
35
|
+
now = datetime(2022, 10, 20, 22, 35, 55, tzinfo=UTC)
|
|
36
36
|
input_dbtestdate = DBTestDate(test_date=now)
|
|
37
37
|
dbsession.add(input_dbtestdate)
|
|
38
38
|
dbsession.commit()
|
|
@@ -42,7 +42,7 @@ class TestDatabase:
|
|
|
42
42
|
dbtestdate = dbsession.execute(select(date_view)).scalar_one()
|
|
43
43
|
assert dbtestdate == now
|
|
44
44
|
|
|
45
|
-
now = datetime(2023, 10, 20, 22, 35, 55, tzinfo=
|
|
45
|
+
now = datetime(2023, 10, 20, 22, 35, 55, tzinfo=UTC)
|
|
46
46
|
rows = [{"test_date": now}]
|
|
47
47
|
dbdatabase.batch_populate(rows, DBTestDate)
|
|
48
48
|
dbtestdate = dbsession.execute(select(DBTestDate)).all()[1][0].test_date
|
|
@@ -82,6 +82,19 @@ class TestDBURI:
|
|
|
82
82
|
result = get_connection_uri(**TestDBURI.params_sq_driver_none)
|
|
83
83
|
assert result == TestDBURI.connection_uri_sq
|
|
84
84
|
|
|
85
|
+
def test_special_characters(self):
|
|
86
|
+
params = {
|
|
87
|
+
**TestDBURI.params_pg,
|
|
88
|
+
"username": "my@user",
|
|
89
|
+
"password": "p@ss/w:rd#?",
|
|
90
|
+
}
|
|
91
|
+
result = get_connection_uri(**params)
|
|
92
|
+
assert (
|
|
93
|
+
result
|
|
94
|
+
== "postgresql+psycopg://my%40user:p%40ss%2Fw%3Ard%23%3F@myserver:1234/mydatabase"
|
|
95
|
+
)
|
|
96
|
+
assert get_params_from_connection_uri(result) == params
|
|
97
|
+
|
|
85
98
|
def test_remove_driver_from_uri(self):
|
|
86
99
|
db_uri_nd = remove_driver_from_uri(TestDBURI.connection_uri_pg)
|
|
87
100
|
assert db_uri_nd == TestDBURI.connection_uri_pg_no_driver
|
{hdx_python_database-1.3.9 → hdx_python_database-1.4.0}/tests/hdx/database/test_postgresql.py
RENAMED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""PostgreSQL Utility Tests"""
|
|
2
2
|
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import psycopg
|
|
3
6
|
import pytest
|
|
4
7
|
|
|
5
8
|
from . import PsycopgConnection
|
|
@@ -20,6 +23,15 @@ class TestPostgreSQL:
|
|
|
20
23
|
wait_for_postgresql(db_uri)
|
|
21
24
|
assert PsycopgConnection.connected is True
|
|
22
25
|
|
|
26
|
+
def test_wait_for_postgresql_timeout(self, db_uri, monkeypatch):
|
|
27
|
+
def connect(*args, **kwargs):
|
|
28
|
+
raise psycopg.OperationalError
|
|
29
|
+
|
|
30
|
+
monkeypatch.setattr(psycopg, "connect", connect)
|
|
31
|
+
monkeypatch.setattr(time, "sleep", lambda _: None)
|
|
32
|
+
with pytest.raises(PostgresError):
|
|
33
|
+
wait_for_postgresql(db_uri, timeout=0)
|
|
34
|
+
|
|
23
35
|
def test_restore_from_pgfile(self, db_uri, mock_subprocess, mock_engine):
|
|
24
36
|
pg_restore_file = "test.pg_restore"
|
|
25
37
|
assert restore_from_pgfile(db_uri, pg_restore_file) == "WORKED!"
|
|
@@ -20,6 +20,7 @@ class TestSSH:
|
|
|
20
20
|
|
|
21
21
|
def test_get_session_ssh(
|
|
22
22
|
self,
|
|
23
|
+
caplog,
|
|
23
24
|
mock_psycopg,
|
|
24
25
|
mock_SSHTunnelForwarder,
|
|
25
26
|
):
|
|
@@ -60,3 +61,11 @@ class TestSSH:
|
|
|
60
61
|
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
61
62
|
)
|
|
62
63
|
assert dbdatabase._base == NoTZBase
|
|
64
|
+
assert "ssh_host_key not supplied" in caplog.text
|
|
65
|
+
|
|
66
|
+
caplog.clear()
|
|
67
|
+
with Database(
|
|
68
|
+
ssh_host="mysshhost", ssh_host_key="AAAAkey", **params
|
|
69
|
+
) as dbdatabase:
|
|
70
|
+
assert dbdatabase._base == NoTZBase
|
|
71
|
+
assert "ssh_host_key not supplied" not in caplog.text
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""View Tests"""
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import column, select, table
|
|
4
|
+
from sqlalchemy.dialects import postgresql, sqlite
|
|
5
|
+
|
|
6
|
+
from hdx.database.views import CreateView, DropView
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestViews:
|
|
10
|
+
selectable = select(table("t", column("a")))
|
|
11
|
+
|
|
12
|
+
def test_view_name_quoted(self):
|
|
13
|
+
for dialect in (sqlite.dialect(), postgresql.dialect()):
|
|
14
|
+
sql = str(CreateView("date_view", self.selectable).compile(dialect=dialect))
|
|
15
|
+
assert sql.startswith("CREATE VIEW date_view AS SELECT")
|
|
16
|
+
sql = str(DropView("date_view").compile(dialect=dialect))
|
|
17
|
+
assert sql == "DROP VIEW date_view"
|
|
18
|
+
|
|
19
|
+
name = 'x"; DROP TABLE t; --'
|
|
20
|
+
sql = str(CreateView(name, self.selectable).compile(dialect=dialect))
|
|
21
|
+
assert sql.startswith('CREATE VIEW "x""; DROP TABLE t; --" AS SELECT')
|
|
22
|
+
sql = str(DropView(name).compile(dialect=dialect))
|
|
23
|
+
assert sql == 'DROP VIEW "x""; DROP TABLE t; --"'
|