From 10e6c202f02e3e15e242a6d254dc3cdac2387da5 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sat, 18 Oct 2025 14:48:32 +0200 Subject: [PATCH] 0.7.3 fix imports (?) in .sqlalchemy, add experimental .glue, docs for .sqlalchemy --- CHANGELOG.md | 6 ++ aliases/sakuragasaki46_suou/pyproject.toml | 5 +- docs/generated/suou.sqlalchemy.orm.rst | 3 +- docs/generated/suou.waiter.rst | 9 +- docs/index.rst | 9 +- docs/sqlalchemy.rst | 45 ++++++++ src/suou/__init__.py | 2 +- src/suou/glue.py | 59 +++++++++++ src/suou/sqlalchemy/asyncio.py | 116 +++++++++++---------- 9 files changed, 184 insertions(+), 70 deletions(-) create mode 100644 docs/sqlalchemy.rst create mode 100644 src/suou/glue.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e68baf..5a9562f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.7.3 + ++ Fixed some broken imports in `.sqlalchemy` ++ Stage `@glue()` for release in 0.8.0 ++ Add docs to `.sqlalchemy` + ## 0.7.2 + `@future()` now can take a `version=` argument diff --git a/aliases/sakuragasaki46_suou/pyproject.toml b/aliases/sakuragasaki46_suou/pyproject.toml index 4e19614..52e3d6b 100644 --- a/aliases/sakuragasaki46_suou/pyproject.toml +++ b/aliases/sakuragasaki46_suou/pyproject.toml @@ -10,7 +10,7 @@ license = "Apache-2.0" readme = "README.md" dependencies = [ - "suou==0.7.1", + "suou==0.7.2", "itsdangerous", "toml", "pydantic", @@ -28,7 +28,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13" + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14" ] [project.urls] diff --git a/docs/generated/suou.sqlalchemy.orm.rst b/docs/generated/suou.sqlalchemy.orm.rst index 41cb2c0..cdd75b0 100644 --- a/docs/generated/suou.sqlalchemy.orm.rst +++ b/docs/generated/suou.sqlalchemy.orm.rst @@ -1,4 +1,4 @@ -suou.sqlalchemy.orm +suou.sqlalchemy.orm =================== .. automodule:: suou.sqlalchemy.orm @@ -22,6 +22,7 @@ suou.sqlalchemy.orm secret_column snowflake_column unbound_fk + username_column want_column .. rubric:: Classes diff --git a/docs/generated/suou.waiter.rst b/docs/generated/suou.waiter.rst index e420b75..e0270c5 100644 --- a/docs/generated/suou.waiter.rst +++ b/docs/generated/suou.waiter.rst @@ -1,4 +1,4 @@ -suou.waiter +suou.waiter =========== .. automodule:: suou.waiter @@ -8,12 +8,7 @@ suou.waiter .. autosummary:: + Waiter ko ok - - .. rubric:: Classes - - .. autosummary:: - - Waiter \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 3bc5bd4..b84454f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,16 +3,15 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -suou documentation +SUOU ================== -SUOU (acronym for ) is a casual Python library providing utilities to -ease programmer's QoL. - - +SUOU (acronym for **SIS Unified Object Underarmour**) is a casual Python library providing utilities to +ease programmer's QoL and write shorter and cleaner code that works. .. toctree:: :maxdepth: 2 + sqlalchemy api \ No newline at end of file diff --git a/docs/sqlalchemy.rst b/docs/sqlalchemy.rst new file mode 100644 index 0000000..7cea449 --- /dev/null +++ b/docs/sqlalchemy.rst @@ -0,0 +1,45 @@ + +sqlalchemy helpers +================== + +.. currentmodule:: suou.sqlalchemy + +SUOU provides several helpers to make sqlalchemy learning curve less steep. + +In fact, there are pre-made column presets for a specific purpose. + + +Columns +------- + +.. autofunction:: id_column + +.. warning:: + ``id_column()`` expects SIQ's! + +.. autofunction:: snowflake_column + +.. autofunction:: match_column + +.. autofunction:: secret_column + +.. autofunction:: bool_column + +.. autofunction:: unbound_fk +.. autofunction:: bound_fk + +Column pairs +------------ + +.. autofunction:: age_pair +.. autofunction:: author_pair +.. autofunction:: parent_children + +Misc +---- + +.. autofunction:: BitSelector +.. autofunction:: match_constraint +.. autofunction:: a_relationship +.. autofunction:: declarative_base +.. autofunction:: want_column \ No newline at end of file diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 60eff4b..96bc8aa 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -37,7 +37,7 @@ from .redact import redact_url_password from .http import WantsContentType from .color import chalk -__version__ = "0.7.2" +__version__ = "0.7.3" __all__ = ( 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', diff --git a/src/suou/glue.py b/src/suou/glue.py new file mode 100644 index 0000000..1d97318 --- /dev/null +++ b/src/suou/glue.py @@ -0,0 +1,59 @@ +""" +Helpers for "Glue" code, aka code meant to adapt or patch other libraries + +--- + +Copyright (c) 2025 Sakuragasaki46. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +See LICENSE for the specific language governing permissions and +limitations under the License. + +This software is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import importlib +from types import ModuleType + +from functools import wraps +from suou.functools import future + + +class FakeModule(ModuleType): + """ + Fake module used in @glue() in case of import error + """ + def __init__(self, name: str, exc: Exception): + super().__init__(name) + self._exc = exc + def __getattr__(self, name: str): + raise AttributeError(f'Module {self.__name__} not found; this feature is not available ({self._exc})') from self._exc + + +@future(version = "0.8.0") +def glue(*modules): + """ + Helper for "glue" code -- it imports the given modules and passes them as keyword arguments to the wrapped functions. + + NEW 0.8.0 + """ + module_dict = dict() + + for module in modules: + try: + module_dict[module] = importlib.import_module(module) + except Exception as e: + module_dict[module] = FakeModule(module, e) + + def decorator(func): + @wraps(func) + def wrapper(*a, **k): + k.update(module_dict) + return func(*a, **k) + return wrapper + return decorator + +# This module is experimental and therefore not re-exported into __init__ +__all__ = ('glue',) \ No newline at end of file diff --git a/src/suou/sqlalchemy/asyncio.py b/src/suou/sqlalchemy/asyncio.py index 331407b..605ec93 100644 --- a/src/suou/sqlalchemy/asyncio.py +++ b/src/suou/sqlalchemy/asyncio.py @@ -25,9 +25,9 @@ from typing import Callable, TypeVar 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.exceptions import NotFoundError +from suou.glue import glue _T = TypeVar('_T') _U = TypeVar('_U') @@ -119,68 +119,76 @@ class SQLAlchemy: # XXX NOT public API! DO NOT USE current_session: ContextVar[AsyncSession] = ContextVar('current_session') -class AsyncSelectPagination(Pagination): - """ - flask_sqlalchemy.SelectPagination but asynchronous. +## experimental +@glue('flask_sqlalchemy') +def _make_AsyncSelectPagination(flask_sqlalchemy): + class AsyncSelectPagination(flask_sqlalchemy.pagination.Pagination): + """ + flask_sqlalchemy.SelectPagination but asynchronous. - Pagination is not part of the public API, therefore expect that it may break - """ + Pagination is not part of the public API, therefore expect that it may break + """ - async def _query_items(self) -> list: - select_q: Select = self._query_args["select"] - select = select_q.limit(self.per_page).offset(self._query_offset) - session: AsyncSession = self._query_args["session"] - out = (await session.execute(select)).scalars() - return out + async def _query_items(self) -> list: + select_q: Select = self._query_args["select"] + select = select_q.limit(self.per_page).offset(self._query_offset) + session: AsyncSession = self._query_args["session"] + out = (await session.execute(select)).scalars() + return out - async def _query_count(self) -> int: - select_q: Select = self._query_args["select"] - sub = select_q.options(lazyload("*")).order_by(None).subquery() - session: AsyncSession = self._query_args["session"] - out = (await session.execute(select(func.count()).select_from(sub))).scalar() - return out + async def _query_count(self) -> int: + select_q: Select = self._query_args["select"] + sub = select_q.options(lazyload("*")).order_by(None).subquery() + session: AsyncSession = self._query_args["session"] + out = (await session.execute(select(func.count()).select_from(sub))).scalar() + return out - def __init__(self, - page: int | None = None, - per_page: int | None = None, - max_per_page: int | None = 100, - error_out: Exception | None = NotFoundError, - count: bool = True, - **kwargs): - ## XXX flask-sqlalchemy says Pagination() is not public API. - ## Things may break; beware. - self._query_args = kwargs - page, per_page = self._prepare_page_args( - page=page, - per_page=per_page, - max_per_page=max_per_page, - error_out=error_out, - ) + def __init__(self, + page: int | None = None, + per_page: int | None = None, + max_per_page: int | None = 100, + error_out: Exception | None = NotFoundError, + count: bool = True, + **kwargs): + ## XXX flask-sqlalchemy says Pagination() is not public API. + ## Things may break; beware. + self._query_args = kwargs + page, per_page = self._prepare_page_args( + page=page, + per_page=per_page, + max_per_page=max_per_page, + error_out=error_out, + ) - self.page: int = page - """The current page.""" + self.page: int = page + """The current page.""" - self.per_page: int = per_page - """The maximum number of items on a page.""" + self.per_page: int = per_page + """The maximum number of items on a page.""" - self.max_per_page: int | None = max_per_page - """The maximum allowed value for ``per_page``.""" + self.max_per_page: int | None = max_per_page + """The maximum allowed value for ``per_page``.""" - self.items = None - self.total = None - self.error_out = error_out - self.has_count = count + self.items = None + self.total = None + self.error_out = error_out + self.has_count = count - async def __aiter__(self): - self.items = await self._query_items() - if self.items is None: - raise RuntimeError('query returned None') - if not self.items and self.page != 1 and self.error_out: - raise self.error_out - if self.has_count: - self.total = await self._query_count() - for i in self.items: - yield i + async def __aiter__(self): + self.items = await self._query_items() + if self.items is None: + raise RuntimeError('query returned None') + if not self.items and self.page != 1 and self.error_out: + raise self.error_out + if self.has_count: + self.total = await self._query_count() + for i in self.items: + yield i + + return AsyncSelectPagination + +AsyncSelectPagination = _make_AsyncSelectPagination() +del _make_AsyncSelectPagination def async_query(db: SQLAlchemy, multi: False):