orm-loader 0.3.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 (33) hide show
  1. orm_loader-0.3.0/PKG-INFO +162 -0
  2. orm_loader-0.3.0/README.md +149 -0
  3. orm_loader-0.3.0/pyproject.toml +37 -0
  4. orm_loader-0.3.0/src/orm_loader/__init__.py +0 -0
  5. orm_loader-0.3.0/src/orm_loader/helpers/__init__.py +23 -0
  6. orm_loader-0.3.0/src/orm_loader/helpers/bootstrap.py +13 -0
  7. orm_loader-0.3.0/src/orm_loader/helpers/bulk.py +90 -0
  8. orm_loader-0.3.0/src/orm_loader/helpers/discovery.py +11 -0
  9. orm_loader-0.3.0/src/orm_loader/helpers/errors.py +6 -0
  10. orm_loader-0.3.0/src/orm_loader/helpers/logging.py +90 -0
  11. orm_loader-0.3.0/src/orm_loader/helpers/metadata.py +15 -0
  12. orm_loader-0.3.0/src/orm_loader/helpers/sqlite.py +32 -0
  13. orm_loader-0.3.0/src/orm_loader/loaders/__init__.py +14 -0
  14. orm_loader-0.3.0/src/orm_loader/loaders/data_classes.py +147 -0
  15. orm_loader-0.3.0/src/orm_loader/loaders/loader_interface.py +274 -0
  16. orm_loader-0.3.0/src/orm_loader/loaders/loading_helpers.py +136 -0
  17. orm_loader-0.3.0/src/orm_loader/py.typed +0 -0
  18. orm_loader-0.3.0/src/orm_loader/registry/__init__.py +67 -0
  19. orm_loader-0.3.0/src/orm_loader/registry/registry.py +167 -0
  20. orm_loader-0.3.0/src/orm_loader/registry/validation.py +230 -0
  21. orm_loader-0.3.0/src/orm_loader/registry/validation_presets.py +14 -0
  22. orm_loader-0.3.0/src/orm_loader/registry/validation_report.py +88 -0
  23. orm_loader-0.3.0/src/orm_loader/registry/validation_runner.py +36 -0
  24. orm_loader-0.3.0/src/orm_loader/tables/__init__.py +25 -0
  25. orm_loader-0.3.0/src/orm_loader/tables/base/__init__.py +15 -0
  26. orm_loader-0.3.0/src/orm_loader/tables/base/allocators.py +22 -0
  27. orm_loader-0.3.0/src/orm_loader/tables/base/loadable_table.py +420 -0
  28. orm_loader-0.3.0/src/orm_loader/tables/base/orm_table.py +76 -0
  29. orm_loader-0.3.0/src/orm_loader/tables/base/serialisable_table.py +48 -0
  30. orm_loader-0.3.0/src/orm_loader/tables/base/typing.py +129 -0
  31. orm_loader-0.3.0/src/orm_loader/tables/data/__init__.py +7 -0
  32. orm_loader-0.3.0/src/orm_loader/tables/data/converters.py +106 -0
  33. orm_loader-0.3.0/src/orm_loader/tables/data/data_type_management.py +110 -0
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.3
2
+ Name: orm-loader
3
+ Version: 0.3.0
4
+ Summary: Generic base classes to handle ORM functionality for multiple downstream datamodels
5
+ Author: gkennos
6
+ Author-email: gkennos <georgina.kennedy@unsw.edu.au>
7
+ Requires-Dist: chardet>=5.2.0
8
+ Requires-Dist: pandas>=2.3.3
9
+ Requires-Dist: pyarrow>=23.0.0
10
+ Requires-Dist: sqlalchemy>=2.0.45
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+
14
+ ## orm-loader
15
+
16
+ A lightweight, reusable foundation for building and validating SQLAlchemy-based clinical (and non-clinical) data models.
17
+
18
+ This library provides general-purpose ORM infrastructure that sits below any specific data model (OMOP, PCORnet, custom CDMs, etc.), focusing on:
19
+
20
+ * declarative base configuration
21
+ * bulk ingestion patterns
22
+ * file-based validation & loading
23
+ * table introspection
24
+ * model-agnostic validation scaffolding
25
+ * safe, database-portable operational helpers
26
+
27
+ It intentionally contains no domain logic and no assumptions about a specific schema.
28
+
29
+
30
+ ### What this library provides:
31
+
32
+ This library provides a small set of composable building blocks for defining, loading, inspecting, and validating SQLAlchemy-based data models.
33
+ All components are model-agnostic and can be selectively combined in downstream libraries.
34
+
35
+ 1. A minimal, opinionated ORM table base
36
+
37
+ ORMTableBase provides structural introspection utilities for SQLAlchemy-mapped tables, without imposing any domain semantics.
38
+
39
+ It supports:
40
+ * mapper access and inspection
41
+ * primary key discovery
42
+ * required (non-nullable) column detection
43
+ * consistent primary key handling across models
44
+ * simple ID allocation helpers for sequence-less databases
45
+
46
+ ```python
47
+ from orm_loader.tables import ORMTableBase
48
+
49
+ class MyTable(ORMTableBase, Base):
50
+ __tablename__ = "my_table"
51
+
52
+ ```
53
+ This base is intended to be inherited by all ORM tables, either directly or via higher-level mixins.
54
+
55
+ 2. CSV-based ingestion mixins
56
+
57
+ CSVLoadableTableInterface adds opt-in CSV loading support for ORM tables using pandas, with a focus on correctness and scalability.
58
+
59
+ Features include:
60
+ * chunked loading for large files
61
+ * optional per-table normalisation logic
62
+ * optional deduplication against existing database rows
63
+ * safe bulk inserts using SQLAlchemy sessions
64
+
65
+ ```python
66
+ class MyTable(CSVLoadableTableInterface, ORMTableBase, Base):
67
+ __tablename__ = "my_table"
68
+
69
+ ```
70
+
71
+ Downstream models may override:
72
+ * normalise_dataframe(...)
73
+ * dedupe_dataframe(...)
74
+ * csv_columns()
75
+ to implement table-specific ingestion policies.
76
+
77
+ 3. Structured serialisation and hashing
78
+
79
+ SerialisableTableInterface adds lightweight, explicit serialisation helpers for ORM rows.
80
+
81
+ It supports:
82
+ * conversion to dictionaries
83
+ * JSON serialisation
84
+ * stable row-level fingerprints
85
+ * iterator-style access to field/value pairs
86
+
87
+ ```python
88
+ row = session.get(MyTable, 1)
89
+ row.to_dict()
90
+ row.to_json()
91
+ row.fingerprint()
92
+ ```
93
+
94
+ This is useful for:
95
+
96
+ * debugging
97
+ * auditing
98
+ * reproducibility checks
99
+ * downstream APIs or exports
100
+
101
+
102
+ 4. Model registry and validation scaffolding
103
+
104
+ The library includes model-agnostic validation infrastructure, designed to compare ORM models against external specifications.
105
+
106
+ This includes:
107
+ * a model registry
108
+ * table and field descriptors
109
+ * validator contracts
110
+ * a validation runner
111
+ * structured validation reports
112
+ Specifications can be loaded from CSV today, with support for other formats (e.g. LinkML) planned.
113
+
114
+ ```python
115
+ registry = ModelRegistry(model_version="1.0")
116
+ registry.load_table_specs(table_csv, field_csv)
117
+ registry.register_models([MyTable])
118
+
119
+ runner = ValidationRunner(validators=always_on_validators())
120
+ report = runner.run(registry)
121
+ ```
122
+
123
+ Validation output is available as:
124
+ * human-readable text
125
+ * structured dictionaries
126
+ * JSON (for CI/CD integration)
127
+ * exit codes suitable for pipelines
128
+
129
+ 5. Database bootstrap helpers
130
+ The library provides lightweight helpers for schema creation and bootstrapping, without imposing a migration strategy.
131
+
132
+ ```python
133
+ from orm_loader.metadata import Base
134
+ from orm_loader.bootstrap import bootstrap
135
+
136
+ bootstrap(engine, create=True)
137
+ ```
138
+
139
+ 6. Safe bulk-loading utilities
140
+
141
+ A reusable context manager simplifies trusted bulk ingestion workflows:
142
+ * temporarily disables foreign key checks where supported
143
+ * suppresses autoflush for performance
144
+ * ensures reliable rollback on failure
145
+
146
+ ## Summary
147
+
148
+ This library intentionally focuses on infrastructure, not semantics.
149
+
150
+ It provides:
151
+ * reusable ORM mixins
152
+ * safe ingestion patterns
153
+ * validation scaffolding
154
+ * database-portable utilities
155
+
156
+ while leaving domain rules, business logic, and schema semantics to downstream libraries.
157
+
158
+ This makes it suitable as a shared foundation for:
159
+ * clinical data models
160
+ * research data marts
161
+ * registry schemas
162
+ * synthetic data pipelines
@@ -0,0 +1,149 @@
1
+ ## orm-loader
2
+
3
+ A lightweight, reusable foundation for building and validating SQLAlchemy-based clinical (and non-clinical) data models.
4
+
5
+ This library provides general-purpose ORM infrastructure that sits below any specific data model (OMOP, PCORnet, custom CDMs, etc.), focusing on:
6
+
7
+ * declarative base configuration
8
+ * bulk ingestion patterns
9
+ * file-based validation & loading
10
+ * table introspection
11
+ * model-agnostic validation scaffolding
12
+ * safe, database-portable operational helpers
13
+
14
+ It intentionally contains no domain logic and no assumptions about a specific schema.
15
+
16
+
17
+ ### What this library provides:
18
+
19
+ This library provides a small set of composable building blocks for defining, loading, inspecting, and validating SQLAlchemy-based data models.
20
+ All components are model-agnostic and can be selectively combined in downstream libraries.
21
+
22
+ 1. A minimal, opinionated ORM table base
23
+
24
+ ORMTableBase provides structural introspection utilities for SQLAlchemy-mapped tables, without imposing any domain semantics.
25
+
26
+ It supports:
27
+ * mapper access and inspection
28
+ * primary key discovery
29
+ * required (non-nullable) column detection
30
+ * consistent primary key handling across models
31
+ * simple ID allocation helpers for sequence-less databases
32
+
33
+ ```python
34
+ from orm_loader.tables import ORMTableBase
35
+
36
+ class MyTable(ORMTableBase, Base):
37
+ __tablename__ = "my_table"
38
+
39
+ ```
40
+ This base is intended to be inherited by all ORM tables, either directly or via higher-level mixins.
41
+
42
+ 2. CSV-based ingestion mixins
43
+
44
+ CSVLoadableTableInterface adds opt-in CSV loading support for ORM tables using pandas, with a focus on correctness and scalability.
45
+
46
+ Features include:
47
+ * chunked loading for large files
48
+ * optional per-table normalisation logic
49
+ * optional deduplication against existing database rows
50
+ * safe bulk inserts using SQLAlchemy sessions
51
+
52
+ ```python
53
+ class MyTable(CSVLoadableTableInterface, ORMTableBase, Base):
54
+ __tablename__ = "my_table"
55
+
56
+ ```
57
+
58
+ Downstream models may override:
59
+ * normalise_dataframe(...)
60
+ * dedupe_dataframe(...)
61
+ * csv_columns()
62
+ to implement table-specific ingestion policies.
63
+
64
+ 3. Structured serialisation and hashing
65
+
66
+ SerialisableTableInterface adds lightweight, explicit serialisation helpers for ORM rows.
67
+
68
+ It supports:
69
+ * conversion to dictionaries
70
+ * JSON serialisation
71
+ * stable row-level fingerprints
72
+ * iterator-style access to field/value pairs
73
+
74
+ ```python
75
+ row = session.get(MyTable, 1)
76
+ row.to_dict()
77
+ row.to_json()
78
+ row.fingerprint()
79
+ ```
80
+
81
+ This is useful for:
82
+
83
+ * debugging
84
+ * auditing
85
+ * reproducibility checks
86
+ * downstream APIs or exports
87
+
88
+
89
+ 4. Model registry and validation scaffolding
90
+
91
+ The library includes model-agnostic validation infrastructure, designed to compare ORM models against external specifications.
92
+
93
+ This includes:
94
+ * a model registry
95
+ * table and field descriptors
96
+ * validator contracts
97
+ * a validation runner
98
+ * structured validation reports
99
+ Specifications can be loaded from CSV today, with support for other formats (e.g. LinkML) planned.
100
+
101
+ ```python
102
+ registry = ModelRegistry(model_version="1.0")
103
+ registry.load_table_specs(table_csv, field_csv)
104
+ registry.register_models([MyTable])
105
+
106
+ runner = ValidationRunner(validators=always_on_validators())
107
+ report = runner.run(registry)
108
+ ```
109
+
110
+ Validation output is available as:
111
+ * human-readable text
112
+ * structured dictionaries
113
+ * JSON (for CI/CD integration)
114
+ * exit codes suitable for pipelines
115
+
116
+ 5. Database bootstrap helpers
117
+ The library provides lightweight helpers for schema creation and bootstrapping, without imposing a migration strategy.
118
+
119
+ ```python
120
+ from orm_loader.metadata import Base
121
+ from orm_loader.bootstrap import bootstrap
122
+
123
+ bootstrap(engine, create=True)
124
+ ```
125
+
126
+ 6. Safe bulk-loading utilities
127
+
128
+ A reusable context manager simplifies trusted bulk ingestion workflows:
129
+ * temporarily disables foreign key checks where supported
130
+ * suppresses autoflush for performance
131
+ * ensures reliable rollback on failure
132
+
133
+ ## Summary
134
+
135
+ This library intentionally focuses on infrastructure, not semantics.
136
+
137
+ It provides:
138
+ * reusable ORM mixins
139
+ * safe ingestion patterns
140
+ * validation scaffolding
141
+ * database-portable utilities
142
+
143
+ while leaving domain rules, business logic, and schema semantics to downstream libraries.
144
+
145
+ This makes it suitable as a shared foundation for:
146
+ * clinical data models
147
+ * research data marts
148
+ * registry schemas
149
+ * synthetic data pipelines
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "orm-loader"
3
+ version = "0.3.0"
4
+ description = "Generic base classes to handle ORM functionality for multiple downstream datamodels"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "gkennos", email = "georgina.kennedy@unsw.edu.au" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "chardet>=5.2.0",
12
+ "pandas>=2.3.3",
13
+ "pyarrow>=23.0.0",
14
+ "sqlalchemy>=2.0.45",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.9.2,<0.10.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "mypy>=1.19.1",
24
+ "pytest>=9.0.2",
25
+ "ruff>=0.14.11",
26
+ ]
27
+
28
+ [tool.setuptools]
29
+ packages = ["orm_loader"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ target-version = "py311"
34
+
35
+ [tool.mypy]
36
+ python_version = "3.11"
37
+ strict = true
File without changes
@@ -0,0 +1,23 @@
1
+ from .errors import IngestError, ValidationError
2
+ from .logging import get_logger, configure_logging
3
+ from .bootstrap import bootstrap, create_db
4
+ from .sqlite import enable_sqlite_foreign_keys, explain_sqlite_fk_error
5
+ from .bulk import bulk_load_context, engine_with_replica_role
6
+ from .metadata import Base
7
+ from .discovery import get_model_by_tablename
8
+
9
+
10
+ __all__ = [
11
+ "IngestError",
12
+ "ValidationError",
13
+ "get_logger",
14
+ "configure_logging",
15
+ "bootstrap",
16
+ "create_db",
17
+ "enable_sqlite_foreign_keys",
18
+ "explain_sqlite_fk_error",
19
+ "bulk_load_context",
20
+ "engine_with_replica_role",
21
+ "Base",
22
+ "get_model_by_tablename",
23
+ ]
@@ -0,0 +1,13 @@
1
+ from .metadata import Base
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+ def create_db(engine):
7
+ logger.debug("Creating database schema")
8
+ Base.metadata.create_all(engine)
9
+
10
+ def bootstrap(engine, *, create: bool = True):
11
+ logger.info("Bootstrapping schema (create=%s)", create)
12
+ if create:
13
+ create_db(engine)
@@ -0,0 +1,90 @@
1
+ from contextlib import contextmanager
2
+ from sqlalchemy import text, Engine
3
+ from sqlalchemy.orm import Session
4
+ import sqlalchemy as sa
5
+ from .logging import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+ @contextmanager
10
+ def bulk_load_context(
11
+ session: Session,
12
+ *,
13
+ disable_fk: bool = True,
14
+ no_autoflush: bool = True,
15
+ ):
16
+ engine = session.get_bind()
17
+ dialect = engine.dialect.name
18
+ fk_disabled = False
19
+
20
+ try:
21
+ if disable_fk:
22
+ if dialect == "postgresql":
23
+ session.execute(text(
24
+ "SET session_replication_role = replica"
25
+ ))
26
+ fk_disabled = True
27
+ elif dialect == "sqlite":
28
+ session.execute(text("PRAGMA foreign_keys = OFF"))
29
+ fk_disabled = True
30
+
31
+ logger.info("Disabled foreign key checks for bulk load")
32
+
33
+ if no_autoflush:
34
+ with session.no_autoflush:
35
+ yield
36
+ else:
37
+ yield
38
+
39
+ except Exception:
40
+ session.rollback()
41
+ raise
42
+
43
+ finally:
44
+ if fk_disabled:
45
+ if dialect == "postgresql":
46
+ session.execute(text(
47
+ "SET session_replication_role = DEFAULT"
48
+ ))
49
+ elif dialect == "sqlite":
50
+ session.execute(text("PRAGMA foreign_keys = ON"))
51
+
52
+ logger.info("Re-enabled foreign key checks after bulk load")
53
+
54
+ @contextmanager
55
+ def engine_with_replica_role(engine: Engine):
56
+ """
57
+ Context manager that:
58
+ - forces session_replication_role=replica on all connections
59
+ - restores DEFAULT on exit
60
+
61
+ this is different to bulk_load_context manager from orm_loader.helpers
62
+ because this is engine scoped where that one is session scoped
63
+
64
+ postgres only
65
+ """
66
+
67
+ @sa.event.listens_for(engine, "connect") # type: ignore
68
+ def _set_replica_role(dbapi_conn, _):
69
+ cur = dbapi_conn.cursor()
70
+ cur.execute("SET session_replication_role = replica")
71
+ cur.close()
72
+
73
+ try:
74
+ yield engine
75
+ finally:
76
+ # Explicitly restore on a fresh connection
77
+ with engine.connect() as conn:
78
+ conn = conn.execution_options(isolation_level="AUTOCOMMIT")
79
+ conn.execute(text("SET session_replication_role = DEFAULT"))
80
+
81
+ role = conn.execute(
82
+ text("SHOW session_replication_role")
83
+ ).scalar()
84
+
85
+ if role != "origin":
86
+ raise RuntimeError(
87
+ "Failed to restore session_replication_role"
88
+ )
89
+
90
+ logger.info("session_replication_role restored to DEFAULT")
@@ -0,0 +1,11 @@
1
+ from typing import Type
2
+ from .metadata import Base
3
+
4
+ def get_model_by_tablename(tablename: str, base: Type[Base] | None = None) -> Type | None:
5
+ tablename = tablename.lower().strip()
6
+ if base is None:
7
+ base = Base
8
+ for cls in base.__subclasses__():
9
+ if getattr(cls, "__tablename__", None) == tablename:
10
+ return cls
11
+ return None
@@ -0,0 +1,6 @@
1
+ class IngestError(Exception):
2
+ """Raised when ingestion fails for structural or runtime reasons."""
3
+
4
+
5
+ class ValidationError(Exception):
6
+ """Raised when schema or specification validation fails."""
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from typing import Literal, Optional
4
+ import re
5
+
6
+ SENSITIVE_KEYS = {
7
+ "password",
8
+ "passwd",
9
+ "secret",
10
+ "token",
11
+ "key",
12
+ "dsn",
13
+ "uri",
14
+ "url",
15
+ }
16
+ LOGGING_NAMESPACE = "sql_loader"
17
+
18
+ def _coerce_log_level(level: int | str) -> int:
19
+ if isinstance(level, int):
20
+ return level
21
+
22
+ if isinstance(level, str):
23
+ s = level.strip().upper()
24
+ if s.isdigit():
25
+ return int(s)
26
+
27
+ mapping = logging.getLevelNamesMapping()
28
+ if s in mapping:
29
+ return mapping[s]
30
+
31
+ raise ValueError(f"Invalid log level: {level!r}")
32
+
33
+ raise TypeError(f"Invalid log level type: {type(level)}")
34
+
35
+
36
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
37
+ """
38
+ Return a namespaced logger.
39
+
40
+ Examples:
41
+ get_logger() -> sql_loader
42
+ get_logger("loadable_table") -> sql_loader.loadable_table
43
+ """
44
+ full_name = LOGGING_NAMESPACE if name is None else f"{LOGGING_NAMESPACE}.{name}"
45
+ return logging.getLogger(full_name)
46
+
47
+
48
+ class RedactingFormatter(logging.Formatter):
49
+ def __init__(self, *args, **kwargs):
50
+ super().__init__(*args, **kwargs)
51
+ self._pattern = re.compile(
52
+ r"(?i)\\b(" + "|".join(SENSITIVE_KEYS) + r")\\b\\s*[:=]\\s*[^\\s,;]+"
53
+ )
54
+
55
+ def format(self, record):
56
+ msg = super().format(record)
57
+ return self._pattern.sub(r"\\1=<REDACTED>", msg)
58
+
59
+ def configure_logging(
60
+ *,
61
+ level: int | str = logging.INFO,
62
+ handler: Optional[logging.Handler] = None,
63
+ format: Optional[str] = None,
64
+ propagate: bool = True,
65
+ redact: bool = True,
66
+ ) -> None:
67
+ """
68
+ Enable logging output for omop_alchemy.
69
+
70
+ Safe to call multiple times.
71
+ """
72
+ logger = get_logger()
73
+ logger.setLevel(_coerce_log_level(level))
74
+
75
+ if handler is None:
76
+ handler = logging.StreamHandler()
77
+
78
+ if format is None:
79
+ format = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
80
+
81
+ formatter_cls = RedactingFormatter if redact else logging.Formatter
82
+ handler.setFormatter(formatter_cls(format))
83
+
84
+ if not any(isinstance(h, type(handler)) for h in logger.handlers):
85
+ logger.addHandler(handler)
86
+
87
+ logger.propagate = propagate
88
+
89
+
90
+ logging.getLogger(LOGGING_NAMESPACE).addHandler(logging.NullHandler())
@@ -0,0 +1,15 @@
1
+ from sqlalchemy import MetaData
2
+ from sqlalchemy.orm import DeclarativeBase
3
+
4
+ NAMING_CONVENTIONS = {
5
+ "ix": "ix_%(column_0_label)s",
6
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
7
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
8
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
9
+ "pk": "pk_%(table_name)s",
10
+ }
11
+
12
+ metadata = MetaData(naming_convention=NAMING_CONVENTIONS)
13
+
14
+ class Base(DeclarativeBase):
15
+ metadata = metadata
@@ -0,0 +1,32 @@
1
+ from sqlalchemy import event, text
2
+ from sqlalchemy.engine import Engine
3
+ from sqlalchemy.exc import IntegrityError
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ @event.listens_for(Engine, "connect")
9
+ def enable_sqlite_foreign_keys(dbapi_connection, connection_record):
10
+ if dbapi_connection.__class__.__module__.startswith("sqlite3"):
11
+ logger.debug("Enabling SQLite foreign key enforcement")
12
+ cursor = dbapi_connection.cursor()
13
+ cursor.execute("PRAGMA defer_foreign_keys = ON;")
14
+ cursor.close()
15
+
16
+ def explain_sqlite_fk_error(session, exc: IntegrityError, raise_error: bool = True):
17
+ engine = session.get_bind()
18
+ if engine.dialect.name != "sqlite":
19
+ raise exc
20
+
21
+ with engine.connect() as conn:
22
+ rows = conn.execute(text("PRAGMA foreign_key_check")).fetchall()
23
+
24
+ if rows:
25
+ for r in rows:
26
+ logger.error(
27
+ "FK violation: table=%s rowid=%s references=%s fk_index=%s",
28
+ r[0], r[1], r[2], r[3]
29
+ )
30
+
31
+ if raise_error:
32
+ raise exc
@@ -0,0 +1,14 @@
1
+ from .loader_interface import LoaderInterface, PandasLoader, ParquetLoader
2
+ from .data_classes import LoaderContext, TableCastingStats
3
+ from .loading_helpers import infer_delim, infer_encoding, quick_load_pg
4
+
5
+ __all__ = [
6
+ "LoaderInterface",
7
+ "LoaderContext",
8
+ "PandasLoader",
9
+ "TableCastingStats",
10
+ "infer_delim",
11
+ "infer_encoding",
12
+ "quick_load_pg",
13
+ "ParquetLoader",
14
+ ]