hdx-python-database 1.3.0__py3-none-any.whl → 1.3.1__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,31 +1,57 @@
1
1
  """Database utilities"""
2
+
2
3
  import logging
3
- from typing import Any, Optional, Type, Union
4
+ from typing import Any, Dict, List, Optional, Tuple, Type, Union
4
5
 
5
- from sqlalchemy import create_engine
6
+ from sqlalchemy import Engine, TableClause, create_engine
7
+ from sqlalchemy.exc import SQLAlchemyError
6
8
  from sqlalchemy.ext.automap import automap_base
7
9
  from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
8
10
  from sqlalchemy.pool import NullPool
11
+ from sqlalchemy.sql.ddl import CreateSchema, DropSchema
12
+ from sqlalchemy.util import Properties
9
13
 
10
14
  from ._version import version as __version__ # noqa: F401
11
15
  from .dburi import get_connection_uri
12
16
  from .no_timezone import Base as NoTZBase
13
17
  from .postgresql import wait_for_postgresql
18
+ from .views import view
14
19
  from .with_timezone import Base as TZBase
15
20
 
16
21
  logger = logging.getLogger(__name__)
17
22
 
18
23
 
24
+ class DatabaseError(Exception):
25
+ pass
26
+
27
+
28
+ def do_nothing_fn():
29
+ return None
30
+
31
+
19
32
  class Database:
20
33
  """Database helper class to handle ssh tunnels, waiting for PostgreSQL to
21
- be up etc. Can be used in a with statement returning a Session object that
34
+ be up etc. Can be used in a with statement returning a Database object that
22
35
  if reflect is True will have a variable reflected_classes containing the
23
- reflected classes. db_has_tz which defaults to False indicates whether
24
- database datetime columns have timezones. If not, conversion occurs between
25
- Python datetimes with timezones to timezoneless database columns (but not
26
- when using reflection). If table_base is supplied, db_has_tz is ignored.
36
+ reflected classes. Either an SQLAlchemy engine, database uri or various
37
+ database parameters must be supplied.
38
+
39
+ db_has_tz which defaults to False indicates whether database datetime
40
+ columns have timezones. If not, conversion occurs between Python datetimes
41
+ with timezones to timezoneless database columns (but not when using
42
+ reflection). If table_base is supplied, db_has_tz is ignored.
43
+
44
+ There is an option to wipe and create an empty schema in the database by
45
+ setting recreate_schema to True and setting a schema_name ("public" is the
46
+ default).
47
+
48
+ If a prepare function is supplied in prepare_fn, it will be executed before
49
+ Base.metadata.create_all and the results of it returned in instance variable
50
+ prepare_results.
27
51
 
28
52
  Args:
53
+ engine (Optional[Engine]): SQLAlchemy engine to use.
54
+ db_uri (Optional[str]): Connection URI.
29
55
  database (Optional[str]): Database name
30
56
  host (Optional[str]): Host where database is located
31
57
  port (Union[int, str, None]): Database port
@@ -37,6 +63,9 @@ class Database:
37
63
  table_base (Optional[Type[DeclarativeBase]]): Override table base. Defaults to None.
38
64
  reflect (bool): Whether to reflect existing tables. Defaults to False.
39
65
  **kwargs: See below
66
+ recreate_schema (bool): Whether to recreate schema
67
+ schema_name (str): Database schema name. Defaults to "public".
68
+ prepare_fn (Callable[[], None]]): Function to call before Base.metadata.create_all.
40
69
  ssh_host (str): SSH host (the server to connect to)
41
70
  ssh_port (int): SSH port. Defaults to 22.
42
71
  ssh_username (str): SSH username
@@ -48,6 +77,8 @@ class Database:
48
77
 
49
78
  def __init__(
50
79
  self,
80
+ engine: Optional[Engine] = None,
81
+ db_uri: Optional[str] = None,
51
82
  database: Optional[str] = None,
52
83
  host: Optional[str] = None,
53
84
  port: Union[int, str, None] = None,
@@ -62,6 +93,14 @@ class Database:
62
93
  ) -> None:
63
94
  if port is not None:
64
95
  port = int(port)
96
+ schema_name = None
97
+ if len(kwargs) == 0:
98
+ recreate_schema = False
99
+ prepare_fn = do_nothing_fn
100
+ else:
101
+ recreate_schema = kwargs.pop("recreate_schema", False)
102
+ schema_name = kwargs.pop("schema", "public")
103
+ prepare_fn = kwargs.pop("prepare_fn", do_nothing_fn)
65
104
  if len(kwargs) != 0:
66
105
  try:
67
106
  import sshtunnel
@@ -89,64 +128,167 @@ class Database:
89
128
  port = self.server.local_bind_port
90
129
  else:
91
130
  self.server = None
92
- db_uri = get_connection_uri(
93
- database,
94
- host,
95
- port,
96
- username,
97
- password,
98
- dialect=dialect,
99
- driver=driver,
100
- )
101
- if dialect == "postgresql":
131
+ if not engine and not db_uri:
132
+ db_uri = get_connection_uri(
133
+ database,
134
+ host,
135
+ port,
136
+ username,
137
+ password,
138
+ dialect=dialect,
139
+ driver=driver,
140
+ )
141
+ if not db_uri:
142
+ raise DatabaseError(
143
+ "No engine, database uri or connection parameters provided!"
144
+ )
145
+ if not engine and dialect == "postgresql":
102
146
  wait_for_postgresql(db_uri)
103
147
  if not table_base:
104
148
  if db_has_tz:
105
149
  table_base = TZBase
106
150
  else:
107
151
  table_base = NoTZBase
108
- self.session = self.get_session(
109
- db_uri, table_base=table_base, reflect=reflect
152
+ if not engine:
153
+ engine = create_engine(db_uri, poolclass=NullPool, echo=False)
154
+ self.engine: Engine = engine
155
+ if recreate_schema:
156
+ self.recreate_schema(engine, schema_name)
157
+ self.prepare_results = prepare_fn()
158
+ self.session, self.reflected_classes = self.create_session(
159
+ engine,
160
+ table_base=table_base,
161
+ reflect=reflect,
110
162
  )
111
163
 
112
- def __enter__(self) -> Session:
113
- return self.session
164
+ def __enter__(self) -> "Database":
165
+ return self
114
166
 
115
167
  def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
116
168
  self.session.close()
117
169
  if self.server is not None:
118
170
  self.server.stop()
119
171
 
172
+ def get_engine(self) -> Engine:
173
+ """Returns SQLAlchemy engine.
174
+
175
+ Returns:
176
+ sqlalchemy.Engine: SQLAlchemy engine
177
+ """
178
+ return self.engine
179
+
180
+ def get_session(self) -> Session:
181
+ """Returns SQLAlchemy session.
182
+
183
+ Returns:
184
+ sqlalchemy.orm.Session: SQLAlchemy session
185
+ """
186
+ return self.session
187
+
188
+ def get_reflected_classes(self) -> Any:
189
+ """Gets reflected classes.
190
+
191
+ Returns:
192
+ Any: Reflected classes
193
+ """
194
+ return self.reflected_classes
195
+
196
+ def get_prepare_results(self) -> Any:
197
+ """Returns results from prepare function.
198
+
199
+ Returns:
200
+ Any: Results from prepare function
201
+ """
202
+ return self.prepare_results
203
+
120
204
  @staticmethod
121
- def get_session(
122
- db_uri: str,
205
+ def create_session(
206
+ engine: Optional[Engine] = None,
207
+ db_uri: Optional[str] = None,
123
208
  table_base: Type[DeclarativeBase] = NoTZBase,
124
209
  reflect: bool = False,
125
- ) -> Session:
126
- """Gets SQLAlchemy session given url. Tables must inherit from Base in
210
+ ) -> Tuple[Session, Optional[Properties]]:
211
+ """Creates SQLAlchemy session given SQLAlchemy engine or database uri
212
+ (one of which must be supplied). Tables must inherit from Base in
127
213
  hdx.utilities.database unless base is defined. If reflect is True,
128
214
  classes will be reflected from an existing database and the reflected
129
- classes are returned in a variable reflected_classes in the returned
130
- Session object. Note that type annotation maps don't work with
131
- reflection.
215
+ classes will be returned. Note that type annotation maps don't work
216
+ with reflection.
132
217
 
133
218
  Args:
134
- db_uri (str): Connection URI
219
+ engine (Optional[Engine]): SQLAlchemy engine to use. Defaults to None (create from db_uri).
220
+ db_uri (Optional[str]): Connection URI. Defaults to None (use engine).
135
221
  table_base (Type[DeclarativeBase]): Base database table class. Defaults to NoTZBase.
136
222
  reflect (bool): Whether to reflect existing tables. Defaults to False.
137
223
 
138
224
  Returns:
139
- sqlalchemy.orm.Session: SQLAlchemy session
225
+ Tuple[Session, Optional[Properties]]: (SQLAlchemy session, reflected classes if available)
140
226
  """
141
- engine = create_engine(db_uri, poolclass=NullPool, echo=False)
227
+ if not engine:
228
+ if db_uri is None:
229
+ raise DatabaseError("No engine or database uri provided!")
230
+ engine = create_engine(db_uri, poolclass=NullPool, echo=False)
142
231
  Session = sessionmaker(bind=engine)
143
232
  if reflect:
144
233
  Base = automap_base(declarative_base=table_base)
145
234
  Base.prepare(autoload_with=engine)
146
- classes = Base.classes
235
+ reflected_classes = Base.classes
147
236
  else:
