diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ddd7a5..37975e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Changelog +## 0.6.1 + +- First release on PyPI under the name `suou`. +- Fix `sqlalchemy.asyncio.SQLAlchemy()` to use context vars; `expire_on_commit=` is now configurable at instantiation. Fix some missing re-exports. + ## 0.6.0 -+ `.sqlalchemy` has been made a subpackage and split; `sqlalchemy_async` has been deprecated. Update your imports. ++ `.sqlalchemy` has been made a subpackage and split; `sqlalchemy_async` (moved to `sqlalchemy.asyncio`) has been deprecated. Update your imports. + Add several new utilities to `.sqlalchemy`: `BitSelector`, `secret_column`, `a_relationship`, `SessionWrapper`, `wrap=` argument to SQLAlchemy. Also removed dead batteries + Add `.waiter` module. For now, non-functional ~ diff --git a/aliases/sakuragasaki46-suou/pyproject.toml b/aliases/sakuragasaki46-suou/pyproject.toml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/aliases/sakuragasaki46-suou/pyproject.toml @@ -0,0 +1 @@ + diff --git a/pyproject.toml b/pyproject.toml index 2194046..984ae9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [project] -name = "sakuragasaki46_suou" +name = "suou" +description = "casual utility library for coding QoL" authors = [ { name = "Sakuragasaki46" } ] @@ -30,7 +31,7 @@ classifiers = [ ] [project.urls] -Repository = "https://github.com/sakuragasaki46/suou" +Repository = "https://nekode.yusur.moe/yusur/suou" [project.optional-dependencies] # the below are all dev dependencies (and probably already installed) @@ -62,13 +63,13 @@ sass = [ ] full = [ - "sakuragasaki46-suou[sqlalchemy]", - "sakuragasaki46-suou[flask]", - "sakuragasaki46-suou[quart]", - "sakuragasaki46-suou[peewee]", - "sakuragasaki46-suou[markdown]", - "sakuragasaki46-suou[flask-sqlalchemy]", - "sakuragasaki46-suou[sass]" + "suou[sqlalchemy]", + "suou[flask]", + "suou[quart]", + "suou[peewee]", + "suou[markdown]", + "suou[flask-sqlalchemy]", + "suou[sass]" ] diff --git a/src/suou/__init__.py b/src/suou/__init__.py index e47d233..3c0efd3 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -36,7 +36,7 @@ from .validators import matches from .redact import redact_url_password from .http import WantsContentType -__version__ = "0.6.0" +__version__ = "0.6.1" __all__ = ( 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', diff --git a/src/suou/sqlalchemy/__init__.py b/src/suou/sqlalchemy/__init__.py index f691d8c..63216b6 100644 --- a/src/suou/sqlalchemy/__init__.py +++ b/src/suou/sqlalchemy/__init__.py @@ -158,7 +158,7 @@ def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str | from .asyncio import SQLAlchemy, AsyncSelectPagination, async_query from .orm import ( - id_column, snowflake_column, match_column, match_constraint, bool_column, declarative_base, + id_column, snowflake_column, match_column, match_constraint, bool_column, declarative_base, parent_children, author_pair, age_pair, bound_fk, unbound_fk, want_column, a_relationship, BitSelector, secret_column ) diff --git a/src/suou/sqlalchemy/asyncio.py b/src/suou/sqlalchemy/asyncio.py index e513652..02a8949 100644 --- a/src/suou/sqlalchemy/asyncio.py +++ b/src/suou/sqlalchemy/asyncio.py @@ -20,12 +20,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. from __future__ import annotations from functools import wraps - -from sqlalchemy import Engine, Select, Table, func, select +from contextvars import ContextVar, Token +from sqlalchemy import Select, Table, func, select from sqlalchemy.orm import DeclarativeBase, lazyload from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from flask_sqlalchemy.pagination import Pagination +from suou.classtools import MISSING from suou.exceptions import NotFoundError class SQLAlchemy: @@ -45,36 +46,44 @@ class SQLAlchemy: NEW 0.5.0 UPDATED 0.6.0: added wrap=True + + UPDATED 0.6.1: expire_on_commit is now configurable per-SQLAlchemy(); + now sessions are stored as context variables """ base: DeclarativeBase engine: AsyncEngine - _sessions: list[AsyncSession] + _session_tok: list[Token[AsyncSession]] _wrapsessions: bool + _xocommit: bool NotFound = NotFoundError - def __init__(self, model_class: DeclarativeBase, *, wrap = False): + def __init__(self, model_class: DeclarativeBase, *, expire_on_commit = False, wrap = False): self.base = model_class self.engine = None self._wrapsessions = wrap - self._sessions = [] + self._xocommit = expire_on_commit def bind(self, url: str): self.engine = create_async_engine(url) def _ensure_engine(self): if self.engine is None: raise RuntimeError('database is not connected') - async def begin(self, *, expire_on_commit = False, wrap = False, **kw) -> AsyncSession: + async def begin(self, *, expire_on_commit = None, wrap = False, **kw) -> AsyncSession: self._ensure_engine() ## XXX is it accurate? - s = AsyncSession(self.engine, expire_on_commit=expire_on_commit, **kw) + s = AsyncSession(self.engine, + expire_on_commit=expire_on_commit if expire_on_commit is not None else self._xocommit, + **kw) if wrap: s = SessionWrapper(s) - self._sessions.append(s) + current_session.set(s) return s async def __aenter__(self) -> AsyncSession: return await self.begin() async def __aexit__(self, e1, e2, e3): ## XXX is it accurate? - s = self._sessions.pop() + s = current_session.get() + if not s: + raise RuntimeError('session not closed') if e1: await s.rollback() else: @@ -104,7 +113,8 @@ class SQLAlchemy: self.engine, checkfirst=checkfirst ) - +# XXX NOT public API! DO NOT USE +current_session: ContextVar[AsyncSession] = ContextVar('current_session') class AsyncSelectPagination(Pagination): """