hdx-python-database 1.3.1__py3-none-any.whl → 1.3.3__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
@@ -6,15 +6,14 @@ from typing import Any, Dict, List, Optional, Tuple, Type, Union
6
6
  from sqlalchemy import Engine, TableClause, create_engine
7
7
  from sqlalchemy.exc import SQLAlchemyError
8
8
  from sqlalchemy.ext.automap import automap_base
9
- from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
9
+ from sqlalchemy.orm import DeclarativeBase, Session
10
10
  from sqlalchemy.pool import NullPool
11
11
  from sqlalchemy.sql.ddl import CreateSchema, DropSchema
12
- from sqlalchemy.util import Properties
13
12
 
14
13
  from ._version import version as __version__ # noqa: F401
15
14
  from .dburi import get_connection_uri
16
15
  from .no_timezone import Base as NoTZBase
17
- from .postgresql import wait_for_postgresql
16
+ from .postgresql import restore_from_pgfile, wait_for_postgresql
18
17
  from .views import view
19
18
  from .with_timezone import Base as TZBase
20
19
 
@@ -41,6 +40,9 @@ class Database:
41
40
  with timezones to timezoneless database columns (but not when using
42
41
  reflection). If table_base is supplied, db_has_tz is ignored.
43
42
 
43
+ The database can be restored from a given pg_restore file supplied in the
44
+ parameter pg_restore_file.
45
+
44
46
  There is an option to wipe and create an empty schema in the database by
45
47
  setting recreate_schema to True and setting a schema_name ("public" is the
46
48
  default).
@@ -63,6 +65,7 @@ class Database:
63
65
  table_base (Optional[Type[DeclarativeBase]]): Override table base. Defaults to None.
64
66
  reflect (bool): Whether to reflect existing tables. Defaults to False.
65
67
  **kwargs: See below
68
+ pg_restore_file (str): Restore database from pg_restore file
66
69
  recreate_schema (bool): Whether to recreate schema
67
70
  schema_name (str): Database schema name. Defaults to "public".
68
71
  prepare_fn (Callable[[], None]]): Function to call before Base.metadata.create_all.
@@ -95,9 +98,11 @@ class Database:
95
98
  port = int(port)
96
99
  schema_name = None
97
100
  if len(kwargs) == 0:
101
+ pg_restore_file = None
98
102
  recreate_schema = False
99
103
  prepare_fn = do_nothing_fn
100
104
  else:
105
+ pg_restore_file = kwargs.pop("pg_restore_file", None)
101
106
  recreate_schema = kwargs.pop("recreate_schema", False)
102
107
  schema_name = kwargs.pop("schema", "public")
103
108
  prepare_fn = kwargs.pop("prepare_fn", do_nothing_fn)
@@ -144,6 +149,8 @@ class Database:
144
149
  )
145
150
  if not engine and dialect == "postgresql":
146
151
  wait_for_postgresql(db_uri)
152
+ if pg_restore_file and db_uri:
153
+ restore_from_pgfile(db_uri, pg_restore_file)
147
154
  if not table_base:
148
155
  if db_has_tz:
149
156
  table_base = TZBase
@@ -155,20 +162,41 @@ class Database:
155
162
  if recreate_schema:
156
163
  self.recreate_schema(engine, schema_name)
157
164
  self.prepare_results = prepare_fn()
