hdx-python-database 1.2.1__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.2.1 → hdx-python-database-1.2.2}/PKG-INFO +1 -1
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/doc/main.md +11 -4
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/requirements.txt +1 -1
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/__init__.py +25 -14
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/_version.py +2 -2
- hdx-python-database-1.2.2/src/hdx/database/no_timezone.py +40 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/postgresql.py +1 -1
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/PKG-INFO +1 -1
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/SOURCES.txt +2 -1
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/test-requirements.txt +1 -1
- hdx-python-database-1.2.2/tests/hdx/database/dbtestdate.py +23 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/test_database.py +26 -2
- hdx-python-database-1.2.1/tests/test_database.db +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.github/workflows/publish.yml +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.github/workflows/run-python-tests.yml +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.gitignore +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.readthedocs.yml +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/LICENSE +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/README.md +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/pyproject.toml +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/setup.cfg +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/dburi.py +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/dependency_links.txt +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/requires.txt +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/top_level.txt +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/zip-safe +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/__init__.py +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/conftest.py +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/test_dburi.py +0 -0
- {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/test_postgresql.py +0 -0
|
@@ -17,11 +17,14 @@ Additional postgresql functionality is available if this library is installed wi
|
|
|
17
17
|
|
|
18
18
|
## Breaking changes
|
|
19
19
|
|
|
20
|
+
From 1.2.2, database datetime columns are assumed to be timezoneless unless
|
|
21
|
+
db_has_tz is set to True.
|
|
22
|
+
|
|
20
23
|
From 1.2.1, wait_for_postgresql takes connection URI not database parameters,
|
|
21
24
|
get_params_from_sqlalchemy_url renamed to get_params_from_connection_uri
|
|
22
25
|
and moved to dburi module, get_sqlalchemy_url renamed to get_connection_uri and
|
|
23
26
|
moved to dburi module. New function remove_driver_from_uri in dburi module.
|
|
24
|
-
Parameter driver replaced by dialect+driver.
|
|
27
|
+
Parameter driver replaced by dialect+driver. Supports Python 3.8 and later.
|
|
25
28
|
|
|
26
29
|
From 1.1.2, the postgres module is renamed postgresql and function wait_for_postgres
|
|
27
30
|
is renamed wait_for_postgresql.
|
|
@@ -34,8 +37,8 @@ Versions from 1.0.6 no longer support Python 2.7.
|
|
|
34
37
|
|
|
35
38
|
## Database
|
|
36
39
|
|
|
37
|
-
Your SQLAlchemy database tables must inherit from Base in
|
|
38
|
-
hdx.
|
|
40
|
+
Your SQLAlchemy database tables must inherit from `Base` in
|
|
41
|
+
`hdx.database` or `hdx.database.database_notz` eg. :
|
|
39
42
|
|
|
40
43
|
from hdx.database import Base
|
|
41
44
|
class MyTable(Base):
|
|
@@ -51,9 +54,13 @@ tunnel:
|
|
|
51
54
|
with Database(database="db", host="1.2.3.4", username="user",
|
|
52
55
|
password="pass", dialect="dialect", driver="driver",
|
|
53
56
|
ssh_host="5.6.7.8", ssh_port=2222, ssh_username="sshuser",
|
|
54
|
-
ssh_private_key="path_to_key") as session:
|
|
57
|
+
ssh_private_key="path_to_key", db_has_tz=True) as session:
|
|
55
58
|
session.query(...)
|
|
56
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
|
+
|
|
57
64
|
## Connection URI
|
|
58
65
|
|
|
59
66
|
There are functions to handle converting from connection URIs to parameters and
|
|
@@ -3,30 +3,32 @@ from .dburi import get_connection_uri
|
|
|
3
3
|
|
|
4
4
|
"""Database utilities"""
|
|
5
5
|
import logging
|
|
6
|
-
from typing import Any, Optional, Union
|
|
6
|
+
from typing import Any, Optional, Type, Union
|
|
7
7
|
|
|
8
8
|
from sqlalchemy import create_engine
|
|
9
|
-
from sqlalchemy.ext.declarative import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -36,6 +38,7 @@ class Database:
|
|
|
36
38
|
password (Optional[str]): Password to log into database
|
|
37
39
|
dialect (str): Database dialect. Defaults to "postgresql".
|
|
38
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.
|
|
39
42
|
**kwargs: See below
|
|
40
43
|
ssh_host (str): SSH host (the server to connect to)
|
|
41
44
|
ssh_port (int): SSH port. Defaults to 22.
|
|
@@ -55,6 +58,7 @@ class Database:
|
|
|
55
58
|
password: Optional[str] = None,
|
|
56
59
|
dialect: str = "postgresql",
|
|
57
60
|
driver: Optional[str] = None,
|
|
61
|
+
db_has_tz: bool = False,
|
|
58
62
|
**kwargs: Any,
|
|
59
63
|
) -> None:
|
|
60
64
|
if port is not None:
|
|
@@ -89,7 +93,11 @@ class Database:
|
|
|
89
93
|
)
|
|
90
94
|
if dialect == "postgresql":
|
|
91
95
|
wait_for_postgresql(db_uri)
|
|
92
|
-
|
|
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)
|
|
93
101
|
|
|
94
102
|
def __enter__(self) -> Session:
|
|
95
103
|
return self.session
|
|
@@ -100,17 +108,20 @@ class Database:
|
|
|
100
108
|
self.server.stop()
|
|
101
109
|
|
|
102
110
|
@staticmethod
|
|
103
|
-
def get_session(
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
106
116
|
|
|
107
117
|
Args:
|
|
108
118
|
db_uri (str): Connection URI
|
|
119
|
+
table_base (Type[DeclarativeBase]): Base database table class. Defaults to NoTZBase.
|
|
109
120
|
|
|
110
121
|
Returns:
|
|
111
122
|
sqlalchemy.orm.Session: SQLAlchemy session
|
|
112
123
|
"""
|
|
113
124
|
engine = create_engine(db_uri, poolclass=NullPool, echo=False)
|
|
114
125
|
Session = sessionmaker(bind=engine)
|
|
115
|
-
|
|
126
|
+
table_base.metadata.create_all(engine)
|
|
116
127
|
return Session()
|
|
@@ -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"
|
{hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/SOURCES.txt
RENAMED
|
@@ -12,6 +12,7 @@ doc/main.md
|
|
|
12
12
|
src/hdx/database/__init__.py
|
|
13
13
|
src/hdx/database/_version.py
|
|
14
14
|
src/hdx/database/dburi.py
|
|
15
|
+
src/hdx/database/no_timezone.py
|
|
15
16
|
src/hdx/database/postgresql.py
|
|
16
17
|
src/hdx_python_database.egg-info/PKG-INFO
|
|
17
18
|
src/hdx_python_database.egg-info/SOURCES.txt
|
|
@@ -19,9 +20,9 @@ src/hdx_python_database.egg-info/dependency_links.txt
|
|
|
19
20
|
src/hdx_python_database.egg-info/requires.txt
|
|
20
21
|
src/hdx_python_database.egg-info/top_level.txt
|
|
21
22
|
src/hdx_python_database.egg-info/zip-safe
|
|
22
|
-
tests/test_database.db
|
|
23
23
|
tests/hdx/database/__init__.py
|
|
24
24
|
tests/hdx/database/conftest.py
|
|
25
|
+
tests/hdx/database/dbtestdate.py
|
|
25
26
|
tests/hdx/database/test_database.py
|
|
26
27
|
tests/hdx/database/test_dburi.py
|
|
27
28
|
tests/hdx/database/test_postgresql.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""SQLAlchemy class representing DBTestDate row. Holds test data for dates.
|
|
2
|
+
"""
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
|
+
|
|
7
|
+
from hdx.database.no_timezone import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DBTestDate(Base):
|
|
11
|
+
"""
|
|
12
|
+
test_date: Mapped[datetime] = mapped_column(primary_key=True)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
test_date: Mapped[datetime] = mapped_column(primary_key=True)
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
"""String representation of DBTestDate row
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str: String representation of DBTestDate row
|
|
22
|
+
"""
|
|
23
|
+
return f"<Test date={str(self.test_date)}>"
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
import copy
|
|
3
3
|
import os
|
|
4
4
|
from collections import namedtuple
|
|
5
|
+
from datetime import datetime, timezone
|
|
5
6
|
from os.path import join
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
9
|
+
from sqlalchemy import select
|
|
8
10
|
from sshtunnel import SSHTunnelForwarder
|
|
9
11
|
|
|
10
|
-
from hdx.database import Database
|
|
12
|
+
from hdx.database import Base, Database
|
|
13
|
+
from hdx.database.no_timezone import Base as NoTZBase
|
|
14
|
+
|
|
15
|
+
from .dbtestdate import DBTestDate
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
class TestDatabase:
|
|
14
19
|
started = False
|
|
15
20
|
stopped = False
|
|
21
|
+
table_base = NoTZBase
|
|
16
22
|
dbpath = join("tests", "test_database.db")
|
|
17
23
|
params_pg = {
|
|
18
24
|
"database": "mydatabase",
|
|
@@ -49,7 +55,7 @@ class TestDatabase:
|
|
|
49
55
|
monkeypatch.setattr(SSHTunnelForwarder, "local_bind_host", "0.0.0.0")
|
|
50
56
|
monkeypatch.setattr(SSHTunnelForwarder, "local_bind_port", 12345)
|
|
51
57
|
|
|
52
|
-
def get_session(_, db_url):
|
|
58
|
+
def get_session(_, db_url, table_base):
|
|
53
59
|
class Session:
|
|
54
60
|
bind = namedtuple("Bind", "engine")
|
|
55
61
|
|
|
@@ -58,6 +64,7 @@ class TestDatabase:
|
|
|
58
64
|
|
|
59
65
|
Session.bind.engine = namedtuple("Engine", "url")
|
|
60
66
|
Session.bind.engine.url = db_url
|
|
67
|
+
TestDatabase.table_base = table_base
|
|
61
68
|
return Session()
|
|
62
69
|
|
|
63
70
|
monkeypatch.setattr(Database, "get_session", get_session)
|
|
@@ -67,6 +74,14 @@ class TestDatabase:
|
|
|
67
74
|
database=TestDatabase.dbpath, port=None, dialect="sqlite"
|
|
68
75
|
) as dbsession:
|
|
69
76
|
assert str(dbsession.bind.engine.url) == nodatabase
|
|
77
|
+
assert TestDatabase.table_base == NoTZBase
|
|
78
|
+
now = datetime(2022, 10, 20, 22, 35, 55, tzinfo=timezone.utc)
|
|
79
|
+
input_dbtestdate = DBTestDate()
|
|
80
|
+
input_dbtestdate.test_date = now
|
|
81
|
+
dbsession.add(input_dbtestdate)
|
|
82
|
+
dbsession.commit()
|
|
83
|
+
dbtestdate = dbsession.execute(select(DBTestDate)).scalar_one()
|
|
84
|
+
assert dbtestdate.test_date == now
|
|
70
85
|
|
|
71
86
|
def test_get_session_ssh(self, mock_psycopg, mock_SSHTunnelForwarder):
|
|
72
87
|
with Database(
|
|
@@ -85,3 +100,12 @@ class TestDatabase:
|
|
|
85
100
|
str(dbsession.bind.engine.url)
|
|
86
101
|
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
87
102
|
)
|
|
103
|
+
assert TestDatabase.table_base == NoTZBase
|
|
104
|
+
with Database(
|
|
105
|
+
ssh_host="mysshhost", ssh_port=25, db_has_tz=True, **params
|
|
106
|
+
) as dbsession:
|
|
107
|
+
assert (
|
|
108
|
+
str(dbsession.bind.engine.url)
|
|
109
|
+
== "postgresql+psycopg://myuser@0.0.0.0:12345/mydatabase"
|
|
110
|
+
)
|
|
111
|
+
assert TestDatabase.table_base == Base
|
|
File without changes
|
|
File without changes
|
{hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.github/workflows/run-python-tests.yml
RENAMED
|
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
|
{hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/zip-safe
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/test_postgresql.py
RENAMED
|
File without changes
|