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.
Files changed (20) hide show
  1. py_auth_sqlalchemy-0.0.3/PKG-INFO +141 -0
  2. py_auth_sqlalchemy-0.0.3/README.md +118 -0
  3. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/pyproject.toml +3 -4
  4. py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy/__init__.py +8 -0
  5. 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
  6. py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy/core.py +350 -0
  7. py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy/utc_datetime.py +58 -0
  8. py_auth_sqlalchemy-0.0.3/src/py_auth_sqlalchemy.egg-info/PKG-INFO +141 -0
  9. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy.egg-info/SOURCES.txt +2 -1
  10. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy.egg-info/requires.txt +1 -1
  11. py_auth_sqlalchemy-0.0.1/PKG-INFO +0 -330
  12. py_auth_sqlalchemy-0.0.1/README.md +0 -306
  13. py_auth_sqlalchemy-0.0.1/src/py_auth_sqlalchemy/__init__.py +0 -10
  14. py_auth_sqlalchemy-0.0.1/src/py_auth_sqlalchemy/core.py +0 -111
  15. py_auth_sqlalchemy-0.0.1/src/py_auth_sqlalchemy.egg-info/PKG-INFO +0 -330
  16. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/LICENSE +0 -0
  17. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/setup.cfg +0 -0
  18. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy/py.typed +0 -0
  19. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.3}/src/py_auth_sqlalchemy.egg-info/dependency_links.txt +0 -0
  20. {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.1"
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.9"
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.1",
29
+ "py-auth-core>=0.0.3"
31
30
  ]
32
31
 
33
32
  [project.urls]
@@ -0,0 +1,8 @@
1
+ """SQLAlchemy adapter for py-auth database persistence with async support."""
2
+
3
+
4
+ from .core import SqlAlchemyAdapter
5
+ from .utc_datetime import UTCDateTime
6
+
7
+ __all__ = ["SqlAlchemyAdapter", "UTCDateTime"]
8
+ __version__ = "0.0.3"
@@ -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
- required_cols: set[str],
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
- col_names = {c.key for c in mapper.columns}
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 required_cols.issubset(col_names):
61
- missing = sorted(required_cols - col_names)
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}': {e}"
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}': {e}"
98
+ f"An unexpected database error occurred in operation '{operation}'"
100
99
  ) from e