macrostrat.database 3.5.0__tar.gz → 3.5.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 (15) hide show
  1. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/PKG-INFO +1 -1
  2. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/__init__.py +26 -2
  3. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/transfer/dump_database.py +1 -0
  4. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/transfer/move_tables.py +1 -0
  5. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/transfer/restore_database.py +1 -0
  6. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/transfer/stream_utils.py +1 -0
  7. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/transfer/utils.py +1 -1
  8. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/utils.py +90 -48
  9. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/pyproject.toml +1 -1
  10. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/mapper/__init__.py +0 -0
  11. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/mapper/base.py +0 -0
  12. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/mapper/cache.py +0 -0
  13. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/mapper/utils.py +0 -0
  14. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/postgresql.py +0 -0
  15. {macrostrat_database-3.5.0 → macrostrat_database-3.5.2}/macrostrat/database/transfer/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: macrostrat.database
3
- Version: 3.5.0
3
+ Version: 3.5.2
4
4
  Summary: A SQLAlchemy-based database toolkit.
5
5
  Author: Daven Quinn
6
6
  Author-email: dev@davenquinn.com
@@ -5,13 +5,14 @@ from typing import Optional, Union
5
5
 
6
6
  from psycopg2.errors import InvalidSavepointSpecification
7
7
  from psycopg2.sql import Identifier
8
- from sqlalchemy import URL, MetaData, create_engine, inspect, Engine
8
+ from sqlalchemy import URL, Engine, MetaData, create_engine, inspect
9
9
  from sqlalchemy.exc import IntegrityError, InternalError
10
10
  from sqlalchemy.ext.compiler import compiles
11
11
  from sqlalchemy.orm import Session, scoped_session, sessionmaker
12
12
  from sqlalchemy.sql.expression import Insert
13
13
 
14
14
  from macrostrat.utils import get_logger
15
+
15
16
  from .mapper import DatabaseMapper
16
17
  from .postgresql import on_conflict, prefix_inserts # noqa
