py-auth-sqlalchemy 0.0.1__tar.gz → 0.0.3__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.
- py_auth_sqlalchemy-0.0.3/PKG-INFO +141 -0
- py_auth_sqlalchemy-0.0.3/README.md +118 -0
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/pyproject.toml +3 -4
- py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy/__init__.py +8 -0
- py_auth_sqlalchemy-0.0.1/src/py_auth_sqlalchemy/utils.py → py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy/_utils.py +9 -10
- py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy/core.py +350 -0
- py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy/utc_datetime.py +58 -0
- py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy.egg-info/PKG-INFO +141 -0
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy.egg-info/SOURCES.txt +2 -1
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy.egg-info/requires.txt +1 -1
- py_auth_sqlalchemy-0.0.1/PKG-INFO +0 -330
- py_auth_sqlalchemy-0.0.1/README.md +0 -306
- py_auth_sqlalchemy-0.0.1/src/py_auth_sqlalchemy/__init__.py +0 -10
- py_auth_sqlalchemy-0.0.1/src/py_auth_sqlalchemy/core.py +0 -111
- py_auth_sqlalchemy-0.0.1/src/py_auth_sqlalchemy.egg-info/PKG-INFO +0 -330
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/LICENSE +0 -0
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/setup.cfg +0 -0
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy/py.typed +0 -0
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy.egg-info/dependency_links.txt +0 -0
- {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-auth-sqlalchemy
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: High-performance, async SQLAlchemy adapter for py-auth-core.
|
|
5
|
+
Author-email: Olatunji Jamaldeen Omotoyosi <jamaldeen.o@yahoo.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jamaldeen09/py-auth
|
|
8
|
+
Project-URL: Repository, https://github.com/jamaldeen09/py-auth
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
20
|
+
Requires-Dist: greenlet>=3.0.0
|
|
21
|
+
Requires-Dist: py-auth-core>=0.0.3
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# py-auth-sqlalchemy
|
|
25
|
+
|
|
26
|
+
Async SQLAlchemy adapter for [`py-auth-core`](https://pypi.org/project/py-auth-core/).
|
|
27
|
+
|
|
28
|
+
You bring your own SQLAlchemy models and an `AsyncEngine`. The adapter implements the py-auth storage contract: users, linked accounts, and sessions. Integrity errors are mapped to `py-auth-core` exceptions (`DuplicateEntryError`, `ForeignKeyViolationError`, and so on).
|
|
29
|
+
|
|
30
|
+
Requires Python 3.10+ and SQLAlchemy 2.0+.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install py-auth-sqlalchemy
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Install an async driver as well:
|
|
39
|
+
|
|
40
|
+
| Database | Driver | Example URL |
|
|
41
|
+
| ---------- | ---------- | ----------- |
|
|
42
|
+
| PostgreSQL | `asyncpg` | `postgresql+asyncpg://...` |
|
|
43
|
+
| MySQL | `aiomysql` | `mysql+aiomysql://...` |
|
|
44
|
+
| SQLite | `aiosqlite`| `sqlite+aiosqlite:///...` |
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install asyncpg # or aiomysql, or aiosqlite
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A sync engine is rejected. Only those three drivers are accepted.
|
|
51
|
+
|
|
52
|
+
## Models
|
|
53
|
+
|
|
54
|
+
Pass your own SQLAlchemy declarative classes. On construction, the adapter checks that each provided model **defines the columns py-auth reads and writes internally**. That check is about **whether the column exists**, not about database constraints.
|
|
55
|
+
|
|
56
|
+
`nullable`, `unique`, `ForeignKey`, defaults, indexes, and extra columns are yours. You can make `email` unique or not, `name` nullable or not, and so on. The adapter does not inspect those.
|
|
57
|
+
|
|
58
|
+
If a required column is missing (for example a `Session` model with no `session_token_hash`), `validate_sqlalchemy_model` raises `AdapterError` immediately. py-auth looks those attributes up by name; without them, later operations would fail unpredictably.
|
|
59
|
+
|
|
60
|
+
**Session** is always required. **User** and **Account** are optional on the adapter; omit them if you do not need user or OAuth-account operations. Calling those methods without the matching model also raises `AdapterError`. Extra columns beyond the lists below are allowed.
|
|
61
|
+
|
|
62
|
+
| Model | When you pass it | Required column *names* |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| Session | Always | `id`, `session_token_hash`, `user_id`, `expires` |
|
|
65
|
+
| User | User CRUD / credentials | `id`, `email`, `name`, `image`, `password_hash` |
|
|
66
|
+
| Account | OAuth account linking | `id`, `user_id`, `type`, `provider`, `provider_account_id`, `access_token`, `refresh_token`, `expires_at`, `token_type`, `scope`, `id_token`, `session_state` |
|
|
67
|
+
|
|
68
|
+
Use `UTCDateTime` for `expires` so values stay timezone-aware UTC across backends. Naive datetimes are rejected. Constraints in the example below (`unique=True`, `nullable=...`) are typical choices, not adapter rules.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from uuid import uuid4
|
|
72
|
+
from datetime import datetime
|
|
73
|
+
from sqlalchemy import ForeignKey, String, UniqueConstraint
|
|
74
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
75
|
+
from py_auth_sqlalchemy import UTCDateTime
|
|
76
|
+
|
|
77
|
+
class Base(DeclarativeBase):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
class User(Base):
|
|
81
|
+
__tablename__ = "users"
|
|
82
|
+
|
|
83
|
+
id: Mapped[str] = mapped_column(String(255), primary_key=True, default=lambda: str(uuid4()))
|
|
84
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
|
85
|
+
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
86
|
+
image: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
87
|
+
password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
88
|
+
|
|
89
|
+
class Session(Base):
|
|
90
|
+
__tablename__ = "sessions"
|
|
91
|
+
|
|
92
|
+
id: Mapped[str] = mapped_column(String(255), primary_key=True, default=lambda: str(uuid4()))
|
|
93
|
+
session_token_hash: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
|
|
94
|
+
user_id: Mapped[str] = mapped_column(String(255), ForeignKey("users.id"), nullable=False)
|
|
95
|
+
expires: Mapped[datetime] = mapped_column(UTCDateTime(), nullable=False)
|
|
96
|
+
|
|
97
|
+
class Account(Base):
|
|
98
|
+
__tablename__ = "accounts"
|
|
99
|
+
|
|
100
|
+
id: Mapped[str] = mapped_column(String(255), primary_key=True, default=lambda: str(uuid4()))
|
|
101
|
+
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False)
|
|
102
|
+
type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
103
|
+
provider: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
104
|
+
provider_account_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
105
|
+
access_token: Mapped[str | None] = mapped_column(nullable=True)
|
|
106
|
+
refresh_token: Mapped[str | None] = mapped_column(nullable=True)
|
|
107
|
+
expires_at: Mapped[int | None] = mapped_column(nullable=True)
|
|
108
|
+
token_type: Mapped[str | None] = mapped_column(nullable=True)
|
|
109
|
+
scope: Mapped[str | None] = mapped_column(nullable=True)
|
|
110
|
+
id_token: Mapped[str | None] = mapped_column(nullable=True)
|
|
111
|
+
session_state: Mapped[str | None] = mapped_column(nullable=True)
|
|
112
|
+
|
|
113
|
+
__table_args__ = (
|
|
114
|
+
UniqueConstraint("provider", "provider_account_id", name="uq_accounts_provider_account"),
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Usage
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
122
|
+
from py_auth import PyAuth
|
|
123
|
+
from py_auth_sqlalchemy import SqlAlchemyAdapter
|
|
124
|
+
|
|
125
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
|
|
126
|
+
|
|
127
|
+
adapter = SqlAlchemyAdapter(
|
|
128
|
+
engine=engine,
|
|
129
|
+
session_model=Session,
|
|
130
|
+
user_model=User,
|
|
131
|
+
account_model=Account,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
auth = PyAuth(adapter=adapter, providers=[...])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Rows come back as dicts keyed by column name. Create the tables yourself (`Base.metadata.create_all` or Alembic); this package does not run migrations.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# py-auth-sqlalchemy
|
|
2
|
+
|
|
3
|
+
Async SQLAlchemy adapter for [`py-auth-core`](https://pypi.org/project/py-auth-core/).
|
|
4
|
+
|
|
5
|
+
You bring your own SQLAlchemy models and an `AsyncEngine`. The adapter implements the py-auth storage contract: users, linked accounts, and sessions. Integrity errors are mapped to `py-auth-core` exceptions (`DuplicateEntryError`, `ForeignKeyViolationError`, and so on).
|
|
6
|
+
|
|
7
|
+
Requires Python 3.10+ and SQLAlchemy 2.0+.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install py-auth-sqlalchemy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install an async driver as well:
|
|
16
|
+
|
|
17
|
+
| Database | Driver | Example URL |
|
|
18
|
+
| ---------- | ---------- | ----------- |
|
|
19
|
+
| PostgreSQL | `asyncpg` | `postgresql+asyncpg://...` |
|
|
20
|
+
| MySQL | `aiomysql` | `mysql+aiomysql://...` |
|
|
21
|
+
| SQLite | `aiosqlite`| `sqlite+aiosqlite:///...` |
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install asyncpg # or aiomysql, or aiosqlite
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A sync engine is rejected. Only those three drivers are accepted.
|
|
28
|
+
|
|
29
|
+
## Models
|
|
30
|
+
|
|
31
|
+
Pass your own SQLAlchemy declarative classes. On construction, the adapter checks that each provided model **defines the columns py-auth reads and writes internally**. That check is about **whether the column exists**, not about database constraints.
|
|
32
|
+
|
|
33
|
+
`nullable`, `unique`, `ForeignKey`, defaults, indexes, and extra columns are yours. You can make `email` unique or not, `name` nullable or not, and so on. The adapter does not inspect those.
|
|
34
|
+
|
|
35
|
+
If a required column is missing (for example a `Session` model with no `session_token_hash`), `validate_sqlalchemy_model` raises `AdapterError` immediately. py-auth looks those attributes up by name; without them, later operations would fail unpredictably.
|
|
36
|
+
|
|
37
|
+
**Session** is always required. **User** and **Account** are optional on the adapter; omit them if you do not need user or OAuth-account operations. Calling those methods without the matching model also raises `AdapterError`. Extra columns beyond the lists below are allowed.
|
|
38
|
+
|
|
39
|
+
| Model | When you pass it | Required column *names* |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| Session | Always | `id`, `session_token_hash`, `user_id`, `expires` |
|
|
42
|
+
| User | User CRUD / credentials | `id`, `email`, `name`, `image`, `password_hash` |
|
|
43
|
+
| Account | OAuth account linking | `id`, `user_id`, `type`, `provider`, `provider_account_id`, `access_token`, `refresh_token`, `expires_at`, `token_type`, `scope`, `id_token`, `session_state` |
|
|
44
|
+
|
|
45
|
+
Use `UTCDateTime` for `expires` so values stay timezone-aware UTC across backends. Naive datetimes are rejected. Constraints in the example below (`unique=True`, `nullable=...`) are typical choices, not adapter rules.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from uuid import uuid4
|
|
49
|
+
from datetime import datetime
|
|
50
|
+
from sqlalchemy import ForeignKey, String, UniqueConstraint
|
|
51
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
52
|
+
from py_auth_sqlalchemy import UTCDateTime
|
|
53
|
+
|
|
54
|
+
class Base(DeclarativeBase):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
class User(Base):
|
|
58
|
+
__tablename__ = "users"
|
|
59
|
+
|
|
60
|
+
id: Mapped[str] = mapped_column(String(255), primary_key=True, default=lambda: str(uuid4()))
|
|
61
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
|
62
|
+
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
63
|
+
image: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
64
|
+
password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
65
|
+
|
|
66
|
+
class Session(Base):
|
|
67
|
+
__tablename__ = "sessions"
|
|
68
|
+
|
|
69
|
+
id: Mapped[str] = mapped_column(String(255), primary_key=True, default=lambda: str(uuid4()))
|
|
70
|
+
session_token_hash: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
|
|
71
|
+
user_id: Mapped[str] = mapped_column(String(255), ForeignKey("users.id"), nullable=False)
|
|
72
|
+
expires: Mapped[datetime] = mapped_column(UTCDateTime(), nullable=False)
|
|
73
|
+
|
|
74
|
+
class Account(Base):
|
|
75
|
+
__tablename__ = "accounts"
|
|
76
|
+
|
|
77
|
+
id: Mapped[str] = mapped_column(String(255), primary_key=True, default=lambda: str(uuid4()))
|
|
78
|
+
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False)
|
|
79
|
+
type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
80
|
+
provider: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
81
|
+
provider_account_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
82
|
+
access_token: Mapped[str | None] = mapped_column(nullable=True)
|
|
83
|
+
refresh_token: Mapped[str | None] = mapped_column(nullable=True)
|
|
84
|
+
expires_at: Mapped[int | None] = mapped_column(nullable=True)
|
|
85
|
+
token_type: Mapped[str | None] = mapped_column(nullable=True)
|
|
86
|
+
scope: Mapped[str | None] = mapped_column(nullable=True)
|
|
87
|
+
id_token: Mapped[str | None] = mapped_column(nullable=True)
|
|
88
|
+
session_state: Mapped[str | None] = mapped_column(nullable=True)
|
|
89
|
+
|
|
90
|
+
__table_args__ = (
|
|
91
|
+
UniqueConstraint("provider", "provider_account_id", name="uq_accounts_provider_account"),
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
99
|
+
from py_auth import PyAuth
|
|
100
|
+
from py_auth_sqlalchemy import SqlAlchemyAdapter
|
|
101
|
+
|
|
102
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
|
|
103
|
+
|
|
104
|
+
adapter = SqlAlchemyAdapter(
|
|
105
|
+
engine=engine,
|
|
106
|
+
session_model=Session,
|
|
107
|
+
user_model=User,
|
|
108
|
+
account_model=Account,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
auth = PyAuth(adapter=adapter, providers=[...])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Rows come back as dicts keyed by column name. Create the tables yourself (`Base.metadata.create_all` or Alembic); this package does not run migrations.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -4,16 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "py-auth-sqlalchemy"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Olatunji Jamaldeen Omotoyosi", email = "jamaldeen.o@yahoo.com" },
|
|
10
10
|
]
|
|
11
11
|
description = "High-performance, async SQLAlchemy adapter for py-auth-core."
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
|
-
"Programming Language :: Python :: 3.9",
|
|
17
16
|
"Programming Language :: Python :: 3.10",
|
|
18
17
|
"Programming Language :: Python :: 3.11",
|
|
19
18
|
"Programming Language :: Python :: 3.12",
|
|
@@ -27,7 +26,7 @@ license-files = ["LICENSE"]
|
|
|
27
26
|
dependencies = [
|
|
28
27
|
"sqlalchemy>=2.0.0",
|
|
29
28
|
"greenlet>=3.0.0",
|
|
30
|
-
"py-auth-core>=0.0.
|
|
29
|
+
"py-auth-core>=0.0.3"
|
|
31
30
|
]
|
|
32
31
|
|
|
33
32
|
[project.urls]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Utility functions for engine validation, model validation, and database error handling."""
|
|
2
|
+
|
|
1
3
|
from contextlib import asynccontextmanager
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
@@ -12,7 +14,6 @@ from sqlalchemy import inspect
|
|
|
12
14
|
from sqlalchemy.exc import IntegrityError, NoResultFound
|
|
13
15
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
14
16
|
|
|
15
|
-
|
|
16
17
|
def validate_async_engine(engine: object) -> AsyncEngine:
|
|
17
18
|
"""Validate that the engine is an asynchronous SQLAlchemy AsyncEngine
|
|
18
19
|
and uses a supported asynchronous database driver.
|
|
@@ -23,7 +24,6 @@ def validate_async_engine(engine: object) -> AsyncEngine:
|
|
|
23
24
|
"'AsyncEngine' (created via create_async_engine)."
|
|
24
25
|
)
|
|
25
26
|
|
|
26
|
-
# Inspect the driver prefix from the engine's URL object
|
|
27
27
|
supported_drivers = ("asyncpg", "aiomysql", "aiosqlite")
|
|
28
28
|
driver = engine.url.get_driver_name()
|
|
29
29
|
|
|
@@ -37,7 +37,7 @@ def validate_async_engine(engine: object) -> AsyncEngine:
|
|
|
37
37
|
|
|
38
38
|
def validate_sqlalchemy_model(
|
|
39
39
|
model: type[Any],
|
|
40
|
-
|
|
40
|
+
required_columns: set[str],
|
|
41
41
|
model_name: str | None = None,
|
|
42
42
|
) -> type[Any]:
|
|
43
43
|
"""Validate that the provided class is a valid SQLAlchemy declarative model
|
|
@@ -51,20 +51,19 @@ def validate_sqlalchemy_model(
|
|
|
51
51
|
|
|
52
52
|
try:
|
|
53
53
|
mapper = inspect(model)
|
|
54
|
-
|
|
54
|
+
column_names = {c.key for c in mapper.columns}
|
|
55
55
|
except Exception as e:
|
|
56
56
|
raise AdapterError(
|
|
57
57
|
f"Provided '{display_name}' must be a valid SQLAlchemy model class: {e}"
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
if not
|
|
61
|
-
missing = sorted(
|
|
60
|
+
if not required_columns.issubset(column_names):
|
|
61
|
+
missing = sorted(required_columns - column_names)
|
|
62
62
|
raise AdapterError(
|
|
63
63
|
f"Custom {display_name} model is missing required py-auth columns: {missing}. "
|
|
64
64
|
"Extra custom columns are allowed, but these base columns are mandatory."
|
|
65
65
|
)
|
|
66
|
-
return model
|
|
67
|
-
|
|
66
|
+
return model
|
|
68
67
|
|
|
69
68
|
@asynccontextmanager
|
|
70
69
|
async def handle_db_errors(operation: str):
|
|
@@ -84,7 +83,7 @@ async def handle_db_errors(operation: str):
|
|
|
84
83
|
f"Failed to complete operation '{operation}': a record with this unique value already exists."
|
|
85
84
|
) from e
|
|
86
85
|
raise PyAuthError(
|
|
87
|
-
f"Database integrity error occurred in operation '{operation}'
|
|
86
|
+
f"Database integrity error occurred in operation '{operation}'"
|
|
88
87
|
) from e
|
|
89
88
|
|
|
90
89
|
except NoResultFound as e:
|
|
@@ -96,5 +95,5 @@ async def handle_db_errors(operation: str):
|
|
|
96
95
|
raise
|
|
97
96
|
except Exception as e:
|
|
98
97
|
raise PyAuthError(
|
|
99
|
-
f"An unexpected database error occurred in operation '{operation}'
|
|
98
|
+
f"An unexpected database error occurred in operation '{operation}'"
|
|
100
99
|
) from e
|