diff --git a/CHANGELOG.md b/CHANGELOG.md index 8922767..c77b9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ + Module `sqlalchemy`: * removed deprecated alias `entity_base()`. use `declarative_base()` instead. * fix imports. -+ Module `functools`: add `cooldown()` ++ Module `functools`: add `cooldown()`, `do_not_flood()` ++ Module `color`: add `ColorFormatter()` ++ Separated `suou[waiter]` dependency from `suou[quart]` ## 0.12.6 diff --git a/pyproject.toml b/pyproject.toml index 5375510..0982b7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,9 @@ markdown = [ ] quart = [ "Quart", - "Quart-Schema", + "Quart-Schema" +] +waiter = [ "starlette>=0.47.2" ] quart_auth = [ @@ -78,7 +80,8 @@ full = [ "suou[quart_auth]", # includes quart, sqlalchemy and quart_sqlalchemy "suou[peewee]", "suou[markdown]", - "suou[sass]" + "suou[sass]", + "suou[waiter]" ] docs = [ diff --git a/src/suou/__init__.py b/src/suou/__init__.py index c90e37c..fadb067 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -18,13 +18,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. from .iding import Siq, SiqCache, SiqType, SiqGen from .codecs import (StringCase, cb32encode, cb32decode, b32lencode, b32ldecode, b64encode, b64decode, b2048encode, b2048decode, - jsonencode, twocolon_list, want_bytes, want_str, ssv_list, want_urlsafe, want_urlsafe_bytes) + jsonencode, twocolon_list, want_bytes, want_str, ssv_list, want_urlsafe, want_urlsafe_bytes, + z85encode, z85decode) from .bits import count_ones, mask_shift, split_bits, join_bits, mod_ceil, mod_floor from .calendar import want_datetime, want_isodate, want_timestamp, age_and_days from .configparse import MissingConfigError, MissingConfigWarning, ConfigOptions, ConfigParserConfigSource, ConfigSource, DictConfigSource, ConfigValue, EnvConfigSource from .collections import TimedDict from .dei import dei_args -from .functools import deprecated, not_implemented, timed_cache, none_pass, alru_cache, future +from .functools import deprecated, not_implemented, timed_cache, none_pass, alru_cache, future, cooldown, do_not_flood from .classtools import Wanted, Incomplete from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem, addattr from .i18n import I18n, JsonI18n, TomlI18n @@ -32,16 +33,18 @@ from .signing import UserSigner from .snowflake import Snowflake, SnowflakeGen from .lex import symbol_table, lex, ilex from .strtools import PrefixIdentifier -from .validators import matches, not_less_than, not_greater_than, yesno +from .validators import matches, not_less_than, not_greater_than, yesno, must_be from .redact import redact_url_password from .http import WantsContentType -from .color import OKLabColor, chalk, WebColor, RGBColor, LinearRGBColor, XYZColor, OKLCHColor +from .color import OKLabColor, chalk, WebColor, RGBColor, LinearRGBColor, \ + XYZColor, OKLCHColor, ColorFormatter from .mat import Matrix from .argparse import LetterSubparsers -__version__ = "0.13.0a2" +__version__ = "0.13.0a4" __all__ = ( + 'ColorFormatter', 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', 'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n', 'LetterSubparsers', 'LinearRGBColor', @@ -52,7 +55,7 @@ __all__ = ( 'WebColor', 'XYZColor', 'addattr', 'additem', 'age_and_days', 'alru_cache', 'b2048decode', 'b2048encode', 'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode', - 'cb32decode', 'chalk', 'count_ones', 'dei_args', 'deprecated', + 'cb32decode', 'chalk', 'cooldown', 'count_ones', 'dei_args', 'deprecated', 'do_not_flood', 'future', 'ilex', 'join_bits', 'jsonencode', 'kwargs_prefix', 'lex', 'ltuple', 'makelist', 'mask_shift', 'matches', 'mod_ceil', 'mod_floor', 'must_be', 'none_pass', 'not_implemented', diff --git a/src/suou/argparse.py b/src/suou/argparse.py index c235a30..563d6be 100644 --- a/src/suou/argparse.py +++ b/src/suou/argparse.py @@ -19,7 +19,7 @@ from __future__ import annotations import argparse import sys -from typing import Callable, Mapping +from typing import Callable class LetterSubparsers(object): """ diff --git a/src/suou/color.py b/src/suou/color.py index 26bf060..5755a9c 100644 --- a/src/suou/color.py +++ b/src/suou/color.py @@ -21,6 +21,7 @@ from __future__ import annotations from collections import namedtuple from functools import lru_cache +import logging import math from suou.functools import deprecated @@ -331,4 +332,48 @@ class OKLCHColor(namedtuple('_OKLCHColor', 'l c h')): return sum(abs(i - j) / k for i, j, k in zip(self, other, (1, 1, 36))) -__all__ = ('chalk', 'WebColor', "RGBColor", 'LinearRGBColor', 'XYZColor', 'OKLabColor', 'OKLCHColor') +class ColorFormatter(logging.Formatter): + """ + Colored logging formatter. + + Opinionated. + + Taken from https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output + """ + + def _get_base_format(color): + return f"[%(asctime)s] {color('%(levelname)s')}{chalk.grey(': ')}{color('%(name)s')}{chalk.grey(': ')}%(message)s" + + FORMATS = { + logging.DEBUG: _get_base_format(chalk.cyan), + logging.INFO: _get_base_format(chalk.green), + logging.WARNING: _get_base_format(chalk.yellow), + logging.ERROR: _get_base_format(chalk.red), + logging.CRITICAL: _get_base_format(chalk.bold.red) + } + + del _get_base_format + + def format(self, record: logging.LogRecord) -> str: + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + @classmethod + def apply_handler(cls, logger: logging.Logger, level = logging.INFO): + """ + Apply the colored formatter to a logger. + + Use this with logging.root, instead of logging.basicConfig(). + + Should not be called more than once. + """ + logger.setLevel(level) + ch = logging.StreamHandler() + ch.setLevel(level) + ch.setFormatter(cls()) + logger.addHandler(ch) + + +__all__ = ('chalk', 'WebColor', "RGBColor", 'LinearRGBColor', 'XYZColor', + 'OKLabColor', 'OKLCHColor', 'ColorFormatter') diff --git a/src/suou/functools.py b/src/suou/functools.py index dafd0d6..d7d1166 100644 --- a/src/suou/functools.py +++ b/src/suou/functools.py @@ -382,6 +382,32 @@ def cooldown(unit: int, /, exception: Exception | None = None): return wrapper return decorator +def do_not_flood(unit = .25): + """ + Implement a calling cooldown for a function or procedure. + + If the decorated function is called during the cooldown, the function + blocks before being called again. + + This is blocking and uses time.sleep(). + """ + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + now = time.time() + if wrapper.timeout_until is not None and wrapper.timeout_until > now: + wrapper.timeout_delay += unit + time.sleep(wrapper.timeout_until - now) + wrapper.timeout_until = now + wrapper.timeout_delay + else: + wrapper.timeout_delay = unit + wrapper.timeout_until = time.time() + unit + return func(*args, **kwargs) + wrapper.timeout_until = None + wrapper.timeout_delay = unit + return wrapper + return decorator + __all__ = ( - 'deprecated', 'not_implemented', 'timed_cache', 'none_pass', 'alru_cache', 'cooldown' + 'deprecated', 'not_implemented', 'timed_cache', 'none_pass', 'alru_cache', 'cooldown', 'do_not_flood' )