Compare commits
No commits in common. "6daedc3dee29bdaec1cd35ee37e27a65dea41c24" and "2c52f9b5612540070db8e08f566d79f865b8e645" have entirely different histories.
6daedc3dee
...
2c52f9b561
8 changed files with 14 additions and 220 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
24
README.md
24
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ##
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
138
src/suou/sass.py
138
src/suou/sass.py
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
@ -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', )
|
||||
Loading…
Add table
Add a link
Reference in a new issue