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.
Files changed (37) hide show
  1. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.github/workflows/publish.yml +3 -3
  2. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.github/workflows/run-python-tests.yml +6 -6
  3. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.readthedocs.yml +4 -4
  4. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/PKG-INFO +4 -5
  5. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/README.md +1 -1
  6. hdx-python-database-1.2.2/doc/main.md +99 -0
  7. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/pyproject.toml +15 -29
  8. hdx-python-database-1.2.2/requirements.txt +3 -0
  9. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/setup.cfg +4 -5
  10. hdx-python-database-1.2.2/src/hdx/database/__init__.py +127 -0
  11. hdx-python-database-1.2.2/src/hdx/database/_version.py +4 -0
  12. hdx-python-database-1.2.2/src/hdx/database/dburi.py +99 -0
  13. hdx-python-database-1.2.2/src/hdx/database/no_timezone.py +40 -0
  14. hdx-python-database-1.2.2/src/hdx/database/postgresql.py +40 -0
  15. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/PKG-INFO +4 -5
  16. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/SOURCES.txt +8 -2
  17. hdx-python-database-1.2.2/src/hdx_python_database.egg-info/requires.txt +5 -0
  18. hdx-python-database-1.2.2/test-requirements.txt +4 -0
  19. hdx-python-database-1.2.2/tests/hdx/database/__init__.py +2 -0
  20. hdx-python-database-1.2.2/tests/hdx/database/conftest.py +22 -0
  21. hdx-python-database-1.2.2/tests/hdx/database/dbtestdate.py +23 -0
  22. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/tests/hdx/database/test_database.py +36 -47
  23. hdx-python-database-1.2.2/tests/hdx/database/test_dburi.py +95 -0
  24. hdx-python-database-1.2.2/tests/hdx/database/test_postgresql.py +16 -0
  25. hdx-python-database-1.1.7/doc/main.md +0 -64
  26. hdx-python-database-1.1.7/requirements.txt +0 -3
  27. hdx-python-database-1.1.7/src/hdx/database/__init__.py +0 -165
  28. hdx-python-database-1.1.7/src/hdx/database/_version.py +0 -5
  29. hdx-python-database-1.1.7/src/hdx/database/postgresql.py +0 -56
  30. hdx-python-database-1.1.7/src/hdx_python_database.egg-info/requires.txt +0 -5
  31. hdx-python-database-1.1.7/test-requirements.txt +0 -5
  32. hdx-python-database-1.1.7/tests/test_database.db +0 -0
  33. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/.gitignore +0 -0
  34. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/LICENSE +0 -0
  35. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/dependency_links.txt +0 -0
  36. {hdx-python-database-1.1.7 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/top_level.txt +0 -0
  37. {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: publish
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@v2
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@v2
18
+ uses: actions/setup-python@v4
19
19
  with:
20
20
  python-version: '3.x'
21
21
  - name: Install dependencies
@@ -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: build
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@v2
19
+ - uses: actions/checkout@v3
20
20
  - name: Set up Python
21
- uses: actions/setup-python@v2
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@v1
32
+ uses: EnricoMi/publish-unit-test-result-action@v2
33
33
  if: always()
34
34
  with:
35
35
  github_token: ${{ secrets.GITHUB_TOKEN }}
36
- files: .tox/*.xml
36
+ junit_files: .tox/*.xml
37
37
  - name: Upload Coverage Results
38
- uses: codecov/codecov-action@v2
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-20.04
8
+ os: ubuntu-22.04
9
9
  tools:
10
- python: "3.10"
10
+ python: "3.11"
11
11
  jobs:
12
12
  pre_build:
13
- - pip install pydoc-markdown==4.5.1
14
- - pip install mkdocs==1.3.0
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.1.7
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.6
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
- [![Build Status](https://github.com/OCHA-DAP/hdx-python-database/workflows/build/badge.svg)](https://github.com/OCHA-DAP/hdx-python-database/actions?query=workflow%3Abuild)
33
+ [![Build Status](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yml/badge.svg)](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yml)
35
34
  [![Coverage Status](https://codecov.io/gh/OCHA-DAP/hdx-python-database/branch/main/graph/badge.svg?token=JpWZc5js4y)](https://codecov.io/gh/OCHA-DAP/hdx-python-database)
36
35
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
37
36
  [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
@@ -1,4 +1,4 @@
1
- [![Build Status](https://github.com/OCHA-DAP/hdx-python-database/workflows/build/badge.svg)](https://github.com/OCHA-DAP/hdx-python-database/actions?query=workflow%3Abuild)
1
+ [![Build Status](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yml/badge.svg)](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yml)
2
2
  [![Coverage Status](https://codecov.io/gh/OCHA-DAP/hdx-python-database/branch/main/graph/badge.svg?token=JpWZc5js4y)](https://codecov.io/gh/OCHA-DAP/hdx-python-database)
3
3
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
4
4
  [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](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>=45", "setuptools_scm[toml]>=6.2", "wheel"]
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.flakeheaven]
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
- # list of plugins and rules for them
27
- [tool.flakeheaven.plugins]
28
- mccabe = ["+*"]
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
- isolated_build = true
101
- envlist = py310, lint
90
+ envlist = py311, lint
102
91
 
103
92
  [gh-actions]
104
93
  python =
105
- 3: py310, lint
94
+ 3: py311, lint
106
95
 
107
96
  [testenv]
108
- recreate = true
109
- wheel = true
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
- flakeheaven lint src tests
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
- wheel_build_env = py310
135
- passenv = SSH_AUTH_SOCK TWINE_USERNAME TWINE_PASSWORD
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 {distdir}/*
126
+ twine upload {work_dir}/{package_env}/dist/*
141
127
  """
@@ -0,0 +1,3 @@
1
+ psycopg[binary]==3.1.8
2
+ sqlalchemy==2.0.4
3
+ sshtunnel==0.4.0
@@ -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.6
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 = psycopg2-binary
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,4 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ __version__ = version = '1.2.2'
4
+ __version_tuple__ = version_tuple = (1, 2, 2)
@@ -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)