hdx-python-database 1.1.7__tar.gz → 1.2.2__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.1.7 → hdx-python-database-1.2.2}/.github/workflows/publish.yml +3 -3
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.github/workflows/run-python-tests.yml +6 -6
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.readthedocs.yml +4 -4
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/PKG-INFO +4 -5
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/README.md +1 -1
- hdx-python-database-1.2.2/doc/main.md +99 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/pyproject.toml +15 -29
- hdx-python-database-1.2.2/requirements.txt +3 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/setup.cfg +4 -5
- hdx-python-database-1.2.2/src/hdx/database/__init__.py +127 -0
- hdx-python-database-1.2.2/src/hdx/database/_version.py +4 -0
- hdx-python-database-1.2.2/src/hdx/database/dburi.py +99 -0
- hdx-python-database-1.2.2/src/hdx/database/no_timezone.py +40 -0
- hdx-python-database-1.2.2/src/hdx/database/postgresql.py +40 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/PKG-INFO +4 -5
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/SOURCES.txt +8 -2
- hdx-python-database-1.2.2/src/hdx_python_database.egg-info/requires.txt +5 -0
- hdx-python-database-1.2.2/test-requirements.txt +4 -0
- hdx-python-database-1.2.2/tests/hdx/database/__init__.py +2 -0
- hdx-python-database-1.2.2/tests/hdx/database/conftest.py +22 -0
- hdx-python-database-1.2.2/tests/hdx/database/dbtestdate.py +23 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/tests/hdx/database/test_database.py +36 -47
- hdx-python-database-1.2.2/tests/hdx/database/test_dburi.py +95 -0
- hdx-python-database-1.2.2/tests/hdx/database/test_postgresql.py +16 -0
- hdx-python-database-1.1.7/doc/main.md +0 -64
- hdx-python-database-1.1.7/requirements.txt +0 -3
- hdx-python-database-1.1.7/src/hdx/database/__init__.py +0 -165
- hdx-python-database-1.1.7/src/hdx/database/_version.py +0 -5
- hdx-python-database-1.1.7/src/hdx/database/postgresql.py +0 -56
- hdx-python-database-1.1.7/src/hdx_python_database.egg-info/requires.txt +0 -5
- hdx-python-database-1.1.7/test-requirements.txt +0 -5
- hdx-python-database-1.1.7/tests/test_database.db +0 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.gitignore +0 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/LICENSE +0 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/dependency_links.txt +0 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/top_level.txt +0 -0
- {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/zip-safe +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: Publish to PyPI
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
@@ -9,13 +9,13 @@ jobs:
|
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
12
|
+
- uses: actions/checkout@v3
|
|
13
13
|
- name: Get history and tags for SCM 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@v4
|
|
19
19
|
with:
|
|
20
20
|
python-version: '3.x'
|
|
21
21
|
- name: Install dependencies
|
{hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.github/workflows/run-python-tests.yml
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# This workflow will install Python dependencies, lint and run tests
|
|
2
2
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
|
3
3
|
|
|
4
|
-
name:
|
|
4
|
+
name: Run tests
|
|
5
5
|
|
|
6
6
|
on:
|
|
7
7
|
push:
|
|
@@ -16,9 +16,9 @@ jobs:
|
|
|
16
16
|
runs-on: ubuntu-latest
|
|
17
17
|
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@v3
|
|
20
20
|
- name: Set up Python
|
|
21
|
-
uses: actions/setup-python@
|
|
21
|
+
uses: actions/setup-python@v4
|
|
22
22
|
with:
|
|
23
23
|
python-version: '3.x'
|
|
24
24
|
- name: Install dependencies
|
|
@@ -29,11 +29,11 @@ jobs:
|
|
|
29
29
|
run: |
|
|
30
30
|
tox
|
|
31
31
|
- name: Publish Unit Test Results
|
|
32
|
-
uses: EnricoMi/publish-unit-test-result-action@
|
|
32
|
+
uses: EnricoMi/publish-unit-test-result-action@v2
|
|
33
33
|
if: always()
|
|
34
34
|
with:
|
|
35
35
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
-
|
|
36
|
+
junit_files: .tox/*.xml
|
|
37
37
|
- name: Upload Coverage Results
|
|
38
|
-
uses: codecov/codecov-action@
|
|
38
|
+
uses: codecov/codecov-action@v3
|
|
39
39
|
if: success()
|
|
@@ -5,13 +5,13 @@ mkdocs:
|
|
|
5
5
|
fail_on_warning: false
|
|
6
6
|
|
|
7
7
|
build:
|
|
8
|
-
os: ubuntu-
|
|
8
|
+
os: ubuntu-22.04
|
|
9
9
|
tools:
|
|
10
|
-
python: "3.
|
|
10
|
+
python: "3.11"
|
|
11
11
|
jobs:
|
|
12
12
|
pre_build:
|
|
13
|
-
- pip install
|
|
14
|
-
- pip install
|
|
13
|
+
- pip install --upgrade mkdocs
|
|
14
|
+
- pip install --upgrade pydoc-markdown
|
|
15
15
|
- pydoc-markdown
|
|
16
16
|
- mv docs/content/* docs
|
|
17
17
|
- rmdir docs/content
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hdx-python-database
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: HDX Python database utilities
|
|
5
5
|
Home-page: https://github.com/OCHA-DAP/hdx-python-database
|
|
6
6
|
Author: Michael Rans
|
|
@@ -13,12 +13,11 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
13
13
|
Classifier: Programming Language :: Python
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.8
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.9
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
21
|
Classifier: Intended Audience :: Developers
|
|
23
22
|
Classifier: License :: OSI Approved :: MIT License
|
|
24
23
|
Classifier: Natural Language :: English
|
|
@@ -26,12 +25,12 @@ Classifier: Operating System :: POSIX :: Linux
|
|
|
26
25
|
Classifier: Operating System :: Unix
|
|
27
26
|
Classifier: Operating System :: MacOS
|
|
28
27
|
Classifier: Operating System :: Microsoft :: Windows
|
|
29
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.8
|
|
30
29
|
Description-Content-Type: text/markdown; charset=UTF-8
|
|
31
30
|
Provides-Extra: postgresql
|
|
32
31
|
License-File: LICENSE
|
|
33
32
|
|
|
34
|
-
[](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yml)
|
|
35
34
|
[](https://codecov.io/gh/OCHA-DAP/hdx-python-database)
|
|
36
35
|
[](https://github.com/psf/black)
|
|
37
36
|
[](https://pycqa.github.io/isort/)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yml)
|
|
2
2
|
[](https://codecov.io/gh/OCHA-DAP/hdx-python-database)
|
|
3
3
|
[](https://github.com/psf/black)
|
|
4
4
|
[](https://pycqa.github.io/isort/)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Summary
|
|
2
|
+
|
|
3
|
+
The HDX Python Database Library provides utilities for connecting to databases in a standard way including
|
|
4
|
+
through an ssh tunnel if needed. It is built on top of SQLAlchemy and simplifies its setup.
|
|
5
|
+
|
|
6
|
+
# Information
|
|
7
|
+
|
|
8
|
+
This library is part of the [Humanitarian Data Exchange](https://data.humdata.org/) (HDX) project. If you have
|
|
9
|
+
humanitarian related data, please upload your datasets to HDX.
|
|
10
|
+
|
|
11
|
+
The code for the library is [here](https://github.com/OCHA-DAP/hdx-python-database).
|
|
12
|
+
The library has detailed API documentation which can be found in the menu at the top.
|
|
13
|
+
|
|
14
|
+
Additional postgresql functionality is available if this library is installed with extra "postgresql":
|
|
15
|
+
|
|
16
|
+
pip install hdx-python-database[postgresql]
|
|
17
|
+
|
|
18
|
+
## Breaking changes
|
|
19
|
+
|
|
20
|
+
From 1.2.2, database datetime columns are assumed to be timezoneless unless
|
|
21
|
+
db_has_tz is set to True.
|
|
22
|
+
|
|
23
|
+
From 1.2.1, wait_for_postgresql takes connection URI not database parameters,
|
|
24
|
+
get_params_from_sqlalchemy_url renamed to get_params_from_connection_uri
|
|
25
|
+
and moved to dburi module, get_sqlalchemy_url renamed to get_connection_uri and
|
|
26
|
+
moved to dburi module. New function remove_driver_from_uri in dburi module.
|
|
27
|
+
Parameter driver replaced by dialect+driver. Supports Python 3.8 and later.
|
|
28
|
+
|
|
29
|
+
From 1.1.2, the postgres module is renamed postgresql and function wait_for_postgres
|
|
30
|
+
is renamed wait_for_postgresql.
|
|
31
|
+
|
|
32
|
+
Versions from 1.0.8 support Python 3.6 and later.
|
|
33
|
+
|
|
34
|
+
Versions from 1.0.6 no longer support Python 2.7.
|
|
35
|
+
|
|
36
|
+
# Description of Utilities
|
|
37
|
+
|
|
38
|
+
## Database
|
|
39
|
+
|
|
40
|
+
Your SQLAlchemy database tables must inherit from `Base` in
|
|
41
|
+
`hdx.database` or `hdx.database.database_notz` eg. :
|
|
42
|
+
|
|
43
|
+
from hdx.database import Base
|
|
44
|
+
class MyTable(Base):
|
|
45
|
+
my_col = Column(Integer, ForeignKey(MyTable2.col2), primary_key=True)
|
|
46
|
+
|
|
47
|
+
Then a connection can be made to a database as follows including through an SSH
|
|
48
|
+
tunnel:
|
|
49
|
+
|
|
50
|
+
# Get SQLAlchemy session object given database parameters and
|
|
51
|
+
# if needed SSH parameters. If database is PostgreSQL, will poll
|
|
52
|
+
# till it is up.
|
|
53
|
+
from hdx.database import Database
|
|
54
|
+
with Database(database="db", host="1.2.3.4", username="user",
|
|
55
|
+
password="pass", dialect="dialect", driver="driver",
|
|
56
|
+
ssh_host="5.6.7.8", ssh_port=2222, ssh_username="sshuser",
|
|
57
|
+
ssh_private_key="path_to_key", db_has_tz=True) as session:
|
|
58
|
+
session.query(...)
|
|
59
|
+
|
|
60
|
+
`db_has_tz` which defaults to `False` indicates whether database datetime
|
|
61
|
+
columns have timezones. If not, conversion occurs between Python datetimes
|
|
62
|
+
with timezones to timezoneless database columns.
|
|
63
|
+
|
|
64
|
+
## Connection URI
|
|
65
|
+
|
|
66
|
+
There are functions to handle converting from connection URIs to parameters and
|
|
67
|
+
vice-versa as well as a function to remove the driver string from a connection
|
|
68
|
+
URI that contains both dialect and driver.
|
|
69
|
+
|
|
70
|
+
# Extract dictionary of parameters from database connection URI
|
|
71
|
+
result = get_params_from_connection_uri(
|
|
72
|
+
"postgresql+psycopg://myuser:mypass@myserver:1234/mydatabase"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Build database connection URI from dictionary of parameters
|
|
76
|
+
params_pg = {
|
|
77
|
+
"database": "mydatabase",
|
|
78
|
+
"host": "myserver",
|
|
79
|
+
"port": 1234,
|
|
80
|
+
"username": "myuser",
|
|
81
|
+
"password": "mypass",
|
|
82
|
+
"dialect": "postgresql",
|
|
83
|
+
"driver": "psycopg",
|
|
84
|
+
}
|
|
85
|
+
result = get_connection_uri(**params_pg)
|
|
86
|
+
|
|
87
|
+
db_uri_nd = remove_driver_from_uri(
|
|
88
|
+
"postgresql+psycopg://myuser:mypass@myserver:1234/mydatabase"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
## PostgreSQL specific
|
|
92
|
+
|
|
93
|
+
There is a PostgreSQL specific call that only returns when the PostgreSQL database
|
|
94
|
+
is available:
|
|
95
|
+
|
|
96
|
+
# Wait until PostgreSQL is up
|
|
97
|
+
# Library should be installed with hdx-python-database[postgresql]
|
|
98
|
+
wait_for_postgresql("mydatabase", "myserver", 5432, "myuser", "mypass")
|
|
99
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = [ "setuptools>=
|
|
2
|
+
requires = [ "setuptools>=65", "setuptools_scm[toml]>=7.1", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[tool.setuptools_scm]
|
|
@@ -15,22 +15,12 @@ profile = "black"
|
|
|
15
15
|
line_length = 79
|
|
16
16
|
multi_line_output = 3
|
|
17
17
|
|
|
18
|
-
[tool.
|
|
19
|
-
# make output nice
|
|
20
|
-
format = "grouped"
|
|
21
|
-
max_line_length = 79
|
|
22
|
-
# show line of source code in output
|
|
23
|
-
show_source = true
|
|
18
|
+
[tool.flake8]
|
|
24
19
|
exclude = ["_version.py"]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
pycodestyle = ["+*", "-E203", "-E501", "-W503"]
|
|
30
|
-
pyflakes = ["+*"]
|
|
31
|
-
pylint = ["+*"]
|
|
32
|
-
flake8-isort = ["+*"]
|
|
33
|
-
flake8-black = ["+*"]
|
|
20
|
+
ignore = ["E203", "E501", "W503"]
|
|
21
|
+
max-line-length = 79
|
|
22
|
+
count = true
|
|
23
|
+
show_source = true
|
|
34
24
|
|
|
35
25
|
[tool.pytest.ini_options]
|
|
36
26
|
pythonpath = "src"
|
|
@@ -97,45 +87,41 @@ output_directory = "docs"
|
|
|
97
87
|
[tool.tox]
|
|
98
88
|
legacy_tox_ini = """
|
|
99
89
|
[tox]
|
|
100
|
-
|
|
101
|
-
envlist = py310, lint
|
|
90
|
+
envlist = py311, lint
|
|
102
91
|
|
|
103
92
|
[gh-actions]
|
|
104
93
|
python =
|
|
105
|
-
3:
|
|
94
|
+
3: py311, lint
|
|
106
95
|
|
|
107
96
|
[testenv]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
wheel_pep517 = build
|
|
97
|
+
package = wheel
|
|
98
|
+
wheel_build_env = .pkg
|
|
111
99
|
deps =
|
|
112
100
|
-r test-requirements.txt
|
|
113
101
|
commands =
|
|
114
102
|
pytest --cov=hdx --no-cov-on-fail --junitxml=.tox/test-results.xml --cov-report=xml --cov-report=term-missing
|
|
115
103
|
|
|
116
104
|
[testenv:lint]
|
|
117
|
-
wheel_build_env = py310
|
|
118
105
|
deps =
|
|
119
|
-
flakeheaven
|
|
120
106
|
flake8
|
|
121
107
|
flake8-isort
|
|
122
108
|
flake8-black
|
|
109
|
+
flake8-pyproject
|
|
123
110
|
commands =
|
|
124
|
-
|
|
111
|
+
flake8 --color=always src tests
|
|
125
112
|
|
|
126
113
|
[testenv:docs]
|
|
127
|
-
wheel_build_env = py310
|
|
128
114
|
deps =
|
|
129
115
|
pydoc-markdown
|
|
130
116
|
commands =
|
|
131
117
|
pydoc-markdown
|
|
132
118
|
|
|
133
119
|
[testenv:publish]
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
package = sdist
|
|
121
|
+
pass_env = SSH_AUTH_SOCK, TWINE_USERNAME, TWINE_PASSWORD
|
|
136
122
|
deps =
|
|
137
123
|
twine
|
|
138
124
|
|
|
139
125
|
commands =
|
|
140
|
-
twine upload {
|
|
126
|
+
twine upload {work_dir}/{package_env}/dist/*
|
|
141
127
|
"""
|
|
@@ -17,12 +17,11 @@ classifiers =
|
|
|
17
17
|
Programming Language :: Python
|
|
18
18
|
Programming Language :: Python :: 3
|
|
19
19
|
Programming Language :: Python :: 3 :: Only
|
|
20
|
-
Programming Language :: Python :: 3.6
|
|
21
|
-
Programming Language :: Python :: 3.7
|
|
22
20
|
Programming Language :: Python :: 3.8
|
|
23
21
|
Programming Language :: Python :: 3.9
|
|
24
22
|
Programming Language :: Python :: 3.10
|
|
25
23
|
Programming Language :: Python :: 3.11
|
|
24
|
+
Programming Language :: Python :: 3.12
|
|
26
25
|
Intended Audience :: Developers
|
|
27
26
|
License :: OSI Approved :: MIT License
|
|
28
27
|
Natural Language :: English
|
|
@@ -37,13 +36,13 @@ packages = find_namespace:
|
|
|
37
36
|
include_package_data = True
|
|
38
37
|
package_dir =
|
|
39
38
|
=src
|
|
40
|
-
python_requires = >=3.
|
|
39
|
+
python_requires = >=3.8
|
|
41
40
|
install_requires =
|
|
42
|
-
sqlalchemy
|
|
41
|
+
sqlalchemy>=2
|
|
43
42
|
sshtunnel
|
|
44
43
|
|
|
45
44
|
[options.extras_require]
|
|
46
|
-
postgresql =
|
|
45
|
+
postgresql = psycopg[binary]
|
|
47
46
|
|
|
48
47
|
[options.packages.find]
|
|
49
48
|
where = src
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from ._version import version as __version__ # noqa: F401
|
|
2
|
+
from .dburi import get_connection_uri
|
|
3
|
+
|
|
4
|
+
"""Database utilities"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Optional, Type, Union
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import create_engine
|
|
9
|
+
from sqlalchemy.ext.declarative import declared_attr
|
|
10
|
+
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
|
11
|
+
from sqlalchemy.pool import NullPool
|
|
12
|
+
from sshtunnel import SSHTunnelForwarder
|
|
13
|
+
|
|
14
|
+
from .no_timezone import Base as NoTZBase
|
|
15
|
+
from .postgresql import wait_for_postgresql
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Base(DeclarativeBase):
|
|
21
|
+
@declared_attr.directive
|
|
22
|
+
def __tablename__(cls):
|
|
23
|
+
return f"{cls.__name__.lower()}s"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Database:
|
|
27
|
+
"""Database helper class to handle ssh tunnels, waiting for PostgreSQL to
|
|
28
|
+
be up etc. Can be used in a with statement returning a Session object.
|
|
29
|
+
db_has_tz which defaults to False indicates whether database datetime
|
|
30
|
+
columns have timezones. If not, conversion occurs between Python datetimes
|
|
31
|
+
with timezones to timezoneless database columns.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
database (Optional[str]): Database name
|
|
35
|
+
host (Optional[str]): Host where database is located
|
|
36
|
+
port (Union[int, str, None]): Database port
|
|
37
|
+
username (Optional[str]): Username to log into database
|
|
38
|
+
password (Optional[str]): Password to log into database
|
|
39
|
+
dialect (str): Database dialect. Defaults to "postgresql".
|
|
40
|
+
driver (Optional[str]): Database driver. Defaults to None (psycopg if postgresql or None)
|
|
41
|
+
db_has_tz (bool): True if db datetime columns have timezone. Defaults to False.
|
|
42
|
+
**kwargs: See below
|
|
43
|
+
ssh_host (str): SSH host (the server to connect to)
|
|
44
|
+
ssh_port (int): SSH port. Defaults to 22.
|
|
45
|
+
ssh_username (str): SSH username
|
|
46
|
+
ssh_password (str): SSH password
|
|
47
|
+
ssh_private_key: Location of SSH private key (instead of ssh_password)
|
|
48
|
+
For more advanced usage, see SSHTunnelForwarder documentation.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
database: Optional[str] = None,
|
|
55
|
+
host: Optional[str] = None,
|
|
56
|
+
port: Union[int, str, None] = None,
|
|
57
|
+
username: Optional[str] = None,
|
|
58
|
+
password: Optional[str] = None,
|
|
59
|
+
dialect: str = "postgresql",
|
|
60
|
+
driver: Optional[str] = None,
|
|
61
|
+
db_has_tz: bool = False,
|
|
62
|
+
**kwargs: Any,
|
|
63
|
+
) -> None:
|
|
64
|
+
if port is not None:
|
|
65
|
+
port = int(port)
|
|
66
|
+
if len(kwargs) != 0:
|
|
67
|
+
ssh_host = kwargs["ssh_host"]
|
|
68
|
+
del kwargs["ssh_host"]
|
|
69
|
+
ssh_port = kwargs.get("ssh_port")
|
|
70
|
+
if ssh_port is not None:
|
|
71
|
+
ssh_port = int(ssh_port)
|
|
72
|
+
del kwargs["ssh_port"]
|
|
73
|
+
else:
|
|
74
|
+
ssh_port = 22
|
|
75
|
+
self.server = SSHTunnelForwarder(
|
|
76
|
+
(ssh_host, ssh_port),
|
|
77
|
+
remote_bind_address=(host, port),
|
|
78
|
+
**kwargs,
|
|
79
|
+
)
|
|
80
|
+
self.server.start()
|
|
81
|
+
host = self.server.local_bind_host
|
|
82
|
+
port = self.server.local_bind_port
|
|
83
|
+
else:
|
|
84
|
+
self.server = None
|
|
85
|
+
db_uri = get_connection_uri(
|
|
86
|
+
database,
|
|
87
|
+
host,
|
|
88
|
+
port,
|
|
89
|
+
username,
|
|
90
|
+
password,
|
|
91
|
+
dialect=dialect,
|
|
92
|
+
driver=driver,
|
|
93
|
+
)
|
|
94
|
+
if dialect == "postgresql":
|
|
95
|
+
wait_for_postgresql(db_uri)
|
|
96
|
+
if db_has_tz:
|
|
97
|
+
table_base = Base
|
|
98
|
+
else:
|
|
99
|
+
table_base = NoTZBase
|
|
100
|
+
self.session = self.get_session(db_uri, table_base=table_base)
|
|
101
|
+
|
|
102
|
+
def __enter__(self) -> Session:
|
|
103
|
+
return self.session
|
|
104
|
+
|
|
105
|
+
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
|
106
|
+
self.session.close()
|
|
107
|
+
if self.server is not None:
|
|
108
|
+
self.server.stop()
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def get_session(
|
|
112
|
+
db_uri: str, table_base: Type[DeclarativeBase] = NoTZBase
|
|
113
|
+
) -> Session:
|
|
114
|
+
"""Gets SQLAlchemy session given url. Tables must inherit from Base in
|
|
115
|
+
hdx.utilities.database unless base is defined.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
db_uri (str): Connection URI
|
|
119
|
+
table_base (Type[DeclarativeBase]): Base database table class. Defaults to NoTZBase.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
sqlalchemy.orm.Session: SQLAlchemy session
|
|
123
|
+
"""
|
|
124
|
+
engine = create_engine(db_uri, poolclass=NullPool, echo=False)
|
|
125
|
+
Session = sessionmaker(bind=engine)
|
|
126
|
+
table_base.metadata.create_all(engine)
|
|
127
|
+
return Session()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Connection URI utilities"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Union
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_params_from_connection_uri(
|
|
8
|
+
db_uri: str,
|
|
9
|
+
include_driver: bool = True,
|
|
10
|
+
) -> Dict[str, Any]:
|
|
11
|
+
"""Gets PostgreSQL database connection parameters from connection URI
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
db_uri (str): Connection URI
|
|
15
|
+
include_driver (bool): Whether to include driver in params. Defaults to True.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Dict[str,Any]: Dictionary of database connection parameters
|
|
19
|
+
"""
|
|
20
|
+
result = urlsplit(db_uri)
|
|
21
|
+
dialectdriver = result.scheme.split("+")
|
|
22
|
+
if len(dialectdriver) == 1:
|
|
23
|
+
dialect = dialectdriver[0]
|
|
24
|
+
driver = None
|
|
25
|
+
else:
|
|
26
|
+
dialect, driver = dialectdriver
|
|
27
|
+
result = {
|
|
28
|
+
"database": result.path[1:],
|
|
29
|
+
"host": result.hostname,
|
|
30
|
+
"port": result.port,
|
|
31
|
+
"username": result.username,
|
|
32
|
+
"password": result.password,
|
|
33
|
+
"dialect": dialect,
|
|
34
|
+
}
|
|
35
|
+
if include_driver:
|
|
36
|
+
result["driver"] = driver
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_connection_uri(
|
|
41
|
+
database: Optional[str] = None,
|
|
42
|
+
host: Optional[str] = None,
|
|
43
|
+
port: Union[int, str, None] = None,
|
|
44
|
+
username: Optional[str] = None,
|
|
45
|
+
password: Optional[str] = None,
|
|
46
|
+
dialect: str = "postgresql",
|
|
47
|
+
driver: Optional[str] = None,
|
|
48
|
+
include_driver: bool = True,
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Gets connection uri from database connection parameters
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
database (Optional[str]): Database name
|
|
54
|
+
host (Optional[str]): Host where database is located
|
|
55
|
+
port (Union[int, str, None]): Database port
|
|
56
|
+
username (Optional[str]): Username to log into database
|
|
57
|
+
password (Optional[str]): Password to log into database
|
|
58
|
+
dialect (str): Database dialect. Defaults to "postgresql".
|
|
59
|
+
driver (Optional[str]): Database driver. Defaults to None (psycopg if postgresql or None)
|
|
60
|
+
include_driver (bool): Whether to include driver in uri. Defaults to True.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: Connection URI
|
|
64
|
+
"""
|
|
65
|
+
strings = [dialect]
|
|
66
|
+
if include_driver:
|
|
67
|
+
if dialect == "postgresql" and driver is None:
|
|
68
|
+
driver = "psycopg"
|
|
69
|
+
if driver:
|
|
70
|
+
strings.append(f"+{driver}")
|
|
71
|
+
strings.append("://")
|
|
72
|
+
if username:
|
|
73
|
+
strings.append(username)
|
|
74
|
+
if password:
|
|
75
|
+
strings.append(f":{password}@")
|
|
76
|
+
else:
|
|
77
|
+
strings.append("@")
|
|
78
|
+
if host:
|
|
79
|
+
strings.append(host)
|
|
80
|
+
if port is not None:
|
|
81
|
+
strings.append(f":{port}")
|
|
82
|
+
if database:
|
|
83
|
+
strings.append(f"/{database}")
|
|
84
|
+
return "".join(strings)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def remove_driver_from_uri(db_uri: str) -> str:
|
|
88
|
+
"""Remove driver from connection URI
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
db_uri (str): Connection URI
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str: Connection URI with driver removed
|
|
95
|
+
"""
|
|
96
|
+
result = urlsplit(db_uri)
|
|
97
|
+
dialectdriver = result.scheme
|
|
98
|
+
dialect = dialectdriver.split("+")[0]
|
|
99
|
+
return db_uri.replace(dialectdriver, dialect)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Utilities for database datetime columns without timezone"""
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import DateTime, TypeDecorator
|
|
6
|
+
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConversionNoTZ(TypeDecorator):
|
|
10
|
+
"""Convert from/to datetime with timezone from database columns that don't
|
|
11
|
+
use timezone"""
|
|
12
|
+
|
|
13
|
+
impl = DateTime
|
|
14
|
+
cache_ok = True
|
|
15
|
+
|
|
16
|
+
def process_bind_param(self, value: Optional[datetime], _):
|
|
17
|
+
if value is None:
|
|
18
|
+
return value
|
|
19
|
+
if value.tzinfo:
|
|
20
|
+
value = value.astimezone(timezone.utc)
|
|
21
|
+
|
|
22
|
+
return value.replace(tzinfo=None)
|
|
23
|
+
|
|
24
|
+
def process_result_value(self, value, _):
|
|
25
|
+
if value is None:
|
|
26
|
+
return value
|
|
27
|
+
if value.tzinfo is None:
|
|
28
|
+
return value.replace(tzinfo=timezone.utc)
|
|
29
|
+
|
|
30
|
+
return value.astimezone(timezone.utc)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Base(DeclarativeBase):
|
|
34
|
+
type_annotation_map = {
|
|
35
|
+
datetime: ConversionNoTZ,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@declared_attr.directive
|
|
39
|
+
def __tablename__(cls):
|
|
40
|
+
return f"{cls.__name__.lower()}s"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""PostgreSQL specific utilities"""
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from .dburi import remove_driver_from_uri
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import psycopg
|
|
9
|
+
except ImportError:
|
|
10
|
+
psycopg = None
|
|
11
|
+
# dependency missing, issue a warning
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
warnings.warn(
|
|
15
|
+
"psycopg not found! Please install hdx-python-database[postgresql] to enable."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def wait_for_postgresql(db_uri: str) -> None:
|
|
22
|
+
"""Waits for PostgreSQL database to be up
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
db_uri (str): Connection URI
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
None
|
|
29
|
+
"""
|
|
30
|
+
connecting_string = "Checking for PostgreSQL..."
|
|
31
|
+
db_uri_nd = remove_driver_from_uri(db_uri)
|
|
32
|
+
while True:
|
|
33
|
+
try:
|
|
34
|
+
logger.info(connecting_string)
|
|
35
|
+
connection = psycopg.connect(db_uri_nd, connect_timeout=3)
|
|
36
|
+
connection.close()
|
|
37
|
+
logger.info("PostgreSQL is running!")
|
|
38
|
+
break
|
|
39
|
+
except psycopg.OperationalError:
|
|
40
|
+
time.sleep(1)
|