Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1d0c62b44 | |||
| ef645bd4da | |||
| e6ee355f2e | |||
| 75adb9fbff | |||
| d123b9c196 | |||
| eca16d781f |
23 changed files with 464 additions and 321 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -29,6 +29,7 @@ aliases/*/src
|
||||||
docs/_build
|
docs/_build
|
||||||
docs/_static
|
docs/_static
|
||||||
docs/templates
|
docs/templates
|
||||||
|
.coverage
|
||||||
|
|
||||||
# changes during CD/CI
|
# changes during CD/CI
|
||||||
aliases/*/pyproject.toml
|
aliases/*/pyproject.toml
|
||||||
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -1,5 +1,16 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.0 "The Color Update"
|
||||||
|
|
||||||
|
* All `AuthSrc()` derivatives, deprecated and never used, have been removed.
|
||||||
|
* New module `mat` adds a shallow reimplementation of `Matrix()` in order to implement matrix multiplication
|
||||||
|
* Removed obsolete `configparse` implementation that has been around since 0.3 and shelved since 0.4.
|
||||||
|
* `color`: added support for conversion from RGB to sRGB, XYZ, OKLab and OKLCH.
|
||||||
|
|
||||||
|
## 0.11.2
|
||||||
|
|
||||||
|
+ increase test coverage of `validators`
|
||||||
|
|
||||||
## 0.11.1
|
## 0.11.1
|
||||||
|
|
||||||
+ make `yesno()` accept boolean types
|
+ make `yesno()` accept boolean types
|
||||||
|
|
|
||||||
19
docs/color.rst
Normal file
19
docs/color.rst
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
Color
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. currentmodule:: suou.color
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
Web colors
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. autoclass:: RGBColor
|
||||||
|
|
||||||
|
|
||||||
|
.. autoclass:: WebColor
|
||||||
|
|
||||||
|
|
||||||
|
.. autoclass:: XYZColor
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
suou.codecs
|
suou.codecs
|
||||||
===========
|
===========
|
||||||
|
|
||||||
.. automodule:: suou.codecs
|
.. automodule:: suou.codecs
|
||||||
|
|
@ -16,6 +16,7 @@ suou.codecs
|
||||||
b64encode
|
b64encode
|
||||||
cb32decode
|
cb32decode
|
||||||
cb32encode
|
cb32encode
|
||||||
|
cb32lencode
|
||||||
jsonencode
|
jsonencode
|
||||||
quote_css_string
|
quote_css_string
|
||||||
rb64decode
|
rb64decode
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
suou.color
|
suou.color
|
||||||
==========
|
==========
|
||||||
|
|
||||||
.. automodule:: suou.color
|
.. automodule:: suou.color
|
||||||
|
|
@ -9,5 +9,7 @@ suou.color
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
|
|
||||||
Chalk
|
Chalk
|
||||||
|
RGBColor
|
||||||
|
SRGBColor
|
||||||
WebColor
|
WebColor
|
||||||
|
|
||||||
|
|
@ -1,18 +1,6 @@
|
||||||
suou.flask\_sqlalchemy
|
suou.flask\_sqlalchemy
|
||||||
======================
|
======================
|
||||||
|
|
||||||
.. automodule:: suou.flask_sqlalchemy
|
.. automodule:: suou.flask_sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
.. rubric:: Functions
|
|
||||||
|
|
||||||
.. autosummary::
|
|
||||||
|
|
||||||
require_auth
|
|
||||||
|
|
||||||
.. rubric:: Classes
|
|
||||||
|
|
||||||
.. autosummary::
|
|
||||||
|
|
||||||
FlaskAuthSrc
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
suou.legal
|
suou.legal
|
||||||
==========
|
==========
|
||||||
|
|
||||||
.. automodule:: suou.legal
|
.. automodule:: suou.legal
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: Classes
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
|
||||||
|
Lawyer
|
||||||
|
|
||||||
23
docs/generated/suou.peewee.rst
Normal file
23
docs/generated/suou.peewee.rst
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
suou.peewee
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. automodule:: suou.peewee
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: Functions
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
|
||||||
|
connect_reconnect
|
||||||
|
|
||||||
|
.. rubric:: Classes
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
|
||||||
|
ConnectToDatabase
|
||||||
|
PeeweeConnectionState
|
||||||
|
ReconnectMysqlDatabase
|
||||||
|
RegexCharField
|
||||||
|
SiqField
|
||||||
|
SnowflakeField
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
suou.strtools
|
suou.strtools
|
||||||
=============
|
=============
|
||||||
|
|
||||||
.. automodule:: suou.strtools
|
.. automodule:: suou.strtools
|
||||||
|
|
@ -9,4 +9,5 @@ suou.strtools
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
|
|
||||||
PrefixIdentifier
|
PrefixIdentifier
|
||||||
|
SpitText
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
suou.validators
|
suou.validators
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. automodule:: suou.validators
|
.. automodule:: suou.validators
|
||||||
|
|
@ -12,4 +12,5 @@ suou.validators
|
||||||
must_be
|
must_be
|
||||||
not_greater_than
|
not_greater_than
|
||||||
not_less_than
|
not_less_than
|
||||||
|
yesno
|
||||||
|
|
||||||
|
|
@ -16,4 +16,5 @@ ease programmer's QoL and write shorter and cleaner code that works.
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
iding
|
iding
|
||||||
validators
|
validators
|
||||||
|
color
|
||||||
api
|
api
|
||||||
|
|
@ -35,17 +35,19 @@ 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
|
||||||
from .redact import redact_url_password
|
from .redact import redact_url_password
|
||||||
from .http import WantsContentType
|
from .http import WantsContentType
|
||||||
from .color import chalk, WebColor
|
from .color import OKLabColor, chalk, WebColor, RGBColor, SRGBColor, XYZColor, OKLabColor
|
||||||
|
from .mat import Matrix
|
||||||
|
|
||||||
__version__ = "0.11.1"
|
__version__ = "0.12.0a5"
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
|
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
|
||||||
'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n',
|
'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n',
|
||||||
'MissingConfigError', 'MissingConfigWarning', 'PrefixIdentifier',
|
'Matrix', 'MissingConfigError', 'MissingConfigWarning', 'OKLabColor',
|
||||||
|
'PrefixIdentifier', 'RGBColor', 'SRGBColor',
|
||||||
'Siq', 'SiqCache', 'SiqGen', 'SiqType', 'Snowflake', 'SnowflakeGen',
|
'Siq', 'SiqCache', 'SiqGen', 'SiqType', 'Snowflake', 'SnowflakeGen',
|
||||||
'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType',
|
'StringCase', 'TimedDict', 'TomlI18n', 'UserSigner', 'Wanted', 'WantsContentType',
|
||||||
'WebColor',
|
'WebColor', 'XYZColor',
|
||||||
'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', 'chalk', 'count_ones', 'dei_args', 'deprecated',
|
'cb32decode', 'chalk', 'count_ones', 'dei_args', 'deprecated',
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
import math
|
||||||
|
|
||||||
|
from suou.mat import Matrix
|
||||||
|
|
||||||
|
|
||||||
class Chalk:
|
class Chalk:
|
||||||
|
|
@ -93,11 +96,14 @@ chalk = Chalk()
|
||||||
|
|
||||||
## Utilities for web colors
|
## Utilities for web colors
|
||||||
|
|
||||||
class WebColor(namedtuple('_WebColor', 'red green blue')):
|
class RGBColor(namedtuple('_WebColor', 'red green blue')):
|
||||||
"""
|
"""
|
||||||
Representation of a color in the TrueColor space (aka rgb).
|
Representation of a color in the RGB TrueColor space.
|
||||||
|
|
||||||
Useful for theming.
|
Useful for theming.
|
||||||
|
|
||||||
|
*Changed in 0.12.0*: name is now RGBColor, with WebColor being an alias.
|
||||||
|
Added conversions to and from OKLCH, OKLab, sRGB, and XYZ.
|
||||||
"""
|
"""
|
||||||
def lighten(self, *, factor = .75):
|
def lighten(self, *, factor = .75):
|
||||||
"""
|
"""
|
||||||
|
|
@ -126,21 +132,176 @@ class WebColor(namedtuple('_WebColor', 'red green blue')):
|
||||||
"""
|
"""
|
||||||
return self.darken(factor=factor) + self.lighten(factor=factor)
|
return self.darken(factor=factor) + self.lighten(factor=factor)
|
||||||
|
|
||||||
def blend_with(self, other: WebColor):
|
def blend_with(self, other: RGBColor):
|
||||||
"""
|
"""
|
||||||
Mix two colors, returning the average.
|
Mix two colors, returning the average.
|
||||||
"""
|
"""
|
||||||
return WebColor (
|
return RGBColor (
|
||||||
(self.red + other.red) // 2,
|
(self.red + other.red) // 2,
|
||||||
(self.green + other.green) // 2,
|
(self.green + other.green) // 2,
|
||||||
(self.blue + other.blue) // 2
|
(self.blue + other.blue) // 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_srgb(self):
|
||||||
|
"""
|
||||||
|
Convert to sRGB space.
|
||||||
|
|
||||||
|
*New in 0.12.0*
|
||||||
|
"""
|
||||||
|
return SRGBColor(*(
|
||||||
|
(i / 12.92 if abs(i) <= 0.04045 else
|
||||||
|
(-1 if i < 0 else 1) * (((abs(i) + 0.55)) / 1.055) ** 2.4) for i in self
|
||||||
|
))
|
||||||
|
|
||||||
|
def to_oklab(self):
|
||||||
|
return self.to_xyz().to_oklab()
|
||||||
|
|
||||||
__add__ = blend_with
|
__add__ = blend_with
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"rgb({self.red}, {self.green}, {self.blue})"
|
return f"rgb({self.red}, {self.green}, {self.blue})"
|
||||||
|
|
||||||
|
RGB_TO_XYZ = Matrix([
|
||||||
|
[0.41239079926595934, 0.357584339383878, 0.1804807884018343],
|
||||||
|
[0.21263900587151027, 0.715168678767756, 0.07219231536073371],
|
||||||
|
[0.01933081871559182, 0.11919477979462598, 0.9505321522496607]
|
||||||
|
])
|
||||||
|
|
||||||
__all__ = ('chalk', 'WebColor')
|
def to_xyz(self):
|
||||||
|
return XYZColor(*(self.RGB_TO_XYZ @ Matrix.as_column(self)).get_column())
|
||||||
|
|
||||||
|
def to_oklch(self):
|
||||||
|
return self.to_xyz().to_oklch()
|
||||||
|
|
||||||
|
def to_oklab(self):
|
||||||
|
return self.to_xyz().to_oklab()
|
||||||
|
|
||||||
|
WebColor = RGBColor
|
||||||
|
|
||||||
|
## The following have been adapted from
|
||||||
|
## https://gist.github.com/dkaraush/65d19d61396f5f3cd8ba7d1b4b3c9432
|
||||||
|
|
||||||
|
class SRGBColor(namedtuple('_SRGBColor', 'red green blue')):
|
||||||
|
"""
|
||||||
|
Represent a color in the sRGB space.
|
||||||
|
|
||||||
|
*New in 0.12.0*
|
||||||
|
"""
|
||||||
|
red: float
|
||||||
|
green: float
|
||||||
|
blue: float
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"srgb({self.red}, {self.green}, {self.blue})"
|
||||||
|
|
||||||
|
def to_rgb(self):
|
||||||
|
return RGBColor(*(
|
||||||
|
((-1 if i < 0 else 1) * (1.055 * (abs(i) ** (1/2.4)) - 0.055)
|
||||||
|
if abs(i) > 0.0031308 else 12.92 * i) for i in self))
|
||||||
|
|
||||||
|
def to_xyz(self):
|
||||||
|
return self.to_rgb().to_xyz()
|
||||||
|
|
||||||
|
def to_oklab(self):
|
||||||
|
return self.to_rgb().to_oklab()
|
||||||
|
|
||||||
|
|
||||||
|
class XYZColor(namedtuple('_XYZColor', 'x y z')):
|
||||||
|
"""
|
||||||
|
Represent a color in the XYZ color space.
|
||||||
|
|
||||||
|
*New in 0.12.0*
|
||||||
|
"""
|
||||||
|
|
||||||
|
XYZ_TO_RGB = Matrix([
|
||||||
|
[ 3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
||||||
|
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
||||||
|
[ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
|
||||||
|
])
|
||||||
|
|
||||||
|
XYZ_TO_LMS = Matrix([
|
||||||
|
[0.8190224379967030, 0.3619062600528904, -0.1288737815209879],
|
||||||
|
[0.0329836539323885, 0.9292868615863434, 0.0361446663506424],
|
||||||
|
[0.0481771893596242, 0.2642395317527308, 0.6335478284694309]
|
||||||
|
])
|
||||||
|
|
||||||
|
LMSG_TO_OKLAB = Matrix([
|
||||||
|
[0.2104542683093140, 0.7936177747023054, -0.0040720430116193],
|
||||||
|
[1.9779985324311684, -2.4285922420485799, 0.4505937096174110],
|
||||||
|
[0.0259040424655478, 0.7827717124575296, -0.8086757549230774]
|
||||||
|
])
|
||||||
|
|
||||||
|
def to_rgb(self):
|
||||||
|
return RGBColor(*(self.XYZ_TO_RGB @ Matrix.as_column(self)).get_column())
|
||||||
|
|
||||||
|
def to_oklab(self):
|
||||||
|
lms = (self.XYZ_TO_LMS @ Matrix.as_column(self)).get_column()
|
||||||
|
lmsg = [math.cbrt(i) for i in lms]
|
||||||
|
oklab = (self.LMSG_TO_OKLAB @ Matrix.as_column(self)).get_column()
|
||||||
|
return OKLabColor(*oklab)
|
||||||
|
|
||||||
|
def to_oklch(self):
|
||||||
|
return self.to_oklab().to_oklch()
|
||||||
|
|
||||||
|
|
||||||
|
class OKLabColor(namedtuple('_OKLabColor', 'l a b')):
|
||||||
|
"""
|
||||||
|
Represent a color in the OKLab color space.
|
||||||
|
|
||||||
|
*New in 0.12.0*
|
||||||
|
"""
|
||||||
|
|
||||||
|
OKLAB_TO_LMSG = Matrix([
|
||||||
|
[1., 0.3963377773761749, 0.2158037573099136],
|
||||||
|
[1., -0.1055613458156586, -0.0638541728258133],
|
||||||
|
[1., -0.0894841775298119, -1.2914855480194092]
|
||||||
|
])
|
||||||
|
|
||||||
|
LMS_TO_XYZ = Matrix([
|
||||||
|
[ 1.2268798758459243, -0.5578149944602171, 0.2813910456659647],
|
||||||
|
[-0.0405757452148008, 1.1122868032803170, -0.0717110580655164],
|
||||||
|
[-0.0763729366746601, -0.4214933324022432, 1.5869240198367816]
|
||||||
|
])
|
||||||
|
|
||||||
|
def to_xyz(self):
|
||||||
|
lmsg = (self.OKLAB_TO_LMSG @ Matrix.as_column(self)).get_column()
|
||||||
|
lms = [i ** 3 for i in lmsg]
|
||||||
|
xyz = (self.LMS_TO_XYZ @ Matrix.as_column(lms)).get_column()
|
||||||
|
return XYZColor(*xyz)
|
||||||
|
|
||||||
|
def to_oklch(self):
|
||||||
|
return OKLCHColor(
|
||||||
|
self.l,
|
||||||
|
math.sqrt(self.a ** 2 + self.b ** 2),
|
||||||
|
0 if abs(self.a) < .0002 and abs(self.b) < .0002 else (((math.atan2(self.b, self.a) * 180) / math.pi % 360) + 360) % 360
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_rgb(self):
|
||||||
|
return self.to_xyz().to_rgb()
|
||||||
|
|
||||||
|
class OKLCHColor(namedtuple('_OKLCHColor', 'l c h')):
|
||||||
|
"""
|
||||||
|
Represent a color in the OKLCH color space.
|
||||||
|
|
||||||
|
*Warning*: conversion to RGB is not bound checked yet!
|
||||||
|
|
||||||
|
*New in 0.12.0*
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
l, c, h = round(self.l, 4), round(self.c, 4), round(self.h, 4)
|
||||||
|
|
||||||
|
return f'oklch({l}, {c}, {h})'
|
||||||
|
|
||||||
|
|
||||||
|
def to_oklab(self):
|
||||||
|
return OKLabColor(
|
||||||
|
self.l,
|
||||||
|
self.c * math.cos(self.h * math.pi / 180),
|
||||||
|
self.h * math.cos(self.h * math.pi / 180)
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_rgb(self):
|
||||||
|
return self.to_oklab().to_rgb()
|
||||||
|
|
||||||
|
__all__ = ('chalk', 'WebColor', "RGBColor", 'SRGBColor', 'XYZColor', 'OKLabColor')
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Utilities for Flask-SQLAlchemy binding.
|
Utilities for Flask-SQLAlchemy binding.
|
||||||
|
|
||||||
This module is deprecated and will be REMOVED in 0.14.0.
|
This module has been emptied in 0.12.0 following deprecation removals.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -16,50 +16,6 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
from typing import Any, Callable, Never
|
|
||||||
|
|
||||||
from flask import abort, request
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from sqlalchemy.orm import DeclarativeBase, Session
|
|
||||||
from .functools import deprecated
|
|
||||||
|
|
||||||
from .codecs import want_bytes
|
|
||||||
from .sqlalchemy import AuthSrc, require_auth_base
|
|
||||||
|
|
||||||
@deprecated('inherits from deprecated and unused class')
|
|
||||||
class FlaskAuthSrc(AuthSrc):
|
|
||||||
'''
|
|
||||||
|
|
||||||
'''
|
|
||||||
db: SQLAlchemy
|
|
||||||
def __init__(self, db: SQLAlchemy):
|
|
||||||
super().__init__()
|
|
||||||
self.db = db
|
|
||||||
def get_session(self) -> Session:
|
|
||||||
return self.db.session
|
|
||||||
def get_token(self):
|
|
||||||
if request.authorization:
|
|
||||||
return request.authorization.token
|
|
||||||
def get_signature(self) -> bytes:
|
|
||||||
sig = request.headers.get('authorization-signature', None)
|
|
||||||
return want_bytes(sig) if sig else None
|
|
||||||
def invalid_exc(self, msg: str = 'validation failed') -> Never:
|
|
||||||
abort(400, msg)
|
|
||||||
def required_exc(self):
|
|
||||||
abort(401, 'Login required')
|
|
||||||
|
|
||||||
@deprecated('not intuitive to use')
|
|
||||||
def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Callable]:
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
def auth_required(**kwargs):
|
|
||||||
return require_auth_base(cls=cls, src=FlaskAuthSrc(db), **kwargs)
|
|
||||||
|
|
||||||
auth_required.__doc__ = require_auth_base.__doc__
|
|
||||||
|
|
||||||
return auth_required
|
|
||||||
|
|
||||||
# Optional dependency: do not import into __init__.py
|
# Optional dependency: do not import into __init__.py
|
||||||
__all__ = ()
|
__all__ = ()
|
||||||
|
|
|
||||||
|
|
@ -249,13 +249,20 @@ class Siq(int):
|
||||||
|
|
||||||
def to_base64(self, length: int = 15, *, strip: bool = True) -> str:
|
def to_base64(self, length: int = 15, *, strip: bool = True) -> str:
|
||||||
return b64encode(self.to_bytes(length), strip=strip)
|
return b64encode(self.to_bytes(length), strip=strip)
|
||||||
|
|
||||||
def to_cb32(self) -> str:
|
def to_cb32(self) -> str:
|
||||||
return cb32encode(self.to_bytes(15, 'big')).lstrip('0')
|
return cb32encode(self.to_bytes(15, 'big')).lstrip('0')
|
||||||
to_crockford = to_cb32
|
to_crockford = to_cb32
|
||||||
|
@classmethod
|
||||||
|
def from_cb32(cls, val: str | bytes):
|
||||||
|
return cls.from_bytes(cb32decode(want_str(val).zfill(24)))
|
||||||
|
|
||||||
def to_hex(self) -> str:
|
def to_hex(self) -> str:
|
||||||
return f'{self:x}'
|
return f'{self:x}'
|
||||||
|
|
||||||
def to_oct(self) -> str:
|
def to_oct(self) -> str:
|
||||||
return f'{self:o}'
|
return f'{self:o}'
|
||||||
|
|
||||||
def to_b32l(self) -> str:
|
def to_b32l(self) -> str:
|
||||||
"""
|
"""
|
||||||
This is NOT the URI serializer!
|
This is NOT the URI serializer!
|
||||||
|
|
@ -305,12 +312,10 @@ class Siq(int):
|
||||||
raise ValueError('checksum mismatch')
|
raise ValueError('checksum mismatch')
|
||||||
return cls(int.from_bytes(b, 'big'))
|
return cls(int.from_bytes(b, 'big'))
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_cb32(cls, val: str | bytes):
|
|
||||||
return cls.from_bytes(cb32decode(want_str(val).zfill(24)))
|
|
||||||
|
|
||||||
def to_mastodon(self, /, domain: str | None = None):
|
def to_mastodon(self, /, domain: str | None = None):
|
||||||
return f'@{self:u}{"@" if domain else ""}{domain}'
|
return f'@{self:u}{"@" if domain else ""}{domain}'
|
||||||
|
|
||||||
def to_matrix(self, /, domain: str):
|
def to_matrix(self, /, domain: str):
|
||||||
return f'@{self:u}:{domain}'
|
return f'@{self:u}:{domain}'
|
||||||
|
|
||||||
|
|
|
||||||
143
src/suou/mat.py
Normal file
143
src/suou/mat.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
"""
|
||||||
|
Matrix (not the movie...)
|
||||||
|
|
||||||
|
*New in 0.12.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 __future__ import annotations
|
||||||
|
from typing import Collection, Iterable, TypeVar
|
||||||
|
from .functools import deprecated
|
||||||
|
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
|
class Matrix(Collection[_T]):
|
||||||
|
"""
|
||||||
|
Minimalist reimplementation of matrices in pure Python.
|
||||||
|
|
||||||
|
This to avoid adding numpy as a dependency.
|
||||||
|
|
||||||
|
*New in 0.12.0*
|
||||||
|
"""
|
||||||
|
_shape: tuple[int, int]
|
||||||
|
_elements: list[_T]
|
||||||
|
|
||||||
|
def shape(self):
|
||||||
|
return self._shape
|
||||||
|
|
||||||
|
def __init__(self, iterable: Iterable[_T] | Iterable[Collection[_T]], shape: tuple[int, int] | None = None):
|
||||||
|
elements = []
|
||||||
|
boundary_x = boundary_y = 0
|
||||||
|
for row in iterable:
|
||||||
|
if isinstance(row, Collection):
|
||||||
|
if not boundary_y:
|
||||||
|
boundary_y = len(row)
|
||||||
|
elements.extend(row)
|
||||||
|
boundary_x += 1
|
||||||
|
elif boundary_y != len(row):
|
||||||
|
raise ValueError('row length mismatch')
|
||||||
|
else:
|
||||||
|
elements.extend(row)
|
||||||
|
boundary_x += 1
|
||||||
|
elif shape:
|
||||||
|
if not boundary_x:
|
||||||
|
boundary_x, boundary_y = shape
|
||||||
|
elements.append(row)
|
||||||
|
self._shape = boundary_x, boundary_y
|
||||||
|
self._elements = elements
|
||||||
|
assert len(self._elements) == boundary_x * boundary_y
|
||||||
|
|
||||||
|
def __getitem__(self, key: tuple[int, int]) -> _T:
|
||||||
|
(x, y), (_, sy) = key, self.shape()
|
||||||
|
|
||||||
|
return self._elements[x * sy + y]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def T(self):
|
||||||
|
sx, sy = self.shape()
|
||||||
|
return Matrix(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
self[j, i] for j in range(sx)
|
||||||
|
] for i in range(sy)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __matmul__(self, other: Matrix) -> Matrix:
|
||||||
|
(ax, ay), (bx, by) = self.shape(), other.shape()
|
||||||
|
|
||||||
|
if ay != bx:
|
||||||
|
raise ValueError('cannot multiply matrices with incompatible shape')
|
||||||
|
|
||||||
|
return Matrix([
|
||||||
|
[
|
||||||
|
sum(self[i, k] * other[k, j] for k in range(ay)) for j in range(by)
|
||||||
|
] for i in range(ax)
|
||||||
|
])
|
||||||
|
|
||||||
|
def __eq__(self, other: Matrix):
|
||||||
|
try:
|
||||||
|
return self._elements == other._elements and self._shape == other._shape
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
ax, ay = self.shape()
|
||||||
|
return ax * ay
|
||||||
|
|
||||||
|
@deprecated('please use .rows() or .columns() instead')
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._elements)
|
||||||
|
|
||||||
|
def __contains__(self, x: object, /) -> bool:
|
||||||
|
return x in self._elements
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'{self.__class__.__name__}({list(self.rows())})'
|
||||||
|
|
||||||
|
def rows(self):
|
||||||
|
sx, sy = self.shape()
|
||||||
|
return (
|
||||||
|
[self[j, i] for j in range(sy)] for i in range(sx)
|
||||||
|
)
|
||||||
|
|
||||||
|
def columns(self):
|
||||||
|
sx, sy = self.shape()
|
||||||
|
return (
|
||||||
|
[self[j, i] for j in range(sx)] for i in range(sy)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_row(cls, iterable: Iterable):
|
||||||
|
return cls([[*iterable]])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_column(cls, iterable: Iterable):
|
||||||
|
return cls([[x] for x in iterable])
|
||||||
|
|
||||||
|
def get_column(self, idx = 0):
|
||||||
|
sx, _ = self.shape()
|
||||||
|
return [
|
||||||
|
self[j, idx] for j in range(sx)
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_row(self, idx = 0):
|
||||||
|
_, sy = self.shape()
|
||||||
|
return [
|
||||||
|
self[idx, j] for j in range(sy)
|
||||||
|
]
|
||||||
|
|
||||||
|
__all__ = ('Matrix', )
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
"""
|
|
||||||
Utilities for parsing config variables.
|
|
||||||
|
|
||||||
BREAKING older, non-generalized version, kept for backwards compability
|
|
||||||
in case 0.4+ version happens to break.
|
|
||||||
|
|
||||||
WILL BE removed in 0.5.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 __future__ import annotations
|
|
||||||
from ast import TypeVar
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from configparser import ConfigParser as _ConfigParser
|
|
||||||
import os
|
|
||||||
from typing import Any, Callable, Iterator
|
|
||||||
from collections import OrderedDict
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from ..functools import deprecated
|
|
||||||
from ..exceptions import MissingConfigError, MissingConfigWarning
|
|
||||||
|
|
||||||
warnings.warn('This module will be removed in 0.5.0 and is kept only in case new implementation breaks!\n'\
|
|
||||||
'Do not use unless you know what you are doing.', DeprecationWarning)
|
|
||||||
|
|
||||||
MISSING = object()
|
|
||||||
_T = TypeVar('T')
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated('use configparse')
|
|
||||||
class ConfigSource(Mapping):
|
|
||||||
'''
|
|
||||||
Abstract config source.
|
|
||||||
'''
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
@deprecated('use configparse')
|
|
||||||
class EnvConfigSource(ConfigSource):
|
|
||||||
'''
|
|
||||||
Config source from os.environ aka .env
|
|
||||||
'''
|
|
||||||
def __getitem__(self, key: str, /) -> str:
|
|
||||||
return os.environ[key]
|
|
||||||
def get(self, key: str, fallback = None, /):
|
|
||||||
return os.getenv(key, fallback)
|
|
||||||
def __contains__(self, key: str, /) -> bool:
|
|
||||||
return key in os.environ
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
|
||||||
yield from os.environ
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(os.environ)
|
|
||||||
|
|
||||||
@deprecated('use configparse')
|
|
||||||
class ConfigParserConfigSource(ConfigSource):
|
|
||||||
'''
|
|
||||||
Config source from ConfigParser
|
|
||||||
'''
|
|
||||||
__slots__ = ('_cfp', )
|
|
||||||
_cfp: _ConfigParser
|
|
||||||
|
|
||||||
def __init__(self, cfp: _ConfigParser):
|
|
||||||
if not isinstance(cfp, _ConfigParser):
|
|
||||||
raise TypeError(f'a ConfigParser object is required (got {cfp.__class__.__name__!r})')
|
|
||||||
self._cfp = cfp
|
|
||||||
def __getitem__(self, key: str, /) -> str:
|
|
||||||
k1, _, k2 = key.partition('.')
|
|
||||||
return self._cfp.get(k1, k2)
|
|
||||||
def get(self, key: str, fallback = None, /):
|
|
||||||
k1, _, k2 = key.partition('.')
|
|
||||||
return self._cfp.get(k1, k2, fallback=fallback)
|
|
||||||
def __contains__(self, key: str, /) -> bool:
|
|
||||||
k1, _, k2 = key.partition('.')
|
|
||||||
return self._cfp.has_option(k1, k2)
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
|
||||||
for k1, v1 in self._cfp.items():
|
|
||||||
for k2 in v1:
|
|
||||||
yield f'{k1}.{k2}'
|
|
||||||
def __len__(self) -> int:
|
|
||||||
## XXX might be incorrect but who cares
|
|
||||||
return sum(len(x) for x in self._cfp)
|
|
||||||
|
|
||||||
@deprecated('use configparse')
|
|
||||||
class DictConfigSource(ConfigSource):
|
|
||||||
'''
|
|
||||||
Config source from Python mappings. Useful with JSON/TOML config
|
|
||||||
'''
|
|
||||||
__slots__ = ('_d',)
|
|
||||||
|
|
||||||
_d: dict[str, Any]
|
|
||||||
|
|
||||||
def __init__(self, mapping: dict[str, Any]):
|
|
||||||
self._d = mapping
|
|
||||||
def __getitem__(self, key: str, /) -> str:
|
|
||||||
return self._d[key]
|
|
||||||
def get(self, key: str, fallback: _T = None, /):
|
|
||||||
return self._d.get(key, fallback)
|
|
||||||
def __contains__(self, key: str, /) -> bool:
|
|
||||||
return key in self._d
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
|
||||||
yield from self._d
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._d)
|
|
||||||
|
|
||||||
@deprecated('use configparse')
|
|
||||||
class ConfigValue:
|
|
||||||
"""
|
|
||||||
A single config property.
|
|
||||||
|
|
||||||
By default, it is sourced from os.environ — i.e. environment variables,
|
|
||||||
and property name is upper cased.
|
|
||||||
|
|
||||||
You can specify further sources, if the parent ConfigOptions class
|
|
||||||
supports them.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
- public: mark value as public, making it available across the app (e.g. in Jinja2 templates).
|
|
||||||
- prefix: src but for the lazy
|
|
||||||
- preserve_case: if True, src is not CAPITALIZED. Useful for parsing from Python dictionaries or ConfigParser's
|
|
||||||
- required: throw an error if empty or not supplied
|
|
||||||
"""
|
|
||||||
# XXX disabled per https://stackoverflow.com/questions/45864273/slots-conflicts-with-a-class-variable-in-a-generic-class
|
|
||||||
#__slots__ = ('_srcs', '_val', '_default', '_cast', '_required', '_preserve_case')
|
|
||||||
|
|
||||||
_srcs: dict[str, str] | None
|
|
||||||
_preserve_case: bool = False
|
|
||||||
_val: Any | MISSING = MISSING
|
|
||||||
_default: Any | None
|
|
||||||
_cast: Callable | None
|
|
||||||
_required: bool
|
|
||||||
_pub_name: str | bool = False
|
|
||||||
def __init__(self, /,
|
|
||||||
src: str | None = None, *, default = None, cast: Callable | None = None,
|
|
||||||
required: bool = False, preserve_case: bool = False, prefix: str | None = None,
|
|
||||||
public: str | bool = False, **kwargs):
|
|
||||||
self._srcs = dict()
|
|
||||||
self._preserve_case = preserve_case
|
|
||||||
if src:
|
|
||||||
self._srcs['default'] = src if preserve_case else src.upper()
|
|
||||||
elif prefix:
|
|
||||||
self._srcs['default'] = f'{prefix if preserve_case else prefix.upper}?'
|
|
||||||
self._default = default
|
|
||||||
self._cast = cast
|
|
||||||
self._required = required
|
|
||||||
self._pub_name = public
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
if k.endswith('_src'):
|
|
||||||
self._srcs[k[:-4]] = v
|
|
||||||
else:
|
|
||||||
raise TypeError(f'unknown keyword argument {k!r}')
|
|
||||||
def __set_name__(self, owner, name: str):
|
|
||||||
if 'default' not in self._srcs:
|
|
||||||
self._srcs['default'] = name if self._preserve_case else name.upper()
|
|
||||||
elif self._srcs['default'].endswith('?'):
|
|
||||||
self._srcs['default'] = self._srcs['default'].rstrip('?') + (name if self._preserve_case else name.upper() )
|
|
||||||
|
|
||||||
if self._pub_name is True:
|
|
||||||
self._pub_name = name
|
|
||||||
if self._pub_name and isinstance(owner, ConfigOptions):
|
|
||||||
owner.expose(self._pub_name, name)
|
|
||||||
def __get__(self, obj: ConfigOptions, owner=False):
|
|
||||||
if self._val is MISSING:
|
|
||||||
v = MISSING
|
|
||||||
for srckey, src in obj._srcs.items():
|
|
||||||
if srckey in self._srcs:
|
|
||||||
v = src.get(self._srcs[srckey], v)
|
|
||||||
if self._required and (not v or v is MISSING):
|
|
||||||
raise MissingConfigError(f'required config {self._srcs['default']} not set!')
|
|
||||||
if v is MISSING:
|
|
||||||
v = self._default
|
|
||||||
if callable(self._cast):
|
|
||||||
v = self._cast(v) if v is not None else self._cast()
|
|
||||||
self._val = v
|
|
||||||
return self._val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source(self, /):
|
|
||||||
return self._srcs['default']
|
|
||||||
|
|
||||||
@deprecated('use configparse')
|
|
||||||
class ConfigOptions:
|
|
||||||
"""
|
|
||||||
Base class for loading config values.
|
|
||||||
|
|
||||||
It is intended to get subclassed; config values must be defined as
|
|
||||||
ConfigValue() properties.
|
|
||||||
|
|
||||||
Further config sources can be added with .add_source()
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ('_srcs', '_pub')
|
|
||||||
|
|
||||||
_srcs: OrderedDict[str, ConfigSource]
|
|
||||||
_pub: dict[str, str]
|
|
||||||
|
|
||||||
def __init__(self, /):
|
|
||||||
self._srcs = OrderedDict(
|
|
||||||
default = EnvConfigSource()
|
|
||||||
)
|
|
||||||
self._pub = dict()
|
|
||||||
|
|
||||||
def add_source(self, key: str, csrc: ConfigSource, /, *, first: bool = False):
|
|
||||||
self._srcs[key] = csrc
|
|
||||||
if first:
|
|
||||||
self._srcs.move_to_end(key, False)
|
|
||||||
|
|
||||||
add_config_source = deprecated_alias(add_source)
|
|
||||||
|
|
||||||
def expose(self, public_name: str, attr_name: str | None = None) -> None:
|
|
||||||
'''
|
|
||||||
Mark a config value as public.
|
|
||||||
|
|
||||||
Called automatically by ConfigValue.__set_name__().
|
|
||||||
'''
|
|
||||||
attr_name = attr_name or public_name
|
|
||||||
self._pub[public_name] = attr_name
|
|
||||||
|
|
||||||
def to_dict(self, /):
|
|
||||||
d = dict()
|
|
||||||
for k, v in self._pub.items():
|
|
||||||
d[k] = getattr(self, v)
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'EnvConfigSource', 'ConfigParserConfigSource', 'DictConfigSource', 'ConfigSource', 'ConfigValue'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
47
tests/test_mat.py
Normal file
47
tests/test_mat.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from suou.mat import Matrix
|
||||||
|
|
||||||
|
|
||||||
|
class TestMat(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.m_a = Matrix([
|
||||||
|
[2, 2],
|
||||||
|
[1, 3]
|
||||||
|
])
|
||||||
|
self.m_b = Matrix([
|
||||||
|
[1], [-4]
|
||||||
|
])
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
...
|
||||||
|
def test_transpose(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.m_a.T,
|
||||||
|
Matrix([
|
||||||
|
[2, 1],
|
||||||
|
[2, 3]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.m_b.T,
|
||||||
|
Matrix([[1, -4]])
|
||||||
|
)
|
||||||
|
def test_mul(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.m_b.T @ self.m_a,
|
||||||
|
Matrix([
|
||||||
|
[-2, -10]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.m_a @ self.m_b,
|
||||||
|
Matrix([
|
||||||
|
[-6], [-11]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
def test_shape(self):
|
||||||
|
self.assertEqual(self.m_a.shape(), (2, 2))
|
||||||
|
self.assertEqual(self.m_b.shape(), (2, 1))
|
||||||
|
self.assertEqual(self.m_b.T.shape(), (1, 2))
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from suou.validators import yesno
|
from suou.calendar import not_greater_than
|
||||||
|
from suou.validators import not_less_than, yesno
|
||||||
|
|
||||||
class TestValidators(unittest.TestCase):
|
class TestValidators(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -22,3 +23,16 @@ class TestValidators(unittest.TestCase):
|
||||||
self.assertTrue(yesno('o'))
|
self.assertTrue(yesno('o'))
|
||||||
self.assertFalse(yesno('oFF'))
|
self.assertFalse(yesno('oFF'))
|
||||||
self.assertFalse(yesno('no'))
|
self.assertFalse(yesno('no'))
|
||||||
|
self.assertFalse(yesno(False))
|
||||||
|
self.assertTrue(yesno(True))
|
||||||
|
self.assertFalse(yesno(''))
|
||||||
|
|
||||||
|
def test_not_greater_than(self):
|
||||||
|
self.assertTrue(not_greater_than(5)(5))
|
||||||
|
self.assertTrue(not_greater_than(5)(3))
|
||||||
|
self.assertFalse(not_greater_than(3)(8))
|
||||||
|
|
||||||
|
def test_not_less_than(self):
|
||||||
|
self.assertTrue(not_less_than(5)(5))
|
||||||
|
self.assertFalse(not_less_than(5)(3))
|
||||||
|
self.assertTrue(not_less_than(3)(8))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue