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()`
|
+ `sqlalchemy`: add `unbound_fk()`, `bound_fk()`
|
||||||
+ Add `sqlalchemy_async` module with `SQLAlchemy()`
|
+ 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()`
|
+ 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)
|
+ Move obsolete stuff to `obsolete` package (includes configparse 0.3 as of now)
|
||||||
+ Add `redact` module with `redact_url_password()`
|
+ Add `redact` module with `redact_url_password()`
|
||||||
+ Add more exceptions: `NotFoundError()`, `BabelTowerError()`
|
+ Add more exceptions: `NotFoundError()`, `BabelTowerError()`
|
||||||
+ Add `sass` module
|
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -1,12 +1,12 @@
|
||||||
# SIS Unified Object Underarmor
|
# 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:
|
It provides utilities such as:
|
||||||
* [SIQ](https://yusur.moe/protocols/siq.html)
|
* [SIQ](https://yusur.moe/protocols/siq.html)
|
||||||
* signing and generation of access tokens, on top of [ItsDangerous](https://github.com/pallets/itsdangerous)
|
* 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
|
* helpers for use in Flask and SQLAlchemy
|
||||||
* i forgor 💀
|
* ...
|
||||||
|
|
||||||
**It is not an ORM** nor a replacement of it; it works along existing ORMs (currently only SQLAlchemy is supported lol).
|
**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.
|
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
|
## License
|
||||||
|
|
||||||
Licensed under the [Apache License, Version 2.0](LICENSE), a non-copyleft free and open source 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.
|
> 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",
|
"itsdangerous",
|
||||||
"toml",
|
"toml",
|
||||||
"pydantic",
|
"pydantic",
|
||||||
"setuptools>=78.0.0",
|
|
||||||
"uvloop; os_name=='posix'"
|
"uvloop; os_name=='posix'"
|
||||||
]
|
]
|
||||||
# - further devdependencies below - #
|
# - further devdependencies below - #
|
||||||
|
|
@ -45,7 +44,6 @@ flask_sqlalchemy = [
|
||||||
"Flask-SqlAlchemy",
|
"Flask-SqlAlchemy",
|
||||||
]
|
]
|
||||||
peewee = [
|
peewee = [
|
||||||
## HEADS UP! peewee has setup.py, may slow down installation
|
|
||||||
"peewee>=3.0.0"
|
"peewee>=3.0.0"
|
||||||
]
|
]
|
||||||
markdown = [
|
markdown = [
|
||||||
|
|
@ -53,12 +51,10 @@ markdown = [
|
||||||
]
|
]
|
||||||
quart = [
|
quart = [
|
||||||
"Quart",
|
"Quart",
|
||||||
"Quart-Schema",
|
"Quart-Schema"
|
||||||
"starlette>=0.47.2"
|
|
||||||
]
|
]
|
||||||
sass = [
|
quart_sqlalchemy = [
|
||||||
## HEADS UP!! libsass carries a C extension + uses setup.py
|
"Quart_SQLALchemy>=3.0.0, <4.0"
|
||||||
"libsass"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
full = [
|
full = [
|
||||||
|
|
@ -67,8 +63,9 @@ full = [
|
||||||
"sakuragasaki46-suou[quart]",
|
"sakuragasaki46-suou[quart]",
|
||||||
"sakuragasaki46-suou[peewee]",
|
"sakuragasaki46-suou[peewee]",
|
||||||
"sakuragasaki46-suou[markdown]",
|
"sakuragasaki46-suou[markdown]",
|
||||||
"sakuragasaki46-suou[flask-sqlalchemy]",
|
"sakuragasaki46-suou[flask-sqlalchemy]"
|
||||||
"sakuragasaki46-suou[sass]"
|
# 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 []
|
||||||
return [x.strip() for x in s.split('::')]
|
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):
|
class StringCase(enum.Enum):
|
||||||
"""
|
"""
|
||||||
Enum values used by regex validators and storage converters.
|
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 math
|
||||||
import time
|
import time
|
||||||
from typing import Callable, TypeVar
|
from typing import Callable
|
||||||
import warnings
|
import warnings
|
||||||
from functools import wraps, lru_cache
|
from functools import wraps, lru_cache
|
||||||
|
|
||||||
_T = TypeVar('_T')
|
|
||||||
_U = TypeVar('_U')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Python <=3.12 does not implement warnings.deprecated
|
# 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.
|
Backport of PEP 702 for Python <=3.12.
|
||||||
The stack_level stuff is not reimplemented on purpose because
|
The stack_level stuff is not reimplemented on purpose because
|
||||||
too obscure for the average programmer.
|
too obscure for the average programmer.
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]:
|
def decorator(func: Callable) -> Callable:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*a, **ka):
|
def wrapper(*a, **ka):
|
||||||
if category is not None:
|
if category is not None:
|
||||||
|
|
@ -92,7 +89,7 @@ def timed_cache(ttl: int, maxsize: int = 128, typed: bool = False) -> Callable[[
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
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.
|
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
|
import re
|
||||||
|
|
||||||
from typing import Any, Iterable, TypeVar
|
|
||||||
|
|
||||||
_T = TypeVar('_T')
|
|
||||||
|
|
||||||
def matches(regex: str | int, /, length: int = 0, *, flags=0):
|
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.
|
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 (not length or len(s) < length) and bool(re.fullmatch(regex, s, flags=flags))
|
||||||
return validator
|
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', )
|
__all__ = ('matches', )
|
||||||
Loading…
Add table
Add a link
Reference in a new issue