158
- self.session, self.reflected_classes = self.create_session(
165
+ self.session, self.base = self.create_session(
159
166
  engine,
160
167
  table_base=table_base,
161
168
  reflect=reflect,
162
169
  )
170
+ if reflect:
171
+ self.reflected_classes = self.base.classes
172
+ else:
173
+ self.reflected_classes = None
163
174
 
164
- def __enter__(self) -> "Database":
165
- return self
175
+ def cleanup(self) -> None:
176
+ """Cleanup SQLAlchemy.
166
177
 
167
- def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
178
+ Returns:
179
+ sqlalchemy.Engine: SQLAlchemy engine
180
+ """
168
181
  self.session.close()
182
+ self.engine.dispose()
169
183
  if self.server is not None:
170
184
  self.server.stop()
171
185
 
186
+ def __enter__(self) -> "Database":
187
+ return self
188
+
189
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
190
+ self.cleanup()
191
+
192
+ def drop_all(self) -> None:
193
+ """Drop all tables.
194
+
195
+ Returns:
196
+ None
197
+ """
198
+ self.base.metadata.drop_all(self.engine)
199
+
172
200
  def get_engine(self) -> Engine:
173
201
  """Returns SQLAlchemy engine.
174
202
 
@@ -207,7 +235,7 @@ class Database:
207
235
  db_uri: Optional[str] = None,
208
236
  table_base: Type[DeclarativeBase] = NoTZBase,
209
237
  reflect: bool = False,
210
- ) -> Tuple[Session, Optional[Properties]]:
238
+ ) -> Tuple[Session, Any]:
211
239
  """Creates SQLAlchemy session given SQLAlchemy engine or database uri
212
240
  (one of which must be supplied). Tables must inherit from Base in
213
241
  hdx.utilities.database unless base is defined. If reflect is True,
@@ -222,22 +250,19 @@ class Database:
222
250
  reflect (bool): Whether to reflect existing tables. Defaults to False.
223
251
 
224
252
  Returns:
225
- Tuple[Session, Optional[Properties]]: (SQLAlchemy session, reflected classes if available)
253
+ Tuple[Session, Any]: (SQLAlchemy session, base)
226
254
  """
227
255
  if not engine:
228
256
  if db_uri is None:
229
257
  raise DatabaseError("No engine or database uri provided!")
230
258
  engine = create_engine(db_uri, poolclass=NullPool, echo=False)
231
- Session = sessionmaker(bind=engine)
232
259
  if reflect:
233
260
  Base = automap_base(declarative_base=table_base)
234
261
  Base.prepare(autoload_with=engine)
235
- reflected_classes = Base.classes
262
+ table_base = Base
236
263
  else:
237
264
  table_base.metadata.create_all(engine)
238
- reflected_classes = None
239
- session = Session()
240
- return session, reflected_classes
265
+ return Session(engine), table_base
241
266
 
242
267
  @staticmethod
243
268
  def recreate_schema(engine: Engine, schema_name: str = "public") -> bool:
@@ -285,6 +310,7 @@ class Database:
285
310
 
286
311
  Args:
287
312
  view_params_list (List[Dict]): List of dictionaries with view parameters
313
+
288
314
  Returns:
289
315
  List[TableClause]: SQLAlchemy Views
290
316
  """
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.1'
16
- __version_tuple__ = version_tuple = (1, 3, 1)
15
+ __version__ = version = '1.3.3'
16
+ __version_tuple__ = version_tuple = (1, 3, 3)
@@ -1,9 +1,11 @@
1
1
  """PostgreSQL specific utilities"""
2
2
 
3
3
  import logging
4
+ import subprocess
4
5
  import time
6
+ from os import environ
5
7
 
6
- from .dburi import remove_driver_from_uri
8
+ from .dburi import get_params_from_connection_uri, remove_driver_from_uri
7
9
 
8
10
  try:
9
11
  import psycopg
@@ -19,6 +21,10 @@ except ImportError:
19
21
  logger = logging.getLogger(__name__)
20
22
 
21
23
 
24
+ class PostgresError(Exception):
25
+ pass
26
+
27
+
22
28
  def wait_for_postgresql(db_uri: str) -> None:
23
29
  """Waits for PostgreSQL database to be up
24
30
 
@@ -39,3 +45,51 @@ def wait_for_postgresql(db_uri: str) -> None:
39
45
  break
40
46
  except psycopg.OperationalError:
41
47
  time.sleep(1)
48
+
49
+
50
+ def restore_from_pgfile(db_uri: str, pg_restore_file: str) -> str:
51
+ """Restore database from a pg_restore file created by pg_backup using the
52
+ pg_restore command.
53
+
54
+ Args:
55
+ db_uri (str): Connection URI
56
+ pg_restore_file (str): Path to the pg_restore database file
57
+
58
+ Returns:
59
+ str: Output from the pg_restore command
60
+ """
61
+ db_params = get_params_from_connection_uri(db_uri)
62
+ subprocess_params = ["pg_restore", "-c"]
63
+ for key, value in db_params.items():
64
+ match key:
65
+ case "database":
66
+ subprocess_params.append("-d")
67
+ case "host":
68
+ subprocess_params.append("-h")
69
+ case "port":
70
+ subprocess_params.append("-p")
71
+ case "username":
72
+ subprocess_params.append("-U")
73
+ case _:
74
+ continue
75
+ subprocess_params.append(f"{value}")
76
+
77
+ subprocess_params.append(f"{pg_restore_file}")
78
+ env = environ.copy()
79
+ password = db_params.get("password")
80
+ if password:
81
+ env["PGPASSWORD"] = password
82
+ process = subprocess.run(
83
+ subprocess_params, env=env, capture_output=True, encoding="utf-8"
84
+ )
85
+ try:
86
+ process.check_returncode()
87
+ except subprocess.CalledProcessError as ex:
88
+ command = " ".join(subprocess_params)
89
+ raise PostgresError(
90
+ f"{command} failed. Return code: {process.returncode}"
91
+ ) from ex
92
+
93
+ for line in process.stdout.splitlines():
94
+ logger.info(line)
95
+ return process.stdout
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hdx-python-database
3
- Version: 1.3.1
3
+ Version: 1.3.3
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>
@@ -0,0 +1,12 @@
1
+ hdx/database/__init__.py,sha256=EYVc16LZCPFULDd9cHHt0S1MKREzE94DtF_MkBhFVng,11589
2
+ hdx/database/_version.py,sha256=VriGPi1kVXIBM0YGAuhpE803XR-FNq1JvTW1Kz2us08,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=BrEuRnuR2dt3HwJ-o_IzGFs8eT3iXKzDqMSNKPyQ18Y,2621
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.3.dist-info/METADATA,sha256=zpM5pfKKaDgOwzDxEfcjctN1me8xY0cKYi88klSrxAM,2691
10
+ hdx_python_database-1.3.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
11
+ hdx_python_database-1.3.3.dist-info/licenses/LICENSE,sha256=wc-4GpMn-ODs-U_bTe1YCiPVgvcjzrpYOx2wPuyAeII,1079
12
+ hdx_python_database-1.3.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.24.2
2
+ Generator: hatchling 1.25.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,12 +0,0 @@
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,,