17
18
  from .utils import ( # noqa
@@ -128,7 +129,7 @@ class Database(object):
128
129
  Returns: Iterator of results from the query.
129
130
  """
130
131
  params = self._setup_params(params, kwargs)
131
- return iter(run_sql(self.session, fn, params, **kwargs))
132
+ return run_sql(self.session, fn, params, **kwargs)
132
133
 
133
134
  def run_query(self, sql, params=None, **kwargs):
134
135
  """Run a single query on the database object, returning the result.
@@ -184,6 +185,29 @@ class Database(object):
184
185
  self.__inspector__ = inspect(self.engine)
185
186
  return self.__inspector__
186
187
 
188
+ def refresh_schema(self, *, automap=None):
189
+ """
190
+ Refresh the current database connection
191
+
192
+ - closes the session and flushes
193
+ - removes the inspector
194
+
195
+ If automap is True, will automap the database after refreshing.
196
+ If automap is False, will not automap the database after refreshing.
197
+ If automap is None, it will re-map the database if it was previously mapped.
198
+ """
199
+ # Close the session
200
+ self.session.flush()
201
+ self.session.close()
202
+ # Remove the inspector
203
+ self.__inspector__ = None
204
+
205
+ if automap is None:
206
+ automap = self.mapper is not None
207
+
208
+ if automap:
209
+ self.automap()
210
+
187
211
  def entity_names(self, **kwargs):
188
212
  """
189
213
  Returns an iterator of names of *schema objects*
@@ -7,6 +7,7 @@ import aiofiles
7
7
  from sqlalchemy.engine import Engine
8
8
 
9
9
  from macrostrat.utils import get_logger
10
+
10
11
  from .stream_utils import print_stdout, print_stream_progress
11
12
  from .utils import _create_command
12
13
 
@@ -3,6 +3,7 @@ import asyncio
3
3
  from sqlalchemy.engine import Engine
4
4
 
5
5
  from macrostrat.utils import get_logger
6
+
6
7
  from .dump_database import pg_dump
7
8
  from .restore_database import pg_restore
8
9
  from .stream_utils import print_stdout, print_stream_progress
@@ -7,6 +7,7 @@ from rich.console import Console
7
7
  from sqlalchemy.engine import Engine
8
8
 
9
9
  from macrostrat.utils import get_logger
10
+
10
11
  from .stream_utils import print_stdout, print_stream_progress
11
12
  from .utils import _create_command, _create_database_if_not_exists
12
13
 
@@ -5,6 +5,7 @@ import zlib
5
5
  from aiofiles.threadpool import AsyncBufferedIOBase
6
6
 
7
7
  from macrostrat.utils import get_logger
8
+
8
9
  from .utils import console
9
10
 
10
11
  log = get_logger(__name__)
@@ -5,7 +5,7 @@ from sqlalchemy.engine import Engine
5
5
  from sqlalchemy.engine.url import URL
6
6
  from sqlalchemy_utils import create_database, database_exists, drop_database
7
7
 
8
- from macrostrat.utils import get_logger, ApplicationError
8
+ from macrostrat.utils import ApplicationError, get_logger
9
9
 
10
10
  console = Console()
11
11
 
@@ -1,15 +1,17 @@
1
1
  from contextlib import contextmanager
2
+ from enum import Enum
2
3
  from pathlib import Path
3
4
  from re import search
5
+ from sys import stderr
4
6
  from time import sleep
5
7
  from typing import IO, Union
6
8
  from warnings import warn
7
9
 
10
+ import psycopg2.errors
8
11
  from click import echo, secho
9
12
  from psycopg2.extensions import set_wait_callback
10
13
  from psycopg2.extras import wait_select
11
14
  from psycopg2.sql import SQL, Composable, Composed
12
- import psycopg2.errors
13
15
  from rich.console import Console
14
16
  from sqlalchemy import MetaData, create_engine, text
15
17
  from sqlalchemy.engine import Connection, Engine
@@ -17,8 +19,8 @@ from sqlalchemy.exc import (
17
19
  IntegrityError,
18
20
  InternalError,
19
21
  InvalidRequestError,
22
+ OperationalError,
20
23
  ProgrammingError,
21
- OperationalError
22
24
  )
23
25
  from sqlalchemy.orm import sessionmaker
24
26
  from sqlalchemy.schema import Table
@@ -46,23 +48,11 @@ def infer_is_sql_text(_string: str) -> bool:
46
48
  if isinstance(_string, bytes):
47
49
  _string = _string.decode("utf-8")
48
50
 
49
- keywords = [
50
- "SELECT",
51
- "INSERT",
52
- "UPDATE",
53
- "CREATE",
54
- "DROP",
55
- "DELETE",
56
- "ALTER",
57
- "SET",
58
- "GRANT",
59
- "WITH",
60
- ]
61
51
  lines = _string.split("\n")
62
52
  if len(lines) > 1:
63
53
  return True
64
54
  _string = _string.lower()
65
- for i in keywords:
55
+ for i in _sql_keywords:
66
56
  if _string.strip().startswith(i.lower() + " "):
67
57
  return True
68
58
  return False
@@ -97,26 +87,40 @@ def get_dataframe(connectable, filename_or_query, **kwargs):
97
87
 
98
88
 
99
89
  def pretty_print(sql, **kwargs):
90
+ """Print and optionally summarize an SQL query"""
91
+ summarize = kwargs.pop("summarize", True)
92
+ if summarize:
93
+ sql = summarize_statement(sql)
94
+ secho(sql, **kwargs)
95
+
96
+
97
+ _sql_keywords = [
98
+ "SELECT",
99
+ "INSERT",
100
+ "UPDATE",
101
+ "CREATE",
102
+ "DROP",
103
+ "DELETE",
104
+ "ALTER",
105
+ "SET",
106
+ "GRANT",
107
+ "WITH",
108
+ "NOTIFY",
109
+ "COPY",
110
+ ]
111
+
112
+
113
+ def summarize_statement(sql):
100
114
  for line in sql.split("\n"):
101
- for i in [
102
- "SELECT",
103
- "INSERT",
104
- "UPDATE",
105
- "CREATE",
106
- "DROP",
107
- "DELETE",
108
- "ALTER",
109
- "SET",
110
- "GRANT",
111
- "WITH",
112
- "NOTIFY",
113
- "COPY",
114
- ]:
115
+ for i in _sql_keywords:
115
116
  if not line.startswith(i):
116
117
  continue
117
- start = line.split("(")[0].strip().rstrip(";").replace(" AS", "")
118
- secho(start, **kwargs)
119
- return
118
+ return line.split("(")[0].strip().rstrip(";").replace(" AS", "")
119
+
120
+
121
+ class DevNull(object):
122
+ def write(self, *_):
123
+ pass
120
124
 
121
125
 
122
126
  def get_sql_text(sql, interpret_as_file=None, echo_file_name=True):
@@ -232,6 +236,16 @@ def infer_has_server_binds(sql):
232
236
  return "%s" in sql or search(r"%\(\w+\)s", sql)
233
237
 
234
238
 
239
+ _default_statement_filter = lambda sql_text, params: True
240
+
241
+
242
+ class PrintMode(Enum):
243
+ NONE = "none"
244
+ ERRORS = "errors"
245
+ SUMMARY = "summary"
246
+ ALL = "all"
247
+
248
+
235
249
  def _run_sql(connectable, sql, params=None, **kwargs):
236
250
  """
237
251
  Internal function for running a query on a SQLAlchemy connectable,
@@ -247,6 +261,12 @@ def _run_sql(connectable, sql, params=None, **kwargs):
247
261
  raise_errors = kwargs.pop("raise_errors", False)
248
262
  has_server_binds = kwargs.pop("has_server_binds", None)
249
263
  ensure_single_query = kwargs.pop("ensure_single_query", False)
264
+ statement_filter = kwargs.pop("statement_filter", _default_statement_filter)
265
+ output_mode = kwargs.pop("output_mode", PrintMode.SUMMARY)
266
+ output_file = kwargs.pop("output_file", stderr)
267
+
268
+ if output_mode == PrintMode.NONE:
269
+ output_file = DevNull()
250
270
 
251
271
  if stop_on_error:
252
272
  raise_errors = True
@@ -279,6 +299,7 @@ def _run_sql(connectable, sql, params=None, **kwargs):
279
299
  query = _render_query(query, connectable)
280
300
 
281
301
  sql_text = str(query)
302
+
282
303
  if isinstance(query, str):
283
304
  sql_text = format(query, strip_comments=True).strip()
284
305
  if sql_text == "":
@@ -288,6 +309,21 @@ def _run_sql(connectable, sql, params=None, **kwargs):
288
309
  if has_server_binds is None:
289
310
  has_server_binds = infer_has_server_binds(sql_text)
290
311
 
312
+ should_run = statement_filter(sql_text, params)
313
+
314
+ # Shorten summary text for printing
315
+ if output_mode != PrintMode.ALL:
316
+ sql_text = summarize_statement(sql_text)
317
+
318
+ if not should_run:
319
+ secho(
320
+ sql_text,
321
+ dim=True,
322
+ strikethrough=True,
323
+ file=output_file,
324
+ )
325
+ continue
326
+
291
327
  # This only does something for postgresql, but it's harmless to run it for other engines
292
328
  set_wait_callback(wait_select)
293
329
 
@@ -309,7 +345,7 @@ def _run_sql(connectable, sql, params=None, **kwargs):
309
345
  trans.commit()
310
346
  elif hasattr(connectable, "commit"):
311
347
  connectable.commit()
312
- pretty_print(sql_text, dim=True)
348
+ secho(sql_text, dim=True, file=output_file)
313
349
  except Exception as err:
314
350
  if trans is not None:
315
351
  trans.rollback()
@@ -318,14 +354,16 @@ def _run_sql(connectable, sql, params=None, **kwargs):
318
354
  if raise_errors or _should_raise_query_error(err):
319
355
  raise err
320
356
 
321
- _print_error(sql_text, err)
357
+ _print_error(sql_text, err, file=output_file)
322
358
  finally:
323
359
  set_wait_callback(None)
324
360
 
325
361
 
326
362
  def _should_raise_query_error(err):
327
363
  """Determine if an error should be raised for a query or not."""
328
- if not isinstance(err, (ProgrammingError, IntegrityError, InternalError, OperationalError)):
364
+ if not isinstance(
365
+ err, (ProgrammingError, IntegrityError, InternalError, OperationalError)
366
+ ):
329
367
  return True
330
368
 
331
369
  orig_err = getattr(err, "orig", None)
@@ -336,23 +374,27 @@ def _should_raise_query_error(err):
336
374
  # We might want to change this behavior in the future, or support more graceful handling of errors from other
337
375
  # database backends.
338
376
  # Ideally we could handle operational errors more gracefully
339
- if isinstance(orig_err, psycopg2.errors.QueryCanceled) or getattr(orig_err, "pgcode", None) == "57014":
377
+ if (
378
+ isinstance(orig_err, psycopg2.errors.QueryCanceled)
379
+ or getattr(orig_err, "pgcode", None) == "57014"
380
+ ):
340
381
  return True
341
382
 
342
383
  return False
343
384
 
344
385
 
345
- def _print_error(sql_text, err):
386
+ def _print_error(sql_text, err, **kwargs):
346
387
  if orig := getattr(err, "orig", None):
347
388
  _err = str(orig)
348
389
  else:
349
390
  _err = str(err)
350
391
  _err = _err.strip()
351
- dim = "already exists" in _err
352
- pretty_print(sql_text, fg=None if dim else "red", dim=True)
392
+ # Decide whether error should be dimmed
393
+ dim = kwargs.pop("dim", "already exists" in _err)
394
+ secho(sql_text, fg=None if dim else "red", dim=True, **kwargs)
353
395
  if dim:
354
396
  _err = " " + _err
355
- secho(_err, fg="red", dim=dim)
397
+ secho(_err, fg="red", dim=dim, **kwargs)
356
398
  log.error(err)
357
399
 
358
400
 
@@ -444,6 +486,9 @@ def run_sql(*args, **kwargs):
444
486
  returning a list after completion.
445
487
  ensure_single_query : bool
446
488
  If True, raise an error if multiple queries are passed when only one is expected.
489
+ statement_filter : Callable
490
+ A function that takes a SQL statement and parameters and returns True if the statement
491
+ should be run, and False if it should be skipped.
447
492
  """
448
493
  res = _run_sql(*args, **kwargs)
449
494
  if kwargs.pop("yield_results", False):
@@ -451,7 +496,9 @@ def run_sql(*args, **kwargs):
451
496
  return list(res)
452
497
 
453
498
 
454
- def execute(connectable, sql, params=None, stop_on_error=False):
499
+ def execute(connectable, sql, params=None, stop_on_error=False, **kwargs):
500
+ output_file = kwargs.pop("output_file", None)
501
+ output_mode = kwargs.pop("output_mode", None)
455
502
  sql = format(sql, strip_comments=True).strip()
456
503
  if sql == "":
457
504
  return
@@ -460,17 +507,12 @@ def execute(connectable, sql, params=None, stop_on_error=False):
460
507
  res = connectable.execute(text(sql), params=params)
461
508
  if hasattr(connectable, "commit"):
462
509
  connectable.commit()
463
- pretty_print(sql, dim=True)
510
+ pretty_print(sql, dim=True, file=output_file, mode=output_mode)
464
511
  return res
465
512
  except (ProgrammingError, IntegrityError) as err:
466
- err = str(err.orig).strip()
467
- dim = "already exists" in err
468
513
  if hasattr(connectable, "rollback"):
469
514
  connectable.rollback()
470
- pretty_print(sql, fg=None if dim else "red", dim=True)
471
- if dim:
472
- err = " " + err
473
- secho(err, fg="red", dim=dim)
515
+ _print_error(sql, dim=True, file=output_file, mode=output_mode)
474
516
  if stop_on_error:
475
517
  return
476
518
  finally:
@@ -3,7 +3,7 @@ authors = ["Daven Quinn <dev@davenquinn.com>"]
3
3
  description = "A SQLAlchemy-based database toolkit."
4
4
  name = "macrostrat.database"
5
5
  packages = [{ include = "macrostrat" }]
6
- version = "3.5.0"
6
+ version = "3.5.2"
7
7
 
8
8
  [tool.poetry.dependencies]
9
9
  GeoAlchemy2 = "^0.15.2"