Compare commits

..

No commits in common. "b27e18a3a05b7eecc49efdec3b97140a9ec384ac" and "b1d0c62b448d490dc18f2c8a246e329b4993416c" have entirely different histories.

6 changed files with 13 additions and 136 deletions

View file

@ -6,7 +6,6 @@
* New module `mat` adds a shallow reimplementation of `Matrix()` in order to implement matrix multiplication * New module `mat` adds a shallow reimplementation of `Matrix()` in order to implement matrix multiplication
* Removed obsolete `configparse` implementation that has been around since 0.3 and shelved since 0.4. * Removed obsolete `configparse` implementation that has been around since 0.3 and shelved since 0.4.
* `color`: added support for conversion from RGB to sRGB, XYZ, OKLab and OKLCH. * `color`: added support for conversion from RGB to sRGB, XYZ, OKLab and OKLCH.
* Added `user-loader` for Quart-Auth and SQLAlchemy
## 0.11.2 ## 0.11.2

View file

@ -39,7 +39,7 @@ Documentation = "https://suou.readthedocs.io"
# the below are all dev dependencies (and probably already installed) # the below are all dev dependencies (and probably already installed)
sqlalchemy = [ sqlalchemy = [
"SQLAlchemy[asyncio]>=2.0.0", "SQLAlchemy[asyncio]>=2.0.0",
"flask-sqlalchemy" # glue code "flask-sqlalchemy"
] ]
flask = [ flask = [
"Flask>=2.0.0", "Flask>=2.0.0",
@ -61,10 +61,6 @@ quart = [
"Quart-Schema", "Quart-Schema",
"starlette>=0.47.2" "starlette>=0.47.2"
] ]
quart_auth = [
"Quart-Auth",
"suou[sqlalchemy]" # glue code
]
sass = [ sass = [
## HEADS UP!! libsass carries a C extension + uses setup.py ## HEADS UP!! libsass carries a C extension + uses setup.py
"libsass" "libsass"
@ -74,7 +70,6 @@ full = [
"suou[sqlalchemy]", "suou[sqlalchemy]",
"suou[flask]", "suou[flask]",
"suou[quart]", "suou[quart]",
"suou[quart_auth]",
"suou[peewee]", "suou[peewee]",
"suou[markdown]", "suou[markdown]",
"suou[sass]" "suou[sass]"

View file

@ -38,7 +38,7 @@ from .http import WantsContentType
from .color import OKLabColor, chalk, WebColor, RGBColor, SRGBColor, XYZColor, OKLabColor from .color import OKLabColor, chalk, WebColor, RGBColor, SRGBColor, XYZColor, OKLabColor
from .mat import Matrix from .mat import Matrix
__version__ = "0.12.0a7" __version__ = "0.12.0a5"
__all__ = ( __all__ = (
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',

View file

@ -23,7 +23,6 @@ from collections import namedtuple
from functools import lru_cache from functools import lru_cache
import math import math
from suou.functools import deprecated
from suou.mat import Matrix from suou.mat import Matrix
@ -103,8 +102,6 @@ class RGBColor(namedtuple('_WebColor', 'red green blue')):
Useful for theming. Useful for theming.
XXX CURRENTLY THE OKLCH CONVERSION DOES NOT WORK
*Changed in 0.12.0*: name is now RGBColor, with WebColor being an alias. *Changed in 0.12.0*: name is now RGBColor, with WebColor being an alias.
Added conversions to and from OKLCH, OKLab, sRGB, and XYZ. Added conversions to and from OKLCH, OKLab, sRGB, and XYZ.
""" """
@ -171,7 +168,7 @@ class RGBColor(namedtuple('_WebColor', 'red green blue')):
]) ])
def to_xyz(self): def to_xyz(self):
return XYZColor(*(self.RGB_TO_XYZ @ Matrix.as_column([x / 255 for x in self])).get_column()) return XYZColor(*(self.RGB_TO_XYZ @ Matrix.as_column(self)).get_column())
def to_oklch(self): def to_oklch(self):
return self.to_xyz().to_oklch() return self.to_xyz().to_oklch()
@ -195,9 +192,7 @@ class SRGBColor(namedtuple('_SRGBColor', 'red green blue')):
blue: float blue: float
def __str__(self): def __str__(self):
r, g, b = round(self.red, 4), round(self.green, 4), round(self.blue, 4) return f"srgb({self.red}, {self.green}, {self.blue})"
return f"srgb({r}, {g}, {b})"
def to_rgb(self): def to_rgb(self):
return RGBColor(*( return RGBColor(*(
@ -237,8 +232,8 @@ class XYZColor(namedtuple('_XYZColor', 'x y z')):
]) ])
def to_rgb(self): def to_rgb(self):
return RGBColor(*[int(x * 255) for x in (self.XYZ_TO_RGB @ Matrix.as_column(self)).get_column()]) return RGBColor(*(self.XYZ_TO_RGB @ Matrix.as_column(self)).get_column())
def to_oklab(self): def to_oklab(self):
lms = (self.XYZ_TO_LMS @ Matrix.as_column(self)).get_column() lms = (self.XYZ_TO_LMS @ Matrix.as_column(self)).get_column()
lmsg = [math.cbrt(i) for i in lms] lmsg = [math.cbrt(i) for i in lms]
@ -248,11 +243,6 @@ class XYZColor(namedtuple('_XYZColor', 'x y z')):
def to_oklch(self): def to_oklch(self):
return self.to_oklab().to_oklch() return self.to_oklab().to_oklch()
def __str__(self):
x, y, z = round(self.x, 4), round(self.y, 4), round(self.z, 4)
return f'xyz({x} {y} {z})'
class OKLabColor(namedtuple('_OKLabColor', 'l a b')): class OKLabColor(namedtuple('_OKLabColor', 'l a b')):
""" """
@ -286,11 +276,6 @@ class OKLabColor(namedtuple('_OKLabColor', 'l a b')):
0 if abs(self.a) < .0002 and abs(self.b) < .0002 else (((math.atan2(self.b, self.a) * 180) / math.pi % 360) + 360) % 360 0 if abs(self.a) < .0002 and abs(self.b) < .0002 else (((math.atan2(self.b, self.a) * 180) / math.pi % 360) + 360) % 360
) )
def __str__(self):
l, c, h = round(self.l, 4), round(self.a, 4), round(self.b, 4)
return f'oklab({l} {c} {h})'
def to_rgb(self): def to_rgb(self):
return self.to_xyz().to_rgb() return self.to_xyz().to_rgb()
@ -304,18 +289,19 @@ class OKLCHColor(namedtuple('_OKLCHColor', 'l c h')):
""" """
def __str__(self): def __str__(self):
l, c, h = round(self.l, 4), round(self.c, 4), round(self.h, 2) l, c, h = round(self.l, 4), round(self.c, 4), round(self.h, 4)
return f'oklch({l}, {c}, {h})'
return f'oklch({l} {c} {h})'
def to_oklab(self): def to_oklab(self):
return OKLabColor( return OKLabColor(
self.l, self.l,
self.c * math.cos(self.h * math.pi / 180), self.c * math.cos(self.h * math.pi / 180),
self.c * math.sin(self.h * math.pi / 180) self.h * math.cos(self.h * math.pi / 180)
) )
def to_rgb(self): def to_rgb(self):
return self.to_oklab().to_rgb() return self.to_oklab().to_rgb()
__all__ = ('chalk', 'WebColor', "RGBColor", 'SRGBColor', 'XYZColor', 'OKLabColor', 'OKLCHColor') __all__ = ('chalk', 'WebColor', "RGBColor", 'SRGBColor', 'XYZColor', 'OKLabColor')