148
237
  table_base.metadata.create_all(engine)
149
- classes = None
238
+ reflected_classes = None
150
239
  session = Session()
151
- session.reflected_classes = classes
152
- return session
240
+ return session, reflected_classes
241
+
242
+ @staticmethod
243
+ def recreate_schema(engine: Engine, schema_name: str = "public") -> bool:
244
+ """Wipe and create empty schema in database using SQLAlchemy.
245
+
246
+ Args:
247
+ engine (Engine): SQLAlchemy engine to use.
248
+ schema_name (str): Schema name. Defaults to "public".
249
+
250
+ Returns:
251
+ bool: True if all successful, False if not
252
+ """
253
+ # Wipe and create an empty schema
254
+ try:
255
+ with engine.connect() as connection:
256
+ connection.execute(
257
+ DropSchema(schema_name, cascade=True, if_exists=True)
258
+ )
259
+ connection.commit()
260
+ connection.execute(
261
+ CreateSchema(schema_name, if_not_exists=True)
262
+ )
263
+ connection.commit()
264
+ return True
265
+ except SQLAlchemyError:
266
+ return False
267
+
268
+ @staticmethod
269
+ def prepare_view(view_params: Dict) -> TableClause:
270
+ """Prepare SQLAlchemy view from dictionary with keys: name, metadata and
271
+ selectable. Must be run before Base.metadata.create_all.
272
+
273
+ Args:
274
+ view_params (Dict): Dictionary with keys name, metadata, selectable
275
+
276
+ Returns:
277
+ TableClause: SQLAlchemy View
278
+ """
279
+ return view(**view_params)
280
+
281
+ @classmethod
282
+ def prepare_views(cls, view_params_list: List[Dict]) -> List[TableClause]:
283
+ """Prepare SQLAlchemy views from a list of dictionaries with keys: name,
284
+ metadata and selectable. Must be run before Base.metadata.create_all.
285
+
286
+ Args:
287
+ view_params_list (List[Dict]): List of dictionaries with view parameters
288
+ Returns:
289
+ List[TableClause]: SQLAlchemy Views
290
+ """
291
+ results = []
292
+ for view_params in view_params_list:
293
+ results.append(cls.prepare_view(view_params))
294
+ return results
hdx/database/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.3.0'
16
- __version_tuple__ = version_tuple = (1, 3, 0)
15
+ __version__ = version = '1.3.1'
16
+ __version_tuple__ = version_tuple = (1, 3, 1)
hdx/database/dburi.py CHANGED
@@ -62,6 +62,8 @@ def get_connection_uri(
62
62
  Returns:
63
63
  str: Connection URI
64
64
  """
65
+ if not database:
66
+ return ""
65
67
  strings = [dialect]
66
68
  if include_driver:
67
69
  if dialect == "postgresql" and driver is None:
@@ -1,4 +1,5 @@
1
1
  """Utilities for database datetime columns without timezone"""
2
+
2
3
  from datetime import datetime, timezone
3
4
  from typing import Optional
4
5
 
@@ -1,4 +1,5 @@
1
1
  """PostgreSQL specific utilities"""
2
+
2
3
  import logging
3
4
  import time
4
5
 
hdx/database/views.py CHANGED
@@ -1,10 +1,10 @@
1
1
  """
2
- Create a DB View.
2
+ Prepare database views ins SQLAlchemy. Must be run before calling
3
+ Base.metadata.create_all.
3
4
 
4
5
  Copied from:
5
6
  https://github.com/sqlalchemy/sqlalchemy/wiki/Views#sqlalchemy-14-20-version
6
7
  """
7
- from typing import Dict, List
8
8
 
9
9
  import sqlalchemy as sa
10
10
  from sqlalchemy import MetaData, Selectable, TableClause
@@ -46,7 +46,7 @@ def view_doesnt_exist(ddl, target, connection, **kw):
46
46
 
47
47
 
48
48
  def view(name: str, metadata: MetaData, selectable: Selectable) -> TableClause:
49
- """Create SQLAlchemy view
49
+ """Prepare SQLAlchemy view. Must be run before Base.metadata.create_all.
50
50
 
51
51
  Args:
52
52
  name (str): View name
@@ -73,31 +73,3 @@ def view(name: str, metadata: MetaData, selectable: Selectable) -> TableClause:
73
73
  DropView(name).execute_if(callable_=view_exists),
74
74
  )
75
75
  return t
76
-
77
-
78
- def build_view(view_params: Dict) -> TableClause:
79
- """Create SQLAlchemy view from dictionary with keys: name, metadata and
80
- selectable
81
-
82
- Args:
83
- view_params (Dict): Dictionary with keys name, metadata, selectable
84
-
85
- Returns:
86
- TableClause: SQLAlchemy View
87
- """
88
- return view(**view_params)
89
-
90
-
91
- def build_views(view_params_list: List[Dict]) -> List[TableClause]:
92
- """Create SQLAlchemy views from a list of dictionaries with keys: name,
93
- metadata and selectable
94
-
95
- Args:
96
- view_params_list (List[Dict]): List of dictionaries with view parameters
97
- Returns:
98
- List[TableClause]: SQLAlchemy Views
99
- """
100
- results = []
101
- for view_params in view_params_list:
102
- results.append(build_view(view_params))
103
- return results
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: hdx-python-database
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: HDX Python database utilities
5
5
  Project-URL: Homepage, https://github.com/OCHA-DAP/hdx-python-database
6
6
  Author-email: Michael Rans <rans@email.com>
@@ -39,8 +39,7 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  [![Build Status](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yaml/badge.svg)](https://github.com/OCHA-DAP/hdx-python-database/actions/workflows/run-python-tests.yaml)
41
41
  [![Coverage Status](https://coveralls.io/repos/github/OCHA-DAP/hdx-python-database/badge.svg?branch=main&ts=1)](https://coveralls.io/github/OCHA-DAP/hdx-python-database?branch=main)
42
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
43
- [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
42
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
44
43
  [![Downloads](https://img.shields.io/pypi/dm/hdx-python-database.svg)](https://pypistats.org/packages/hdx-python-database)
45
44
 
46
45
  The HDX Python Database Library provides utilities for connecting to databases in a standard way including
@@ -0,0 +1,12 @@
1
+ hdx/database/__init__.py,sha256=ApMtv6Ul4dwZCeS1Wc_Igm9ZnYKkWo_HBi9DBKFHx3Q,10957
2
+ hdx/database/_version.py,sha256=cOVPCvD2h2G_2KB6G3ddreYkIQfAnS6WqgAkF_qgGOQ,411
3
+ hdx/database/dburi.py,sha256=rw8RuLpU85vxYl5-Zk1SdZRAz-tqre_ot5aEnkjhxAQ,2977
4
+ hdx/database/no_timezone.py,sha256=Lw087G3qOf8H70o8Hq_VvOMZ3ANei-lIA_sUlpFU_Uo,1130
5
+ hdx/database/postgresql.py,sha256=idtRdJ3zbnpInsjf-_JvWSxtDvU8NQPYcvgHq25ju-k,970
6
+ hdx/database/utils.py,sha256=6lLymWriMFCq8BZgrDt4V5z14GR3PbvG_WXZeBc99Ws,415
7
+ hdx/database/views.py,sha256=R_1Ni0U2CL3IlydxcB1vyZp7QoDZim6g-807jQIaWQU,1950
8
+ hdx/database/with_timezone.py,sha256=GUUylmTwQHp22yedmCVKpVJzvjWgdS9lg9KeqciNJkY,235
9
+ hdx_python_database-1.3.1.dist-info/METADATA,sha256=qxQBQ56bNMZP_DoLVHidyBvpjA0Dm2Qbm0dWtsRfIwk,2691
10
+ hdx_python_database-1.3.1.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
11
+ hdx_python_database-1.3.1.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
12
+ hdx_python_database-1.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.21.1
2
+ Generator: hatchling 1.24.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,12 +0,0 @@
1
- hdx/database/__init__.py,sha256=0CpgEC9GAqCtmZtBsTvwoDsW3T0VHY0aCNAh53piP6g,5835
2
- hdx/database/_version.py,sha256=HGwtpza1HCPtlyqElUvIyH97K44TO13CYiYVZNezQ1M,411
3
- hdx/database/dburi.py,sha256=hNksrjaCC7AtD_M5ef0DCjkS0rJCofQqdXVD2BHBIGc,2938
4
- hdx/database/no_timezone.py,sha256=jawupasbCwNo-lDbX24CtxaSilb6LS-jLx-wZAa58nM,1129
5
- hdx/database/postgresql.py,sha256=9VlcdHhBURZPDaazj1kEq8gA4hUFJY-5hIN9uzibOlM,969
6
- hdx/database/utils.py,sha256=6lLymWriMFCq8BZgrDt4V5z14GR3PbvG_WXZeBc99Ws,415
7
- hdx/database/views.py,sha256=qJ_f6DpBUSzAGuBbgOXwQDZGni7b-DOUlAdAjSXKwhs,2635
8
- hdx/database/with_timezone.py,sha256=GUUylmTwQHp22yedmCVKpVJzvjWgdS9lg9KeqciNJkY,235
9
- hdx_python_database-1.3.0.dist-info/METADATA,sha256=8lJk615nDYiwB9vT9JUzfd-mpGVa6rIFA6kP9WVMgxI,2785
10
- hdx_python_database-1.3.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
11
- hdx_python_database-1.3.0.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
12
- hdx_python_database-1.3.0.dist-info/RECORD,,