0.7.3 fix imports (?) in .sqlalchemy, add experimental .glue, docs for .sqlalchemy
This commit is contained in:
parent
7e80c84de6
commit
10e6c202f0
9 changed files with 184 additions and 70 deletions
|
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# 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
|
## 0.7.2
|
||||||
|
|
||||||
+ `@future()` now can take a `version=` argument
|
+ `@future()` now can take a `version=` argument
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ license = "Apache-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"suou==0.7.1",
|
"suou==0.7.2",
|
||||||
"itsdangerous",
|
"itsdangerous",
|
||||||
"toml",
|
"toml",
|
||||||
"pydantic",
|
"pydantic",
|
||||||
|
|
@ -28,7 +28,8 @@ classifiers = [
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13"
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
suou.sqlalchemy.orm
|
suou.sqlalchemy.orm
|
||||||
===================
|
===================
|
||||||
|
|
||||||
.. automodule:: suou.sqlalchemy.orm
|
.. automodule:: suou.sqlalchemy.orm
|
||||||
|
|
@ -22,6 +22,7 @@ suou.sqlalchemy.orm
|
||||||
secret_column
|
secret_column
|
||||||
snowflake_column
|
snowflake_column
|
||||||
unbound_fk
|
unbound_fk
|
||||||
|
username_column
|
||||||
want_column
|
want_column
|
||||||
|
|
||||||
.. rubric:: Classes
|
.. rubric:: Classes
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
suou.waiter
|
suou.waiter
|
||||||
===========
|
===========
|
||||||
|
|
||||||
.. automodule:: suou.waiter
|
.. automodule:: suou.waiter
|
||||||
|
|
@ -8,12 +8,7 @@ suou.waiter
|
||||||
|
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
|
|
||||||
|
Waiter
|
||||||
ko
|
ko
|
||||||
ok
|
ok
|
||||||
|
|
||||||
.. rubric:: Classes
|
|
||||||
|
|
||||||
.. autosummary::
|
|
||||||
|
|
||||||
Waiter
|
|
||||||
|
|
||||||
|
|
@ -3,16 +3,15 @@
|
||||||
You can adapt this file completely to your liking, but it should at least
|
You can adapt this file completely to your liking, but it should at least
|
||||||
contain the root `toctree` directive.
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
suou documentation
|
SUOU
|
||||||
==================
|
==================
|
||||||
|
|
||||||
SUOU (acronym for ) is a casual Python library providing utilities to
|
SUOU (acronym for **SIS Unified Object Underarmour**) is a casual Python library providing utilities to
|
||||||
ease programmer's QoL.
|
ease programmer's QoL and write shorter and cleaner code that works.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
sqlalchemy
|
||||||
api
|
api
|
||||||
45
docs/sqlalchemy.rst
Normal file
45
docs/sqlalchemy.rst
Normal file
|
|
@ -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
|
||||||
|
|
@ -37,7 +37,7 @@ from .redact import redact_url_password
|
||||||
from .http import WantsContentType
|
from .http import WantsContentType
|
||||||
from .color import chalk
|
from .color import chalk
|
||||||
|
|
||||||
__version__ = "0.7.2"
|
__version__ = "0.7.3"
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
|
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
|
||||||
|
|
|
||||||
59
src/suou/glue.py
Normal file
59
src/suou/glue.py
Normal file
|
|
@ -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',)
|
||||||
|
|
@ -25,9 +25,9 @@ from typing import Callable, TypeVar
|
||||||
from sqlalchemy import Select, Table, func, select
|
from sqlalchemy import Select, Table, func, select
|
||||||
from sqlalchemy.orm import DeclarativeBase, lazyload
|
from sqlalchemy.orm import DeclarativeBase, lazyload
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
||||||
from flask_sqlalchemy.pagination import Pagination
|
|
||||||
|
|
||||||
from suou.exceptions import NotFoundError
|
from suou.exceptions import NotFoundError
|
||||||
|
from suou.glue import glue
|
||||||
|
|
||||||
_T = TypeVar('_T')
|
_T = TypeVar('_T')
|
||||||
_U = TypeVar('_U')
|
_U = TypeVar('_U')
|
||||||
|
|
@ -119,68 +119,76 @@ class SQLAlchemy:
|
||||||
# XXX NOT public API! DO NOT USE
|
# XXX NOT public API! DO NOT USE
|
||||||
current_session: ContextVar[AsyncSession] = ContextVar('current_session')
|
current_session: ContextVar[AsyncSession] = ContextVar('current_session')
|
||||||
|
|
||||||
class AsyncSelectPagination(Pagination):
|
## experimental
|
||||||
"""
|
@glue('flask_sqlalchemy')
|
||||||
flask_sqlalchemy.SelectPagination but asynchronous.
|
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:
|
async def _query_items(self) -> list:
|
||||||
select_q: Select = self._query_args["select"]
|
select_q: Select = self._query_args["select"]
|
||||||
select = select_q.limit(self.per_page).offset(self._query_offset)
|
select = select_q.limit(self.per_page).offset(self._query_offset)
|
||||||
session: AsyncSession = self._query_args["session"]
|
session: AsyncSession = self._query_args["session"]
|
||||||
out = (await session.execute(select)).scalars()
|
out = (await session.execute(select)).scalars()
|
||||||
return out
|
return out
|
||||||
|
|
||||||
async def _query_count(self) -> int:
|
async def _query_count(self) -> int:
|
||||||
select_q: Select = self._query_args["select"]
|
select_q: Select = self._query_args["select"]
|
||||||
sub = select_q.options(lazyload("*")).order_by(None).subquery()
|
sub = select_q.options(lazyload("*")).order_by(None).subquery()
|
||||||
session: AsyncSession = self._query_args["session"]
|
session: AsyncSession = self._query_args["session"]
|
||||||
out = (await session.execute(select(func.count()).select_from(sub))).scalar()
|
out = (await session.execute(select(func.count()).select_from(sub))).scalar()
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
page: int | None = None,
|
page: int | None = None,
|
||||||
per_page: int | None = None,
|
per_page: int | None = None,
|
||||||
max_per_page: int | None = 100,
|
max_per_page: int | None = 100,
|
||||||
error_out: Exception | None = NotFoundError,
|
error_out: Exception | None = NotFoundError,
|
||||||
count: bool = True,
|
count: bool = True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
## XXX flask-sqlalchemy says Pagination() is not public API.
|
## XXX flask-sqlalchemy says Pagination() is not public API.
|
||||||
## Things may break; beware.
|
## Things may break; beware.
|
||||||
self._query_args = kwargs
|
self._query_args = kwargs
|
||||||
page, per_page = self._prepare_page_args(
|
page, per_page = self._prepare_page_args(
|
||||||
page=page,
|
page=page,
|
||||||
per_page=per_page,
|
per_page=per_page,
|
||||||
max_per_page=max_per_page,
|
max_per_page=max_per_page,
|
||||||
error_out=error_out,
|
error_out=error_out,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.page: int = page
|
self.page: int = page
|
||||||
"""The current page."""
|
"""The current page."""
|
||||||
|
|
||||||
self.per_page: int = per_page
|
self.per_page: int = per_page
|
||||||
"""The maximum number of items on a page."""
|
"""The maximum number of items on a page."""
|
||||||
|
|
||||||
self.max_per_page: int | None = max_per_page
|
self.max_per_page: int | None = max_per_page
|
||||||
"""The maximum allowed value for ``per_page``."""
|
"""The maximum allowed value for ``per_page``."""
|
||||||
|
|
||||||
self.items = None
|
self.items = None
|
||||||
self.total = None
|
self.total = None
|
||||||
self.error_out = error_out
|
self.error_out = error_out
|
||||||
self.has_count = count
|
self.has_count = count
|
||||||
|
|
||||||
async def __aiter__(self):
|
async def __aiter__(self):
|
||||||
self.items = await self._query_items()
|
self.items = await self._query_items()
|
||||||
if self.items is None:
|
if self.items is None:
|
||||||
raise RuntimeError('query returned None')
|
raise RuntimeError('query returned None')
|
||||||
if not self.items and self.page != 1 and self.error_out:
|
if not self.items and self.page != 1 and self.error_out:
|
||||||
raise self.error_out
|
raise self.error_out
|
||||||
if self.has_count:
|
if self.has_count:
|
||||||
self.total = await self._query_count()
|
self.total = await self._query_count()
|
||||||
for i in self.items:
|
for i in self.items:
|
||||||
yield i
|
yield i
|
||||||
|
|
||||||
|
return AsyncSelectPagination
|
||||||
|
|
||||||
|
AsyncSelectPagination = _make_AsyncSelectPagination()
|
||||||
|
del _make_AsyncSelectPagination
|
||||||
|
|
||||||
|
|
||||||
def async_query(db: SQLAlchemy, multi: False):
|
def async_query(db: SQLAlchemy, multi: False):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue