py-auth-sqlalchemy 0.0.1__tar.gz → 0.0.2__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.2/PKG-INFO +22 -0
  2. py_auth_sqlalchemy-0.0.2/README.md +0 -0
  3. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.2}/pyproject.toml +3 -4
  4. py_auth_sqlalchemy-0.0.2/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.2/src/py_auth_sqlalchemy/_utils.py +9 -10
  6. py_auth_sqlalchemy-0.0.2/src/py_auth_sqlalchemy/core.py +350 -0
  7. py_auth_sqlalchemy-0.0.2/src/py_auth_sqlalchemy/utc_datetime.py +58 -0
  8. py_auth_sqlalchemy-0.0.2/src/py_auth_sqlalchemy.egg-info/PKG-INFO +22 -0
  9. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.2}/src/py_auth_sqlalchemy.egg-info/SOURCES.txt +2 -1
  10. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.2}/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.2}/LICENSE +0 -0
  17. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.2}/setup.cfg +0 -0
  18. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.2}/src/py_auth_sqlalchemy/py.typed +0 -0
  19. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.2}/src/py_auth_sqlalchemy.egg-info/dependency_links.txt +0 -0
  20. {py_auth_sqlalchemy-0.0.1 → py_auth_sqlalchemy-0.0.2}/src/py_auth_sqlalchemy.egg-info/top_level.txt +0 -0
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-auth-sqlalchemy
3
+ Version: 0.0.2
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
File without changes
@@ -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.2"
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.2"
@@ -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
@@ -0,0 +1,350 @@
1
+ """SQLAlchemy adapter implementation for py-auth database operations."""
2
+
3
+ from ._utils import handle_db_errors, validate_async_engine, validate_sqlalchemy_model
4
+
5
+ from typing import Any, Dict, Type, List
6
+ from sqlalchemy import delete, select, and_
7
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
8
+ from py_auth.exceptions import AdapterError, DuplicateEntryError
9
+
10
+ class SqlAlchemyAdapter:
11
+ """SQLAlchemy adapter implementation for py-auth database operations with async support."""
12
+
13
+ def __init__(
14
+ self,
15
+ engine: AsyncEngine,
16
+ session_model: Type[Any],
17
+ user_model: Type[Any] | None = None,
18
+ account_model: Type[Any] | None = None,
19
+ ):
20
+ self.session_model = validate_sqlalchemy_model(
21
+ model=session_model,
22
+ model_name="Session",
23
+ required_columns={
24
+ "id",
25
+ "session_token_hash",
26
+ "user_id",
27
+ "expires",
28
+ },
29
+ )
30
+
31
+ if user_model:
32
+ self.user_model = validate_sqlalchemy_model(
33
+ model=user_model,
34
+ model_name="User",
35
+ required_columns={
36
+ "id",
37
+ "email",
38
+ "name",
39
+ "image",
40
+ "password_hash"
41
+ }
42
+ )
43
+
44
+ if account_model:
45
+ self.account_model = validate_sqlalchemy_model(
46
+ model=account_model,
47
+ model_name="Account",
48
+ required_columns={
49
+ "id",
50
+ "user_id",
51
+ "type",
52
+ "provider",
53
+ "provider_account_id",
54
+ "access_token",
55
+ "refresh_token",
56
+ "expires_at",
57
+ "token_type",
58
+ "scope",
59
+ "id_token",
60
+ "session_state"
61
+ }
62
+ )
63
+
64
+
65
+ self.engine = validate_async_engine(engine=engine)
66
+ self.session_maker = async_sessionmaker(
67
+ bind=self.engine, class_=AsyncSession, expire_on_commit=False
68
+ )
69
+
70
+ def _row_to_dict(self, instance: Any) -> Dict[str, Any] | None:
71
+ """Convert ORM instance to a dictionary using its table column names."""
72
+ if instance is None:
73
+ return None
74
+ cols = [c.name for c in instance.__table__.columns]
75
+ return {name: getattr(instance, name) for name in cols}
76
+
77
+
78
+
79
+
80
+ async def create_user(self, user_data: dict) -> Dict[str, Any] | None:
81
+ if not self.user_model:
82
+ raise AdapterError(
83
+ "Attempted to call 'create_user', "
84
+ "but 'user_model' is not configured."
85
+ )
86
+
87
+ async with handle_db_errors(operation="create_user"):
88
+ async with self.session_maker() as session:
89
+ async with session.begin():
90
+ user = self.user_model(**user_data)
91
+ session.add(user)
92
+ return self._row_to_dict(user)
93
+
94
+ async def get_user(self, user_id: str) -> Dict[str, Any] | None:
95
+ if not self.user_model:
96
+ raise AdapterError(
97
+ "Attempted to call 'get_user', "
98
+ "but 'user_model' is not configured."
99
+ )
100
+
101
+ async with handle_db_errors(operation="get_user"):
102
+ async with self.session_maker() as session:
103
+ stmt = select(self.user_model).where(self.user_model.id == user_id)
104
+ result = await session.execute(stmt)
105
+ user = result.scalars().first()
106
+ return self._row_to_dict(user)
107
+
108
+ async def get_user_by_email(self, email: str) -> Dict[str, Any] | None:
109
+ if not self.user_model:
110
+ raise AdapterError(
111
+ "Attempted to call 'get_user_by_email', "
112
+ "but 'user_model' is not configured."
113
+ )
114
+
115
+ async with handle_db_errors(operation="get_user_by_email"):
116
+ async with self.session_maker() as session:
117
+ stmt = select(self.user_model).where(self.user_model.email == email)
118
+ result = await session.execute(stmt)
119
+ user = result.scalars().first()
120
+ return self._row_to_dict(user)
121
+
122
+ async def update_user(self, user_id: str, updates: Dict[str, Any]) -> Dict[str, Any] | None:
123
+ if not self.user_model:
124
+ raise AdapterError(
125
+ "Attempted to call 'update_user', "
126
+ "but 'user_model' is not configured."
127
+ )
128
+
129
+ async with handle_db_errors(operation="update_user"):
130
+ async with self.session_maker() as session:
131
+ async with session.begin():
132
+ result = await session.execute(
133
+ select(self.user_model)
134
+ .where(self.user_model.id == user_id)
135
+ .with_for_update()
136
+ )
137
+ user = result.scalars().first()
138
+
139
+ if user is None:
140
+ return None
141
+
142
+ for key, value in updates.items():
143
+ if hasattr(user, key) and key != "id":
144
+ setattr(user, key, value)
145
+
146
+ await session.refresh(user)
147
+ return self._row_to_dict(user)
148
+
149
+ async def delete_user(self, user_id: str) -> None:
150
+ if not self.user_model:
151
+ raise AdapterError(
152
+ "Attempted to call 'delete_user', "
153
+ "but 'user_model' is not configured."
154
+ )
155
+
156
+ async with handle_db_errors(operation="delete_user"):
157
+ async with self.session_maker() as session:
158
+ async with session.begin():
159
+ user = await session.get(self.user_model, user_id)
160
+ if user is None:
161
+ return
162
+ await session.delete(user)
163
+
164
+
165
+ #
166
+
167
+
168
+
169
+
170
+ async def list_accounts_for_user(self, user_id: str) -> List[(Dict[str, Any] | None)]:
171
+ if not self.account_model:
172
+ raise AdapterError(
173
+ "Attempted to call 'list_accounts_for_user', "
174
+ "but 'account_model' is not configured."
175
+ )
176
+ async with handle_db_errors(operation="list_accounts_for_user"):
177
+ async with self.session_maker() as session:
178
+ stmt = select(self.account_model).where(self.account_model.user_id == user_id)
179
+ result = await session.execute(stmt)
180
+ return [self._row_to_dict(a) for a in result.scalars().all()]
181
+
182
+ async def link_account(self, account_data: Dict[str, Any]) -> Dict[str, Any] | None:
183
+ if not self.account_model:
184
+ raise AdapterError(
185
+ "Attempted to call 'link_account', "
186
+ "but 'account_model' is not configured."
187
+ )
188
+
189
+ async with handle_db_errors(operation="link_account"):
190
+ async with self.session_maker() as session:
191
+ async with session.begin():
192
+ account = self.account_model(**account_data)
193
+ session.add(account)
194
+ await session.flush()
195
+ return self._row_to_dict(account)
196
+
197
+ async def unlink_account(self, provider: str, provider_account_id: str) -> None:
198
+ if not self.account_model:
199
+ raise AdapterError(
200
+ "Attempted to call 'unlink_account', "
201
+ "but 'account_model' is not configured."
202
+ )
203
+
204
+ async with self.session_maker() as session:
205
+ async with session.begin():
206
+ stmt = (
207
+ delete(self.account_model)
208
+ .where(
209
+ and_(
210
+ self.account_model.provider == provider,
211
+ self.account_model.provider_account_id == provider_account_id,
212
+ )
213
+ )
214
+ )
215
+ await session.execute(stmt)
216
+
217
+
218
+
219
+
220
+ async def create_session(self, session_data: Dict[str, Any]) -> Dict[str, Any] | None:
221
+ """Create and persist a new session record."""
222
+ async with handle_db_errors(operation="create_session"):
223
+ async with self.session_maker() as session:
224
+ async with session.begin():
225
+ s = self.session_model(**session_data)
226
+ session.add(s)
227
+ await session.flush()
228
+ return self._row_to_dict(s)
229
+
230
+ async def list_sessions_for_user(self, user_id: str) -> List[(Dict[str, Any] | None)]:
231
+ async with handle_db_errors(operation="list_sessions_for_user"):
232
+ async with self.session_maker() as session:
233
+ stmt = select(self.session_model).where(self.session_model.user_id == user_id)
234
+ result = await session.execute(stmt)
235
+ return [self._row_to_dict(s) for s in result.scalars().all()]
236
+
237
+ async def update_session(
238
+ self, session_id: str, updates: Dict[str, Any]
239
+ ) -> Dict[str, Any] | None:
240
+ async with handle_db_errors(operation="update_session"):
241
+ async with self.session_maker() as session:
242
+ async with session.begin():
243
+ result = await session.execute(
244
+ select(self.session_model)
245
+ .where(self.session_model.id == session_id)
246
+ .with_for_update()
247
+ )
248
+ _session = result.scalars().first()
249
+
250
+ if _session is None:
251
+ return None
252
+
253
+ for key, value in updates.items():
254
+ if hasattr(_session, key) and key != "id":
255
+ setattr(_session, key, value)
256
+
257
+ await session.refresh(_session)
258
+ return self._row_to_dict(_session)
259
+
260
+ async def get_session_by_session_token_hash(
261
+ self, session_token_hash: str
262
+ ) -> Dict[str, Any] | None:
263
+ """Retrieve a session record by its hashed session token."""
264
+ async with handle_db_errors(operation="get_session_by_session_token_hash"):
265
+ async with self.session_maker() as session:
266
+ stmt = select(self.session_model).where(
267
+ self.session_model.session_token_hash == session_token_hash
268
+ )
269
+ result = await session.execute(stmt)
270
+ s = result.scalar_one_or_none()
271
+ if not s:
272
+ return None
273
+ return self._row_to_dict(s)
274
+
275
+ async def delete_session_by_session_token_hash(
276
+ self, session_token_hash: str
277
+ ) -> None:
278
+ """Delete a session record by its hashed session token."""
279
+ async with handle_db_errors(operation="delete_session_by_session_token_hash"):
280
+ async with self.session_maker() as session:
281
+ async with session.begin():
282
+ stmt = delete(self.session_model).where(
283
+ self.session_model.session_token_hash == session_token_hash
284
+ )
285
+ await session.execute(stmt)
286
+
287
+ async def delete_session(self, session_id: Any) -> None:
288
+ """Delete a session record by its unique identifier."""
289
+ async with handle_db_errors(operation="delete_session_by_id"):
290
+ async with self.session_maker() as session:
291
+ async with session.begin():
292
+ stmt = delete(self.session_model).where(
293
+ self.session_model.id == session_id
294
+ )
295
+ await session.execute(stmt)
296
+
297
+
298
+
299
+
300
+ async def get_or_create_user_and_link_account(self, email: str, user_data: Dict[str, Any], account_data: Dict[str, Any]) -> Dict[str, Any] | None:
301
+ if not self.user_model:
302
+ raise AdapterError(
303
+ "Attempted to call 'get_or_create_user_and_link_account', "
304
+ "but 'user_model' is not configured."
305
+ )
306
+
307
+ if not self.account_model:
308
+ raise AdapterError(
309
+ "Attempted to call 'get_or_create_user_and_link_account', "
310
+ "but 'account_model' is not configured."
311
+ )
312
+
313
+ lookup_email = (user_data.get("email") or email).lower()
314
+ user_data = {**user_data, "email": lookup_email}
315
+
316
+ async with handle_db_errors(operation="get_or_create_user_and_link_account"):
317
+ async with self.session_maker() as session:
318
+ async with session.begin():
319
+ user_result = await session.execute(
320
+ select(self.user_model).where(self.user_model.email == lookup_email)
321
+ )
322
+ user = user_result.scalar_one_or_none()
323
+
324
+ if user is None:
325
+ user = self.user_model(**user_data)
326
+ session.add(user)
327
+ await session.flush()
328
+
329
+ provider = account_data["provider"]
330
+ provider_account_id = account_data["provider_account_id"]
331
+ account_result = await session.execute(select(self.account_model).where(
332
+ self.account_model.provider == provider,
333
+ self.account_model.provider_account_id == provider_account_id,
334
+ ))
335
+ account = account_result.scalar_one_or_none()
336
+
337
+ if account is None:
338
+ account = self.account_model(**account_data, user_id=user.id)
339
+ session.add(account)
340
+ await session.flush()
341
+
342
+ elif account.user_id != user.id:
343
+ raise DuplicateEntryError(
344
+ "This social account is already linked to another user profile."
345
+ )
346
+
347
+ user_dict = self._row_to_dict(user)
348
+ account_dict = self._row_to_dict(account)
349
+ return {"user": user_dict, "account": account_dict}
350
+
@@ -0,0 +1,58 @@
1
+ """SQLAlchemy datetime type for consistent UTC-aware datetime handling across database backends."""
2
+
3
+ from sqlalchemy import DateTime
4
+ from datetime import datetime, timezone
5
+ from sqlalchemy.dialects.mysql import DATETIME
6
+ from sqlalchemy.types import TypeDecorator
7
+
8
+ class UTCDateTime(TypeDecorator):
9
+ """SQLAlchemy datetime type that provides consistent UTC-aware datetimes across supported database backends."""
10
+
11
+ impl = DateTime
12
+ cache_ok = True
13
+
14
+ def load_dialect_impl(self, dialect):
15
+ if dialect.name == "mysql":
16
+ return dialect.type_descriptor(
17
+ DATETIME(fsp=6)
18
+ )
19
+
20
+ if dialect.name == "sqlite":
21
+ return dialect.type_descriptor(
22
+ DateTime()
23
+ )
24
+
25
+ return dialect.type_descriptor(
26
+ DateTime(timezone=True)
27
+ )
28
+
29
+ def process_bind_param(
30
+ self,
31
+ value: datetime | None,
32
+ dialect,
33
+ ):
34
+ if value is None:
35
+ return None
36
+
37
+ if value.tzinfo is None:
38
+ raise ValueError("UTCDateTime requires a timezone-aware datetime")
39
+
40
+ value = value.astimezone(timezone.utc)
41
+
42
+ if dialect.name in ("mysql", "sqlite"):
43
+ return value.replace(tzinfo=None)
44
+
45
+ return value
46
+
47
+ def process_result_value(
48
+ self,
49
+ value: datetime | None,
50
+ dialect,
51
+ ):
52
+ if value is None:
53
+ return None
54
+
55
+ if value.tzinfo is None:
56
+ return value.replace(tzinfo=timezone.utc)
57
+
58
+ return value.astimezone(timezone.utc)
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-auth-sqlalchemy
3
+ Version: 0.0.2
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
@@ -2,9 +2,10 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/py_auth_sqlalchemy/__init__.py
5
+ src/py_auth_sqlalchemy/_utils.py
5
6
  src/py_auth_sqlalchemy/core.py
6
7
  src/py_auth_sqlalchemy/py.typed
7
- src/py_auth_sqlalchemy/utils.py
8
+ src/py_auth_sqlalchemy/utc_datetime.py
8
9
  src/py_auth_sqlalchemy.egg-info/PKG-INFO
9
10
  src/py_auth_sqlalchemy.egg-info/SOURCES.txt
10
11
  src/py_auth_sqlalchemy.egg-info/dependency_links.txt
@@ -1,3 +1,3 @@
1
1
  sqlalchemy>=2.0.0
2
2
  greenlet>=3.0.0
3
- py-auth-core>=0.0.1
3
+ py-auth-core>=0.0.3