hdx-python-database 1.3.1__tar.gz → 1.3.3__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.1 → hdx_python_database-1.3.3}/.config/pre-commit-config.yaml +4 -4
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/.github/workflows/publish.yaml +4 -3
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/.github/workflows/run-python-tests.yaml +6 -5
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/PKG-INFO +1 -1
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/documentation/main.md +14 -3
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/pyproject.toml +12 -16
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/requirements.txt +25 -20
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/src/hdx/database/__init__.py +40 -14
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/src/hdx/database/_version.py +2 -2
- hdx_python_database-1.3.3/src/hdx/database/postgresql.py +95 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/tests/hdx/database/conftest.py +42 -11
- hdx_python_database-1.3.3/tests/hdx/database/test_database.py +58 -0
- hdx_python_database-1.3.3/tests/hdx/database/test_postgresql.py +30 -0
- hdx_python_database-1.3.3/tests/hdx/database/test_recreate.py +8 -0
- hdx_python_database-1.3.3/tests/hdx/database/test_reflect.py +46 -0
- hdx_python_database-1.3.3/tests/hdx/database/test_ssh.py +64 -0
- hdx_python_database-1.3.1/src/hdx/database/postgresql.py +0 -41
- hdx_python_database-1.3.1/tests/hdx/database/test_database.py +0 -149
- hdx_python_database-1.3.1/tests/hdx/database/test_postgresql.py +0 -15
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/.config/coveragerc +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/.config/pytest.ini +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/.config/ruff.toml +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/.gitignore +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/CONTRIBUTING.md +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/LICENSE +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/README.md +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/documentation/.readthedocs.yaml +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/documentation/pydoc-markdown.yaml +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/src/hdx/database/dburi.py +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/src/hdx/database/no_timezone.py +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/src/hdx/database/utils.py +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/src/hdx/database/views.py +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/src/hdx/database/with_timezone.py +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/tests/fixtures/test.db +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/tests/hdx/database/__init__.py +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/tests/hdx/database/dbtestdate.py +0 -0
- {hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/tests/hdx/database/test_dburi.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
default_language_version:
|
|
2
|
-
python: python3.
|
|
2
|
+
python: python3.12
|
|
3
3
|
repos:
|
|
4
4
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
5
|
-
rev: v4.
|
|
5
|
+
rev: v4.6.0
|
|
6
6
|
hooks:
|
|
7
7
|
- id: trailing-whitespace
|
|
8
8
|
- id: end-of-file-fixer
|
|
9
9
|
- id: check-ast
|
|
10
10
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
11
|
-
rev: v0.
|
|
11
|
+
rev: v0.5.6
|
|
12
12
|
hooks:
|
|
13
13
|
# Run the linter.
|
|
14
14
|
- id: ruff
|
|
@@ -17,7 +17,7 @@ repos:
|
|
|
17
17
|
- id: ruff-format
|
|
18
18
|
args: [--config, .config/ruff.toml]
|
|
19
19
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
20
|
-
rev: 0.
|
|
20
|
+
rev: 0.2.33
|
|
21
21
|
hooks:
|
|
22
22
|
# Run the pip compile
|
|
23
23
|
- id: pip-compile
|
|
@@ -9,19 +9,20 @@ jobs:
|
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
13
|
- name: Get history and tags for versioning to work
|
|
14
14
|
run: |
|
|
15
15
|
git fetch --prune --unshallow
|
|
16
16
|
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
|
17
17
|
- name: Set up Python
|
|
18
|
-
uses: actions/setup-python@
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
19
|
with:
|
|
20
20
|
python-version: '3.x'
|
|
21
21
|
- name: Install dependencies
|
|
22
22
|
run: |
|
|
23
23
|
python -m pip install --upgrade pip
|
|
24
|
-
|
|
24
|
+
- name: Install Hatch
|
|
25
|
+
uses: pypa/hatch@install
|
|
25
26
|
- name: Build with hatch
|
|
26
27
|
run: |
|
|
27
28
|
hatch build
|
{hdx_python_database-1.3.1 → hdx_python_database-1.3.3}/.github/workflows/run-python-tests.yaml
RENAMED
|
@@ -18,22 +18,23 @@ jobs:
|
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
|
|
20
20
|
steps:
|
|
21
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
22
|
- name: Set up Python
|
|
23
|
-
uses: actions/setup-python@
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
24
|
with:
|
|
25
25
|
python-version: '3.x'
|
|
26
26
|
- name: Install dependencies
|
|
27
27
|
run: |
|
|
28
28
|
python -m pip install --upgrade pip
|
|
29
|
-
|
|
29
|
+
- name: Install Hatch
|
|
30
|
+
uses: pypa/hatch@install
|
|
30
31
|
- name: Test with hatch/pytest
|
|
31
32
|
run: |
|
|
32
|
-
hatch
|
|
33
|
+
hatch test
|
|
33
34
|
- name: Check styling
|
|
34
35
|
if: always()
|
|
35
36
|
run: |
|
|
36
|
-
hatch
|
|
37
|
+
hatch fmt --check
|
|
37
38
|
- name: Publish Unit Test Results
|
|
38
39
|
uses: EnricoMi/publish-unit-test-result-action@v2
|
|
39
40
|
if: always()
|
|
@@ -105,6 +105,10 @@ maps don't work with reflection and hence `db_has_tz` will be ignored ie.
|
|
|
105
105
|
there will be no conversion between Python datetimes with timezones to
|
|
106
106
|
timezoneless database columns.
|
|
107
107
|
|
|
108
|
+
A PostgreSQL database can be restored from a file generated by the `pg_dump`
|
|
109
|
+
command line utility by supplying `pg_restore_file` with the path to the file
|
|
110
|
+
to be restored.
|
|
111
|
+
|
|
108
112
|
There is an option to wipe and create an empty schema in the database by
|
|
109
113
|
setting `recreate_schema` to `True` and setting a `schema_name` ("public" is
|
|
110
114
|
the default).
|
|
@@ -178,9 +182,16 @@ View preparation must be done before calling Base.metadata.create_all.
|
|
|
178
182
|
|
|
179
183
|
## PostgreSQL specific
|
|
180
184
|
|
|
181
|
-
There is a PostgreSQL specific call that only returns when the PostgreSQL
|
|
182
|
-
|
|
185
|
+
There is a PostgreSQL specific call that only returns when the PostgreSQL
|
|
186
|
+
database becomes available:
|
|
183
187
|
|
|
184
188
|
# Wait until PostgreSQL is up
|
|
185
189
|
# Library should be installed with hdx-python-database[postgresql]
|
|
186
|
-
|
|
190
|
+
db_uri = "postgresql+psycopg://myuser:mypass@1.2.3.4:12345/mydatabase"
|
|
191
|
+
wait_for_postgresql(db_uri)
|
|
192
|
+
|
|
193
|
+
The database can be restored from a file generated by `pg_dump` like this:
|
|
194
|
+
|
|
195
|
+
db_uri = "postgresql+psycopg://myuser:mypass@1.2.3.4:12345/mydatabase"
|
|
196
|
+
pg_restore_file = "backup.pg_restore"
|
|
197
|
+
assert restore_from_pgfile(db_uri, pg_restore_file) == "WORKED!"
|
|
@@ -75,25 +75,21 @@ version_scheme = "python-simplified-semver"
|
|
|
75
75
|
|
|
76
76
|
# Tests
|
|
77
77
|
|
|
78
|
-
[tool.hatch.envs.test]
|
|
78
|
+
[tool.hatch.envs.hatch-test]
|
|
79
79
|
features = ["sshtunnel", "postgresql", "test"]
|
|
80
80
|
|
|
81
|
-
[tool.hatch.envs.test.
|
|
82
|
-
|
|
81
|
+
[[tool.hatch.envs.hatch-test.matrix]]
|
|
82
|
+
python = ["3.12"]
|
|
83
|
+
|
|
84
|
+
[tool.hatch.envs.hatch-test.scripts]
|
|
85
|
+
run = """
|
|
83
86
|
pytest -c .config/pytest.ini --rootdir=. --junitxml=test-results.xml \
|
|
84
87
|
--cov --cov-config=.config/coveragerc --no-cov-on-fail \
|
|
85
|
-
--cov-report=lcov --cov-report=term-missing
|
|
88
|
+
--cov-report=lcov --cov-report=term-missing -p no:randomly
|
|
86
89
|
"""
|
|
87
90
|
|
|
88
|
-
[
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
[
|
|
92
|
-
|
|
93
|
-
dependencies = ["ruff"]
|
|
94
|
-
|
|
95
|
-
[tool.hatch.envs.lint.scripts]
|
|
96
|
-
style = [
|
|
97
|
-
"ruff check --config .config/ruff.toml --diff {args:.}",
|
|
98
|
-
"ruff format --config .config/ruff.toml --diff {args:.}",
|
|
99
|
-
]
|
|
91
|
+
[tool.hatch.envs.hatch-static-analysis.scripts]
|
|
92
|
+
format-check = ["ruff format --config .config/ruff.toml --check --diff {args:.}",]
|
|
93
|
+
format-fix = ["ruff format --config .config/ruff.toml {args:.}",]
|
|
94
|
+
lint-check = ["ruff check --config .config/ruff.toml {args:.}",]
|
|
95
|
+
lint-fix = ["ruff check --config .config/ruff.toml --fix {args:.}",]
|
|
@@ -1,57 +1,62 @@
|
|
|
1
1
|
# This file was autogenerated by uv via the following command:
|
|
2
2
|
# uv pip compile pyproject.toml --resolver=backtracking --all-extras -o requirements.txt
|
|
3
|
-
bcrypt==4.
|
|
3
|
+
bcrypt==4.2.0
|
|
4
4
|
# via paramiko
|
|
5
|
-
cffi==1.
|
|
5
|
+
cffi==1.17.0
|
|
6
6
|
# via
|
|
7
7
|
# cryptography
|
|
8
8
|
# pynacl
|
|
9
9
|
cfgv==3.4.0
|
|
10
10
|
# via pre-commit
|
|
11
|
-
coverage==7.
|
|
11
|
+
coverage==7.6.1
|
|
12
12
|
# via pytest-cov
|
|
13
|
-
cryptography==
|
|
13
|
+
cryptography==43.0.0
|
|
14
14
|
# via paramiko
|
|
15
15
|
distlib==0.3.8
|
|
16
16
|
# via virtualenv
|
|
17
|
-
filelock==3.
|
|
17
|
+
filelock==3.15.4
|
|
18
18
|
# via virtualenv
|
|
19
19
|
greenlet==3.0.3
|
|
20
20
|
# via sqlalchemy
|
|
21
|
-
identify==2.
|
|
21
|
+
identify==2.6.0
|
|
22
22
|
# via pre-commit
|
|
23
23
|
iniconfig==2.0.0
|
|
24
24
|
# via pytest
|
|
25
|
-
nodeenv==1.
|
|
25
|
+
nodeenv==1.9.1
|
|
26
26
|
# via pre-commit
|
|
27
|
-
packaging==24.
|
|
27
|
+
packaging==24.1
|
|
28
28
|
# via pytest
|
|
29
29
|
paramiko==3.4.0
|
|
30
30
|
# via sshtunnel
|
|
31
|
-
platformdirs==4.2.
|
|
31
|
+
platformdirs==4.2.2
|
|
32
32
|
# via virtualenv
|
|
33
33
|
pluggy==1.5.0
|
|
34
34
|
# via pytest
|
|
35
|
-
pre-commit==3.
|
|
36
|
-
|
|
37
|
-
psycopg
|
|
35
|
+
pre-commit==3.8.0
|
|
36
|
+
# via hdx-python-database (pyproject.toml)
|
|
37
|
+
psycopg==3.2.1
|
|
38
|
+
# via hdx-python-database (pyproject.toml)
|
|
39
|
+
psycopg-binary==3.2.1
|
|
38
40
|
# via psycopg
|
|
39
41
|
pycparser==2.22
|
|
40
42
|
# via cffi
|
|
41
43
|
pynacl==1.5.0
|
|
42
44
|
# via paramiko
|
|
43
|
-
pytest==8.2
|
|
44
|
-
# via
|
|
45
|
+
pytest==8.3.2
|
|
46
|
+
# via
|
|
47
|
+
# hdx-python-database (pyproject.toml)
|
|
48
|
+
# pytest-cov
|
|
45
49
|
pytest-cov==5.0.0
|
|
46
|
-
|
|
50
|
+
# via hdx-python-database (pyproject.toml)
|
|
51
|
+
pyyaml==6.0.2
|
|
47
52
|
# via pre-commit
|
|
48
|
-
|
|
49
|
-
# via
|
|
50
|
-
sqlalchemy==2.0.30
|
|
53
|
+
sqlalchemy==2.0.32
|
|
54
|
+
# via hdx-python-database (pyproject.toml)
|
|
51
55
|
sshtunnel==0.4.0
|
|
52
|
-
|
|
56
|
+
# via hdx-python-database (pyproject.toml)
|
|
57
|
+
typing-extensions==4.12.2
|
|
53
58
|
# via
|
|
54
59
|
# psycopg
|
|
55
60
|
# sqlalchemy
|
|
56
|
-
virtualenv==20.26.
|
|
61
|
+
virtualenv==20.26.3
|
|
57
62
|
# via pre-commit
|
|
@@ -6,15 +6,14 @@ from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
|
6
6
|
from sqlalchemy import Engine, TableClause, create_engine
|
|
7
7
|
from sqlalchemy.exc import SQLAlchemyError
|
|
8
8
|
from sqlalchemy.ext.automap import automap_base
|
|
9
|
-
from sqlalchemy.orm import DeclarativeBase, Session
|
|
9
|
+
from sqlalchemy.orm import DeclarativeBase, Session
|
|
10
10
|
from sqlalchemy.pool import NullPool
|
|
11
11
|
from sqlalchemy.sql.ddl import CreateSchema, DropSchema
|
|
12
|
-
from sqlalchemy.util import Properties
|
|
13
12
|
|
|
14
13
|
from ._version import version as __version__ # noqa: F401
|
|
15
14
|
from .dburi import get_connection_uri
|
|
16
15
|
from .no_timezone import Base as NoTZBase
|
|
17
|
-
from .postgresql import wait_for_postgresql
|
|
16
|
+
from .postgresql import restore_from_pgfile, wait_for_postgresql
|
|
18
17
|
from .views import view
|
|
19
18
|
from .with_timezone import Base as TZBase
|
|
20
19
|
|
|
@@ -41,6 +40,9 @@ class Database:
|
|
|
41
40
|
with timezones to timezoneless database columns (but not when using
|
|
42
41
|
reflection). If table_base is supplied, db_has_tz is ignored.
|
|
43
42
|
|
|
43
|
+
The database can be restored from a given pg_restore file supplied in the
|
|
44
|
+
parameter pg_restore_file.
|
|
45
|
+
|
|
44
46
|
There is an option to wipe and create an empty schema in the database by
|
|
45
47
|
setting recreate_schema to True and setting a schema_name ("public" is the
|
|
46
48
|
default).
|
|
@@ -63,6 +65,7 @@ class Database:
|
|
|
63
65
|
table_base (Optional[Type[DeclarativeBase]]): Override table base. Defaults to None.
|
|
64
66
|
reflect (bool): Whether to reflect existing tables. Defaults to False.
|
|
65
67
|
**kwargs: See below
|
|
68
|
+
pg_restore_file (str): Restore database from pg_restore file
|
|
66
69
|
recreate_schema (bool): Whether to recreate schema
|
|
67
70
|
schema_name (str): Database schema name. Defaults to "public".
|
|
68
71
|
prepare_fn (Callable[[], None]]): Function to call before Base.metadata.create_all.
|
|
@@ -95,9 +98,11 @@ class Database:
|
|
|
95
98
|
port = int(port)
|
|
96
99
|
schema_name = None
|
|
97
100
|
if len(kwargs) == 0:
|
|
101
|
+
pg_restore_file = None
|
|
98
102
|
recreate_schema = False
|
|
99
103
|
prepare_fn = do_nothing_fn
|
|
100
104
|
else:
|
|
105
|
+
pg_restore_file = kwargs.pop("pg_restore_file", None)
|
|
101
106
|
recreate_schema = kwargs.pop("recreate_schema", False)
|
|
102
107
|
schema_name = kwargs.pop("schema", "public")
|
|
103
108
|
prepare_fn = kwargs.pop("prepare_fn", do_nothing_fn)
|
|
@@ -144,6 +149,8 @@ class Database:
|
|
|
144
149
|
)
|
|
145
150
|
if not engine and dialect == "postgresql":
|
|
146
151
|
wait_for_postgresql(db_uri)
|
|
152
|
+
if pg_restore_file and db_uri:
|
|
153
|
+
restore_from_pgfile(db_uri, pg_restore_file)
|
|
147
154
|
if not table_base:
|
|
148
155
|
if db_has_tz:
|
|
149
156
|
table_base = TZBase
|
|
@@ -155,20 +162,41 @@ class Database:
|
|
|
155
162
|
if recreate_schema:
|
|
156
163
|
self.recreate_schema(engine, schema_name)
|
|
157
164
|
self.prepare_results = prepare_fn()
|
|
158
|
-
self.session, self.
|
|
165
|
+
self.session, self.base = self.create_session(
|
|
159
166
|
engine,
|
|
160
167
|
table_base=table_base,
|
|
161
168
|
reflect=reflect,
|
|
162
169
|
)
|
|
170
|
+
if reflect:
|
|
171
|
+
self.reflected_classes = self.base.classes
|
|
172
|
+
else:
|
|
173
|
+
self.reflected_classes = None
|
|
163
174
|
|
|
164
|
-
def
|
|
165
|
-
|
|
175
|
+
def cleanup(self) -> None:
|
|
176
|
+
"""Cleanup SQLAlchemy.
|
|
166
177
|
|
|
167
|
-
|
|
178
|
+
Returns:
|
|
179
|
+
sqlalchemy.Engine: SQLAlchemy engine
|
|
180
|
+
"""
|
|
168
181
|
self.session.close()
|
|
182
|
+
self.engine.dispose()
|
|
169
183
|
if self.server is not None:
|
|
170
184
|
self.server.stop()
|
|
171
185
|
|
|
186
|
+
def __enter__(self) -> "Database":
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
|
190
|
+
self.cleanup()
|
|
191
|
+
|
|
192
|
+
def drop_all(self) -> None:
|
|
193
|
+
"""Drop all tables.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
None
|
|
197
|
+
"""
|
|
198
|
+
self.base.metadata.drop_all(self.engine)
|
|
199
|
+
|
|
172
200
|
def get_engine(self) -> Engine:
|
|
173
201
|
"""Returns SQLAlchemy engine.
|
|
174
202
|
|
|
@@ -207,7 +235,7 @@ class Database:
|
|
|
207
235
|
db_uri: Optional[str] = None,
|
|
208
236
|
table_base: Type[DeclarativeBase] = NoTZBase,
|
|
209
237
|
reflect: bool = False,
|
|
210
|
-
) -> Tuple[Session,
|
|
238
|
+
) -> Tuple[Session, Any]:
|
|
211
239
|
"""Creates SQLAlchemy session given SQLAlchemy engine or database uri
|
|
212
240
|
(one of which must be supplied). Tables must inherit from Base in
|
|
213
241
|
hdx.utilities.database unless base is defined. If reflect is True,
|
|
@@ -222,22 +250,19 @@ class Database:
|
|
|
222
250
|
reflect (bool): Whether to reflect existing tables. Defaults to False.
|
|
223
251
|
|
|
224
252
|
Returns:
|
|
225
|
-
Tuple[Session,
|
|
253
|
+
Tuple[Session, Any]: (SQLAlchemy session, base)
|
|
226
254
|
"""
|
|
227
255
|
if not engine:
|
|
228
256
|
if db_uri is None:
|
|
229
257
|
raise DatabaseError("No engine or database uri provided!")
|
|
230
258
|
engine = create_engine(db_uri, poolclass=NullPool, echo=False)
|
|
231
|
-
Session = sessionmaker(bind=engine)
|
|
232
259
|
if reflect:
|
|
233
260
|
Base = automap_base(declarative_base=table_base)
|
|
234
261
|
Base.prepare(autoload_with=engine)
|
|
235
|
-
|
|
262
|
+
table_base = Base
|
|
236
263
|
else:
|
|
237
264
|
table_base.metadata.create_all(engine)
|
|
238
|
-
|
|
239
|
-
session = Session()
|
|
240
|
-
return session, reflected_classes
|
|
265
|
+
return Session(engine), table_base
|
|
241
266
|
|
|
242
267
|
@staticmethod
|
|
243
268
|
def recreate_schema(engine: Engine, schema_name: str = "public") -> bool:
|
|
@@ -285,6 +310,7 @@ class Database:
|
|
|
285
310
|
|
|
286
311
|
Args:
|
|
287
312
|
view_params_list (List[Dict]): List of dictionaries with view parameters
|
|
313
|
+
|
|
288
314
|
Returns:
|
|
289
315
|
List[TableClause]: SQLAlchemy Views
|
|
290
316
|
"""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""PostgreSQL specific utilities"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from os import environ
|
|
7
|
+
|
|
8
|
+
from .dburi import get_params_from_connection_uri, remove_driver_from_uri
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import psycopg
|
|
12
|
+
except ImportError:
|
|
13
|
+
psycopg = None
|
|
14
|
+
# dependency missing, issue a warning
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
17
|
+
warnings.warn(
|
|
18
|
+
"psycopg not found! Please install hdx-python-database[postgresql] to enable."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PostgresError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def wait_for_postgresql(db_uri: str) -> None:
|
|
29
|
+
"""Waits for PostgreSQL database to be up
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
db_uri (str): Connection URI
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
None
|
|
36
|
+
"""
|
|
37
|
+
connecting_string = "Checking for PostgreSQL..."
|
|
38
|
+
db_uri_nd = remove_driver_from_uri(db_uri)
|
|
39
|
+
while True:
|
|
40
|
+
try:
|
|
41
|
+
logger.info(connecting_string)
|
|
42
|
+
connection = psycopg.connect(db_uri_nd, connect_timeout=3)
|
|
43
|
+
connection.close()
|
|
44
|
+
logger.info("PostgreSQL is running!")
|
|
45
|
+
break
|
|
46
|
+
except psycopg.OperationalError:
|
|
47
|
+
time.sleep(1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def restore_from_pgfile(db_uri: str, pg_restore_file: str) -> str:
|
|
51
|
+
"""Restore database from a pg_restore file created by pg_backup using the
|
|
52
|
+
pg_restore command.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
db_uri (str): Connection URI
|
|
56
|
+
pg_restore_file (str): Path to the pg_restore database file
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
str: Output from the pg_restore command
|
|
60
|
+
"""
|
|
61
|
+
db_params = get_params_from_connection_uri(db_uri)
|
|
62
|
+
subprocess_params = ["pg_restore", "-c"]
|
|
63
|
+
for key, value in db_params.items():
|
|
64
|
+
match key:
|
|
65
|
+
case "database":
|
|
66
|
+
subprocess_params.append("-d")
|
|
67
|
+
case "host":
|
|
68
|
+
subprocess_params.append("-h")
|
|
69
|
+
case "port":
|
|
70
|
+
subprocess_params.append("-p")
|
|
71
|
+
case "username":
|
|
72
|
+
subprocess_params.append("-U")
|
|
73
|
+
case _:
|
|
74
|
+
continue
|
|
75
|
+
subprocess_params.append(f"{value}")
|
|
76
|
+
|
|
77
|
+
subprocess_params.append(f"{pg_restore_file}")
|
|
78
|
+
env = environ.copy()
|
|
79
|
+
password = db_params.get("password")
|
|
80
|
+
if password:
|
|
81
|
+
env["PGPASSWORD"] = password
|
|
82
|
+
process = subprocess.run(
|
|
83
|
+
subprocess_params, env=env, capture_output=True, encoding="utf-8"
|
|
84
|
+
)
|
|
85
|
+
try:
|
|
86
|
+
process.check_returncode()
|
|
87
|
+
except subprocess.CalledProcessError as ex:
|
|
88
|
+
command = " ".join(subprocess_params)
|
|
89
|
+
raise PostgresError(
|
|
90
|
+
f"{command} failed. Return code: {process.returncode}"
|
|
91
|
+
) from ex
|
|
92
|
+
|
|
93
|
+
for line in process.stdout.splitlines():
|
|
94
|
+
logger.info(line)
|
|
95
|
+
return process.stdout
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import subprocess
|
|
1
2
|
from collections import namedtuple
|
|
2
3
|
|
|
3
4
|
import psycopg
|
|
@@ -25,7 +26,11 @@ def mock_psycopg(monkeypatch):
|
|
|
25
26
|
PsycopgConnection.connected = True
|
|
26
27
|
raise psycopg.OperationalError
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
try:
|
|
30
|
+
monkeypatch.setattr(psycopg, "connect", connect)
|
|
31
|
+
yield
|
|
32
|
+
finally:
|
|
33
|
+
monkeypatch.delattr(psycopg, "connect")
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
@pytest.fixture(scope="function")
|
|
@@ -39,12 +44,6 @@ def mock_SSHTunnelForwarder(monkeypatch):
|
|
|
39
44
|
def stop(_):
|
|
40
45
|
TestDatabase.stopped = True
|
|
41
46
|
|
|
42
|
-
monkeypatch.setattr(SSHTunnelForwarder, "__init__", init)
|
|
43
|
-
monkeypatch.setattr(SSHTunnelForwarder, "start", start)
|
|
44
|
-
monkeypatch.setattr(SSHTunnelForwarder, "stop", stop)
|
|
45
|
-
monkeypatch.setattr(SSHTunnelForwarder, "local_bind_host", "0.0.0.0")
|
|
46
|
-
monkeypatch.setattr(SSHTunnelForwarder, "local_bind_port", 12345)
|
|
47
|
-
|
|
48
47
|
def create_session(_, engine, table_base, reflect):
|
|
49
48
|
class Session:
|
|
50
49
|
bind = namedtuple("Bind", "engine")
|
|
@@ -53,10 +52,23 @@ def mock_SSHTunnelForwarder(monkeypatch):
|
|
|
53
52
|
return None
|
|
54
53
|
|
|
55
54
|
Session.bind.engine = engine
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
return Session(), table_base
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
monkeypatch.setattr(SSHTunnelForwarder, "__init__", init)
|
|
59
|
+
monkeypatch.setattr(SSHTunnelForwarder, "start", start)
|
|
60
|
+
monkeypatch.setattr(SSHTunnelForwarder, "stop", stop)
|
|
61
|
+
monkeypatch.setattr(SSHTunnelForwarder, "local_bind_host", "0.0.0.0")
|
|
62
|
+
monkeypatch.setattr(SSHTunnelForwarder, "local_bind_port", 12345)
|
|
63
|
+
monkeypatch.setattr(Database, "create_session", create_session)
|
|
64
|
+
yield
|
|
65
|
+
finally:
|
|
66
|
+
monkeypatch.delattr(SSHTunnelForwarder, "__init__")
|
|
67
|
+
monkeypatch.delattr(SSHTunnelForwarder, "start")
|
|
68
|
+
monkeypatch.delattr(SSHTunnelForwarder, "stop")
|
|
69
|
+
monkeypatch.delattr(SSHTunnelForwarder, "local_bind_host")
|
|
70
|
+
monkeypatch.delattr(SSHTunnelForwarder, "local_bind_port")
|
|
71
|
+
monkeypatch.delattr(Database, "create_session")
|
|
60
72
|
|
|
61
73
|
|
|
62
74
|
@pytest.fixture(scope="function")
|
|
@@ -84,3 +96,22 @@ def mock_engine():
|
|
|
84
96
|
return SQAConnection()
|
|
85
97
|
|
|
86
98
|
return MockEngine()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.fixture(scope="function")
|
|
102
|
+
def mock_subprocess(monkeypatch):
|
|
103
|
+
class CompletedProcess:
|
|
104
|
+
stdout = "WORKED!"
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def check_returncode():
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
def run(*args, **kwargs):
|
|
111
|
+
return CompletedProcess()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
monkeypatch.setattr(subprocess, "run", run)
|
|
115
|
+
yield
|
|
116
|
+
finally:
|
|
117
|
+
monkeypatch.delattr(subprocess, "run")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Database Utility Tests"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from os import remove
|
|
5
|
+
from os.path import join
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
|
|
10
|
+
from .dbtestdate import DBTestDate, date_view_params
|
|
11
|
+
from hdx.database import Database, DatabaseError
|
|
12
|
+
from hdx.database.no_timezone import Base as NoTZBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestDatabase:
|
|
16
|
+
dbpath = join("tests", "test_database.db")
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(scope="function")
|
|
19
|
+
def nodatabase(self):
|
|
20
|
+
try:
|
|
21
|
+
remove(TestDatabase.dbpath)
|
|
22
|
+
except OSError:
|
|
23
|
+
pass
|
|
24
|
+
return f"sqlite:///{TestDatabase.dbpath}"
|
|
25
|
+
|
|
26
|
+
def test_get_session(self, nodatabase):
|
|
27
|
+
assert DBTestDate.__tablename__ == "db_test_date"
|
|
28
|
+
|
|
29
|
+
def prepare_fn():
|
|
30
|
+
return Database.prepare_views([date_view_params])
|
|
31
|
+
|
|
32
|
+
with Database(
|
|
33
|
+
database=TestDatabase.dbpath,
|
|
34
|
+
port=None,
|
|
35
|
+
dialect="sqlite",
|
|
36
|
+
prepare_fn=prepare_fn,
|
|
37
|
+
) as dbdatabase:
|
|
38
|
+
dbsession = dbdatabase.get_session()
|
|
39
|
+
assert str(dbsession.bind.engine.url) == nodatabase
|
|
40
|
+
assert dbdatabase.base == NoTZBase
|
|
41
|
+
now = datetime(2022, 10, 20, 22, 35, 55, tzinfo=timezone.utc)
|
|
42
|
+
input_dbtestdate = DBTestDate()
|
|
43
|
+
input_dbtestdate.test_date = now
|
|
44
|
+
dbsession.add(input_dbtestdate)
|
|
45
|
+
dbsession.commit()
|
|
46
|
+
dbtestdate = dbsession.execute(select(DBTestDate)).scalar_one()
|
|
47
|
+
assert dbtestdate.test_date == now
|
|
48
|
+
date_view = dbdatabase.get_prepare_results()[0]
|
|
49
|
+
dbtestdate = dbsession.execute(select(date_view)).scalar_one()
|
|
50
|
+
assert dbtestdate == now
|
|
51
|
+
|
|
52
|
+
def test_errors(self):
|
|
53
|
+
with pytest.raises(DatabaseError):
|
|
54
|
+
Database.create_session()
|
|
55
|
+
|
|
56
|
+
with pytest.raises(DatabaseError):
|
|
57
|
+
with Database():
|
|
58
|
+
pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""PostgreSQL Utility Tests"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from . import PsycopgConnection
|
|
6
|
+
from hdx.database.postgresql import (
|
|
7
|
+
PostgresError,
|
|
8
|
+
restore_from_pgfile,
|
|
9
|
+
wait_for_postgresql,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestPostgreSQL:
|
|
14
|
+
@pytest.fixture(scope="class")
|
|
15
|
+
def db_uri(self):
|
|
16
|
+
return "postgresql+psycopg://myuser:mypass@myserver:1234/mydatabase"
|
|
17
|
+
|
|
18
|
+
def test_wait_for_postgresql(self, db_uri, mock_psycopg):
|
|
19
|
+
PsycopgConnection.connected = False
|
|
20
|
+
wait_for_postgresql(db_uri)
|
|
21
|
+
assert PsycopgConnection.connected is True
|
|
22
|
+
|
|
23
|
+
def test_restore_from_pgfile(self, db_uri, mock_subprocess, mock_engine):
|
|
24
|
+
pg_restore_file = "test.pg_restore"
|
|
25
|
+
assert restore_from_pgfile(db_uri, pg_restore_file) == "WORKED!"
|
|
26
|
+
|
|
27
|
+
def test_restore_from_pgfile_raises(self, db_uri):
|
|
28
|
+
pg_restore_file = "NOT_EXIST.pg_restore"
|
|
29
|
+
with pytest.raises(PostgresError):
|
|
30
|
+
restore_from_pgfile(db_uri, pg_restore_file)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from hdx.database import Database
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_recreate_schema(mock_engine):
|
|
5
|
+
db_uri = "postgresql+psycopg://myuser:mypass@0.0.0.0:12345/mydatabase"
|
|
6
|
+
assert Database.recreate_schema(mock_engine, db_uri) is True
|
|
7
|
+
db_uri = "Error"
|
|
8
|
+
assert Database.recreate_schema(mock_engine, db_uri) is False
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Database Utility Tests"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from os import remove
|
|
5
|
+
from os.path import join
|
|
6
|
+
from shutil import copyfile
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from sqlalchemy import select
|
|
10
|
+
from sqlalchemy.orm.decl_api import DeclarativeAttributeIntercept
|
|
11
|
+
|
|
12
|
+
from hdx.database import Database
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestReflect:
|
|
16
|
+
dbpath = join("tests", "test_database.db")
|
|
17
|
+
testdb = join("tests", "fixtures", "test.db")
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="function")
|
|
20
|
+
def database_to_reflect(self):
|
|
21
|
+
try:
|
|
22
|
+
remove(TestReflect.dbpath)
|
|
23
|
+
copyfile(TestReflect.testdb, TestReflect.dbpath)
|
|
24
|
+
except OSError:
|
|
25
|
+
pass
|
|
26
|
+
return f"sqlite:///{TestReflect.dbpath}"
|
|
27
|
+
|
|
28
|
+
def test_get_reflect_session(self, database_to_reflect):
|
|
29
|
+
with Database(
|
|
30
|
+
database=TestReflect.dbpath,
|
|
31
|
+
port=None,
|
|
32
|
+
dialect="sqlite",
|
|
33
|
+
reflect=True,
|
|
34
|
+
) as dbdatabase:
|
|
35
|
+
dbengine = dbdatabase.get_engine()
|
|
36
|
+
assert str(dbengine.url) == "sqlite:///tests/test_database.db"
|
|
37
|
+
dbsession = dbdatabase.get_session()
|
|
38
|
+
assert isinstance(dbdatabase.base, DeclarativeAttributeIntercept)
|
|
39
|
+
assert str(dbsession.bind.engine.url) == database_to_reflect
|
|
40
|
+
Table1 = dbdatabase.get_reflected_classes().table1
|
|
41
|
+
row = dbsession.execute(select(Table1)).scalar_one()
|
|
42
|
+
assert row.id == "1"
|
|
43
|
+
assert row.col1 == "wfrefds"
|
|
44
|
+
# with reflection, type annotation maps do not work and hence
|
|
45
|
+
# we don't have a timezone here
|
|
46
|
+
assert row.date1 == datetime(1993, 9, 23, 14, 12, 56, 111000)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Database Utility Tests"""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
from hdx.database import Database
|
|
6
|
+
from hdx.database.no_timezone import Base as NoTZBase
|
|
7
|
+
from hdx.database.with_timezone import Base as TZBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestSSH:
|
|
11
|
+
params_pg = {
|
|
12
|
+
"database": "mydatabase",
|
|
13
|
+
"host": "myserver",
|
|
14
|
+
"port": 1234,
|
|
15
|
+
"username": "myuser",
|
|
16
|
+
"password": "mypass",
|
|
17
|
+
"dialect": "postgresql",
|
|
18
|
+
"driver": "psycopg",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def test_get_session_ssh(
|
|
22
|
+
self,
|
|
23
|
+
mock_psycopg,
|
|
24
|
+
mock_SSHTunnelForwarder,
|
|
25
|
+
):
|
|
26
|
+
params = deepcopy(self.params_pg)
|
|
27
|
+
del params["password"]
|
|
28
|
+
with Database(
|
|
29
|
+
ssh_host="mysshhost", ssh_port=25, db_has_tz=True, **params
|
|
30
|
+
) as dbdatabase:
|
|
31
|
+
dbsession = dbdatabase.get_session()
|
|
32
|
+
assert (
|
|
33
|
+
str(dbsession.bind.engine.url)
|
|
34
|
+
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
35
|
+
)
|
|
36
|
+
assert dbdatabase.base == TZBase
|
|
37
|
+
|
|
38
|
+
with Database(
|
|
39
|
+
ssh_host="mysshhost", ssh_port=25, table_base=TZBase, **params
|
|
40
|
+
) as dbdatabase:
|
|
41
|
+
dbsession = dbdatabase.get_session()
|
|
42
|
+
assert (
|
|
43
|
+
str(dbsession.bind.engine.url)
|
|
44
|
+
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
45
|
+
)
|
|
46
|
+
assert dbdatabase.base == TZBase
|
|
47
|
+
|
|
48
|
+
with Database(ssh_host="mysshhost", **self.params_pg) as dbdatabase:
|
|
49
|
+
dbsession = dbdatabase.get_session()
|
|
50
|
+
assert (
|
|
51
|
+
str(dbsession.bind.engine.url)
|
|
52
|
+
== "postgresql+psycopg://myuser:***@0.0.0.0:12345/mydatabase"
|
|
53
|
+
)
|
|
54
|
+
assert dbdatabase.base == NoTZBase
|
|
55
|
+
|
|
56
|
+
with Database(
|
|
57
|
+
ssh_host="mysshhost", ssh_port=25, **params
|
|
58
|
+
) as dbdatabase:
|
|
59
|
+
dbsession = dbdatabase.get_session()
|
|
60
|
+
assert (
|
|
61
|
+
str(dbsession.bind.engine.url)
|
|
62
|
+
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
63
|
+
)
|
|
64
|
+
assert dbdatabase.base == NoTZBase
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""PostgreSQL specific utilities"""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
from .dburi import remove_driver_from_uri
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
import psycopg
|
|
10
|
-
except ImportError:
|
|
11
|
-
psycopg = None
|
|
12
|
-
# dependency missing, issue a warning
|
|
13
|
-
import warnings
|
|
14
|
-
|
|
15
|
-
warnings.warn(
|
|
16
|
-
"psycopg not found! Please install hdx-python-database[postgresql] to enable."
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def wait_for_postgresql(db_uri: str) -> None:
|
|
23
|
-
"""Waits for PostgreSQL database to be up
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
db_uri (str): Connection URI
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
None
|
|
30
|
-
"""
|
|
31
|
-
connecting_string = "Checking for PostgreSQL..."
|
|
32
|
-
db_uri_nd = remove_driver_from_uri(db_uri)
|
|
33
|
-
while True:
|
|
34
|
-
try:
|
|
35
|
-
logger.info(connecting_string)
|
|
36
|
-
connection = psycopg.connect(db_uri_nd, connect_timeout=3)
|
|
37
|
-
connection.close()
|
|
38
|
-
logger.info("PostgreSQL is running!")
|
|
39
|
-
break
|
|
40
|
-
except psycopg.OperationalError:
|
|
41
|
-
time.sleep(1)
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
"""Database Utility Tests"""
|
|
2
|
-
|
|
3
|
-
from copy import deepcopy
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
|
-
from os import remove
|
|
6
|
-
from os.path import join
|
|
7
|
-
from shutil import copyfile
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
from sqlalchemy import select
|
|
11
|
-
|
|
12
|
-
from .dbtestdate import DBTestDate, date_view_params
|
|
13
|
-
from hdx.database import Database, DatabaseError
|
|
14
|
-
from hdx.database.no_timezone import Base as NoTZBase
|
|
15
|
-
from hdx.database.with_timezone import Base as TZBase
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class TestDatabase:
|
|
19
|
-
started = False
|
|
20
|
-
stopped = False
|
|
21
|
-
table_base = NoTZBase
|
|
22
|
-
dbpath = join("tests", "test_database.db")
|
|
23
|
-
testdb = join("tests", "fixtures", "test.db")
|
|
24
|
-
params_pg = {
|
|
25
|
-
"database": "mydatabase",
|
|
26
|
-
"host": "myserver",
|
|
27
|
-
"port": 1234,
|
|
28
|
-
"username": "myuser",
|
|
29
|
-
"password": "mypass",
|
|
30
|
-
"dialect": "postgresql",
|
|
31
|
-
"driver": "psycopg",
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@pytest.fixture(scope="function")
|
|
35
|
-
def nodatabase(self):
|
|
36
|
-
try:
|
|
37
|
-
remove(TestDatabase.dbpath)
|
|
38
|
-
except OSError:
|
|
39
|
-
pass
|
|
40
|
-
return f"sqlite:///{TestDatabase.dbpath}"
|
|
41
|
-
|
|
42
|
-
@pytest.fixture(scope="function")
|
|
43
|
-
def database_to_reflect(self):
|
|
44
|
-
try:
|
|
45
|
-
remove(TestDatabase.dbpath)
|
|
46
|
-
copyfile(TestDatabase.testdb, TestDatabase.dbpath)
|
|
47
|
-
except OSError:
|
|
48
|
-
pass
|
|
49
|
-
return f"sqlite:///{TestDatabase.dbpath}"
|
|
50
|
-
|
|
51
|
-
def test_get_session(self, nodatabase):
|
|
52
|
-
assert DBTestDate.__tablename__ == "db_test_date"
|
|
53
|
-
|
|
54
|
-
def prepare_fn():
|
|
55
|
-
return Database.prepare_views([date_view_params])
|
|
56
|
-
|
|
57
|
-
with Database(
|
|
58
|
-
database=TestDatabase.dbpath,
|
|
59
|
-
port=None,
|
|
60
|
-
dialect="sqlite",
|
|
61
|
-
prepare_fn=prepare_fn,
|
|
62
|
-
) as dbdatabase:
|
|
63
|
-
dbsession = dbdatabase.session
|
|
64
|
-
assert str(dbsession.bind.engine.url) == nodatabase
|
|
65
|
-
assert TestDatabase.table_base == NoTZBase
|
|
66
|
-
now = datetime(2022, 10, 20, 22, 35, 55, tzinfo=timezone.utc)
|
|
67
|
-
input_dbtestdate = DBTestDate()
|
|
68
|
-
input_dbtestdate.test_date = now
|
|
69
|
-
dbsession.add(input_dbtestdate)
|
|
70
|
-
dbsession.commit()
|
|
71
|
-
dbtestdate = dbsession.execute(select(DBTestDate)).scalar_one()
|
|
72
|
-
assert dbtestdate.test_date == now
|
|
73
|
-
date_view = dbdatabase.get_prepare_results()[0]
|
|
74
|
-
dbtestdate = dbsession.execute(select(date_view)).scalar_one()
|
|
75
|
-
assert dbtestdate == now
|
|
76
|
-
with pytest.raises(DatabaseError):
|
|
77
|
-
Database.create_session()
|
|
78
|
-
with pytest.raises(DatabaseError):
|
|
79
|
-
with Database():
|
|
80
|
-
pass
|
|
81
|
-
|
|
82
|
-
def test_get_reflect_session(self, database_to_reflect):
|
|
83
|
-
with Database(
|
|
84
|
-
database=TestDatabase.dbpath,
|
|
85
|
-
port=None,
|
|
86
|
-
dialect="sqlite",
|
|
87
|
-
reflect=True,
|
|
88
|
-
) as dbdatabase:
|
|
89
|
-
dbengine = dbdatabase.get_engine()
|
|
90
|
-
assert str(dbengine.url) == "sqlite:///tests/test_database.db"
|
|
91
|
-
dbsession = dbdatabase.get_session()
|
|
92
|
-
assert TestDatabase.table_base == NoTZBase
|
|
93
|
-
assert str(dbsession.bind.engine.url) == database_to_reflect
|
|
94
|
-
Table1 = dbdatabase.get_reflected_classes().table1
|
|
95
|
-
row = dbsession.execute(select(Table1)).scalar_one()
|
|
96
|
-
assert row.id == "1"
|
|
97
|
-
assert row.col1 == "wfrefds"
|
|
98
|
-
# with reflection, type annotation maps do not work and hence
|
|
99
|
-
# we don't have a timezone here
|
|
100
|
-
assert row.date1 == datetime(1993, 9, 23, 14, 12, 56, 111000)
|
|
101
|
-
|
|
102
|
-
def test_get_session_ssh(
|
|
103
|
-
self,
|
|
104
|
-
mock_psycopg,
|
|
105
|
-
mock_SSHTunnelForwarder,
|
|
106
|
-
):
|
|
107
|
-
with Database(
|
|
108
|
-
ssh_host="mysshhost", **TestDatabase.params_pg
|
|
109
|
-
) as dbdatabase:
|
|
110
|
-
dbsession = dbdatabase.get_session()
|
|
111
|
-
assert (
|
|
112
|
-
str(dbsession.bind.engine.url)
|
|
113
|
-
== "postgresql+psycopg://myuser:***@0.0.0.0:12345/mydatabase"
|
|
114
|
-
)
|
|
115
|
-
params = deepcopy(TestDatabase.params_pg)
|
|
116
|
-
del params["password"]
|
|
117
|
-
with Database(
|
|
118
|
-
ssh_host="mysshhost", ssh_port=25, **params
|
|
119
|
-
) as dbdatabase:
|
|
120
|
-
dbsession = dbdatabase.get_session()
|
|
121
|
-
assert (
|
|
122
|
-
str(dbsession.bind.engine.url)
|
|
123
|
-
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
124
|
-
)
|
|
125
|
-
assert TestDatabase.table_base == NoTZBase
|
|
126
|
-
with Database(
|
|
127
|
-
ssh_host="mysshhost", ssh_port=25, db_has_tz=True, **params
|
|
128
|
-
) as dbdatabase:
|
|
129
|
-
dbsession = dbdatabase.get_session()
|
|
130
|
-
assert (
|
|
131
|
-
str(dbsession.bind.engine.url)
|
|
132
|
-
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
133
|
-
)
|
|
134
|
-
assert TestDatabase.table_base == TZBase
|
|
135
|
-
with Database(
|
|
136
|
-
ssh_host="mysshhost", ssh_port=25, table_base=TZBase, **params
|
|
137
|
-
) as dbdatabase:
|
|
138
|
-
dbsession = dbdatabase.get_session()
|
|
139
|
-
assert (
|
|
140
|
-
str(dbsession.bind.engine.url)
|
|
141
|
-
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
142
|
-
)
|
|
143
|
-
assert TestDatabase.table_base == TZBase
|
|
144
|
-
|
|
145
|
-
def test_recreate_schema(self, mock_engine):
|
|
146
|
-
db_uri = "postgresql+psycopg://myuser:mypass@0.0.0.0:12345/mydatabase"
|
|
147
|
-
assert Database.recreate_schema(mock_engine, db_uri) is True
|
|
148
|
-
db_uri = "Error"
|
|
149
|
-
assert Database.recreate_schema(mock_engine, db_uri) is False
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"""PostgreSQL Utility Tests"""
|
|
2
|
-
|
|
3
|
-
from . import PsycopgConnection
|
|
4
|
-
from hdx.database.postgresql import wait_for_postgresql
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestPostgreSQL:
|
|
8
|
-
connection_uri_pg = (
|
|
9
|
-
"postgresql+psycopg://myuser:mypass@myserver:1234/mydatabase"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
def test_wait_for_postgresql(self, mock_psycopg):
|
|
13
|
-
PsycopgConnection.connected = False
|
|
14
|
-
wait_for_postgresql(TestPostgreSQL.connection_uri_pg)
|
|
15
|
-
assert PsycopgConnection.connected is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|