From bc4ea9b10191fcbd9f7769e47a485d665826ba33 Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Thu, 19 Jun 2025 14:39:26 +0200 Subject: [PATCH 01/12] fix missing imports --- src/suou/__init__.py | 2 +- src/suou/flask_sqlalchemy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 97743c8..39a7b30 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.3" +__version__ = "0.3.4-dev24" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 8ef0d12..6af4cec 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -22,7 +22,7 @@ from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import DeclarativeBase, Session from .codecs import want_bytes -from .sqlalchemy import require_auth_base +from .sqlalchemy import AuthSrc, require_auth_base class FlaskAuthSrc(AuthSrc): ''' From 1c2bd11212fc2c333c969855f87c29cac433a7e7 Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Thu, 19 Jun 2025 14:58:30 +0200 Subject: [PATCH 02/12] bugfix #2 --- CHANGELOG.md | 4 ++++ src/suou/flask_sqlalchemy.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eab0c55..543a1d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.3.4 + +- Bug fixes in `.flask_sqlalchemy` + ## 0.3.3 - Fixed leftovers in `snowflake` module from unchecked code copying — i.e. `SnowflakeGen.generate_one()` used to require an unused typ= parameter diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 6af4cec..7cd5b41 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -35,7 +35,8 @@ class FlaskAuthSrc(AuthSrc): def get_session(self) -> Session: return self.db.session def get_token(self): - return request.authorization.token + if request.authorization: + return request.authorization.token def get_signature(self) -> bytes: sig = request.headers.get('authorization-signature', None) return want_bytes(sig) if sig else None @@ -51,6 +52,9 @@ def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Ca This looks for a token in the Authorization header, validates it, loads the appropriate object, and injects it as the user= parameter. + NOTE: the actual decorator to be used on routes is **auth_required()**, + NOT require_auth() which is the **constructor** for it. + cls is a SQLAlchemy table. db is a flask_sqlalchemy.SQLAlchemy() binding. From 04ce86a43e15164906104e5c3b9ceed3fdaece0e Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Fri, 27 Jun 2025 22:28:38 +0200 Subject: [PATCH 03/12] release candidate for staging. BUGS ARE NOT FIXED YET. --- CHANGELOG.md | 2 +- src/suou/__init__.py | 2 +- src/suou/flask_restx.py | 3 +++ src/suou/flask_sqlalchemy.py | 11 ++++++++--- src/suou/sqlalchemy.py | 9 ++++----- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 543a1d8..4011856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.3.4 -- Bug fixes in `.flask_sqlalchemy` +- Bug fixes in `.flask_sqlalchemy` and `.sqlalchemy` — `require_auth()` is unusable before this point! ## 0.3.3 diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 39a7b30..51253f2 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.4-dev24" +__version__ = "0.3.4.rc1" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/flask_restx.py b/src/suou/flask_restx.py index 9d4955a..ecdf3da 100644 --- a/src/suou/flask_restx.py +++ b/src/suou/flask_restx.py @@ -54,7 +54,10 @@ class Api(_Api): Notably, all JSON is whitespace-free and .message is remapped to .error """ def handle_error(self, e): + ### XXX apparently this handle_error does not get called AT ALL. + print(e) res = super().handle_error(e) + print(res) if isinstance(res, Mapping) and 'message' in res: res['error'] = res['message'] del res['message'] diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 7cd5b41..508e296 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -43,9 +43,9 @@ class FlaskAuthSrc(AuthSrc): def invalid_exc(self, msg: str = 'validation failed') -> Never: abort(400, msg) def required_exc(self): - abort(401) + abort(401, 'Login required') -def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Callable]: +def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable: """ Make an auth_required() decorator for Flask views. @@ -67,7 +67,12 @@ def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Ca def super_secret_stuff(user): pass """ - return partial(require_auth_base, cls=cls, src=FlaskAuthSrc(db)) + def auth_required(**kwargs): + return require_auth_base(cls=cls, src=FlaskAuthSrc(db), **kwargs) + + auth_required.__doc__ = require_auth_base.__doc__ + + return auth_required __all__ = ('require_auth', ) diff --git a/src/suou/sqlalchemy.py b/src/suou/sqlalchemy.py index 2019424..8ff32a4 100644 --- a/src/suou/sqlalchemy.py +++ b/src/suou/sqlalchemy.py @@ -253,8 +253,7 @@ class AuthSrc(metaclass=ABCMeta): def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str | Column[_T] = 'id', dest: str = 'user', - required: bool = False, signed: bool = False, sig_dest: str = 'signature', validators: Callable | Iterable[Callable] | None = None, - invalid_exc: Callable | None = None, required_exc: Callable | None = None): + required: bool = False, signed: bool = False, sig_dest: str = 'signature', validators: Callable | Iterable[Callable] | None = None): ''' Inject the current user into a view, given the Authorization: Bearer header. @@ -275,11 +274,11 @@ def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str | except Exception: return None - def _default_invalid(msg: str): + def _default_invalid(msg: str = 'validation failed'): raise ValueError(msg) - invalid_exc = invalid_exc or _default_invalid - required_exc = required_exc or (lambda: _default_invalid()) + invalid_exc = src.invalid_exc or _default_invalid + required_exc = src.required_exc or (lambda: _default_invalid()) def decorator(func: Callable): @wraps(func) From 96d25e0e85b83cc2c1f010716aacf80698fe0cee Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Mon, 30 Jun 2025 13:51:03 +0200 Subject: [PATCH 04/12] fixes in .configparse --- CHANGELOG.md | 1 + src/suou/__init__.py | 2 +- src/suou/configparse.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4011856..3d5856c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.3.4 - Bug fixes in `.flask_sqlalchemy` and `.sqlalchemy` — `require_auth()` is unusable before this point! +- Fixed a bug in `.configparse` dealing with unset values from multiple sources ## 0.3.3 diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 51253f2..0f45634 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.4.rc1" +__version__ = "0.3.4.rc2" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/configparse.py b/src/suou/configparse.py index ace075f..d9a0506 100644 --- a/src/suou/configparse.py +++ b/src/suou/configparse.py @@ -20,7 +20,7 @@ from ast import TypeVar from collections.abc import Mapping from configparser import ConfigParser as _ConfigParser import os -from typing import Any, Callable, Iterable, Iterator +from typing import Any, Callable, Iterator from collections import OrderedDict from .functools import deprecated_alias @@ -78,6 +78,8 @@ class ConfigParserConfigSource(ConfigSource): _cfp: _ConfigParser def __init__(self, cfp: _ConfigParser): + if not isinstance(cfp, _ConfigParser): + raise TypeError(f'a ConfigParser object is required (got {cfp.__class__.__name__!r})') self._cfp = cfp def __getitem__(self, key: str, /) -> str: k1, _, k2 = key.partition('.') @@ -174,10 +176,12 @@ class ConfigValue: owner.expose(self._pub_name, name) def __get__(self, obj: ConfigOptions, owner=False): if self._val is MISSING: + v = MISSING for srckey, src in obj._srcs.items(): - v = src.get(self._srcs[srckey], MISSING) - if self._required and not v: - raise MissingConfigError(f'required config {self._src} not set!') + if srckey in self._srcs: + v = src.get(self._srcs[srckey], v) + if self._required and (not v or v is MISSING): + raise MissingConfigError(f'required config {self._srcs['default']} not set!') if v is MISSING: v = self._default if callable(self._cast): From f0a109983bf78d901b1bf196f887cad48656aa6a Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Tue, 1 Jul 2025 14:48:58 +0200 Subject: [PATCH 05/12] fix flask_restx error handler, update README.md, give up on fixing require_auth() on flask_restx --- CHANGELOG.md | 2 +- README.md | 2 +- src/suou/__init__.py | 2 +- src/suou/codecs.py | 2 +- src/suou/flask_restx.py | 18 +++++++++++++----- src/suou/flask_sqlalchemy.py | 2 ++ 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5856c..0895211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.3.4 -- Bug fixes in `.flask_sqlalchemy` and `.sqlalchemy` — `require_auth()` is unusable before this point! +- Bug fixes in `.flask_restx` regarding error handling - Fixed a bug in `.configparse` dealing with unset values from multiple sources ## 0.3.3 diff --git a/README.md b/README.md index 5ae6d56..29ee187 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Good morning, my brother! Welcome the SUOU (SIS Unified Object Underarmor), a library for the management of the storage of objects into a database. -It provides utilities such as [SIQ](https://sakux.moe/protocols/siq.html), signing and generation of access tokens (on top of [ItsDangerous](https://github.com/pallets/itsdangerous)) and various utilities, including helpers for use in Flask and SQLAlchemy. +It provides utilities such as [SIQ](https://yusur.moe/protocols/siq.html), signing and generation of access tokens (on top of [ItsDangerous](https://github.com/pallets/itsdangerous)) and various utilities, including helpers for use in Flask and SQLAlchemy. **It is not an ORM** nor a replacement of it; it works along existing ORMs (currently only SQLAlchemy is supported lol). diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 0f45634..a0aa794 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.4.rc2" +__version__ = "0.3.4" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/codecs.py b/src/suou/codecs.py index 2bee255..3efe53f 100644 --- a/src/suou/codecs.py +++ b/src/suou/codecs.py @@ -227,7 +227,7 @@ def jsonencode(obj: dict, *, skipkeys: bool = True, separators: tuple[str, str] ''' return json.dumps(obj, skipkeys=skipkeys, separators=separators, default=_json_default(default), **kwargs) -jsondecode = deprecated('just use json.loads()')(json.loads) +jsondecode: Callable[Any, dict] = deprecated('just use json.loads()')(json.loads) def ssv_list(s: str, *, sep_chars = ',;') -> list[str]: """ diff --git a/src/suou/flask_restx.py b/src/suou/flask_restx.py index ecdf3da..8dd9d86 100644 --- a/src/suou/flask_restx.py +++ b/src/suou/flask_restx.py @@ -16,10 +16,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. from typing import Any, Mapping import warnings -from flask import current_app, make_response +from flask import current_app, Response, make_response from flask_restx import Api as _Api -from .codecs import jsonencode +from .codecs import jsondecode, jsonencode, want_bytes, want_str def output_json(data, code, headers=None): @@ -54,13 +54,21 @@ class Api(_Api): Notably, all JSON is whitespace-free and .message is remapped to .error """ def handle_error(self, e): - ### XXX apparently this handle_error does not get called AT ALL. - print(e) + ### XXX in order for errors to get handled the correct way, import + ### suou.flask_restx.Api() NOT flask_restx.Api() !!!! res = super().handle_error(e) - print(res) if isinstance(res, Mapping) and 'message' in res: res['error'] = res['message'] del res['message'] + elif isinstance(res, Response): + try: + body = want_str(res.response[0]) + bodj = jsondecode(body) + if 'message' in bodj: + bodj['error'] = bodj.pop('message') + res.response = [want_bytes(jsonencode(bodj))] + except (IndexError, KeyError): + pass return res def __init__(self, *a, **ka): super().__init__(*a, **ka) diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 508e296..8c587e1 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -66,6 +66,8 @@ def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable: @auth_required(validators=[lambda x: x.is_administrator]) def super_secret_stuff(user): pass + + NOTE: require_auth() DOES NOT work with flask_restx. """ def auth_required(**kwargs): return require_auth_base(cls=cls, src=FlaskAuthSrc(db), **kwargs) From cb99c3911c44f7ccb1701367d5e3b5a344e9750a Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sun, 6 Jul 2025 22:52:49 +0200 Subject: [PATCH 06/12] improve cb32 handling --- CHANGELOG.md | 8 ++++++++ src/suou/__init__.py | 2 +- src/suou/flask_sqlalchemy.py | 2 +- src/suou/iding.py | 9 +++++++-- src/suou/sqlalchemy.py | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0895211..30fbc5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.4.0 + +👀 + +## 0.3.5 + +- Fixed cb32 handling. Now leading zeros in SIQ's are stripped, and `.from_cb32()` was implemented. + ## 0.3.4 - Bug fixes in `.flask_restx` regarding error handling diff --git a/src/suou/__init__.py b/src/suou/__init__.py index a0aa794..74ff48f 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.4" +__version__ = "0.3.5" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 8c587e1..963b34d 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -40,7 +40,7 @@ class FlaskAuthSrc(AuthSrc): def get_signature(self) -> bytes: sig = request.headers.get('authorization-signature', None) return want_bytes(sig) if sig else None - def invalid_exc(self, msg: str = 'validation failed') -> Never: + def invalid_exc(self, msg: str = 'Validation failed') -> Never: abort(400, msg) def required_exc(self): abort(401, 'Login required') diff --git a/src/suou/iding.py b/src/suou/iding.py index 6cfcd5e..8797340 100644 --- a/src/suou/iding.py +++ b/src/suou/iding.py @@ -41,7 +41,7 @@ from typing import Iterable, override import warnings from .functools import deprecated -from .codecs import b32lencode, b64encode, cb32encode +from .codecs import b32lencode, b64encode, cb32decode, cb32encode, want_str class SiqType(enum.Enum): @@ -235,9 +235,14 @@ class Siq(int): def to_base64(self, length: int = 15, *, strip: bool = True) -> str: return b64encode(self.to_bytes(length), strip=strip) + def to_cb32(self) -> str: - return cb32encode(self.to_bytes(15, 'big')) + return cb32encode(self.to_bytes(15, 'big')).lstrip('0') to_crockford = to_cb32 + @classmethod + def from_cb32(cls, val: str | bytes): + return cls.from_bytes(cb32decode(want_str(val).zfill(24))) + def to_hex(self) -> str: return f'{self:x}' def to_oct(self) -> str: diff --git a/src/suou/sqlalchemy.py b/src/suou/sqlalchemy.py index 8ff32a4..5a7bc81 100644 --- a/src/suou/sqlalchemy.py +++ b/src/suou/sqlalchemy.py @@ -278,7 +278,7 @@ def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str | raise ValueError(msg) invalid_exc = src.invalid_exc or _default_invalid - required_exc = src.required_exc or (lambda: _default_invalid()) + required_exc = src.required_exc or (lambda: _default_invalid('Login required')) def decorator(func: Callable): @wraps(func) From f5030ef25a130ec7b419641f48ef0b68afeaaa57 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 9 Jul 2025 16:26:52 +0200 Subject: [PATCH 07/12] 0.3.6: fixed config handling with multiple sources --- CHANGELOG.md | 4 ++++ src/suou/__init__.py | 2 +- src/suou/configparse.py | 14 ++++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30fbc5f..9ca4b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 👀 +## 0.3.6 + +- Fixed `ConfigValue` behavior with multiple sources. It used to iterate through all the sources, possibly overwriting; now, iteration stops at first non-missing value. + ## 0.3.5 - Fixed cb32 handling. Now leading zeros in SIQ's are stripped, and `.from_cb32()` was implemented. diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 74ff48f..4fad4a2 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.5" +__version__ = "0.3.6" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/configparse.py b/src/suou/configparse.py index d9a0506..b30c83f 100644 --- a/src/suou/configparse.py +++ b/src/suou/configparse.py @@ -29,6 +29,9 @@ from .functools import deprecated_alias MISSING = object() _T = TypeVar('T') +def _not_missing(v) -> bool: + return v and v is not MISSING + class MissingConfigError(LookupError): """ @@ -180,10 +183,13 @@ class ConfigValue: for srckey, src in obj._srcs.items(): if srckey in self._srcs: v = src.get(self._srcs[srckey], v) - if self._required and (not v or v is MISSING): - raise MissingConfigError(f'required config {self._srcs['default']} not set!') - if v is MISSING: - v = self._default + if _not_missing(v): + break + if not _not_missing(v): + if self._required: + raise MissingConfigError(f'required config {self._srcs['default']} not set!') + else: + v = self._default if callable(self._cast): v = self._cast(v) if v is not None else self._cast() self._val = v From 678d6ce2bbd879864317da41f2a52bfd250fa260 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sat, 19 Jul 2025 14:16:57 +0200 Subject: [PATCH 08/12] 0.3.7: fix b64encode() incorrect padding --- .gitignore | 1 + CHANGELOG.md | 4 ++++ src/suou/__init__.py | 2 +- src/suou/codecs.py | 4 ++-- src/suou/configparse.py | 4 ++-- src/suou/flask_sqlalchemy.py | 4 ++-- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 2e2c6b7..7201aa6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist/ .err .vscode /run.sh +ROADMAP.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca4b52..f2f1f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 👀 +## 0.3.7 + +- Fixed a bug in `b64decode()` padding handling which made the function inconsistent and non injective. Now, leading `'A'` is NEVER stripped. + ## 0.3.6 - Fixed `ConfigValue` behavior with multiple sources. It used to iterate through all the sources, possibly overwriting; now, iteration stops at first non-missing value. diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 4fad4a2..2a06368 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.6" +__version__ = "0.3.7" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/codecs.py b/src/suou/codecs.py index 3efe53f..9c77424 100644 --- a/src/suou/codecs.py +++ b/src/suou/codecs.py @@ -178,10 +178,10 @@ def b32ldecode(val: bytes | str) -> bytes: def b64encode(val: bytes, *, strip: bool = True) -> str: ''' - Wrapper around base64.urlsafe_b64encode() which also strips trailing '=' and leading 'A'. + Wrapper around base64.urlsafe_b64encode() which also strips trailing '='. ''' b = want_str(base64.urlsafe_b64encode(val)) - return b.lstrip('A').rstrip('=') if strip else b + return b.rstrip('=') if strip else b def b64decode(val: bytes | str) -> bytes: ''' diff --git a/src/suou/configparse.py b/src/suou/configparse.py index b30c83f..f91a2b2 100644 --- a/src/suou/configparse.py +++ b/src/suou/configparse.py @@ -23,7 +23,7 @@ import os from typing import Any, Callable, Iterator from collections import OrderedDict -from .functools import deprecated_alias +from .functools import deprecated MISSING = object() @@ -226,7 +226,7 @@ class ConfigOptions: if first: self._srcs.move_to_end(key, False) - add_config_source = deprecated_alias(add_source) + add_config_source = deprecated('use add_source() instead')(add_source) def expose(self, public_name: str, attr_name: str | None = None) -> None: ''' diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 963b34d..f114d91 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -40,12 +40,12 @@ class FlaskAuthSrc(AuthSrc): def get_signature(self) -> bytes: sig = request.headers.get('authorization-signature', None) return want_bytes(sig) if sig else None - def invalid_exc(self, msg: str = 'Validation failed') -> Never: + def invalid_exc(self, msg: str = 'validation failed') -> Never: abort(400, msg) def required_exc(self): abort(401, 'Login required') -def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable: +def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Callable]: """ Make an auth_required() decorator for Flask views. From 8d45fa808973f675af6c1319848dbbf02a5d71e1 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sat, 23 Aug 2025 14:43:28 +0200 Subject: [PATCH 09/12] fix type return in declarative_base() --- src/suou/sqlalchemy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/suou/sqlalchemy.py b/src/suou/sqlalchemy.py index 5a7bc81..0f0111d 100644 --- a/src/suou/sqlalchemy.py +++ b/src/suou/sqlalchemy.py @@ -120,7 +120,8 @@ def match_column(length: int, regex: str, /, case: StringCase = StringCase.AS_IS constraint_name=constraint_name or f'{x.__tablename__}_{n}_valid')), *args, **kwargs) -def declarative_base(domain_name: str, master_secret: bytes, metadata: dict | None = None, **kwargs): + +def declarative_base(domain_name: str, master_secret: bytes, metadata: dict | None = None, **kwargs) -> type[DeclarativeBase]: """ Drop-in replacement for sqlalchemy.orm.declarative_base() taking in account requirements for SIQ generation (i.e. domain name). From ddb28d089cb3c560003c0a1bea77e50957f187a8 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sat, 23 Aug 2025 14:48:56 +0200 Subject: [PATCH 10/12] version advance --- src/suou/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 2a06368..6df10f5 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -27,7 +27,7 @@ from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen -__version__ = "0.3.7" +__version__ = "0.3.8" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', From a66f59199775382e57c0c1c66ea4c5a92f40a4e7 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sat, 23 Aug 2025 14:54:58 +0200 Subject: [PATCH 11/12] update changelog, add lazy= to parent_children() --- CHANGELOG.md | 5 +++++ src/suou/sqlalchemy.py | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2f1f01..1c591b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ 👀 +## 0.3.8 + +- Fixed return types for `.sqlalchemy` module. +- `sqlalchemy.parent_children()` now takes a `lazy` parameter. Backported from 0.5.0. + ## 0.3.7 - Fixed a bug in `b64decode()` padding handling which made the function inconsistent and non injective. Now, leading `'A'` is NEVER stripped. diff --git a/src/suou/sqlalchemy.py b/src/suou/sqlalchemy.py index 0f0111d..5490ecb 100644 --- a/src/suou/sqlalchemy.py +++ b/src/suou/sqlalchemy.py @@ -21,7 +21,7 @@ from functools import wraps from typing import Callable, Iterable, Never, TypeVar import warnings from sqlalchemy import BigInteger, CheckConstraint, Date, Dialect, ForeignKey, LargeBinary, Column, MetaData, SmallInteger, String, create_engine, select, text -from sqlalchemy.orm import DeclarativeBase, Session, declarative_base as _declarative_base, relationship +from sqlalchemy.orm import DeclarativeBase, Relationship, Session, declarative_base as _declarative_base, relationship from .snowflake import SnowflakeGen from .itertools import kwargs_prefix, makelist @@ -194,7 +194,7 @@ def age_pair(*, nullable: bool = False, **ka) -> tuple[Column, Column]: return (date_col, acc_col) -def parent_children(keyword: str, /, **kwargs): +def parent_children(keyword: str, /, lazy='selectin', **kwargs) -> tuple[Incomplete[Relationship], Incomplete[Relationship]]: """ Self-referential one-to-many relationship pair. Parent comes first, children come later. @@ -209,8 +209,8 @@ def parent_children(keyword: str, /, **kwargs): parent_kwargs = kwargs_prefix(kwargs, 'parent_') child_kwargs = kwargs_prefix(kwargs, 'child_') - parent = Incomplete(relationship, Wanted(lambda o, n: o.__name__), back_populates=f'child_{keyword}s', **parent_kwargs) - child = Incomplete(relationship, Wanted(lambda o, n: o.__name__), back_populates=f'parent_{keyword}', **child_kwargs) + parent = Incomplete(relationship, Wanted(lambda o, n: o.__name__), back_populates=f'child_{keyword}s', lazy=lazy, **parent_kwargs) + child = Incomplete(relationship, Wanted(lambda o, n: o.__name__), back_populates=f'parent_{keyword}', lazy=lazy, **child_kwargs) return parent, child From 029b12867f51d9a9127e2cb591467180fa7d84b1 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sat, 23 Aug 2025 15:12:35 +0200 Subject: [PATCH 12/12] make lazy keyword-only as in 0.5.0 --- src/suou/sqlalchemy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/suou/sqlalchemy.py b/src/suou/sqlalchemy.py index 5490ecb..0245ed6 100644 --- a/src/suou/sqlalchemy.py +++ b/src/suou/sqlalchemy.py @@ -194,7 +194,7 @@ def age_pair(*, nullable: bool = False, **ka) -> tuple[Column, Column]: return (date_col, acc_col) -def parent_children(keyword: str, /, lazy='selectin', **kwargs) -> tuple[Incomplete[Relationship], Incomplete[Relationship]]: +def parent_children(keyword: str, /, *, lazy: str = 'selectin', **kwargs) -> tuple[Incomplete[Relationship], Incomplete[Relationship]]: """ Self-referential one-to-many relationship pair. Parent comes first, children come later.