macrostrat.database 3.5.4__tar.gz → 4.0.0__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.4 → macrostrat_database-4.0.0}/PKG-INFO +5 -3
  2. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/__init__.py +9 -12
  3. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/postgresql.py +5 -4
  4. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/utils.py +46 -18
  5. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/pyproject.toml +4 -3
  6. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/mapper/__init__.py +0 -0
  7. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/mapper/base.py +0 -0
  8. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/mapper/cache.py +0 -0
  9. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/mapper/utils.py +0 -0
  10. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/transfer/__init__.py +0 -0
  11. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/transfer/dump_database.py +0 -0
  12. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/transfer/move_tables.py +0 -0
  13. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/transfer/restore_database.py +0 -0
  14. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/transfer/stream_utils.py +0 -0
  15. {macrostrat_database-3.5.4 → macrostrat_database-4.0.0}/macrostrat/database/transfer/utils.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: macrostrat.database
3
- Version: 3.5.4
3
+ Version: 4.0.0
4
4
  Summary: A SQLAlchemy-based database toolkit.
5
5
  Author: Daven Quinn
6
6
  Author-email: dev@davenquinn.com
@@ -9,12 +9,14 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
12
13
  Requires-Dist: GeoAlchemy2 (>=0.15.2,<0.16.0)
13
14
  Requires-Dist: SQLAlchemy (>=2.0.18,<3.0.0)
14
15
  Requires-Dist: SQLAlchemy-Utils (>=0.41.1,<0.42.0)
15
16
  Requires-Dist: aiofiles (>=23.2.1,<24.0.0)
16
17
  Requires-Dist: click (>=8.1.3,<9.0.0)
17
18
  Requires-Dist: macrostrat.utils (>=1.3.0,<2.0.0)
18
- Requires-Dist: psycopg2-binary (>=2.9.6,<3.0.0)
19
+ Requires-Dist: psycopg (>=3.2.1,<4.0.0)
20
+ Requires-Dist: psycopg2 (>=2.9.11,<3.0.0)
19
21
  Requires-Dist: rich (>=13.7.1,<14.0.0)
20
22
  Requires-Dist: sqlparse (>=0.5.1,<0.6.0)
@@ -3,16 +3,15 @@ from contextlib import contextmanager
3
3
  from pathlib import Path
4
4
  from typing import Optional, Union
5
5
 
6
- from psycopg2.errors import InvalidSavepointSpecification
7
- from psycopg2.sql import Identifier
8
- from sqlalchemy import URL, Engine, MetaData, create_engine, inspect
9
- from sqlalchemy.exc import IntegrityError, InternalError
6
+ from psycopg.errors import InvalidSavepointSpecification
7
+ from psycopg.sql import Identifier
8
+ from sqlalchemy import URL, Engine, MetaData, inspect
9
+ from sqlalchemy.exc import IntegrityError, OperationalError
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
-
16
15
  from .mapper import DatabaseMapper
17
16
  from .postgresql import on_conflict, prefix_inserts # noqa
