hdx-python-database 1.2.0__py2.py3-none-any.whl → 1.2.2__py2.py3-none-any.whl

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/database/__init__.py CHANGED
@@ -1,32 +1,34 @@
1
1
  from ._version import version as __version__ # noqa: F401
2
+ from .dburi import get_connection_uri
2
3
 
3
4
  """Database utilities"""
4
5
  import logging
5
- from typing import Any, Dict, Optional, Union
6
- from urllib.parse import urlsplit
6
+ from typing import Any, Optional, Type, Union
7
7
 
8
8
  from sqlalchemy import create_engine
9
- from sqlalchemy.ext.declarative import declarative_base, declared_attr
10
- from sqlalchemy.orm import Session, sessionmaker
9
+ from sqlalchemy.ext.declarative import declared_attr
10
+ from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
11
11
  from sqlalchemy.pool import NullPool
12
12
  from sshtunnel import SSHTunnelForwarder
13
13
 
14
- from hdx.database.postgresql import wait_for_postgresql
14
+ from .no_timezone import Base as NoTZBase
15
+ from .postgresql import wait_for_postgresql
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
18
19
 
19
- class HDXBase:
20
+ class Base(DeclarativeBase):
20
21
  @declared_attr.directive
21
22
  def __tablename__(cls):
22
23
  return f"{cls.__name__.lower()}s"
23
24
 
24
25
 
25
- Base = declarative_base(cls=HDXBase)
26
-
27
-
28
26
  class Database:
29
- """Database helper class to handle ssh tunnels, waiting for PostgreSQL to be up etc.
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.
30
32
 
31
33
  Args:
32
34
  database (Optional[str]): Database name
@@ -34,7 +36,9 @@ class Database:
34
36
  port (Union[int, str, None]): Database port
35
37
  username (Optional[str]): Username to log into database
36
38
  password (Optional[str]): Password to log into database
37
- driver (str): Database driver. Defaults to "postgresql".
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.
38
42
  **kwargs: See below
39
43
  ssh_host (str): SSH host (the server to connect to)
40
44
  ssh_port (int): SSH port. Defaults to 22.
@@ -52,7 +56,9 @@ class Database:
52
56
  port: Union[int, str, None] = None,
53
57
  username: Optional[str] = None,
54
58
  password: Optional[str] = None,
55
- driver: str = "postgresql",
59
+ dialect: str = "postgresql",
60
+ driver: Optional[str] = None,
61
+ db_has_tz: bool = False,
56
62
  **kwargs: Any,
57
63
  ) -> None:
58
64
  if port is not None:
@@ -76,12 +82,22 @@ class Database:
76
82
  port = self.server.local_bind_port
77
83
  else:
78
84
  self.server = None
79
- if driver == "postgresql":
80
- wait_for_postgresql(database, host, port, username, password)
81
- db_url = self.get_sqlalchemy_url(
82
- database, host, port, username, password, driver=driver
85
+ db_uri = get_connection_uri(
86
+ database,
87
+ host,
88
+ port,
89
+ username,
90
+ password,
91
+ dialect=dialect,
92
+ driver=driver,
83
93
  )
84
- self.session = self.get_session(db_url)
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)
85
101
 
86
102
  def __enter__(self) -> Session:
87
103
  return self.session
@@ -92,74 +108,20 @@ class Database:
92
108
  self.server.stop()
93
109
 
94
110
  @staticmethod
95
- def get_session(db_url: str) -> Session:
96
- """Gets SQLAlchemy session given url. Your tables must inherit
97
- from Base in hdx.utilities.database.
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.
98
116
 
99
117
  Args:
100
- db_url (str): SQLAlchemy url
118
+ db_uri (str): Connection URI
119
+ table_base (Type[DeclarativeBase]): Base database table class. Defaults to NoTZBase.
101
120
 
102
121
  Returns:
103
122
  sqlalchemy.orm.Session: SQLAlchemy session
