py-auth-sqlalchemy 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,10 @@
1
+ """
2
+ py-auth-sqlalchemy: High-performance, async SQLAlchemy adapter for py-auth.
3
+ """
4
+
5
+
6
+ from .core import SqlAlchemyAdapter
7
+
8
+
9
+ __all__ = ["SqlAlchemyAdapter"]
10
+ __version__ = "0.0.1"
@@ -0,0 +1,111 @@
1
+ from .utils import handle_db_errors, validate_async_engine, validate_sqlalchemy_model
2
+
3
+ from typing import Any, Dict, Optional, Type
4
+ from sqlalchemy import delete, select
5
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
6
+
7
+ class SqlAlchemyAdapter:
8
+ """SQLAlchemy ORM adapter for py-auth.
9
+
10
+ Provides asynchronous session persistence using SQLAlchemy 2.0.
11
+ User lookup and creation is handled entirely within the authorize()
12
+ callback of your CredentialsProvider.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ engine: AsyncEngine,
18
+ session_model: Optional[Type[Any]] = None,
19
+ ):
20
+ self.session_model = validate_sqlalchemy_model(
21
+ model=session_model,
22
+ model_name="Session",
23
+ required_cols={
24
+ "id",
25
+ "session_token_hash",
26
+ "user_id",
27
+ "expires",
28
+ "csrf_token",
29
+ },
30
+ )
31
+
32
+ self.engine = validate_async_engine(engine=engine)
33
+ self.session_maker = async_sessionmaker(
34
+ bind=self.engine, class_=AsyncSession, expire_on_commit=False
35
+ )
36
+
37
+ def _row_to_dict(self, instance: Any) -> Optional[Dict[str, Any]]:
38
+ """Convert ORM instance to a dictionary using its table column names."""
39
+ if instance is None:
40
+ return None
41
+ cols = [c.name for c in instance.__table__.columns]
42
+ return {name: getattr(instance, name) for name in cols}
43
+
44
+ async def create_session(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
45
+ """Create and persist a new session record."""
46
+ async with handle_db_errors(operation="create_session"):
47
+ async with self.session_maker() as session:
48
+ async with session.begin():
49
+ s = self.session_model(**session_data)
50
+ session.add(s)
51
+ await session.flush()
52
+ await session.refresh(s)
53
+ return self._row_to_dict(s)
54
+
55
+ async def update_session(
56
+ self, session_id: str, updates: Dict[str, Any]
57
+ ) -> Optional[Dict[str, Any]]:
58
+ async with handle_db_errors(operation="update_session"):
59
+ async with self.session_maker() as session:
60
+ async with session.begin():
61
+ stmt = (
62
+ select(self.session_model)
63
+ .where(self.session_model.id == session_id)
64
+ .with_for_update()
65
+ )
66
+ result = await session.execute(stmt)
67
+ s = result.scalars().first()
68
+ if s is None:
69
+ return None
70
+ for k, v in updates.items():
71
+ if hasattr(s, k) and k != "id":
72
+ setattr(s, k, v)
73
+ await session.refresh(s)
74
+ return self._row_to_dict(s)
75
+
76
+ async def get_session_by_session_token_hash(
77
+ self, session_token_hash: str
78
+ ) -> Optional[Dict[str, Any]]:
79
+ """Retrieve a session record by its hashed session token."""
80
+ async with handle_db_errors(operation="get_session_by_session_token_hash"):
81
+ async with self.session_maker() as session:
82
+ stmt = select(self.session_model).where(
83
+ self.session_model.session_token_hash == session_token_hash
84
+ )
85
+ result = await session.execute(stmt)
86
+ s = result.scalar_one_or_none()
87
+ if not s:
88
+ return None
89
+ return self._row_to_dict(s)
90
+
91
+ async def delete_session_by_session_token_hash(
92
+ self, session_token_hash: str
93
+ ) -> None:
94
+ """Delete a session record by its hashed session token."""
95
+ async with handle_db_errors(operation="delete_session_by_session_token_hash"):
96
+ async with self.session_maker() as session:
97
+ async with session.begin():
98
+ stmt = delete(self.session_model).where(
99
+ self.session_model.session_token_hash == session_token_hash
100
+ )
101
+ await session.execute(stmt)
102
+
103
+ async def delete_session(self, session_id: Any) -> None:
104
+ """Delete a session record by its unique identifier."""
105
+ async with handle_db_errors(operation="delete_session_by_id"):
106
+ async with self.session_maker() as session:
107
+ async with session.begin():
108
+ stmt = delete(self.session_model).where(
109
+ self.session_model.id == session_id
110
+ )
111
+ await session.execute(stmt)
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561.
@@ -0,0 +1,100 @@
1
+ from contextlib import asynccontextmanager
2
+ from typing import Any
3
+
4
+ from py_auth.exceptions import (
5
+ AdapterError,
6
+ DuplicateEntryError,
7
+ ForeignKeyViolationError,
8
+ PyAuthError,
9
+ RecordNotFoundError,
10
+ )
11
+ from sqlalchemy import inspect
12
+ from sqlalchemy.exc import IntegrityError, NoResultFound
13
+ from sqlalchemy.ext.asyncio import AsyncEngine
14
+
15
+
16
+ def validate_async_engine(engine: object) -> AsyncEngine:
17
+ """Validate that the engine is an asynchronous SQLAlchemy AsyncEngine
18
+ and uses a supported asynchronous database driver.
19
+ """
20
+ if not isinstance(engine, AsyncEngine):
21
+ raise TypeError(
22
+ "Invalid database engine. py-auth requires an asynchronous SQLAlchemy "
23
+ "'AsyncEngine' (created via create_async_engine)."
24
+ )
25
+
26
+ # Inspect the driver prefix from the engine's URL object
27
+ supported_drivers = ("asyncpg", "aiomysql", "aiosqlite")
28
+ driver = engine.url.get_driver_name()
29
+
30
+ if driver not in supported_drivers:
31
+ raise AdapterError(
32
+ f"Unsupported database driver '{driver}'. "
33
+ f"py-auth requires one of the following async drivers: {supported_drivers}"
34
+ )
35
+ return engine
36
+
37
+
38
+ def validate_sqlalchemy_model(
39
+ model: type[Any],
40
+ required_cols: set[str],
41
+ model_name: str | None = None,
42
+ ) -> type[Any]:
43
+ """Validate that the provided class is a valid SQLAlchemy declarative model
44
+ and defines all required py-auth columns.
45
+ """
46
+ if model is None:
47
+ name = model_name or "Model"
48
+ raise AdapterError(f"'{name}' model is required and cannot be None.")
49
+
50
+ display_name = getattr(model, "__name__", None) or model_name or "Model"
51
+
52
+ try:
53
+ mapper = inspect(model)
54
+ col_names = {c.key for c in mapper.columns}
55
+ except Exception as e:
56
+ raise AdapterError(
57
+ f"Provided '{display_name}' must be a valid SQLAlchemy model class: {e}"
58
+ )
59
+
60
+ if not required_cols.issubset(col_names):
61
+ missing = sorted(required_cols - col_names)
62
+ raise AdapterError(
63
+ f"Custom {display_name} model is missing required py-auth columns: {missing}. "
64
+ "Extra custom columns are allowed, but these base columns are mandatory."
65
+ )
66
+ return model
67
+
68
+
69
+ @asynccontextmanager
70
+ async def handle_db_errors(operation: str):
71
+ """Context manager for the SQLAlchemy adapter to intercept database-specific
72
+ exceptions and translate them into py-auth core exceptions.
73
+ """
74
+ try:
75
+ yield
76
+ except IntegrityError as e:
77
+ error_msg = str(e.orig or e).lower()
78
+ if "foreign key" in error_msg or "violates foreign key" in error_msg:
79
+ raise ForeignKeyViolationError(
80
+ f"Failed to complete operation '{operation}': the referenced record does not exist."
81
+ ) from e
82
+ elif "unique" in error_msg or "duplicate" in error_msg:
83
+ raise DuplicateEntryError(
84
+ f"Failed to complete operation '{operation}': a record with this unique value already exists."
85
+ ) from e
86
+ raise PyAuthError(
87
+ f"Database integrity error occurred in operation '{operation}': {e}"
88
+ ) from e
89
+
90
+ except NoResultFound as e:
91
+ raise RecordNotFoundError(
92
+ f"Failed to complete operation '{operation}': the requested record was not found."
93
+ ) from e
94
+
95
+ except PyAuthError:
96
+ raise
97
+ except Exception as e:
98
+ raise PyAuthError(
99
+ f"An unexpected database error occurred in operation '{operation}': {e}"
100
+ ) from e
@@ -0,0 +1,330 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-auth-sqlalchemy
3
+ Version: 0.0.1
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.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: sqlalchemy>=2.0.0
21
+ Requires-Dist: greenlet>=3.0.0
22
+ Requires-Dist: py-auth-core>=0.0.1
23
+ Dynamic: license-file
24
+
25
+ # py-auth-sqlalchemy
26
+
27
+ **High-performance, async SQLAlchemy 2.0 adapter for [py-auth-core](https://pypi.org/project/py-auth-core/).**
28
+
29
+ Plug this adapter into `PyAuth` to get automatic, async session persistence across PostgreSQL, MySQL, and SQLite — with built-in error translation from SQLAlchemy exceptions into `py-auth-core` exceptions.
30
+
31
+ [![PyPI version](https://img.shields.io/pypi/v/py-auth-sqlalchemy.svg)](https://pypi.org/project/py-auth-sqlalchemy/)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/py-auth-sqlalchemy.svg)](https://pypi.org/project/py-auth-sqlalchemy/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ ---
36
+
37
+ ## Table of Contents
38
+
39
+ - [Features](#features)
40
+ - [Installation](#installation)
41
+ - [Supported Databases](#supported-databases)
42
+ - [Quick Start](#quick-start)
43
+ - [Session Model Requirements](#session-model-requirements)
44
+ - [API Reference](#api-reference)
45
+ - [SqlAlchemyAdapter](#sqlalchemyadapter)
46
+ - [Error Translation](#error-translation)
47
+ - [Full FastAPI Example](#full-fastapi-example)
48
+ - [Security Notes](#security-notes)
49
+ - [License](#license)
50
+
51
+ ---
52
+
53
+ ## Features
54
+
55
+ - ✅ **Async-native** — built entirely on SQLAlchemy `AsyncEngine` + `AsyncSession`
56
+ - ✅ **BYO model** — bring your own declarative SQLAlchemy model; extra columns are fine
57
+ - ✅ **Strict validation at startup** — engine type and required model columns are checked immediately, before any request is served
58
+ - ✅ **Automatic error translation** — `IntegrityError` → `DuplicateEntryError` / `ForeignKeyViolationError`, `NoResultFound` → `RecordNotFoundError`
59
+ - ✅ **Session-only** — user lookup and creation live in your `authorize()` callback, giving you full control
60
+
61
+ ---
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install py-auth-sqlalchemy
67
+ ```
68
+
69
+ Then install the async driver for your database:
70
+
71
+ ```bash
72
+ # PostgreSQL
73
+ pip install asyncpg
74
+
75
+ # MySQL / MariaDB
76
+ pip install aiomysql
77
+
78
+ # SQLite
79
+ pip install aiosqlite
80
+ ```
81
+
82
+ **Requirements:** Python ≥ 3.9, SQLAlchemy ≥ 2.0, py-auth-core ≥ 0.0.1.
83
+
84
+ ---
85
+
86
+ ## Supported Databases
87
+
88
+ | Database | Async driver | Connection URL prefix |
89
+ |---|---|---|
90
+ | PostgreSQL | `asyncpg` | `postgresql+asyncpg://` |
91
+ | MySQL / MariaDB | `aiomysql` | `mysql+aiomysql://` |
92
+ | SQLite | `aiosqlite` | `sqlite+aiosqlite:///` |
93
+
94
+ The adapter validates the driver at startup and raises `AdapterError` for unsupported drivers.
95
+
96
+ ---
97
+
98
+ ## Quick Start
99
+
100
+ ```python
101
+ from sqlalchemy.ext.asyncio import create_async_engine
102
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
103
+ from sqlalchemy import DateTime, String
104
+ import uuid, datetime
105
+
106
+ from py_auth_sqlalchemy import SqlAlchemyAdapter
107
+ from py_auth import PyAuth, CredentialsProvider
108
+
109
+ # 1. Define your SQLAlchemy session model
110
+ class Base(DeclarativeBase):
111
+ pass
112
+
113
+ class Session(Base):
114
+ __tablename__ = "sessions"
115
+
116
+ # Required columns — do NOT rename these
117
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
118
+ session_token_hash: Mapped[str] = mapped_column(String, unique=True, nullable=False)
119
+ user_id: Mapped[str] = mapped_column(String, nullable=False)
120
+ csrf_token: Mapped[str] = mapped_column(String, nullable=False)
121
+ expires: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False)
122
+
123
+ # Any extra columns you want are fine
124
+ created_at: Mapped[datetime.datetime] = mapped_column(
125
+ DateTime, default=datetime.datetime.utcnow
126
+ )
127
+
128
+ # 2. Create the async engine
129
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
130
+
131
+ # 3. Create the adapter
132
+ adapter = SqlAlchemyAdapter(engine=engine, session_model=Session)
133
+
134
+ # 4. Wire up PyAuth
135
+ auth = PyAuth(adapter=adapter, providers=[credentials_provider])
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Session Model Requirements
141
+
142
+ Your SQLAlchemy session model **must** define the following columns with exactly these names:
143
+
144
+ | Column | Recommended type | Description |
145
+ |---|---|---|
146
+ | `id` | `String` / `UUID` (primary key) | Unique session identifier |
147
+ | `session_token_hash` | `String` (unique, non-null) | SHA-256 hash of the raw session token |
148
+ | `user_id` | `String` / `UUID` (non-null) | Reference to the owning user |
149
+ | `csrf_token` | `String` (non-null) | Raw CSRF token for double-submit validation |
150
+ | `expires` | `DateTime` (non-null) | Naive UTC datetime when the session expires |
151
+
152
+ > **Extra columns are allowed.** You can freely add `created_at`, `ip_address`, `user_agent`, or any other columns your application needs.
153
+
154
+ If any required column is missing, `SqlAlchemyAdapter.__init__` raises `AdapterError` with a descriptive message listing the missing columns.
155
+
156
+ ---
157
+
158
+ ## API Reference
159
+
160
+ ### `SqlAlchemyAdapter`
161
+
162
+ ```python
163
+ SqlAlchemyAdapter(
164
+ engine: AsyncEngine,
165
+ session_model: Type[Any],
166
+ )
167
+ ```
168
+
169
+ | Parameter | Type | Description |
170
+ |---|---|---|
171
+ | `engine` | `AsyncEngine` | An async SQLAlchemy engine created via `create_async_engine` |
172
+ | `session_model` | `Type[DeclarativeBase]` | Your SQLAlchemy declarative model class for sessions |
173
+
174
+ Both parameters are validated immediately in `__init__`:
175
+ - `engine` must be an `AsyncEngine` using a supported async driver (`asyncpg`, `aiomysql`, `aiosqlite`)
176
+ - `session_model` must be a valid SQLAlchemy declarative class with all required columns present
177
+
178
+ **Methods** (all async)
179
+
180
+ | Method | Signature | Description |
181
+ |---|---|---|
182
+ | `create_session` | `(session_data: dict) -> dict` | Inserts a new session row and returns it as a dict |
183
+ | `get_session_by_session_token_hash` | `(token_hash: str) -> dict \| None` | Fetches a session by hashed token, or `None` if not found |
184
+ | `update_session` | `(session_id: str, updates: dict) -> dict \| None` | Updates fields on an existing session row and returns the updated record as a dict, or `None` if not found |
185
+ | `delete_session_by_session_token_hash` | `(token_hash: str) -> None` | Deletes a session by hashed token |
186
+ | `delete_session` | `(session_id: Any) -> None` | Deletes a session by its primary key / ID |
187
+
188
+ ---
189
+
190
+ ### Error Translation
191
+
192
+ The adapter automatically maps SQLAlchemy exceptions to `py-auth-core` exceptions so `PyAuth` can handle them uniformly:
193
+
194
+ | SQLAlchemy exception | py-auth-core exception | Trigger |
195
+ |---|---|---|
196
+ | `IntegrityError` (unique / duplicate) | `DuplicateEntryError` (409) | Duplicate `session_token_hash` |
197
+ | `IntegrityError` (foreign key) | `ForeignKeyViolationError` (400) | `user_id` references a non-existent user |
198
+ | `NoResultFound` | `RecordNotFoundError` (404) | Query returned no rows |
199
+ | Any other `Exception` | `PyAuthError` (500) | Unexpected database error |
200
+
201
+ `PyAuth.signin_with_credentials` automatically retries on `DuplicateEntryError` (up to 3 times with a fresh token) without any extra code on your part.
202
+
203
+ ---
204
+
205
+ ## Full FastAPI Example
206
+
207
+ This example shows the full flow: user lookup, sign-in, and sign-up all handled inside a single `authorize()` callback — no separate signup endpoint needed.
208
+
209
+ ```python
210
+ import datetime, uuid
211
+ from fastapi import FastAPI, Request, Response
212
+ from pydantic import BaseModel, EmailStr
213
+ from passlib.context import CryptContext
214
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
215
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
216
+ from sqlalchemy import DateTime, String, select
217
+
218
+ from py_auth import PyAuth, CredentialsProvider
219
+ from py_auth_sqlalchemy import SqlAlchemyAdapter
220
+
221
+ pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
222
+
223
+ # --- SQLAlchemy models ---
224
+ class Base(DeclarativeBase):
225
+ pass
226
+
227
+ class User(Base):
228
+ __tablename__ = "users"
229
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
230
+ email: Mapped[str] = mapped_column(String, unique=True, nullable=False)
231
+ hashed_password: Mapped[str] = mapped_column(String, nullable=False)
232
+
233
+ class Session(Base):
234
+ __tablename__ = "sessions"
235
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
236
+ session_token_hash: Mapped[str] = mapped_column(String, unique=True, nullable=False)
237
+ user_id: Mapped[str] = mapped_column(String, nullable=False)
238
+ csrf_token: Mapped[str] = mapped_column(String, nullable=False)
239
+ expires: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False)
240
+
241
+ # --- Engine and adapter ---
242
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
243
+ adapter = SqlAlchemyAdapter(engine=engine, session_model=Session)
244
+
245
+ # --- Credentials schema ---
246
+ class LoginSchema(BaseModel):
247
+ email: EmailStr
248
+ password: str
249
+
250
+ # --- authorize() handles both sign-in and sign-up ---
251
+ async def authorize(credentials: dict) -> dict | None:
252
+ """
253
+ Check if the user exists:
254
+ - If yes, verify their password and return their details.
255
+ - If no, create them and return the new user's details.
256
+ - Return None to reject (wrong password).
257
+ """
258
+ async with AsyncSession(engine) as db:
259
+ result = await db.execute(select(User).where(User.email == credentials["email"]))
260
+ user = result.scalar_one_or_none()
261
+
262
+ if user:
263
+ # Existing user — verify credentials
264
+ if not pwd_ctx.verify(credentials["password"], user.hashed_password):
265
+ return None
266
+ return {"id": user.id, "email": user.email}
267
+
268
+ # New user — create and return
269
+ async with db.begin():
270
+ new_user = User(
271
+ email=credentials["email"],
272
+ hashed_password=pwd_ctx.hash(credentials["password"]),
273
+ )
274
+ db.add(new_user)
275
+ await db.flush()
276
+ await db.refresh(new_user)
277
+ return {"id": new_user.id, "email": new_user.email}
278
+
279
+ # --- Wire up PyAuth ---
280
+ auth = PyAuth(
281
+ adapter=adapter,
282
+ providers=[CredentialsProvider(model=LoginSchema, authorize=authorize)],
283
+ )
284
+
285
+ # --- FastAPI routes ---
286
+ app = FastAPI()
287
+
288
+ @app.on_event("startup")
289
+ async def startup():
290
+ async with engine.begin() as conn:
291
+ await conn.run_sync(Base.metadata.create_all)
292
+
293
+ @app.post("/auth/signin")
294
+ async def signin(request: Request, response: Response):
295
+ body = await request.json()
296
+ result = await auth.signin_with_credentials(body)
297
+ if result["error"]:
298
+ return result
299
+ data = result["data"]
300
+ response.set_cookie(auth.cookies["session_token"]["name"], data["session_token"],
301
+ **auth.cookies["session_token"]["options"])
302
+ response.set_cookie(auth.cookies["csrf_token"]["name"], data["csrf_token"],
303
+ **auth.cookies["csrf_token"]["options"])
304
+ return {"user": data["user"]}
305
+
306
+ @app.get("/auth/verify")
307
+ async def verify(request: Request):
308
+ session_token = request.cookies.get(auth.cookies["session_token"]["name"])
309
+ csrf_token = request.cookies.get(auth.cookies["csrf_token"]["name"])
310
+ return await auth.verify_session(session_token, csrf_token)
311
+
312
+ @app.post("/auth/signout")
313
+ async def signout(session_id: str):
314
+ return await auth.signout(session_id)
315
+ ```
316
+
317
+ ---
318
+
319
+ ## Security Notes
320
+
321
+ - **Only hashed session tokens are stored in the database.** The raw token is set in a `httpOnly` cookie and never persisted.
322
+ - **Always hash passwords** before persisting — use bcrypt, argon2, or scrypt. Never store plain text.
323
+ - Use `expire_on_commit=False` (set by the adapter automatically) so ORM instances remain accessible after `session.commit()`.
324
+ - For PostgreSQL, ensure your `Session` table has a **unique index** on `session_token_hash` at the database level to guarantee the uniqueness constraint that `DuplicateEntryError` depends on.
325
+
326
+ ---
327
+
328
+ ## License
329
+
330
+ MIT — see [LICENSE](./LICENSE) for details.
@@ -0,0 +1,9 @@
1
+ py_auth_sqlalchemy/__init__.py,sha256=donNShJB77B6KiID9kI9MTJhfBWpmcEHnyR9pxfIXEU,178
2
+ py_auth_sqlalchemy/core.py,sha256=7zvMoVlaVnEKDM8ykzaC6qO_wZBLadgUSNqNM9Hkmqo,4659
3
+ py_auth_sqlalchemy/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
4
+ py_auth_sqlalchemy/utils.py,sha256=t8jlfAjBL9D9AEcmn5NPunZF70wWsrIXsU39jINcaP0,3539
5
+ py_auth_sqlalchemy-0.0.1.dist-info/licenses/LICENSE,sha256=I-lV8EG8M1xQi7woapfebjyhp5CiY-QESJVVf4yur24,1078
6
+ py_auth_sqlalchemy-0.0.1.dist-info/METADATA,sha256=t05_29QdS0jcLyJFaW0wYI4SBYf38UOmwbQRDZHFlcs,12552
7
+ py_auth_sqlalchemy-0.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ py_auth_sqlalchemy-0.0.1.dist-info/top_level.txt,sha256=cnXB4AYSlGQBIjqIUuamg7amflJZa4ni6ygRN7Xll_Q,19
9
+ py_auth_sqlalchemy-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2026] [Olatunji Jamaldeen]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ py_auth_sqlalchemy