18
17
  from .utils import ( # noqa
@@ -25,6 +24,7 @@ from .utils import ( # noqa
25
24
  run_fixtures,
26
25
  run_query,
27
26
  run_sql,
27
+ create_engine,
28
28
  )
29
29
 
30
30
  metadata = MetaData()
@@ -60,12 +60,8 @@ class Database(object):
60
60
 
61
61
  self.instance_params = kwargs.pop("instance_params", {})
62
62
 
63
- if isinstance(db_conn, Engine):
64
- log.info(f"Set up database connection with engine {db_conn.url}")
65
- self.engine = db_conn
66
- else:
67
- log.info(f"Setting up database connection with URL '{db_conn}'")
68
- self.engine = create_engine(db_conn, echo=echo_sql, **kwargs)
63
+ self.engine = create_engine(db_conn, echo=echo_sql, **kwargs)
64
+
69
65
  self.metadata = kwargs.get("metadata", metadata)
70
66
 
71
67
  # Scoped session for database
@@ -334,6 +330,7 @@ class Database(object):
334
330
 
335
331
  if connection is None:
336
332
  connection = self.session.connection()
333
+
337
334
  params = {"name": Identifier(name)}
338
335
  run_query(connection, "SAVEPOINT {name}", params)
339
336
  should_rollback = rollback == "always"
@@ -356,7 +353,7 @@ def _clear_savepoint(connection, name, rollback=True):
356
353
  run_query(connection, "ROLLBACK TO SAVEPOINT {name}", params)
357
354
  else:
358
355
  run_query(connection, "RELEASE SAVEPOINT {name}", params)
359
- except InternalError as err:
356
+ except OperationalError as err:
360
357
  if isinstance(err.orig, InvalidSavepointSpecification):
361
358
  log.warning(
362
359
  f"Savepoint {name} does not exist; we may have already rolled back."
@@ -4,11 +4,11 @@ from contextlib import contextmanager
4
4
  from contextvars import ContextVar
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- import psycopg2
8
7
  from sqlalchemy.dialects import postgresql
9
8
  from sqlalchemy.exc import CompileError
10
9
  from sqlalchemy.ext.compiler import compiles
11
- from sqlalchemy.sql.expression import Insert, text
10
+ from sqlalchemy.sql.dml import Insert
11
+ from sqlalchemy.sql.expression import text
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from ..database import Database
@@ -26,9 +26,10 @@ def on_conflict(action="restrict"):
26
26
  _insert_mode.reset(token)
27
27
 
28
28
 
29
- # @compiles(Insert, "postgresql")
29
+ @compiles(Insert, "postgresql")
30
30
  def prefix_inserts(insert, compiler, **kw):
31
31
  """Conditionally adapt insert statements to use on-conflict resolution (a PostgreSQL feature)"""
32
+
32
33
  if insert._post_values_clause is not None:
33
34
  return compiler.visit_insert(insert, **kw)
34
35
 
@@ -63,7 +64,7 @@ def prefix_inserts(insert, compiler, **kw):
63
64
  def table_exists(db: Database, table_name: str, schema: str = "public") -> bool:
64
65
  """Check if a table exists in a PostgreSQL database."""
65
66
  sql = """SELECT EXISTS (
66
- SELECT FROM information_schema.tables
67
+ SELECT FROM information_schema.tables
67
68
  WHERE table_schema = :schema
68
69
  AND table_name = :table_name
69
70
  );"""
@@ -4,18 +4,17 @@ from enum import Enum
4
4
  from pathlib import Path
5
5
  from re import search
6
6
  from sys import stderr
7
- from time import sleep
8
7
  from typing import IO, Union
9
8
  from warnings import warn
10
9
 
11
- import psycopg2.errors
12
10
  from click import echo, secho
13
- from psycopg2.extensions import set_wait_callback
14
- from psycopg2.extras import wait_select
15
- from psycopg2.sql import SQL, Composable, Composed
11
+ from psycopg.errors import QueryCanceled
12
+ from psycopg.sql import SQL, Composable, Composed
16
13
  from rich.console import Console
17
- from sqlalchemy import MetaData, create_engine, text
14
+ from sqlalchemy import MetaData, text
15
+ from sqlalchemy import create_engine as base_create_engine
18
16
  from sqlalchemy.engine import Connection, Engine
17
+ from sqlalchemy.engine.url import make_url
19
18
  from sqlalchemy.exc import (
20
19
  IntegrityError,
21
20
  InternalError,
@@ -29,6 +28,7 @@ from sqlalchemy.sql.elements import ClauseElement, TextClause
29
28
  from sqlalchemy_utils import create_database as _create_database
30
29
  from sqlalchemy_utils import database_exists, drop_database
31
30
  from sqlparse import format, split
31
+ from time import sleep
32
32
 
33
33
  from macrostrat.utils import cmd, get_logger
34
34
 
@@ -196,6 +196,8 @@ def _get_cursor(connectable):
196
196
  while hasattr(conn, "driver_connection") or hasattr(conn, "connection"):
197
197
  if hasattr(conn, "driver_connection"):
198
198
  conn = conn.driver_connection
199
+ elif conn.connection == conn:
200
+ break
199
201
  else:
200
202
  conn = conn.connection
201
203
  if callable(conn):
@@ -296,7 +298,7 @@ def _run_sql(connectable, sql, params=None, **kwargs):
296
298
  if pre_bind_params is not None:
297
299
  if not isinstance(query, SQL):
298
300
  query = SQL(query)
299
- # Pre-bind the parameters using PsycoPG2
301
+ # Pre-bind the parameters using psycopg
300
302
  query = query.format(**pre_bind_params)
301
303
 
302
304
  if isinstance(query, (SQL, Composed)):
@@ -328,9 +330,6 @@ def _run_sql(connectable, sql, params=None, **kwargs):
328
330
  )
329
331
  continue
330
332
 
331
- # This only does something for postgresql, but it's harmless to run it for other engines
332
- set_wait_callback(wait_select)
333
-
334
333
  try:
335
334
  trans = connectable.begin()
336
335
  except InvalidRequestError:
@@ -360,7 +359,7 @@ def _run_sql(connectable, sql, params=None, **kwargs):
360
359
 
361
360
  _print_error(sql_text, err, file=output_file)
362
361
  finally:
363
- set_wait_callback(None)
362
+ pass
364
363
 
365
364
 
366
365
  def _should_raise_query_error(err):
@@ -379,7 +378,7 @@ def _should_raise_query_error(err):
379
378
  # database backends.
380
379
  # Ideally we could handle operational errors more gracefully
381
380
  if (
382
- isinstance(orig_err, psycopg2.errors.QueryCanceled)
381
+ isinstance(orig_err, QueryCanceled)
383
382
  or getattr(orig_err, "pgcode", None) == "57014"
384
383
  ):
385
384
  return True
@@ -602,6 +601,26 @@ def create_database(url, **kwargs):
602
601
  _create_database(url, **kwargs)
603
602
 
604
603
 
604
+ def create_engine(db_conn, **kwargs):
605
+ if isinstance(db_conn, Engine):
606
+ log.info(f"Set up database connection with engine {db_conn.url}")
607
+ if db_conn.driver == "psycopg2":
608
+ log.warning(
609
+ "The psycopg2 driver is deprecated. Please use psycopg3 instead."
610
+ )
611
+ return db_conn
612
+ else:
613
+ log.info(f"Setting up database connection with URL '{db_conn}'")
614
+ url = db_conn
615
+ if isinstance(url, str):
616
+ url = make_url(url)
617
+ # Set the driver to psycopg if not already set
618
+ if url.drivername != "postgresql+psycopg":
619
+ url = url.set(drivername="postgresql+psycopg")
620
+
621
+ return base_create_engine(url, **kwargs)
622
+
623
+
605
624
  def connection_args(engine):
606
625
  """Get PostgreSQL connection arguments for an engine"""
607
626
  _psql_flags = {"-U": "username", "-h": "host", "-p": "port", "-P": "password"}
@@ -617,15 +636,24 @@ def connection_args(engine):
617
636
  return flags, engine.url.database
618
637
 
619
638
 
620
- def db_isready(engine_or_url):
621
- args, _ = connection_args(engine_or_url)
622
- c = cmd("pg_isready", args, capture_output=True)
623
- return c.returncode == 0
639
+ def db_isready(engine_or_url, use_shell_command=False):
640
+ if use_shell_command:
641
+ args, _ = connection_args(engine_or_url)
642
+ c = cmd("pg_isready", args, capture_output=True)
643
+ return c.returncode == 0
644
+ # Use a more typical sqlalchemy connection approach
645
+ engine = create_engine(engine_or_url)
646
+ try:
647
+ with engine.connect() as conn:
648
+ conn.execute(text("SELECT 1"))
649
+ return True
650
+ except OperationalError:
651
+ return False
624
652
 
625
653
 
626
- def wait_for_database(engine_or_url, quiet=False):
654
+ def wait_for_database(engine_or_url, *, quiet=False, use_shell_command=False):
627
655
  msg = "Waiting for database..."
628
- while not db_isready(engine_or_url):
656
+ while not db_isready(engine_or_url, use_shell_command=use_shell_command):
629
657
  if not quiet:
630
658
  echo(msg, err=True)
631
659
  log.info(msg)
@@ -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.4"
6
+ version = "4.0.0"
7
7
 
8
8
  [tool.poetry.dependencies]
9
9
  GeoAlchemy2 = "^0.15.2"
@@ -11,13 +11,14 @@ SQLAlchemy = "^2.0.18"
11
11
  SQLAlchemy-Utils = "^0.41.1"
12
12
  click = "^8.1.3"
13
13
  "macrostrat.utils" = "^1.3.0"
14
- psycopg2-binary = "^2.9.6"
15
14
  python = "^3.10"
16
15
  sqlparse = "^0.5.1"
17
16
  aiofiles = "^23.2.1"
18
17
  rich = "^13.7.1"
18
+ psycopg = "^3.2.1"
19
+ psycopg2 = "^2.9.11"
19
20
 
20
- [tool.poetry.dev-dependencies]
21
+ [tool.poetry.group.dev.dependencies]
21
22
  "macrostrat.utils" = { path = "../utils", develop = true }
22
23
 
23
24
  [build-system]