104
123
  """
105
- engine = create_engine(db_url, poolclass=NullPool, echo=False)
124
+ engine = create_engine(db_uri, poolclass=NullPool, echo=False)
106
125
  Session = sessionmaker(bind=engine)
107
- Base.metadata.create_all(engine)
126
+ table_base.metadata.create_all(engine)
108
127
  return Session()
109
-
110
- @staticmethod
111
- def get_params_from_sqlalchemy_url(db_url: str) -> Dict[str, Any]:
112
- """Gets PostgreSQL database connection parameters from SQLAlchemy url
113
-
114
- Args:
115
- db_url (str): SQLAlchemy url
116
-
117
- Returns:
118
- Dict[str,Any]: Dictionary of database connection parameters
119
- """
120
- result = urlsplit(db_url)
121
- return {
122
- "database": result.path[1:],
123
- "host": result.hostname,
124
- "port": result.port,
125
- "username": result.username,
126
- "password": result.password,
127
- "driver": result.scheme,
128
- }
129
-
130
- @staticmethod
131
- def get_sqlalchemy_url(
132
- database: Optional[str] = None,
133
- host: Optional[str] = None,
134
- port: Union[int, str, None] = None,
135
- username: Optional[str] = None,
136
- password: Optional[str] = None,
137
- driver: str = "postgresql",
138
- ) -> str:
139
- """Gets SQLAlchemy url from database connection parameters
140
-
141
- Args:
142
- database (Optional[str]): Database name
143
- host (Optional[str]): Host where database is located
144
- port (Union[int, str, None]): Database port
145
- username (Optional[str]): Username to log into database
146
- password (Optional[str]): Password to log into database
147
- driver (str): Database driver. Defaults to "postgresql".
148
-
149
- Returns:
150
- db_url (str): SQLAlchemy url
151
- """
152
- strings = [f"{driver}://"]
153
- if username:
154
- strings.append(username)
155
- if password:
156
- strings.append(f":{password}@")
157
- else:
158
- strings.append("@")
159
- if host:
160
- strings.append(host)
161
- if port is not None:
162
- strings.append(f":{port}")
163
- if database:
164
- strings.append(f"/{database}")
165
- return "".join(strings)
hdx/database/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # file generated by setuptools_scm
2
2
  # don't change, don't track in version control
3
- __version__ = version = '1.2.0'
4
- __version_tuple__ = version_tuple = (1, 2, 0)
3
+ __version__ = version = '1.2.2'
4
+ __version_tuple__ = version_tuple = (1, 2, 2)
hdx/database/dburi.py ADDED
@@ -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"
@@ -1,56 +1,40 @@
1
1
  """PostgreSQL specific utilities"""
2
2
  import logging
3
3
  import time
4
- from typing import Optional, Union
4
+
5
+ from .dburi import remove_driver_from_uri
5
6
 
6
7
  try:
7
- import psycopg2
8
+ import psycopg
8
9
  except ImportError:
10
+ psycopg = None
9
11
  # dependency missing, issue a warning
10
12
  import warnings
11
13
 
12
14
  warnings.warn(
13
- "psycopg2 not found! Please install hdx-python-database[postgresql] to enable."
15
+ "psycopg not found! Please install hdx-python-database[postgresql] to enable."
14
16
  )
15
17
 
16
18
  logger = logging.getLogger(__name__)
17
19
 
18
20
 
19
- def wait_for_postgresql(
20
- database: Optional[str],
21
- host: Optional[str],
22
- port: Union[int, str, None],
23
- username: Optional[str],
24
- password: Optional[str],
25
- ) -> None:
21
+ def wait_for_postgresql(db_uri: str) -> None:
26
22
  """Waits for PostgreSQL database to be up
27
23
 
28
24
  Args:
29
- database (Optional[str]): Database name
30
- host (Optional[str]): Host where database is located
31
- port (Union[int, str, None]): Database port
32
- username (Optional[str]): Username to log into database
33
- password (Optional[str]): Password to log into database
25
+ db_uri (str): Connection URI
34
26
 
35
27
  Returns:
36
28
  None
37
29
  """
38
30
  connecting_string = "Checking for PostgreSQL..."
39
- if port is not None:
40
- port = int(port)
31
+ db_uri_nd = remove_driver_from_uri(db_uri)
41
32
  while True:
42
33
  try:
43
34
  logger.info(connecting_string)
44
- connection = psycopg2.connect(
45
- database=database,
46
- host=host,
47
- port=port,
48
- user=username,
49
- password=password,
50
- connect_timeout=3,
51
- )
35
+ connection = psycopg.connect(db_uri_nd, connect_timeout=3)
52
36
  connection.close()
53
37
  logger.info("PostgreSQL is running!")
54
38
  break
55
- except psycopg2.OperationalError:
39
+ except psycopg.OperationalError:
56
40
  time.sleep(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hdx-python-database
3
- Version: 1.2.0
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,7 +13,6 @@ 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.7
17
16
  Classifier: Programming Language :: Python :: 3.8
18
17
  Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
@@ -26,15 +25,15 @@ 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.7
28
+ Requires-Python: >=3.8
30
29
  Description-Content-Type: text/markdown; charset=UTF-8
31
30
  License-File: LICENSE
32
31
  Requires-Dist: sqlalchemy (>=2)
33
32
  Requires-Dist: sshtunnel
34
33
  Provides-Extra: postgresql
35
- Requires-Dist: psycopg2-binary ; extra == 'postgresql'
34
+ Requires-Dist: psycopg[binary] ; extra == 'postgresql'
36
35
 
37
- [![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)
36
+ [![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)
38
37
  [![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)
39
38
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
40
39
  [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
@@ -0,0 +1,11 @@
1
+ hdx/database/__init__.py,sha256=HPKyvzWk-Gqd63sE3kcoyEO9n5h_9l3IeuzhzFnMWfQ,4458
2
+ hdx/database/_version.py,sha256=O2qpyKb1ilx0S5H2I5jnx1k8SSt3R0rHyv19gH5I8Ws,160
3
+ hdx/database/dburi.py,sha256=hNksrjaCC7AtD_M5ef0DCjkS0rJCofQqdXVD2BHBIGc,2938
4
+ hdx/database/no_timezone.py,sha256=7Gnidmu317AWAWx4aPY4oIWBio9dqqSsiUTndEi5hMY,1082
5
+ hdx/database/postgresql.py,sha256=9VlcdHhBURZPDaazj1kEq8gA4hUFJY-5hIN9uzibOlM,969
6
+ hdx_python_database-1.2.2.dist-info/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
7
+ hdx_python_database-1.2.2.dist-info/METADATA,sha256=ykW44dWeZ6DVkzUerqYbitQNoJE5WiBOkhBZXo3Rtzg,2464
8
+ hdx_python_database-1.2.2.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
9
+ hdx_python_database-1.2.2.dist-info/top_level.txt,sha256=cfo3TyDjnu-B1jb_itPPFQCH00c5VT5GuZ30eNrGmlI,4
10
+ hdx_python_database-1.2.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ hdx_python_database-1.2.2.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- hdx/database/__init__.py,sha256=-X6o1GRxotnDS36Av4Ek9-9qwsH38auCgMYYPw0N2DM,5443
2
- hdx/database/_version.py,sha256=CilGHycSRzmSEnCMQvTRJ5J_LWxMs---UqkVbOmQ2rA,160
3
- hdx/database/postgresql.py,sha256=mvekzE3OE_LgoUyoJSGXwYh9K4DwzgSNi-6MsaHQ9RE,1511
4
- hdx_python_database-1.2.0.dist-info/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
5
- hdx_python_database-1.2.0.dist-info/METADATA,sha256=TS90b_YLxudhzOc3wTPyBj0voEpFA-uZRls-nZcqxHY,2483
6
- hdx_python_database-1.2.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
7
- hdx_python_database-1.2.0.dist-info/top_level.txt,sha256=cfo3TyDjnu-B1jb_itPPFQCH00c5VT5GuZ30eNrGmlI,4
8
- hdx_python_database-1.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
9
- hdx_python_database-1.2.0.dist-info/RECORD,,