diff --git a/CHANGELOG.md b/CHANGELOG.md index dfdeaef..6dafe7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,11 @@ + `sqlalchemy`: add `unbound_fk()`, `bound_fk()` + Add `sqlalchemy_async` module with `SQLAlchemy()` -+ Add `timed_cache()`, `TimedDict()`, `none_pass()`, `twocolon_list()`, `quote_css_string()`, `must_be()` ++ Add `timed_cache()`, `TimedDict()`, `none_pass()`, `twocolon_list()` + Add module `calendar` with `want_*` date type conversion utilities and `age_and_days()` + Move obsolete stuff to `obsolete` package (includes configparse 0.3 as of now) + Add `redact` module with `redact_url_password()` + Add more exceptions: `NotFoundError()`, `BabelTowerError()` -+ Add `sass` module ## 0.4.0 diff --git a/README.md b/README.md index 56da550..948b4af 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # SIS Unified Object Underarmor -Good morning, my brother! Welcome **SUOU** (**S**IS **U**nified **O**bject **U**nderarmor), the Python library which makes API development faster for developing API's, database schemas and stuff in Python. +Good morning, my brother! Welcome the SUOU (SIS Unified Object Underarmor), an utility library for developing API's, database schemas and stuff in Python. 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) -* helpers for use in Flask, SQLAlchemy, and other popular frameworks -* i forgor 💀 +* 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). @@ -26,22 +26,6 @@ $ pip install sakuragasaki46-suou[sqlalchemy] Please note that you probably already have those dependencies, if you just use the library. -## Features - -... - -## Support - -Just a heads up: SUOU was made to support Sakuragasaki46 (me)'s own selfish, egoistic needs. Not to provide a service to the public. - -As a consequence, 'add this add that' stuff is best-effort. - -Expect breaking changes, disruptive renames in bugfix releases, sudden deprecations, years of unmantainment, or sudden removal of SUOU from GH or pip. - -Don't want to depend on my codebase for moral reasons (albeit unrelated)? It's fine. I did not ask you. - -**DO NOT ASK TO MAKE SUOU SAFE FOR CHILDREN**. Enjoy having your fingers cut. - ## License Licensed under the [Apache License, Version 2.0](LICENSE), a non-copyleft free and open source license. @@ -52,5 +36,3 @@ I (sakuragasaki46) may NOT be held accountable for Your use of my code. > It's pointless to file a lawsuit because you feel damaged, and it's only going to turn against you. What a waste of money you could have spent on a vacation or charity, or invested in stocks. -Happy hacking. - diff --git a/pyproject.toml b/pyproject.toml index 2194046..d5ae206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "itsdangerous", "toml", "pydantic", - "setuptools>=78.0.0", "uvloop; os_name=='posix'" ] # - further devdependencies below - # @@ -45,7 +44,6 @@ flask_sqlalchemy = [ "Flask-SqlAlchemy", ] peewee = [ - ## HEADS UP! peewee has setup.py, may slow down installation "peewee>=3.0.0" ] markdown = [ @@ -53,12 +51,10 @@ markdown = [ ] quart = [ "Quart", - "Quart-Schema", - "starlette>=0.47.2" + "Quart-Schema" ] -sass = [ - ## HEADS UP!! libsass carries a C extension + uses setup.py - "libsass" +quart_sqlalchemy = [ + "Quart_SQLALchemy>=3.0.0, <4.0" ] full = [ @@ -67,8 +63,9 @@ full = [ "sakuragasaki46-suou[quart]", "sakuragasaki46-suou[peewee]", "sakuragasaki46-suou[markdown]", - "sakuragasaki46-suou[flask-sqlalchemy]", - "sakuragasaki46-suou[sass]" + "sakuragasaki46-suou[flask-sqlalchemy]" + # disabled: quart-sqlalchemy causes issues with anyone else. WON'T BE IMPLEMENTED + #"sakuragasaki46-suou[quart-sqlalchemy]" ] diff --git a/src/suou/asgi.py b/src/suou/asgi.py deleted file mode 100644 index 3f3fd70..0000000 --- a/src/suou/asgi.py +++ /dev/null @@ -1,25 +0,0 @@ -""" - -""" - -from typing import Any, Awaitable, Callable, MutableMapping, ParamSpec, Protocol - - -## TYPES ## - -# all the following is copied from Starlette -# available in starlette.types as of starlette==0.47.2 -P = ParamSpec("P") - -ASGIScope = MutableMapping[str, Any] -ASGIMessage = MutableMapping[str, Any] - -ASGIReceive = Callable[[], Awaitable[ASGIMessage]] -ASGISend = Callable[[ASGIMessage], Awaitable[None]] -ASGIApp = Callable[[ASGIScope, ASGIReceive, ASGISend], Awaitable[None]] - -class _MiddlewareFactory(Protocol[P]): - def __call__(self, app: ASGIApp, /, *args: P.args, **kwargs: P.kwargs) -> ASGIApp: ... # pragma: no cover - -## end TYPES ## - diff --git a/src/suou/codecs.py b/src/suou/codecs.py index e5f94af..22e52f5 100644 --- a/src/suou/codecs.py +++ b/src/suou/codecs.py @@ -304,12 +304,6 @@ def twocolon_list(s: str | None) -> list[str]: return [] return [x.strip() for x in s.split('::')] -def quote_css_string(s): - """Quotes a string as CSS string literal. - - Source: libsass==0.23.0""" - return "'" + ''.join(('\\%06x' % ord(c)) for c in s) + "'" - class StringCase(enum.Enum): """ Enum values used by regex validators and storage converters. diff --git a/src/suou/functools.py b/src/suou/functools.py index 90f807e..128a1ec 100644 --- a/src/suou/functools.py +++ b/src/suou/functools.py @@ -16,24 +16,21 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. import math import time -from typing import Callable, TypeVar +from typing import Callable import warnings from functools import wraps, lru_cache -_T = TypeVar('_T') -_U = TypeVar('_U') - try: from warnings import deprecated except ImportError: # Python <=3.12 does not implement warnings.deprecated - def deprecated(message: str, /, *, category=DeprecationWarning, stacklevel: int = 1) -> Callable[[Callable[_T, _U]], Callable[_T, _U]]: + def deprecated(message: str, /, *, category=DeprecationWarning, stacklevel: int = 1): """ Backport of PEP 702 for Python <=3.12. The stack_level stuff is not reimplemented on purpose because too obscure for the average programmer. """ - def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]: + def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*a, **ka): if category is not None: @@ -92,7 +89,7 @@ def timed_cache(ttl: int, maxsize: int = 128, typed: bool = False) -> Callable[[ return wrapper return decorator -def none_pass(func: Callable, *args, **kwargs) -> Callable: +def none_pass(func: Callable, *args, **kwargs): """ Wrap callable so that gets called only on not None values. diff --git a/src/suou/sass.py b/src/suou/sass.py deleted file mode 100644 index c986d63..0000000 --- a/src/suou/sass.py +++ /dev/null @@ -1,138 +0,0 @@ -""" - -""" - - -import datetime -import logging -import os -from typing import Callable, Mapping -from sass import CompileError -from sassutils.builder import Manifest -from importlib.metadata import version as _get_version - -from .codecs import quote_css_string -from .validators import must_be -from .asgi import _MiddlewareFactory, ASGIApp, ASGIReceive, ASGIScope, ASGISend -from . import __version__ as _suou_version - -from pkg_resources import resource_filename - -logger = logging.getLogger(__name__) - -## NOTE Python/PSF recommends use of importlib.metadata for version checks. -_libsass_version = _get_version('libsass') - -class SassAsyncMiddleware(_MiddlewareFactory): - """ - ASGI middleware for development purpose. - Every time a CSS file has requested it finds a matched - Sass/SCSS source file andm then compiled it into CSS. - - Eventual syntax errors are displayed in three ways: - - heading CSS comment (i.e. `/* Error: invalid pro*/`) - - **red text** in `body::before` (in most cases very evident, since every other - style fails to render!) - - server-side logging (level is *error*, remember to enable logging!) - - app = ASGI application to wrap - manifests = a Mapping of build settings, see sass_manifests= option - in `setup.py` - - Shamelessly adapted from libsass==0.23.0 with modifications - - XXX experimental and untested! - """ - - def __init__( - self, app: ASGIApp, manifests: Mapping, package_dir = {}, - error_status = '200 OK' - ): - self.app = must_be(app, Callable, 'app must be a ASGI-compliant callable') - self.manifests = Manifest.normalize_manifests(manifests) - self.package_dir = dict(must_be(package_dir, Mapping, 'package_dir must be a mapping')) - ## ??? - self.error_status = error_status - for package_name in self.manifests: - if package_name in self.package_dir: - continue - self.package_dir[package_name] = resource_filename(package_name, '') - self.paths: list[tuple[str, str, Manifest]] = [] - for pkgname, manifest in self.manifests.items(): - ## WSGI path — is it valid for ASGI as well?? - asgi_path = f'/{manifest.wsgi_path.strip('/')}/' - pkg_dir = self.package_dir[pkgname] - self.paths.append((asgi_path, package_dir, manifest)) - - async def __call__(self, /, scope: ASGIScope, receive: ASGIReceive, send: ASGISend): - path: str = scope.get('path') - if path.endswith('.css'): - for prefix, package_dir, manifest in self.paths: - if not path.startswith(prefix): - continue - css_filename = path[len(prefix):] - sass_filename = manifest.unresolve_filename(package_dir, css_filename) - try: - ## TODO consider async?? - result = manifest.build_one( - package_dir, - sass_filename, - source_map=True - ) - except OSError: - break - except CompileError as e: - logger.error(str(e)) - await send({ - 'type': 'http.response.start', - 'status': self.error_status, - 'headers': [ - 'Content-Type: text/css; charset=utf-8' - ] - }) - await send({ - 'type': 'http.response.body', - 'body': '\n'.join([ - '/*', - str(e), - '***', - f'libsass {_libsass_version} + suou {_suou_version} {datetime.datetime.now().isoformat()}', - '*/', - '', - 'body::before {', - f' content: {quote_css_string(str(e))};', - ' color: maroon;', - ' background-color: white;', - ' white-space: pre-wrap;', - ' display: block;', - ' font-family: monospace;', - ' user-select: text;' - '}' - ]).encode('utf-8') - }) - - async def _read_file(path): - with open(path, 'rb') as f: - while True: - chunk = f.read(4096) - if chunk: - yield chunk - else: - break - - await send({ - 'type': 'http.response.start', - 'status': 200, - 'headers': [ - 'Content-Type: text/css; charset=utf-8' - ] - }) - async for chunk in _read_file(os.path.join(package_dir, result)): - await send({ - 'type': 'http.response.body', - 'body': chunk - }) - - await self.app(scope, receive, send) - - diff --git a/src/suou/validators.py b/src/suou/validators.py index 609b99e..037d2b6 100644 --- a/src/suou/validators.py +++ b/src/suou/validators.py @@ -16,10 +16,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. import re -from typing import Any, Iterable, TypeVar - -_T = TypeVar('_T') - def matches(regex: str | int, /, length: int = 0, *, flags=0): """ Return a function which returns true if X is shorter than length and matches the given regex. @@ -31,13 +27,5 @@ def matches(regex: str | int, /, length: int = 0, *, flags=0): return (not length or len(s) < length) and bool(re.fullmatch(regex, s, flags=flags)) return validator -def must_be(obj: _T | Any, typ: type[_T] | Iterable[type], message: str, *, exc = TypeError) -> _T: - """ - Raise TypeError if the requested object is not of the desired type(s), with a nice message. - """ - if not isinstance(obj, typ): - raise TypeError(f'{message}, not {obj.__class__.__name__!r}') - return obj - __all__ = ('matches', ) \ No newline at end of file