View file

@ -1,97 +0,0 @@
"""
Utilities for Quart-Auth
---
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.
"""
from __future__ import annotations
from typing import Callable, TypeVar
from quart_auth import AuthUser, Action
from sqlalchemy import select
from sqlalchemy.orm import DeclarativeBase
from .sqlalchemy.asyncio import AsyncSession, SQLAlchemy
_T = TypeVar('_T')
def user_loader(database: SQLAlchemy, user_class: type[DeclarativeBase], *,
attr_loader: Callable[[type[AuthUser], str], _T] = lambda x, y: x.id == int(y)
):
"""
Returns a properly subclassed AuthUser loader for use in Quart-Auth.
Actual User object is at .user; other attributes are proxied.
Requires to be awaited before request before usage.
Uses SQLAlchemy's AsyncSession.
Parameters:
* database The database instance.
* user_class The user class.
* attr_loader A lambda taking user_class and auth_id, default (user_class, auth_id : user_class.id == int(auth_id))
*New in 0.12.0*
"""
class UserLoader(AuthUser):
_auth_id: str | None
_auth_obj: user_class | None
id: _T
def __init__(self, auth_id: str | None, action: Action = Action.PASS):
self._auth_id = auth_id
self._auth_obj = None
self._auth_sess: AsyncSession | None = None
self.action = action
@property
def auth_id(self) -> str | None:
return self._auth_id
@property
async def is_authenticated(self) -> bool:
await self._load()
return self._auth_id is not None
async def _load(self):
if self._auth_obj is None and self._auth_id is not None:
async with database as session:
self._auth_obj = (await session.execute(select(user_class).where(attr_loader(user_class, self._auth_id)))).scalar()
if self._auth_obj is None:
raise RuntimeError('failed to fetch user')
def __getattr__(self, key):
if self._auth_obj is None:
raise RuntimeError('user is not loaded')
return getattr(self._auth_obj, key)
def __bool__(self):
return self._auth_obj is not None
@property
def session(self):
return self._auth_sess
async def _unload(self):
# user is not expected to mutate
if self._auth_sess:
await self._auth_sess.rollback()
@property
def user(self):
return self._auth_obj
return UserLoader
# Optional dependency: do not import into __init__.py
__all__ = ('user_loader',)

View file

@ -2,8 +2,7 @@
import unittest import unittest
from suou import RGBColor, chalk from suou import chalk
from suou.color import OKLCHColor
class TestColor(unittest.TestCase): class TestColor(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
@ -25,9 +24,4 @@ class TestColor(unittest.TestCase):
strg = "The quick brown fox jumps over the lazy dog" strg = "The quick brown fox jumps over the lazy dog"
self.assertEqual(f'\x1b[1m{strg}\x1b[22m', chalk.bold(strg)) self.assertEqual(f'\x1b[1m{strg}\x1b[22m', chalk.bold(strg))
self.assertEqual(f'\x1b[2m{strg}\x1b[22m', chalk.faint(strg)) self.assertEqual(f'\x1b[2m{strg}\x1b[22m', chalk.faint(strg))
self.assertEqual(f'\x1b[1m\x1b[33m{strg}\x1b[39m\x1b[22m', chalk.bold.yellow(strg)) self.assertEqual(f'\x1b[1m\x1b[33m{strg}\x1b[39m\x1b[22m', chalk.bold.yellow(strg))
def test_oklch_to_rgb(self):
self.assertEqual(OKLCHColor(0.628, 0.2577, 29.23).to_rgb(), RGBColor(255, 0, 0))
self.assertEqual(OKLCHColor(0.7653, 0.1306, 194.77).to_rgb(), RGBColor(0, 204, 204))
self.assertEqual(OKLCHColor(0.5931, 0., 0.).to_rgb(), RGBColor(126, 126, 126))