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.
Files changed (30) hide show
  1. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/PKG-INFO +1 -1
  2. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/doc/main.md +11 -4
  3. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/requirements.txt +1 -1
  4. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/__init__.py +25 -14
  5. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/_version.py +2 -2
  6. hdx-python-database-1.2.2/src/hdx/database/no_timezone.py +40 -0
  7. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/postgresql.py +1 -1
  8. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/PKG-INFO +1 -1
  9. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/SOURCES.txt +2 -1
  10. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/test-requirements.txt +1 -1
  11. hdx-python-database-1.2.2/tests/hdx/database/dbtestdate.py +23 -0
  12. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/test_database.py +26 -2
  13. hdx-python-database-1.2.1/tests/test_database.db +0 -0
  14. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.github/workflows/publish.yml +0 -0
  15. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.github/workflows/run-python-tests.yml +0 -0
  16. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.gitignore +0 -0
  17. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/.readthedocs.yml +0 -0
  18. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/LICENSE +0 -0
  19. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/README.md +0 -0
  20. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/pyproject.toml +0 -0
  21. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/setup.cfg +0 -0
  22. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx/database/dburi.py +0 -0
  23. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/dependency_links.txt +0 -0
  24. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/requires.txt +0 -0
  25. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/top_level.txt +0 -0
  26. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/src/hdx_python_database.egg-info/zip-safe +0 -0
  27. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/__init__.py +0 -0
  28. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/conftest.py +0 -0
  29. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/test_dburi.py +0 -0
  30. {hdx-python-database-1.2.1 → hdx-python-database-1.2.2}/tests/hdx/database/test_postgresql.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hdx-python-database
3
- Version: 1.2.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
@@ -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.utilities.database eg. :
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
@@ -1,3 +1,3 @@
1
1
  psycopg[binary]==3.1.8
2
- sqlalchemy==2.0.3
2
+ sqlalchemy==2.0.4
3
3
  sshtunnel==0.4.0
@@ -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 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
@@ -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
- self.session = self.get_session(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)
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(db_uri: str) -> Session:
104
- """Gets SQLAlchemy session given url. Your tables must inherit
105
- 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.
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
- Base.metadata.create_all(engine)
126
+ table_base.metadata.create_all(engine)
116
127
  return Session()
@@ -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.1'
4
- __version_tuple__ = version_tuple = (1, 2, 1)
3
+ __version__ = version = '1.2.2'
4
+ __version_tuple__ = version_tuple = (1, 2, 2)
@@ -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"
@@ -2,7 +2,7 @@
2
2
  import logging
3
3
  import time
4
4
 
5
- from hdx.database.dburi import remove_driver_from_uri
5
+ from .dburi import remove_driver_from_uri
6
6
 
7
7
  try:
8
8
  import psycopg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hdx-python-database
3
- Version: 1.2.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
@@ -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
@@ -1,4 +1,4 @@
1
1
  pytest==7.2.1
2
2
  pytest-cov==4.0.0
3
- tox==4.4.5
3
+ tox==4.4.6
4
4
  -r requirements.txt
@@ -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