Compare commits

..

3 commits

Author SHA1 Message Date
83ab616e13 add chalk 2025-09-19 15:39:44 +02:00
a2fdc9166f 0.7.x: @lucky, @rng_overload and more exceptions 2025-09-19 13:34:51 +02:00
3de5a3629d fix build artifacts 2025-09-13 21:14:31 +02:00
11 changed files with 275 additions and 4 deletions

View file

@ -1,5 +1,11 @@
# Changelog # Changelog
## 0.7.0 "The Lucky Update"
+ Add RNG/random selection overloads such as `luck()`, `rng_overload()`
+ Add 7 new throwable exceptions
+ Add color utilities: `chalk` module
## 0.6.1 ## 0.6.1
- First release on PyPI under the name `suou`. - First release on PyPI under the name `suou`.

View file

@ -1 +0,0 @@

View file

@ -0,0 +1 @@
moved to [suou](https://pypi.org/project/suou)

View file

@ -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"

View file

@ -35,8 +35,9 @@ from .strtools import PrefixIdentifier
from .validators import matches from .validators import matches
from .redact import redact_url_password from .redact import redact_url_password
from .http import WantsContentType from .http import WantsContentType
from .color import chalk
__version__ = "0.6.1" __version__ = "0.7.0-dev37"
__all__ = ( __all__ = (
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
@ -46,7 +47,7 @@ __all__ = (
'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType', 'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType',
'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode', 'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode',
'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode', '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', 'jsonencode', 'kwargs_prefix', 'lex', 'ltuple', 'makelist', 'mask_shift',
'matches', 'mod_ceil', 'mod_floor', 'none_pass', 'not_implemented', 'matches', 'mod_ceil', 'mod_floor', 'none_pass', 'not_implemented',
'redact_url_password', 'rtuple', 'split_bits', 'ssv_list', 'symbol_table', 'redact_url_password', 'rtuple', 'split_bits', 'ssv_list', 'symbol_table',

View file

@ -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 from typing import Any, Awaitable, Callable, MutableMapping, ParamSpec, Protocol

79
src/suou/color.py Normal file
View file

@ -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()

View file

@ -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. 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): class MissingConfigError(LookupError):
""" """
Config variable not found. Config variable not found.
@ -53,6 +64,35 @@ class BabelTowerError(NotFoundError):
The user requested a language that cannot be understood. 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__ = ( __all__ = (
'MissingConfigError', 'MissingConfigWarning', 'LexError', 'InconsistencyError', 'NotFoundError' 'MissingConfigError', 'MissingConfigWarning', 'LexError', 'InconsistencyError', 'NotFoundError'
) )

View file

@ -2,6 +2,16 @@
Utilities for tokenization of text. 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 from re import Match

114
src/suou/luck.py Normal file
View file

@ -0,0 +1,114 @@
"""
Fortune' RNG and esoterism.
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 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
# This module is experimental and therefore not re-exported into __init__
__all__ = ('lucky', 'rng_overload')

View file

@ -54,4 +54,5 @@ def ko(status: int, /, content = None, **ka):
return PlainTextResponse(content, status_code=status, **ka) return PlainTextResponse(content, status_code=status, **ka)
return content return content
# This module is experimental and therefore not re-exported into __init__
__all__ = ('ko', 'ok', 'Waiter') __all__ = ('ko', 'ok', 'Waiter')