From 3de5a3629d9ad333fb8602819c552d3a300452a8 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Sat, 13 Sep 2025 21:14:31 +0200 Subject: [PATCH 1/3] fix build artifacts --- aliases/sakuragasaki46-suou/pyproject.toml | 1 - aliases/sakuragasaki46_suou/README.md | 1 + aliases/sakuragasaki46_suou/pyproject.toml | 8 ++++++++ 3 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 aliases/sakuragasaki46-suou/pyproject.toml create mode 100644 aliases/sakuragasaki46_suou/README.md create mode 100644 aliases/sakuragasaki46_suou/pyproject.toml diff --git a/aliases/sakuragasaki46-suou/pyproject.toml b/aliases/sakuragasaki46-suou/pyproject.toml deleted file mode 100644 index 8b13789..0000000 --- a/aliases/sakuragasaki46-suou/pyproject.toml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/aliases/sakuragasaki46_suou/README.md b/aliases/sakuragasaki46_suou/README.md new file mode 100644 index 0000000..2b29355 --- /dev/null +++ b/aliases/sakuragasaki46_suou/README.md @@ -0,0 +1 @@ +moved to [suou](https://pypi.org/project/suou) diff --git a/aliases/sakuragasaki46_suou/pyproject.toml b/aliases/sakuragasaki46_suou/pyproject.toml new file mode 100644 index 0000000..035fdb5 --- /dev/null +++ b/aliases/sakuragasaki46_suou/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "sakuragasaki46_suou" +authors = [ { name = "Sakuragasaki46" } ] +version = "0.6.1" +requires-python = ">=3.10" +dependencies = [ "suou==0.6.1" ] +readme = "README.md" + From a2fdc9166fb505a2b590e9d87c825455f68f9bc9 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Fri, 19 Sep 2025 13:34:51 +0200 Subject: [PATCH 2/3] 0.7.x: @lucky, @rng_overload and more exceptions --- CHANGELOG.md | 5 ++ src/suou/__init__.py | 2 +- src/suou/exceptions.py | 42 +++++++++++++++- src/suou/lex.py | 10 ++++ src/suou/luck.py | 112 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/suou/luck.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 37975e7..80f1123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.7.0 "The Lucky Update" + ++ Add RNG/random selection overloads such as `luck()`, `rng_overload()`. ++ Add 7 new throwable exceptions. + ## 0.6.1 - First release on PyPI under the name `suou`. diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 3c0efd3..9f80088 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.1" +__version__ = "0.7.0-dev37" __all__ = ( 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', diff --git a/src/suou/exceptions.py b/src/suou/exceptions.py index 7952bc7..74ea7ec 100644 --- a/src/suou/exceptions.py +++ b/src/suou/exceptions.py @@ -1,5 +1,5 @@ """ -Exceptions and throwables for various purposes +Exceptions and throwables for all purposes! --- @@ -14,6 +14,17 @@ This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ + +class PoliticalError(Exception): + """ + Base class for anything that is refused to be executed for political reasons. + """ + +class PoliticalWarning(PoliticalError, Warning): + """ + Base class for politically suspicious behaviors. + """ + class MissingConfigError(LookupError): """ Config variable not found. @@ -53,6 +64,35 @@ class BabelTowerError(NotFoundError): The user requested a language that cannot be understood. """ +class BadLuckError(Exception): + """ + Stuff did not go as expected. + + Raised by @lucky decorator. + """ + +class TerminalRequiredError(OSError): + """ + Raised by terminal_required() decorator when a function is called from a non-interactive environment. + """ + +class BrokenStringsError(OSError): + """ + Issues related to audio happened, i.e. appropriate executables/libraries/drivers are not installed. + """ + +class Fahrenheit451Error(PoliticalError): + """ + Base class for thought crimes related to arts (e.g. writing, visual arts, music) + """ + +class FuckAroundFindOutError(PoliticalError): + """ + Raised when there is no actual grounds to raise an exception, but you did something in the past to deserve this outcome. + + Ideal for permanent service bans or something. + """ + __all__ = ( 'MissingConfigError', 'MissingConfigWarning', 'LexError', 'InconsistencyError', 'NotFoundError' ) \ No newline at end of file diff --git a/src/suou/lex.py b/src/suou/lex.py index 15791c3..5655eea 100644 --- a/src/suou/lex.py +++ b/src/suou/lex.py @@ -2,6 +2,16 @@ Utilities for tokenization of text. --- + +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 re import Match diff --git a/src/suou/luck.py b/src/suou/luck.py new file mode 100644 index 0000000..c22b552 --- /dev/null +++ b/src/suou/luck.py @@ -0,0 +1,112 @@ +""" +Fortune and esoterism helpers. + +--- + +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 typing import Callable, Generic, Iterable, TypeVar +import random +from suou.exceptions import BadLuckError + +_T = TypeVar('_T') +_U = TypeVar('_U') + + +def lucky(validators: Iterable[Callable[[_U], bool]] = ()): + """ + Add one or more constraint on a function's return value. + Each validator must return a boolean. If false, the result is considered + unlucky and BadLuckError() is raised. + + UNTESTED + + NEW 0.7.0 + """ + def decorator(func: Callable[_T, _U]): + @wraps(func) + def wrapper(*args, **kwargs) -> _U: + try: + result = func(*args, **kwargs) + except Exception as e: + raise BadLuckError(f'exception happened: {e}') from e + for v in validators: + try: + if not v(result): + raise BadLuckError(f'result not expected: {result!r}') + except BadLuckError: + raise + except Exception as e: + raise BadLuckError(f'cannot validate: {e}') from e + return result + return wrapper + return decorator + +class RngCallable(Callable, Generic[_T, _U]): + """ + Overloaded ... randomly chosen callable. + + UNTESTED + + NEW 0.7.0 + """ + def __init__(self, /, func: Callable[_T, _U] | None = None, weight: int = 1): + self._callables = [] + self._max_weight = 0 + if callable(func): + self.add_callable(func, weight) + def add_callable(self, func: Callable[_T, _U], weight: int = 1): + """ + """ + weight = int(weight) + if weight <= 0: + return + self._callables.append((func, weight)) + self._max_weight += weight + def __call__(self, *a, **ka) -> _U: + choice = random.randrange(self._max_weight) + for w, c in self._callables: + if choice < w: + return c(*a, **ka) + elif choice < 0: + raise RuntimeError('inconsistent state') + else: + choice -= w + + +def rng_overload(prev_func: RngCallable[_T, _U] | int | None, /, *, weight: int = 1) -> RngCallable[_T, _U]: + """ + Decorate the first function with @rng_overload and the weight= parameter + (default 1, must be an integer) to create a "RNG" overloaded callable. + + Each call chooses randomly one candidate (weight is taken in consideration) + , calls it, and returns the result. + + UNTESTED + + NEW 0.7.0 + """ + if isinstance(prev_func, int) and weight == 1: + weight, prev_func = prev_func, None + + def decorator(func: Callable[_T, _U]): + nonlocal prev_func + if prev_func is None: + prev_func = RngCallable(func, weight=weight) + else: + prev_func.add_callable(func, weight=weight) + return prev_func + return decorator + + + + \ No newline at end of file From 83ab616e13ffa4d5505e5cc23d462a5ce3627566 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Fri, 19 Sep 2025 15:39:44 +0200 Subject: [PATCH 3/3] add chalk --- CHANGELOG.md | 5 +-- src/suou/__init__.py | 3 +- src/suou/asgi.py | 12 +++++++ src/suou/color.py | 79 ++++++++++++++++++++++++++++++++++++++++++++ src/suou/luck.py | 8 +++-- src/suou/waiter.py | 1 + 6 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 src/suou/color.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f1123..c1fee43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## 0.7.0 "The Lucky Update" -+ Add RNG/random selection overloads such as `luck()`, `rng_overload()`. -+ Add 7 new throwable exceptions. ++ Add RNG/random selection overloads such as `luck()`, `rng_overload()` ++ Add 7 new throwable exceptions ++ Add color utilities: `chalk` module ## 0.6.1 diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 9f80088..a33a879 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -35,6 +35,7 @@ from .strtools import PrefixIdentifier from .validators import matches from .redact import redact_url_password from .http import WantsContentType +from .color import chalk __version__ = "0.7.0-dev37" @@ -46,7 +47,7 @@ __all__ = ( 'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType', 'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode', 'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode', - 'cb32decode', 'count_ones', 'dei_args', 'deprecated', 'ilex', 'join_bits', + 'cb32decode', 'chalk', 'count_ones', 'dei_args', 'deprecated', 'ilex', 'join_bits', 'jsonencode', 'kwargs_prefix', 'lex', 'ltuple', 'makelist', 'mask_shift', 'matches', 'mod_ceil', 'mod_floor', 'none_pass', 'not_implemented', 'redact_url_password', 'rtuple', 'split_bits', 'ssv_list', 'symbol_table', diff --git a/src/suou/asgi.py b/src/suou/asgi.py index 3f3fd70..7229920 100644 --- a/src/suou/asgi.py +++ b/src/suou/asgi.py @@ -1,5 +1,17 @@ """ +ASGI stuff +--- + +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 typing import Any, Awaitable, Callable, MutableMapping, ParamSpec, Protocol diff --git a/src/suou/color.py b/src/suou/color.py new file mode 100644 index 0000000..31ea031 --- /dev/null +++ b/src/suou/color.py @@ -0,0 +1,79 @@ +""" +Colors for coding artists + +NEW 0.7.0 + +--- + +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 functools import lru_cache + + +class Chalk: + """ + ANSI escape codes for terminal colors, similar to JavaScript's `chalk` library. + + Best used with Python 3.12+ that allows arbitrary nesting of f-strings. + + Yes, I am aware colorama exists. + + UNTESTED + + NEW 0.7.0 + """ + CSI = '\x1b[' + RED = CSI + "31m" + GREEN = CSI + "32m" + YELLOW = CSI + "33m" + BLUE = CSI + "34m" + CYAN = CSI + "36m" + PURPLE = CSI + "35m" + GREY = CSI + "90m" + END_COLOR = CSI + "39m" + BOLD = CSI + "1m" + END_BOLD = CSI + "22m" + FAINT = CSI + "2m" + def __init__(self, flags = (), ends = ()): + self._flags = tuple(flags) + self._ends = tuple(ends) + @lru_cache() + def _wrap(self, beg, end): + return Chalk(self._flags + (beg,), self._ends + (end,)) + def __call__(self, s: str) -> str: + return ''.join(self._flags) + s + ''.join(reversed(self._ends)) + def red(self): + return self._wrap(self.RED, self.END_COLOR) + def green(self): + return self._wrap(self.GREEN, self.END_COLOR) + def blue(self): + return self._wrap(self.BLUE, self.END_COLOR) + def yellow(self): + return self._wrap(self.YELLOW, self.END_COLOR) + def cyan(self): + return self._wrap(self.CYAN, self.END_COLOR) + def purple(self): + return self._wrap(self.PURPLE, self.END_COLOR) + def grey(self): + return self._wrap(self.GREY, self.END_COLOR) + gray = grey + marine = blue + def bold(self): + return self._wrap(self.BOLD, self.END_BOLD) + def faint(self): + return self._wrap(self.FAINT, self.END_BOLD) + + +## TODO make it lazy? +chalk = Chalk() diff --git a/src/suou/luck.py b/src/suou/luck.py index c22b552..7b8192e 100644 --- a/src/suou/luck.py +++ b/src/suou/luck.py @@ -1,5 +1,7 @@ """ -Fortune and esoterism helpers. +Fortune' RNG and esoterism. + +NEW 0.7.0 --- @@ -108,5 +110,5 @@ def rng_overload(prev_func: RngCallable[_T, _U] | int | None, /, *, weight: int return decorator - - \ No newline at end of file +# This module is experimental and therefore not re-exported into __init__ +__all__ = ('lucky', 'rng_overload') \ No newline at end of file diff --git a/src/suou/waiter.py b/src/suou/waiter.py index 74d2d0e..a210f45 100644 --- a/src/suou/waiter.py +++ b/src/suou/waiter.py @@ -54,4 +54,5 @@ def ko(status: int, /, content = None, **ka): return PlainTextResponse(content, status_code=status, **ka) return content +# This module is experimental and therefore not re-exported into __init__ __all__ = ('ko', 'ok', 'Waiter') \ No newline at end of file