Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebe22e9c82 | |||
| 764efd9530 | |||
| d454eaea2c | |||
| db49a47ce5 |
45 changed files with 507 additions and 811 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -29,7 +29,3 @@ aliases/*/src
|
||||||
docs/_build
|
docs/_build
|
||||||
docs/_static
|
docs/_static
|
||||||
docs/templates
|
docs/templates
|
||||||
.coverage
|
|
||||||
|
|
||||||
# changes during CD/CI
|
|
||||||
aliases/*/pyproject.toml
|
|
||||||
|
|
|
||||||
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,30 +1,5 @@
|
||||||
# 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
|
|
||||||
|
|
||||||
+ make `yesno()` accept boolean types
|
|
||||||
|
|
||||||
## 0.11.0
|
|
||||||
|
|
||||||
+ **Breaking**: sessions returned by `SQLAlchemy()` are now wrapped by default. Restore original behavior by passing `wrap=False` to the constructor or to `begin()`
|
|
||||||
+ Slate unused `require_auth()` and derivatives for removal in 0.14.0
|
|
||||||
+ Add `cb32lencode()`
|
|
||||||
+ `Snowflake()`: add `.from_cb32()`, `.from_base64()`, `.from_oct()`, `.from_hex()` classmethods
|
|
||||||
+ Add `SpitText()`
|
|
||||||
+ Add `Lawyer()` with seven methods
|
|
||||||
+ Style changes to docstrings
|
|
||||||
|
|
||||||
## 0.10.2 and 0.7.11
|
## 0.10.2 and 0.7.11
|
||||||
|
|
||||||
+ fix incorrect types on `cb32decode()`
|
+ fix incorrect types on `cb32decode()`
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,11 @@ Please note that you probably already have those dependencies, if you just use t
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Read the [documentation](https://suou.readthedocs.io/).
|
...
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Just a heads up: SUOU was made to support Sakuragasaki46 (me)'s own selfish, egoistic needs. Not certainly to provide a service to the public.
|
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.
|
As a consequence, 'add this add that' stuff is best-effort.
|
||||||
|
|
||||||
|
|
|
||||||
87
aliases/sakuragasaki46_suou/pyproject.toml
Normal file
87
aliases/sakuragasaki46_suou/pyproject.toml
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
[project]
|
||||||
|
name = "sakuragasaki46_suou"
|
||||||
|
description = "casual utility library for coding QoL"
|
||||||
|
authors = [
|
||||||
|
{ name = "Sakuragasaki46" }
|
||||||
|
]
|
||||||
|
dynamic = [ "version" ]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"suou==0.7.9",
|
||||||
|
"itsdangerous",
|
||||||
|
"toml",
|
||||||
|
"pydantic",
|
||||||
|
"setuptools>=78.0.0",
|
||||||
|
"uvloop; os_name=='posix'"
|
||||||
|
]
|
||||||
|
# - further devdependencies below - #
|
||||||
|
|
||||||
|
# - publishing -
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 2 - Pre-Alpha",
|
||||||
|
|
||||||
|
# actively supported Pythons
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://nekode.yusur.moe/yusur/suou"
|
||||||
|
Documentation = "https://suou.readthedocs.io"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
# the below are all dev dependencies (and probably already installed)
|
||||||
|
sqlalchemy = [
|
||||||
|
"SQLAlchemy[asyncio]>=2.0.0",
|
||||||
|
"flask-sqlalchemy"
|
||||||
|
]
|
||||||
|
flask = [
|
||||||
|
"Flask>=2.0.0",
|
||||||
|
"Flask-RestX"
|
||||||
|
]
|
||||||
|
flask_sqlalchemy = [
|
||||||
|
"sakuragasaki46_suou[sqlalchemy]",
|
||||||
|
"sakuragasaki46_suou[flask]"
|
||||||
|
]
|
||||||
|
peewee = [
|
||||||
|
## HEADS UP! peewee has setup.py, may slow down installation
|
||||||
|
"peewee>=3.0.0"
|
||||||
|
]
|
||||||
|
markdown = [
|
||||||
|
"markdown>=3.0.0"
|
||||||
|
]
|
||||||
|
quart = [
|
||||||
|
"Quart",
|
||||||
|
"Quart-Schema",
|
||||||
|
"starlette>=0.47.2"
|
||||||
|
]
|
||||||
|
sass = [
|
||||||
|
## HEADS UP!! libsass carries a C extension + uses setup.py
|
||||||
|
"libsass"
|
||||||
|
]
|
||||||
|
|
||||||
|
full = [
|
||||||
|
"sakuragasaki46_suou[sqlalchemy]",
|
||||||
|
"sakuragasaki46_suou[flask]",
|
||||||
|
"sakuragasaki46_suou[quart]",
|
||||||
|
"sakuragasaki46_suou[peewee]",
|
||||||
|
"sakuragasaki46_suou[markdown]",
|
||||||
|
"sakuragasaki46_suou[sass]"
|
||||||
|
]
|
||||||
|
|
||||||
|
docs = [
|
||||||
|
"sphinx>=2.1",
|
||||||
|
"myst_parser",
|
||||||
|
"sphinx_rtd_theme"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = { attr = "suou.__version__" }
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
|
|
||||||
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,7 +16,6 @@
|
||||||
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,7 +9,5 @@
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
|
|
||||||
Chalk
|
Chalk
|
||||||
RGBColor
|
|
||||||
SRGBColor
|
|
||||||
WebColor
|
WebColor
|
||||||
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
suou.flask\_sqlalchemy
|
suou.flask\_sqlalchemy
|
||||||
======================
|
======================
|
||||||
|
|
||||||
.. automodule:: suou.flask_sqlalchemy
|
.. automodule:: suou.flask_sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: Functions
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
|
||||||
|
require_auth
|
||||||
|
|
||||||
|
.. rubric:: Classes
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
|
||||||
|
FlaskAuthSrc
|
||||||
|
|
||||||
|
|
@ -1,12 +1,6 @@
|
||||||
suou.legal
|
suou.legal
|
||||||
==========
|
==========
|
||||||
|
|
||||||
.. automodule:: suou.legal
|
.. automodule:: suou.legal
|
||||||
|
|
||||||
|
|
||||||
.. rubric:: Classes
|
|
||||||
|
|
||||||
.. autosummary::
|
|
||||||
|
|
||||||
Lawyer
|
|
||||||
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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,5 +9,4 @@
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
|
|
||||||
PrefixIdentifier
|
PrefixIdentifier
|
||||||
SpitText
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
suou.validators
|
suou.validators
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. automodule:: suou.validators
|
.. automodule:: suou.validators
|
||||||
|
|
@ -12,5 +12,4 @@
|
||||||
must_be
|
must_be
|
||||||
not_greater_than
|
not_greater_than
|
||||||
not_less_than
|
not_less_than
|
||||||
yesno
|
|
||||||
|
|
||||||
|
|
@ -15,6 +15,4 @@ ease programmer's QoL and write shorter and cleaner code that works.
|
||||||
|
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
iding
|
iding
|
||||||
validators
|
|
||||||
color
|
|
||||||
api
|
api
|
||||||
|
|
@ -25,8 +25,6 @@ Columns
|
||||||
|
|
||||||
.. autofunction:: bool_column
|
.. autofunction:: bool_column
|
||||||
|
|
||||||
.. autofunction:: username_column
|
|
||||||
|
|
||||||
.. autofunction:: unbound_fk
|
.. autofunction:: unbound_fk
|
||||||
.. autofunction:: bound_fk
|
.. autofunction:: bound_fk
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
|
|
||||||
validators
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. currentmodule:: suou.validators
|
|
||||||
|
|
||||||
Validators for use in frameworks such as Pydantic or Marshmallow.
|
|
||||||
|
|
||||||
.. autofunction:: matches
|
|
||||||
|
|
||||||
.. autofunction:: not_greater_than
|
|
||||||
|
|
||||||
.. autofunction:: not_less_than
|
|
||||||
|
|
||||||
.. autofunction:: yesno
|
|
||||||
|
|
@ -32,31 +32,30 @@ from .signing import UserSigner
|
||||||
from .snowflake import Snowflake, SnowflakeGen
|
from .snowflake import Snowflake, SnowflakeGen
|
||||||
from .lex import symbol_table, lex, ilex
|
from .lex import symbol_table, lex, ilex
|
||||||
from .strtools import PrefixIdentifier
|
from .strtools import PrefixIdentifier
|
||||||
from .validators import matches, not_less_than, not_greater_than, yesno
|
from .validators import matches
|
||||||
from .redact import redact_url_password
|
from .redact import redact_url_password
|
||||||
from .http import WantsContentType
|
from .http import WantsContentType
|
||||||
from .color import OKLabColor, chalk, WebColor, RGBColor, SRGBColor, XYZColor, OKLabColor
|
from .color import chalk, WebColor
|
||||||
from .mat import Matrix
|
|
||||||
|
|
||||||
|
__version__ = "0.7.11"
|
||||||
|
|
||||||
__version__ = "0.12.0a5"
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
|
'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue',
|
||||||
'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n',
|
'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n',
|
||||||
'Matrix', 'MissingConfigError', 'MissingConfigWarning', 'OKLabColor',
|
'MissingConfigError', 'MissingConfigWarning', 'PrefixIdentifier',
|
||||||
'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', 'XYZColor',
|
'WebColor',
|
||||||
'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',
|
||||||
'future', 'ilex', 'join_bits',
|
'future', 'ilex', 'join_bits',
|
||||||
'jsonencode', 'kwargs_prefix', 'lex', 'ltuple', 'makelist', 'mask_shift',
|
'jsonencode', 'kwargs_prefix', 'lex', 'ltuple', 'makelist', 'mask_shift',
|
||||||
'matches', 'mod_ceil', 'mod_floor', 'must_be', 'none_pass', 'not_implemented',
|
'matches', 'mod_ceil', 'mod_floor', 'none_pass', 'not_implemented',
|
||||||
'not_less_than', 'not_greater_than',
|
|
||||||
'redact_url_password', 'rtuple', 'split_bits', 'ssv_list', 'symbol_table',
|
'redact_url_password', 'rtuple', 'split_bits', 'ssv_list', 'symbol_table',
|
||||||
'timed_cache', 'twocolon_list', 'want_bytes', 'want_datetime', 'want_isodate',
|
'timed_cache', 'twocolon_list', 'want_bytes', 'want_datetime', 'want_isodate',
|
||||||
'want_str', 'want_timestamp', 'want_urlsafe', 'want_urlsafe_bytes', 'yesno',
|
'want_str', 'want_timestamp', 'want_urlsafe', 'want_urlsafe_bytes',
|
||||||
'z85encode', 'z85decode'
|
'z85encode', 'z85decode'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from suou.functools import not_implemented
|
||||||
from suou.luck import lucky
|
from suou.luck import lucky
|
||||||
from suou.validators import not_greater_than
|
from suou.validators import not_greater_than
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,12 +179,6 @@ def cb32encode(val: bytes) -> str:
|
||||||
'''
|
'''
|
||||||
return want_str(base64.b32encode(val)).translate(B32_TO_CROCKFORD)
|
return want_str(base64.b32encode(val)).translate(B32_TO_CROCKFORD)
|
||||||
|
|
||||||
def cb32lencode(val: bytes) -> str:
|
|
||||||
'''
|
|
||||||
Encode bytes in Crockford Base32, lowercased.
|
|
||||||
'''
|
|
||||||
return want_str(base64.b32encode(val)).translate(B32_TO_CROCKFORD).lower()
|
|
||||||
|
|
||||||
def cb32decode(val: bytes | str) -> bytes:
|
def cb32decode(val: bytes | str) -> bytes:
|
||||||
'''
|
'''
|
||||||
Decode bytes from Crockford Base32.
|
Decode bytes from Crockford Base32.
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class TimedDict(dict[_KT, _VT]):
|
||||||
"""
|
"""
|
||||||
Dictionary where keys expire after the defined time to live, expressed in seconds.
|
Dictionary where keys expire after the defined time to live, expressed in seconds.
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
"""
|
"""
|
||||||
_expires: dict[_KT, int]
|
_expires: dict[_KT, int]
|
||||||
_ttl: int
|
_ttl: int
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Colors for coding artists
|
Colors for coding artists
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -21,9 +21,6 @@ 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:
|
||||||
|
|
@ -36,7 +33,7 @@ class Chalk:
|
||||||
|
|
||||||
UNTESTED
|
UNTESTED
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
"""
|
"""
|
||||||
CSI = '\x1b['
|
CSI = '\x1b['
|
||||||
RED = CSI + "31m"
|
RED = CSI + "31m"
|
||||||
|
|
@ -96,14 +93,11 @@ chalk = Chalk()
|
||||||
|
|
||||||
## Utilities for web colors
|
## Utilities for web colors
|
||||||
|
|
||||||
class RGBColor(namedtuple('_WebColor', 'red green blue')):
|
class WebColor(namedtuple('_WebColor', 'red green blue')):
|
||||||
"""
|
"""
|
||||||
Representation of a color in the RGB TrueColor space.
|
Representation of a color in the TrueColor space (aka rgb).
|
||||||
|
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
|
|
@ -132,176 +126,21 @@ class RGBColor(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: RGBColor):
|
def blend_with(self, other: WebColor):
|
||||||
"""
|
"""
|
||||||
Mix two colors, returning the average.
|
Mix two colors, returning the average.
|
||||||
"""
|
"""
|
||||||
return RGBColor (
|
return WebColor (
|
||||||
(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]
|
|
||||||
])
|
|
||||||
|
|
||||||
def to_xyz(self):
|
__all__ = ('chalk', 'WebColor')
|
||||||
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')
|
|
||||||
|
|
|
||||||
|
|
@ -109,10 +109,9 @@ class DictConfigSource(ConfigSource):
|
||||||
|
|
||||||
class ArgConfigSource(ValueSource):
|
class ArgConfigSource(ValueSource):
|
||||||
"""
|
"""
|
||||||
Config source that assumes arguments have already been parsed.
|
It assumes arguments have already been parsed
|
||||||
|
|
||||||
*New in 0.6.0*
|
NEW 0.6"""
|
||||||
"""
|
|
||||||
_ns: Namespace
|
_ns: Namespace
|
||||||
def __init__(self, ns: Namespace):
|
def __init__(self, ns: Namespace):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Utilities for Flask-SQLAlchemy binding.
|
Utilities for Flask-SQLAlchemy binding.
|
||||||
|
|
||||||
This module has been emptied in 0.12.0 following deprecation removals.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Copyright (c) 2025 Sakuragasaki46.
|
Copyright (c) 2025 Sakuragasaki46.
|
||||||
|
|
@ -16,6 +14,70 @@ 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]:
|
||||||
|
"""
|
||||||
|
Make an auth_required() decorator for Flask views.
|
||||||
|
|
||||||
|
This looks for a token in the Authorization header, validates it, loads the
|
||||||
|
appropriate object, and injects it as the user= parameter.
|
||||||
|
|
||||||
|
NOTE: the actual decorator to be used on routes is **auth_required()**,
|
||||||
|
NOT require_auth() which is the **constructor** for it.
|
||||||
|
|
||||||
|
cls is a SQLAlchemy table.
|
||||||
|
db is a flask_sqlalchemy.SQLAlchemy() binding.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
auth_required = require_auth(User, db)
|
||||||
|
|
||||||
|
@route('/admin')
|
||||||
|
@auth_required(validators=[lambda x: x.is_administrator])
|
||||||
|
def super_secret_stuff(user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
NOTE: require_auth() DOES NOT work with flask_restx.
|
||||||
|
"""
|
||||||
|
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__ = ()
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ def future(message: str | None = None, *, version: str = None):
|
||||||
|
|
||||||
version= is the intended version release.
|
version= is the intended version release.
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]:
|
def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
@ -135,7 +135,7 @@ def _make_alru_cache(_CacheInfo):
|
||||||
|
|
||||||
PSA there is no C speed up. Unlike PSL. Sorry.
|
PSA there is no C speed up. Unlike PSL. Sorry.
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Users should only access the lru_cache through its public API:
|
# Users should only access the lru_cache through its public API:
|
||||||
|
|
@ -292,7 +292,7 @@ def timed_cache(ttl: int, maxsize: int = 128, typed: bool = False, *, async_: bo
|
||||||
|
|
||||||
Supports coroutines with async_=True.
|
Supports coroutines with async_=True.
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]:
|
def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]:
|
||||||
start_time = None
|
start_time = None
|
||||||
|
|
@ -330,7 +330,7 @@ def none_pass(func: Callable[_T, _U], *args, **kwargs) -> Callable[_T, _U]:
|
||||||
|
|
||||||
Shorthand for func(x) if x is not None else None
|
Shorthand for func(x) if x is not None else None
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
"""
|
"""
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(x):
|
def wrapper(x):
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ from suou.classtools import MISSING
|
||||||
from suou.functools import future
|
from suou.functools import future
|
||||||
|
|
||||||
|
|
||||||
@future()
|
@future(version="0.9.0")
|
||||||
class FakeModule(ModuleType):
|
class FakeModule(ModuleType):
|
||||||
"""
|
"""
|
||||||
Fake module used in @glue() in case of import error
|
Fake module used in @glue() in case of import error
|
||||||
|
|
@ -34,12 +34,12 @@ class FakeModule(ModuleType):
|
||||||
raise AttributeError(f'Module {self.__name__} not found; this feature is not available ({self._exc})') from self._exc
|
raise AttributeError(f'Module {self.__name__} not found; this feature is not available ({self._exc})') from self._exc
|
||||||
|
|
||||||
|
|
||||||
@future()
|
@future(version = "0.9.0")
|
||||||
def glue(*modules):
|
def glue(*modules):
|
||||||
"""
|
"""
|
||||||
Helper for "glue" code -- it imports the given modules and passes them as keyword arguments to the wrapped functions.
|
Helper for "glue" code -- it imports the given modules and passes them as keyword arguments to the wrapped functions.
|
||||||
|
|
||||||
EXPERIMENTAL
|
NEW 0.9.0
|
||||||
"""
|
"""
|
||||||
module_dict = dict()
|
module_dict = dict()
|
||||||
imports_succeeded = True
|
imports_succeeded = True
|
||||||
|
|
|
||||||
|
|
@ -249,20 +249,13 @@ 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!
|
||||||
|
|
@ -312,10 +305,12 @@ 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}'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,12 @@ from suou.classtools import MISSING
|
||||||
|
|
||||||
_T = TypeVar('_T')
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
def makelist(l: Any, wrap: bool = True) -> list | Callable[Any, list]:
|
def makelist(l: Any, *, wrap: bool = True) -> list | Callable[Any, list]:
|
||||||
'''
|
'''
|
||||||
Make a list out of an iterable or a single value.
|
Make a list out of an iterable or a single value.
|
||||||
|
|
||||||
*Changed in 0.4.0* Now supports a callable: can be used to decorate generators and turn them into lists.
|
NEW 0.4.0: Now supports a callable: can be used to decorate generators and turn them into lists.
|
||||||
Pass wrap=False to return instead the unwrapped function in a list.
|
Pass wrap=False to return instead the unwrapped function in a list.
|
||||||
|
|
||||||
*Changed in 0.11.0*: ``wrap`` argument is now no more keyword only.
|
|
||||||
'''
|
'''
|
||||||
if callable(l) and wrap:
|
if callable(l) and wrap:
|
||||||
return wraps(l)(lambda *a, **k: makelist(l(*a, **k), wrap=False))
|
return wraps(l)(lambda *a, **k: makelist(l(*a, **k), wrap=False))
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
# TODO more snippets
|
# TODO more snippets
|
||||||
|
|
||||||
from .strtools import SpitText
|
|
||||||
|
|
||||||
|
|
||||||
INDEMNIFY = """
|
INDEMNIFY = """
|
||||||
You agree to indemnify and hold harmless {0} from any and all claims, damages, liabilities, costs and expenses, including reasonable and unreasonable counsel and attorney’s fees, arising out of any breach of this agreement.
|
You agree to indemnify and hold harmless {0} from any and all claims, damages, liabilities, costs and expenses, including reasonable and unreasonable counsel and attorney’s fees, arising out of any breach of this agreement.
|
||||||
"""
|
"""
|
||||||
|
|
@ -30,7 +27,7 @@ Except as represented in this agreement, the {0} is provided “AS IS”. Other
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GOVERNING_LAW = """
|
GOVERNING_LAW = """
|
||||||
These terms of services are governed by, and shall be interpreted in accordance with, the laws of {0}. You consent to the sole jurisdiction of {1} for all disputes between You and {2}, and You consent to the sole application of {3} law for all such disputes.
|
These terms of services are governed by, and shall be interpreted in accordance with, the laws of {0}. You consent to the sole jurisdiction of {1} for all disputes between You and , and You consent to the sole application of {2} law for all such disputes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ENGLISH_FIRST = """
|
ENGLISH_FIRST = """
|
||||||
|
|
@ -48,51 +45,5 @@ If one clause of these Terms of Service or any policy incorporated here by refer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
COMPLETENESS = """
|
COMPLETENESS = """
|
||||||
These Terms, together with the other policies incorporated into them by reference, contain all the terms and conditions agreed upon by You and {0} regarding Your use of the {0} service. No other agreement, oral or otherwise, will be deemed to exist or to bind either of the parties to this Agreement.
|
These Terms, together with the other policies incorporated into them by reference, contain all the terms and conditions agreed upon by You and {{ app_name }} regarding Your use of the {{ app_name }} service. No other agreement, oral or otherwise, will be deemed to exist or to bind either of the parties to this Agreement.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Lawyer(SpitText):
|
|
||||||
"""
|
|
||||||
A tool to ease the writing of Terms of Service for web apps.
|
|
||||||
|
|
||||||
NOT A REPLACEMENT FOR A REAL LAWYER AND NOT LEGAL ADVICE
|
|
||||||
|
|
||||||
*New in 0.11.0*
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, /,
|
|
||||||
app_name: str, domain_name: str,
|
|
||||||
company_name: str, jurisdiction: str,
|
|
||||||
country: str, country_adjective: str
|
|
||||||
):
|
|
||||||
self.app_name = app_name
|
|
||||||
self.domain_name = domain_name
|
|
||||||
self.company_name = company_name
|
|
||||||
self.jurisdiction = jurisdiction
|
|
||||||
self.country = country
|
|
||||||
self.country_adjective = country_adjective
|
|
||||||
|
|
||||||
def indemnify(self):
|
|
||||||
return self.format(INDEMNIFY, 'app_name')
|
|
||||||
|
|
||||||
def no_warranty(self):
|
|
||||||
return self.format(NO_WARRANTY, 'app_name', 'company_name')
|
|
||||||
|
|
||||||
def governing_law(self) -> str:
|
|
||||||
return self.format(GOVERNING_LAW, 'country', 'jurisdiction', 'app_name', 'country_adjective')
|
|
||||||
|
|
||||||
def english_first(self) -> str:
|
|
||||||
return ENGLISH_FIRST
|
|
||||||
|
|
||||||
def expect_updates(self) -> str:
|
|
||||||
return self.format(EXPECT_UPDATES, 'app_name')
|
|
||||||
|
|
||||||
def severability(self) -> str:
|
|
||||||
return SEVERABILITY
|
|
||||||
|
|
||||||
def completeness(self) -> str:
|
|
||||||
return self.format(COMPLETENESS, 'app_name')
|
|
||||||
|
|
||||||
# This module is experimental and therefore not re-exported into __init__
|
|
||||||
__all__ = ('Lawyer',)
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Fortune, RNG and esoterism.
|
Fortune, RNG and esoterism.
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ def lucky(validators: Iterable[Callable[[_U], bool]] = ()):
|
||||||
|
|
||||||
UNTESTED
|
UNTESTED
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]:
|
def decorator(func: Callable[_T, _U]) -> Callable[_T, _U]:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
@ -61,7 +61,7 @@ class RngCallable(Callable, Generic[_T, _U]):
|
||||||
|
|
||||||
UNTESTED
|
UNTESTED
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
"""
|
"""
|
||||||
def __init__(self, /, func: Callable[_T, _U] | None = None, weight: int = 1):
|
def __init__(self, /, func: Callable[_T, _U] | None = None, weight: int = 1):
|
||||||
self._callables = []
|
self._callables = []
|
||||||
|
|
@ -97,7 +97,7 @@ def rng_overload(prev_func: RngCallable[..., _U] | int | None, /, *, weight: int
|
||||||
|
|
||||||
UNTESTED
|
UNTESTED
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
"""
|
"""
|
||||||
if isinstance(prev_func, int) and weight == 1:
|
if isinstance(prev_func, int) and weight == 1:
|
||||||
weight, prev_func = prev_func, None
|
weight, prev_func = prev_func, None
|
||||||
|
|
|
||||||
143
src/suou/mat.py
143
src/suou/mat.py
|
|
@ -1,143 +0,0 @@
|
||||||
"""
|
|
||||||
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', )
|
|
||||||
|
|
||||||
|
|
||||||
239
src/suou/obsolete/configparsev0_3.py
Normal file
239
src/suou/obsolete/configparsev0_3.py
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
"""
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,11 +18,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from playhouse.shortcuts import ReconnectMixin
|
from playhouse.shortcuts import ReconnectMixin
|
||||||
from peewee import BigIntegerField, CharField, Database, Field, MySQLDatabase, _ConnectionState
|
from peewee import CharField, Database, Field, MySQLDatabase, _ConnectionState
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from suou.iding import Siq
|
from suou.iding import Siq
|
||||||
from suou.snowflake import Snowflake
|
|
||||||
|
|
||||||
from .codecs import StringCase
|
from .codecs import StringCase
|
||||||
|
|
||||||
|
|
@ -118,26 +117,6 @@ class SiqField(Field):
|
||||||
def python_value(self, value: bytes) -> Siq:
|
def python_value(self, value: bytes) -> Siq:
|
||||||
return Siq.from_bytes(value)
|
return Siq.from_bytes(value)
|
||||||
|
|
||||||
|
|
||||||
class SnowflakeField(BigIntegerField):
|
|
||||||
'''
|
|
||||||
Field holding a snowflake.
|
|
||||||
|
|
||||||
Stored as bigint.
|
|
||||||
|
|
||||||
XXX UNTESTED!
|
|
||||||
'''
|
|
||||||
field_type = 'bigint'
|
|
||||||
|
|
||||||
def db_value(self, value: int | Snowflake) -> int:
|
|
||||||
if isinstance(value, Snowflake):
|
|
||||||
value = int(value)
|
|
||||||
if not isinstance(value, int):
|
|
||||||
raise TypeError
|
|
||||||
return value
|
|
||||||
def python_value(self, value: int) -> Snowflake:
|
|
||||||
return Snowflake(value)
|
|
||||||
|
|
||||||
# Optional dependency: do not import into __init__.py
|
# Optional dependency: do not import into __init__.py
|
||||||
__all__ = ('connect_reconnect', 'RegexCharField', 'SiqField', 'Snowflake')
|
__all__ = ('connect_reconnect', 'RegexCharField', 'SiqField')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
"Security through obscurity" helpers for less sensitive logging
|
"Security through obscurity" helpers for less sensitive logging
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ def redact_url_password(u: str) -> str:
|
||||||
scheme://username:password@hostname/path?query
|
scheme://username:password@hostname/path?query
|
||||||
^------^
|
^------^
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
"""
|
"""
|
||||||
return re.sub(r':[^@:/ ]+@', ':***@', u)
|
return re.sub(r':[^@:/ ]+@', ':***@', u)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from binascii import unhexlify
|
|
||||||
import os
|
import os
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
import time
|
import time
|
||||||
|
|
@ -29,7 +28,7 @@ import warnings
|
||||||
|
|
||||||
from .migrate import SnowflakeSiqMigrator
|
from .migrate import SnowflakeSiqMigrator
|
||||||
from .iding import SiqType
|
from .iding import SiqType
|
||||||
from .codecs import b32ldecode, b32lencode, b64encode, b64decode, cb32encode, cb32decode
|
from .codecs import b32ldecode, b32lencode, b64encode, cb32encode
|
||||||
from .functools import deprecated
|
from .functools import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -122,46 +121,27 @@ class Snowflake(int):
|
||||||
|
|
||||||
def to_bytes(self, length: int = 14, byteorder = "big", *, signed: bool = False) -> bytes:
|
def to_bytes(self, length: int = 14, byteorder = "big", *, signed: bool = False) -> bytes:
|
||||||
return super().to_bytes(length, byteorder, signed=signed)
|
return super().to_bytes(length, byteorder, signed=signed)
|
||||||
|
def to_base64(self, length: int = 9, *, strip: bool = True) -> str:
|
||||||
|
return b64encode(self.to_bytes(length), strip=strip)
|
||||||
|
def to_cb32(self)-> str:
|
||||||
|
return cb32encode(self.to_bytes(8, 'big'))
|
||||||
|
to_crockford = to_cb32
|
||||||
|
def to_hex(self) -> str:
|
||||||
|
return f'{self:x}'
|
||||||
|
def to_oct(self) -> str:
|
||||||
|
return f'{self:o}'
|
||||||
|
def to_b32l(self) -> str:
|
||||||
|
# PSA Snowflake Base32 representations are padded to 10 bytes!
|
||||||
|
if self < 0:
|
||||||
|
return '_' + Snowflake.to_b32l(-self)
|
||||||
|
return b32lencode(self.to_bytes(10, 'big')).lstrip('a')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, b: bytes, byteorder = 'big', *, signed: bool = False) -> Snowflake:
|
def from_bytes(cls, b: bytes, byteorder = 'big', *, signed: bool = False) -> Snowflake:
|
||||||
if len(b) not in (8, 10):
|
if len(b) not in (8, 10):
|
||||||
warnings.warn('Snowflakes are exactly 8 bytes long', BytesWarning)
|
warnings.warn('Snowflakes are exactly 8 bytes long', BytesWarning)
|
||||||
return super().from_bytes(b, byteorder, signed=signed)
|
return super().from_bytes(b, byteorder, signed=signed)
|
||||||
|
|
||||||
def to_base64(self, length: int = 9, *, strip: bool = True) -> str:
|
|
||||||
return b64encode(self.to_bytes(length), strip=strip)
|
|
||||||
@classmethod
|
|
||||||
def from_base64(cls, val:str) -> Snowflake:
|
|
||||||
return Snowflake.from_bytes(b64decode(val))
|
|
||||||
|
|
||||||
def to_cb32(self)-> str:
|
|
||||||
return cb32encode(self.to_bytes(8, 'big'))
|
|
||||||
to_crockford = to_cb32
|
|
||||||
@classmethod
|
|
||||||
def from_cb32(cls, val:str) -> Snowflake:
|
|
||||||
return Snowflake.from_bytes(cb32decode(val))
|
|
||||||
|
|
||||||
def to_hex(self) -> str:
|
|
||||||
return f'{self:x}'
|
|
||||||
@classmethod
|
|
||||||
def from_hex(cls, val:str) -> Snowflake:
|
|
||||||
if val.startswith('_'):
|
|
||||||
return -cls.from_hex(val.lstrip('_'))
|
|
||||||
return Snowflake.from_bytes(unhexlify(val))
|
|
||||||
|
|
||||||
def to_oct(self) -> str:
|
|
||||||
return f'{self:o}'
|
|
||||||
@classmethod
|
|
||||||
def from_oct(cls, val:str) -> Snowflake:
|
|
||||||
if val.startswith('_'):
|
|
||||||
return -cls.from_hex(val.lstrip('_'))
|
|
||||||
return Snowflake(int(val, base=8))
|
|
||||||
|
|
||||||
def to_b32l(self) -> str:
|
|
||||||
# PSA Snowflake Base32 representations are padded to 10 bytes!
|
|
||||||
if self < 0:
|
|
||||||
return '_' + Snowflake.to_b32l(-self)
|
|
||||||
return b32lencode(self.to_bytes(10, 'big')).lstrip('a')
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_b32l(cls, val: str) -> Snowflake:
|
def from_b32l(cls, val: str) -> Snowflake:
|
||||||
if val.startswith('_'):
|
if val.startswith('_'):
|
||||||
|
|
@ -169,14 +149,6 @@ class Snowflake(int):
|
||||||
return -cls.from_b32l(val.lstrip('_'))
|
return -cls.from_b32l(val.lstrip('_'))
|
||||||
return Snowflake.from_bytes(b32ldecode(val.rjust(16, 'a')))
|
return Snowflake.from_bytes(b32ldecode(val.rjust(16, 'a')))
|
||||||
|
|
||||||
def to_siq(self, domain: str, epoch: int, target_type: SiqType, **kwargs):
|
|
||||||
"""
|
|
||||||
Convenience method for conversion to SIQ.
|
|
||||||
|
|
||||||
(!) This does not check for existence! Always do the check yourself.
|
|
||||||
"""
|
|
||||||
return SnowflakeSiqMigrator(domain, epoch, **kwargs).to_siq(self, target_type)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __format__(self, opt: str, /) -> str:
|
def __format__(self, opt: str, /) -> str:
|
||||||
try:
|
try:
|
||||||
|
|
@ -207,6 +179,15 @@ class Snowflake(int):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'{self.__class__.__name__}({super().__repr__()})'
|
return f'{self.__class__.__name__}({super().__repr__()})'
|
||||||
|
|
||||||
|
def to_siq(self, domain: str, epoch: int, target_type: SiqType, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenience method for conversion to SIQ.
|
||||||
|
|
||||||
|
(!) This does not check for existence! Always do the check yourself.
|
||||||
|
"""
|
||||||
|
return SnowflakeSiqMigrator(domain, epoch, **kwargs).to_siq(self, target_type)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Snowflake', 'SnowflakeGen'
|
'Snowflake', 'SnowflakeGen'
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ def token_signer(id_attr: Column | str, secret_attr: Column | str) -> Incomplete
|
||||||
|
|
||||||
## (in)Utilities for use in web apps below
|
## (in)Utilities for use in web apps below
|
||||||
|
|
||||||
@deprecated('not part of the public API and not even working. Will be removed in 0.14.0')
|
@deprecated('not part of the public API and not even working')
|
||||||
class AuthSrc(metaclass=ABCMeta):
|
class AuthSrc(metaclass=ABCMeta):
|
||||||
'''
|
'''
|
||||||
AuthSrc object required for require_auth_base().
|
AuthSrc object required for require_auth_base().
|
||||||
|
|
@ -113,7 +113,7 @@ class AuthSrc(metaclass=ABCMeta):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@deprecated('not working and too complex to use. Will be removed in 0.14.0')
|
@deprecated('not working and too complex to use')
|
||||||
def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str | Column[_T] = 'id', dest: str = 'user',
|
def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str | Column[_T] = 'id', dest: str = 'user',
|
||||||
required: bool = False, signed: bool = False, sig_dest: str = 'signature', validators: Callable | Iterable[Callable] | None = None):
|
required: bool = False, signed: bool = False, sig_dest: str = 'signature', validators: Callable | Iterable[Callable] | None = None):
|
||||||
'''
|
'''
|
||||||
|
|
@ -161,7 +161,7 @@ def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str |
|
||||||
from .asyncio import SQLAlchemy, AsyncSelectPagination, async_query
|
from .asyncio import SQLAlchemy, AsyncSelectPagination, async_query
|
||||||
from .orm import (
|
from .orm import (
|
||||||
id_column, snowflake_column, match_column, match_constraint, bool_column, declarative_base, parent_children,
|
id_column, snowflake_column, match_column, match_constraint, bool_column, declarative_base, parent_children,
|
||||||
author_pair, age_pair, bound_fk, unbound_fk, want_column, a_relationship, BitSelector, secret_column, username_column
|
author_pair, age_pair, bound_fk, unbound_fk, want_column, a_relationship, BitSelector, secret_column
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optional dependency: do not import into __init__.py
|
# Optional dependency: do not import into __init__.py
|
||||||
|
|
@ -169,7 +169,7 @@ __all__ = (
|
||||||
'IdType', 'id_column', 'snowflake_column', 'entity_base', 'declarative_base', 'token_signer',
|
'IdType', 'id_column', 'snowflake_column', 'entity_base', 'declarative_base', 'token_signer',
|
||||||
'match_column', 'match_constraint', 'bool_column', 'parent_children',
|
'match_column', 'match_constraint', 'bool_column', 'parent_children',
|
||||||
'author_pair', 'age_pair', 'bound_fk', 'unbound_fk', 'want_column',
|
'author_pair', 'age_pair', 'bound_fk', 'unbound_fk', 'want_column',
|
||||||
'a_relationship', 'BitSelector', 'secret_column', 'username_column',
|
'a_relationship', 'BitSelector', 'secret_column',
|
||||||
# .asyncio
|
# .asyncio
|
||||||
'SQLAlchemy', 'AsyncSelectPagination', 'async_query', 'SessionWrapper'
|
'SQLAlchemy', 'AsyncSelectPagination', 'async_query', 'SessionWrapper'
|
||||||
)
|
)
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"""
|
"""
|
||||||
Helpers for asynchronous use of SQLAlchemy.
|
Helpers for asynchronous use of SQLAlchemy.
|
||||||
|
|
||||||
*New in 0.5.0; moved to current location in 0.6.0*
|
NEW 0.5.0; moved to current location 0.6.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -47,23 +47,21 @@ class SQLAlchemy:
|
||||||
user = (await session.execute(select(User).where(User.id == userid))).scalar()
|
user = (await session.execute(select(User).where(User.id == userid))).scalar()
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
|
|
||||||
*Changed in 0.6.0*: added wrap=True
|
UPDATED 0.6.0: added wrap=True
|
||||||
|
|
||||||
*Changed in 0.6.1*: expire_on_commit is now configurable per-SQLAlchemy();
|
UPDATED 0.6.1: expire_on_commit is now configurable per-SQLAlchemy();
|
||||||
now sessions are stored as context variables
|
now sessions are stored as context variables
|
||||||
|
|
||||||
*Changed in 0.11.0*: sessions are now wrapped by default; turn it off by instantiating it with wrap=False
|
|
||||||
"""
|
"""
|
||||||
base: DeclarativeBase
|
base: DeclarativeBase
|
||||||
engine: AsyncEngine
|
engine: AsyncEngine
|
||||||
_session_tok: list[Token[AsyncSession]]
|
_session_tok: list[Token[AsyncSession]]
|
||||||
_wrapsessions: bool | None
|
_wrapsessions: bool
|
||||||
_xocommit: bool | None
|
_xocommit: bool
|
||||||
NotFound = NotFoundError
|
NotFound = NotFoundError
|
||||||
|
|
||||||
def __init__(self, model_class: DeclarativeBase, *, expire_on_commit = False, wrap = True):
|
def __init__(self, model_class: DeclarativeBase, *, expire_on_commit = False, wrap = False):
|
||||||
self.base = model_class
|
self.base = model_class
|
||||||
self.engine = None
|
self.engine = None
|
||||||
self._wrapsessions = wrap
|
self._wrapsessions = wrap
|
||||||
|
|
@ -73,13 +71,13 @@ class SQLAlchemy:
|
||||||
def _ensure_engine(self):
|
def _ensure_engine(self):
|
||||||
if self.engine is None:
|
if self.engine is None:
|
||||||
raise RuntimeError('database is not connected')
|
raise RuntimeError('database is not connected')
|
||||||
async def begin(self, *, expire_on_commit = None, wrap = None, **kw) -> AsyncSession:
|
async def begin(self, *, expire_on_commit = None, wrap = False, **kw) -> AsyncSession:
|
||||||
self._ensure_engine()
|
self._ensure_engine()
|
||||||
## XXX is it accurate?
|
## XXX is it accurate?
|
||||||
s = AsyncSession(self.engine,
|
s = AsyncSession(self.engine,
|
||||||
expire_on_commit=expire_on_commit if expire_on_commit is not None else self._xocommit,
|
expire_on_commit=expire_on_commit if expire_on_commit is not None else self._xocommit,
|
||||||
**kw)
|
**kw)
|
||||||
if (wrap if wrap is not None else self._wrapsessions):
|
if wrap:
|
||||||
s = SessionWrapper(s)
|
s = SessionWrapper(s)
|
||||||
current_session.set(s)
|
current_session.set(s)
|
||||||
return s
|
return s
|
||||||
|
|
@ -254,8 +252,5 @@ class SessionWrapper:
|
||||||
"""
|
"""
|
||||||
return getattr(self._session, key)
|
return getattr(self._session, key)
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self._session.close()
|
|
||||||
|
|
||||||
# Optional dependency: do not import into __init__.py
|
# Optional dependency: do not import into __init__.py
|
||||||
__all__ = ('SQLAlchemy', 'AsyncSelectPagination', 'async_query', 'SessionWrapper')
|
__all__ = ('SQLAlchemy', 'AsyncSelectPagination', 'async_query', 'SessionWrapper')
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Utilities for SQLAlchemy; ORM
|
Utilities for SQLAlchemy; ORM
|
||||||
|
|
||||||
*New in 0.6.0 (moved)*
|
NEW 0.6.0 (moved)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -115,6 +115,7 @@ def match_column(length: int, regex: str | re.Pattern, /, case: StringCase = Str
|
||||||
constraint_name=constraint_name or f'{x.__tablename__}_{n}_valid')), *args, **kwargs)
|
constraint_name=constraint_name or f'{x.__tablename__}_{n}_valid')), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@future(version='0.8.0')
|
||||||
def username_column(
|
def username_column(
|
||||||
length: int = 32, regex: str | re.Pattern = '[a-z_][a-z0-9_-]+', *args, case: StringCase = StringCase.LOWER,
|
length: int = 32, regex: str | re.Pattern = '[a-z_][a-z0-9_-]+', *args, case: StringCase = StringCase.LOWER,
|
||||||
nullable : bool = False, **kwargs) -> Incomplete[Column[str] | Column[str | None]]:
|
nullable : bool = False, **kwargs) -> Incomplete[Column[str] | Column[str | None]]:
|
||||||
|
|
@ -123,7 +124,7 @@ def username_column(
|
||||||
|
|
||||||
Username must match the given `regex` and be at most `length` characters long.
|
Username must match the given `regex` and be at most `length` characters long.
|
||||||
|
|
||||||
*New in 0.8.0*
|
NEW 0.8.0
|
||||||
"""
|
"""
|
||||||
if case is StringCase.AS_IS:
|
if case is StringCase.AS_IS:
|
||||||
warnings.warn('case sensitive usernames may lead to impersonation and unexpected behavior', UserWarning)
|
warnings.warn('case sensitive usernames may lead to impersonation and unexpected behavior', UserWarning)
|
||||||
|
|
@ -135,7 +136,7 @@ def bool_column(value: bool = False, nullable: bool = False, **kwargs) -> Column
|
||||||
"""
|
"""
|
||||||
Column for a single boolean value.
|
Column for a single boolean value.
|
||||||
|
|
||||||
*New in 0.4.0*
|
NEW in 0.4.0
|
||||||
"""
|
"""
|
||||||
def_val = text('true') if value else text('false')
|
def_val = text('true') if value else text('false')
|
||||||
return Column(Boolean, server_default=def_val, nullable=nullable, **kwargs)
|
return Column(Boolean, server_default=def_val, nullable=nullable, **kwargs)
|
||||||
|
|
@ -197,7 +198,7 @@ def secret_column(length: int = 64, max_length: int | None = None, gen: Callable
|
||||||
"""
|
"""
|
||||||
Column filled in by default with random bits (64 by default). Useful for secrets.
|
Column filled in by default with random bits (64 by default). Useful for secrets.
|
||||||
|
|
||||||
*New in 0.6.0*
|
NEW 0.6.0
|
||||||
"""
|
"""
|
||||||
max_length = max_length or length
|
max_length = max_length or length
|
||||||
return Column(LargeBinary(max_length), default=lambda: gen(length), nullable=nullable, **kwargs)
|
return Column(LargeBinary(max_length), default=lambda: gen(length), nullable=nullable, **kwargs)
|
||||||
|
|
@ -215,7 +216,7 @@ def parent_children(keyword: str, /, *, lazy='selectin', **kwargs) -> tuple[Inco
|
||||||
Additional keyword arguments can be sourced with parent_ and child_ argument prefixes,
|
Additional keyword arguments can be sourced with parent_ and child_ argument prefixes,
|
||||||
obviously.
|
obviously.
|
||||||
|
|
||||||
*Changed in 0.5.0*: the both relationship()s use lazy='selectin' attribute now by default.
|
CHANGED 0.5.0: the both relationship()s use lazy='selectin' attribute now by default.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parent_kwargs = kwargs_prefix(kwargs, 'parent_')
|
parent_kwargs = kwargs_prefix(kwargs, 'parent_')
|
||||||
|
|
@ -231,7 +232,7 @@ def a_relationship(primary = None, /, j=None, *, lazy='selectin', **kwargs):
|
||||||
"""
|
"""
|
||||||
Shorthand for relationship() that sets lazy='selectin' by default.
|
Shorthand for relationship() that sets lazy='selectin' by default.
|
||||||
|
|
||||||
*New in 0.6.0*
|
NEW 0.6.0
|
||||||
"""
|
"""
|
||||||
if j:
|
if j:
|
||||||
kwargs['primaryjoin'] = j
|
kwargs['primaryjoin'] = j
|
||||||
|
|
@ -246,7 +247,7 @@ def unbound_fk(target: str | Column | InstrumentedAttribute, typ: _T | None = No
|
||||||
|
|
||||||
If target is a string, make sure to pass the column type at typ= (default: IdType aka varbinary(16))!
|
If target is a string, make sure to pass the column type at typ= (default: IdType aka varbinary(16))!
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
"""
|
"""
|
||||||
if isinstance(target, (Column, InstrumentedAttribute)):
|
if isinstance(target, (Column, InstrumentedAttribute)):
|
||||||
target_name = f'{target.table.name}.{target.name}'
|
target_name = f'{target.table.name}.{target.name}'
|
||||||
|
|
@ -269,7 +270,7 @@ def bound_fk(target: str | Column | InstrumentedAttribute, typ: _T = None, **kwa
|
||||||
|
|
||||||
If target is a string, make sure to pass the column type at typ= (default: IdType aka varbinary(16))!
|
If target is a string, make sure to pass the column type at typ= (default: IdType aka varbinary(16))!
|
||||||
|
|
||||||
*New in 0.5.0*
|
NEW 0.5.0
|
||||||
"""
|
"""
|
||||||
if isinstance(target, (Column, InstrumentedAttribute)):
|
if isinstance(target, (Column, InstrumentedAttribute)):
|
||||||
target_name = f'{target.table.name}.{target.name}'
|
target_name = f'{target.table.name}.{target.name}'
|
||||||
|
|
@ -288,7 +289,7 @@ class _BitComparator(Comparator):
|
||||||
"""
|
"""
|
||||||
Comparator object for BitSelector()
|
Comparator object for BitSelector()
|
||||||
|
|
||||||
*New in 0.6.0*
|
NEW 0.6.0
|
||||||
"""
|
"""
|
||||||
_column: Column
|
_column: Column
|
||||||
_flag: int
|
_flag: int
|
||||||
|
|
@ -314,7 +315,7 @@ class BitSelector:
|
||||||
|
|
||||||
Mimicks peewee's 'BitField()' behavior, with SQLAlchemy.
|
Mimicks peewee's 'BitField()' behavior, with SQLAlchemy.
|
||||||
|
|
||||||
*New in 0.6.0*
|
NEW 0.6.0
|
||||||
"""
|
"""
|
||||||
_column: Column
|
_column: Column
|
||||||
_flag: int
|
_flag: int
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Helpers for asynchronous use of SQLAlchemy.
|
Helpers for asynchronous use of SQLAlchemy.
|
||||||
|
|
||||||
*New in 0.5.0; moved to ``sqlalchemy.asyncio`` in 0.6.0*
|
NEW 0.5.0; MOVED to sqlalchemy.asyncio in 0.6.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,5 @@ class PrefixIdentifier:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self._prefix}'
|
return f'{self._prefix}'
|
||||||
|
|
||||||
|
|
||||||
class SpitText:
|
|
||||||
"""
|
|
||||||
A formatter for pre-compiled strings.
|
|
||||||
|
|
||||||
*New in 0.11.0*
|
|
||||||
"""
|
|
||||||
|
|
||||||
def format(self, templ: str, *attrs: Iterable[str]) -> str:
|
|
||||||
attrs = [getattr(self, attr, f'{{{{ {attr} }}}}') for attr in attrs]
|
|
||||||
return templ.format(*attrs).strip()
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('PrefixIdentifier',)
|
__all__ = ('PrefixIdentifier',)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ def terminal_required(func):
|
||||||
"""
|
"""
|
||||||
Requires the decorated callable to be fully connected to a terminal.
|
Requires the decorated callable to be fully connected to a terminal.
|
||||||
|
|
||||||
*New in 0.7.0*
|
NEW 0.7.0
|
||||||
"""
|
"""
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*a, **ka):
|
def wrapper(*a, **ka):
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ import re
|
||||||
|
|
||||||
from typing import Any, Iterable, TypeVar
|
from typing import Any, Iterable, TypeVar
|
||||||
|
|
||||||
from suou.classtools import MISSING
|
|
||||||
|
|
||||||
_T = TypeVar('_T')
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
def matches(regex: str | int, /, length: int = 0, *, flags=0):
|
def matches(regex: str | int, /, length: int = 0, *, flags=0):
|
||||||
|
|
@ -57,24 +55,5 @@ def not_less_than(y):
|
||||||
"""
|
"""
|
||||||
return lambda x: x >= y
|
return lambda x: x >= y
|
||||||
|
|
||||||
def yesno(x: str | int | bool | None) -> bool:
|
__all__ = ('matches', 'not_greater_than')
|
||||||
"""
|
|
||||||
Returns False if x.lower() is in '0', '', 'no', 'n', 'false' or 'off'.
|
|
||||||
|
|
||||||
*New in 0.9.0*
|
|
||||||
|
|
||||||
*Changed in 0.11.1*: now accepts None and bool.
|
|
||||||
"""
|
|
||||||
if x in (None, MISSING):
|
|
||||||
return False
|
|
||||||
if isinstance(x, bool):
|
|
||||||
return x
|
|
||||||
if isinstance(x, int):
|
|
||||||
return x != 0
|
|
||||||
if isinstance(x, str):
|
|
||||||
return x.lower() not in ('', '0', 'off', 'n', 'no', 'false', 'f')
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('matches', 'must_be', 'not_greater_than', 'not_less_than', 'yesno')
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Content serving API over HTTP, based on Starlette.
|
Content serving API over HTTP, based on Starlette.
|
||||||
|
|
||||||
*New in 0.6.0*
|
NEW 0.6.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -16,55 +16,24 @@ 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 typing import Callable
|
|
||||||
import warnings
|
import warnings
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
from starlette.responses import JSONResponse, PlainTextResponse, Response
|
from starlette.responses import JSONResponse, PlainTextResponse, Response
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
|
|
||||||
from suou.itertools import makelist
|
|
||||||
from suou.functools import future
|
from suou.functools import future
|
||||||
|
|
||||||
@future()
|
@future()
|
||||||
class Waiter():
|
class Waiter():
|
||||||
_cached_app: Callable | None = None
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.routes: list[Route] = []
|
self.routes: list[Route] = []
|
||||||
self.production = False
|
self.production = False
|
||||||
|
|
||||||
async def __call__(self, *args):
|
|
||||||
return await self._build_app()(*args)
|
|
||||||
|
|
||||||
def _build_app(self) -> Starlette:
|
def _build_app(self) -> Starlette:
|
||||||
if not self._cached_app:
|
return Starlette(
|
||||||
self._cached_app = Starlette(
|
debug = not self.production,
|
||||||
debug = not self.production,
|
routes= self.routes
|
||||||
routes= self.routes
|
)
|
||||||
)
|
|
||||||
return self._cached_app
|
|
||||||
|
|
||||||
def get(self, endpoint: str, *a, **k):
|
|
||||||
return self._route('GET', endpoint, *a, **k)
|
|
||||||
|
|
||||||
def post(self, endpoint: str, *a, **k):
|
|
||||||
return self._route('POST', endpoint, *a, **k)
|
|
||||||
|
|
||||||
def delete(self, endpoint: str, *a, **k):
|
|
||||||
return self._route('DELETE', endpoint, *a, **k)
|
|
||||||
|
|
||||||
def put(self, endpoint: str, *a, **k):
|
|
||||||
return self._route('PUT', endpoint, *a, **k)
|
|
||||||
|
|
||||||
def patch(self, endpoint: str, *a, **k):
|
|
||||||
return self._route('PATCH', endpoint, *a, **k)
|
|
||||||
|
|
||||||
def _route(self, methods: list[str], endpoint: str, **kwargs):
|
|
||||||
def decorator(func):
|
|
||||||
self.routes.append(Route(endpoint, func, methods=makelist(methods, False), **kwargs))
|
|
||||||
return func
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
## TODO get, post, etc.
|
## TODO get, post, etc.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from suou.legal import Lawyer
|
|
||||||
|
|
||||||
|
|
||||||
EXPECTED_INDEMNIFY = """
|
|
||||||
You agree to indemnify and hold harmless TNT from any and all claims, damages, liabilities, costs and expenses, including reasonable and unreasonable counsel and attorney’s fees, arising out of any breach of this agreement.
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
EXPECTED_GOVERNING_LAW = """
|
|
||||||
These terms of services are governed by, and shall be interpreted in accordance with, the laws of Wakanda. You consent to the sole jurisdiction of Asgard, Wakanda for all disputes between You and TNT, and You consent to the sole application of Wakandan law for all such disputes.
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
class TestLegal(unittest.TestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.lawyer = Lawyer(
|
|
||||||
app_name = "TNT",
|
|
||||||
company_name= "ACME, Ltd.",
|
|
||||||
country = "Wakanda",
|
|
||||||
domain_name= "example.com",
|
|
||||||
jurisdiction= "Asgard, Wakanda",
|
|
||||||
country_adjective= "Wakandan"
|
|
||||||
)
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
def test_indemnify(self):
|
|
||||||
self.assertEqual(
|
|
||||||
self.lawyer.indemnify(),
|
|
||||||
EXPECTED_INDEMNIFY
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_governing_law(self):
|
|
||||||
self.assertEqual(
|
|
||||||
self.lawyer.governing_law(),
|
|
||||||
EXPECTED_GOVERNING_LAW
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
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,38 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
from suou.calendar import not_greater_than
|
|
||||||
from suou.validators import not_less_than, yesno
|
|
||||||
|
|
||||||
class TestValidators(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
...
|
|
||||||
def tearDown(self):
|
|
||||||
...
|
|
||||||
def test_yesno(self):
|
|
||||||
self.assertFalse(yesno('false'))
|
|
||||||
self.assertFalse(yesno('FALSe'))
|
|
||||||
self.assertTrue(yesno('fasle'))
|
|
||||||
self.assertTrue(yesno('falso'))
|
|
||||||
self.assertTrue(yesno('zero'))
|
|
||||||
self.assertTrue(yesno('true'))
|
|
||||||
self.assertFalse(yesno('0'))
|
|
||||||
self.assertTrue(yesno('00'))
|
|
||||||
self.assertTrue(yesno('.'))
|
|
||||||
self.assertTrue(yesno('2'))
|
|
||||||
self.assertTrue(yesno('o'))
|
|
||||||
self.assertFalse(yesno('oFF'))
|
|
||||||
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