0.14.0a1 add ast module + email_column()
This commit is contained in:
parent
82d4fc2ab2
commit
4aefac0e99
9 changed files with 155 additions and 19 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -30,6 +30,7 @@ docs/_build
|
|||
docs/_static
|
||||
docs/templates
|
||||
.coverage
|
||||
.pytest_cache/
|
||||
|
||||
# changes during CD/CI
|
||||
aliases/*/pyproject.toml
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
# Changelog
|
||||
|
||||
## 0.13.1
|
||||
## 0.14.0
|
||||
|
||||
* Added `ast` module
|
||||
* Deprecate `dei_args()` for problems with the typing system. The function is not going away tho
|
||||
* Module `sqlalchemy`: added `email_column()`
|
||||
|
||||
## 0.13.1 and 0.12.7
|
||||
|
||||
+ Typing fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ from .color import OKLabColor, chalk, WebColor, RGBColor, LinearRGBColor, \
|
|||
from .mat import Matrix
|
||||
from .argparse import LetterSubparsers
|
||||
|
||||
__version__ = "0.13.1"
|
||||
__version__ = "0.14.0a1"
|
||||
|
||||
__all__ = (
|
||||
'ColorFormatter',
|
||||
|
|
|
|||
116
src/suou/ast.py
Normal file
116
src/suou/ast.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
Experimental AST module
|
||||
|
||||
---
|
||||
|
||||
Copyright (c) 2026 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 typing import Any, Collection
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
class Node:
|
||||
__slots__ = ('_children', )
|
||||
|
||||
_count: int
|
||||
_children: list[Node]
|
||||
_types: Collection[type]
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({', '.join(repr(x) for x in self._children)})"
|
||||
|
||||
def __init__(self, *args):
|
||||
if self._count != len(args):
|
||||
raise TypeError(f'{self.__class__.__name__} must be instanced with {self._count} arguments, got {len(args)}')
|
||||
|
||||
for i, (a, t) in enumerate(zip(args, self._types)):
|
||||
if t != Any and not isinstance(a, t):
|
||||
raise TypeError(f"argument {i} must be of type {t.__name__!r}, got {a.__class__.__name__!r}")
|
||||
|
||||
self._children = list(args)
|
||||
|
||||
def eval(self, ctx: dict):
|
||||
raise TypeError(f'{self.__class__.__name__} cannot be eval()ed')
|
||||
|
||||
def __getitem__(self, key: int):
|
||||
if key >= self._count:
|
||||
raise IndexError('child node index out of range')
|
||||
return self._children[key]
|
||||
|
||||
def __len__(self):
|
||||
return self._count
|
||||
|
||||
|
||||
class ZeroOp(Node):
|
||||
_count = 0
|
||||
_types = ()
|
||||
|
||||
|
||||
class UnaryOp(Node):
|
||||
_count = 1
|
||||
_types = (Any,)
|
||||
|
||||
|
||||
class BinaryOp(Node):
|
||||
_count = 2
|
||||
_types = (Any, Any)
|
||||
|
||||
|
||||
class TernaryOp(Node):
|
||||
_count = 3
|
||||
_types = (Any, Any, Any)
|
||||
|
||||
|
||||
class MultiOp(Node):
|
||||
_count = 1
|
||||
_types = (list,)
|
||||
|
||||
def __getitem__(self, key: int):
|
||||
return self._children[0][key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._children[0])
|
||||
|
||||
def __len__(self):
|
||||
return len(self._children[0])
|
||||
|
||||
|
||||
|
||||
class Literal(UnaryOp):
|
||||
"""A literal evals to the enclosed value."""
|
||||
def eval(self, ctx: dict):
|
||||
return self[0]
|
||||
|
||||
|
||||
## SUOU provides some ready-made literals, for the sake of ease-of-use.
|
||||
|
||||
class IntLiteral(Literal):
|
||||
_types = (int,)
|
||||
|
||||
class FloatLiteral(Literal):
|
||||
"""
|
||||
WARNING: may be subject to loss of precision.
|
||||
"""
|
||||
_types = (float,)
|
||||
|
||||
class StringLiteral(Literal):
|
||||
_types = (str,)
|
||||
|
||||
|
||||
# This module is experimental and therefore not re-exported into __init__
|
||||
__all__ = (
|
||||
'Node', 'ZeroOp', 'UnaryOp', 'BinaryOp', 'TernaryOp',
|
||||
'MultiOp', 'Literal', 'IntLiteral', 'FloatLiteral',
|
||||
'StringLiteral'
|
||||
)
|
||||
|
|
@ -21,6 +21,8 @@ from __future__ import annotations
|
|||
from functools import wraps
|
||||
from typing import Callable, Collection, TypeVar, Any
|
||||
|
||||
from . import deprecated
|
||||
|
||||
_T = TypeVar('_T')
|
||||
_U = TypeVar('_U')
|
||||
|
||||
|
|
@ -115,6 +117,7 @@ class Pronoun(int):
|
|||
|
||||
|
||||
|
||||
@deprecated('breaks the typing system somewhat; won\'t be removed tho')
|
||||
def dei_args(**renames: dict[str, str]):
|
||||
"""
|
||||
Allow for aliases in the keyword argument names, in form alias='real_name'.
|
||||
|
|
|
|||
|
|
@ -22,11 +22,8 @@ from itsdangerous import TimestampSigner
|
|||
from itsdangerous import Signer as _Signer
|
||||
from itsdangerous.encoding import int_to_bytes as _int_to_bytes
|
||||
|
||||
from suou.dei import dei_args
|
||||
from suou.itertools import rtuple
|
||||
|
||||
from .functools import not_implemented
|
||||
from .codecs import jsondecode, jsonencode, rb64decode, want_bytes, want_str, b64decode, b64encode
|
||||
from .itertools import rtuple
|
||||
from .codecs import jsondecode, jsonencode, want_bytes, want_str, b64decode, b64encode
|
||||
from .iding import Siq
|
||||
from .classtools import MISSING
|
||||
|
||||
|
|
@ -35,7 +32,6 @@ class UserSigner(TimestampSigner):
|
|||
itsdangerous.TimestampSigner() instanced from a user ID, with token generation and validation capabilities.
|
||||
"""
|
||||
user_id: int
|
||||
@dei_args(primary_secret='master_secret')
|
||||
def __init__(self, master_secret: bytes, user_id: int, user_secret: bytes, **kwargs):
|
||||
super().__init__(master_secret + user_secret, salt=Siq(user_id).to_bytes(), **kwargs)
|
||||
self.user_id = user_id
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ from functools import wraps
|
|||
|
||||
from contextvars import ContextVar, Token
|
||||
from typing import Callable, TypeVar
|
||||
from sqlalchemy import Select, Table, func, select
|
||||
from sqlalchemy.orm import DeclarativeBase, lazyload
|
||||
from sqlalchemy import Select, Table, select
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
||||
|
||||
try:
|
||||
|
|
@ -31,8 +31,7 @@ try:
|
|||
except ImportError:
|
||||
AsyncSelectPagination = None
|
||||
|
||||
from suou.exceptions import NotFoundError
|
||||
from suou.glue import glue
|
||||
from ..exceptions import NotFoundError
|
||||
|
||||
_T = TypeVar('_T')
|
||||
_U = TypeVar('_U')
|
||||
|
|
@ -126,9 +125,11 @@ class SQLAlchemy:
|
|||
self.engine, checkfirst=checkfirst
|
||||
)
|
||||
|
||||
# XXX NOT public API! DO NOT USE
|
||||
current_session: ContextVar[AsyncSession] = ContextVar('current_session')
|
||||
|
||||
current_session: ContextVar[AsyncSession] = ContextVar('current_session')
|
||||
"""
|
||||
XXX NOT public API! DO NOT USE
|
||||
"""
|
||||
|
||||
def async_query(db: SQLAlchemy, multi: False):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
|
||||
|
||||
|
||||
from binascii import Incomplete
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
|
@ -27,10 +26,8 @@ from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, Date, Forei
|
|||
from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, Relationship, declarative_base as _declarative_base, relationship
|
||||
from sqlalchemy.types import TypeEngine
|
||||
from sqlalchemy.ext.hybrid import Comparator
|
||||
from suou.functools import future
|
||||
from suou.classtools import Wanted, Incomplete
|
||||
from suou.codecs import StringCase
|
||||
from suou.dei import dei_args
|
||||
from suou.iding import Siq, SiqCache, SiqGen, SiqType
|
||||
from suou.itertools import kwargs_prefix
|
||||
from suou.snowflake import SnowflakeGen
|
||||
|
|
@ -131,6 +128,23 @@ def username_column(
|
|||
return match_column(length, regex, case=case, nullable=nullable, unique=True, *args, **kwargs)
|
||||
|
||||
|
||||
EMAIL_RE_USERNAME = r"[a-z0-9-]+(\.[a-z0-9-]+)*"
|
||||
EMAIL_RE_DOMAIN = r"([a-z0-9-]+\.)+[a-z0-9-]{2,15}"
|
||||
EMAIL_RE_YESALIASES = EMAIL_RE_USERNAME + r"(\+" + EMAIL_RE_USERNAME + ")?@" + EMAIL_RE_DOMAIN
|
||||
EMAIL_RE_NOALIASES = EMAIL_RE_USERNAME + r"@" + EMAIL_RE_DOMAIN
|
||||
|
||||
def email_column(
|
||||
length: int = 256, *args, allow_aliases: bool = True, nullable: bool = False, unique: bool = True, **kwargs
|
||||
):
|
||||
"""
|
||||
Construct a column containing a email address.
|
||||
|
||||
*New in 0.14.0*
|
||||
"""
|
||||
return match_column(length, EMAIL_RE_YESALIASES if allow_aliases else EMAIL_RE_NOALIASES, case = StringCase.FORCE_LOWER,
|
||||
unique = unique, nullable = nullable, *args, **kwargs)
|
||||
|
||||
|
||||
def bool_column(value: bool = False, nullable: bool = False, **kwargs) -> Column[bool]:
|
||||
"""
|
||||
Column for a single boolean value.
|
||||
|
|
@ -141,7 +155,6 @@ def bool_column(value: bool = False, nullable: bool = False, **kwargs) -> Column
|
|||
return Column(Boolean, server_default=def_val, nullable=nullable, **kwargs)
|
||||
|
||||
|
||||
@dei_args(primary_secret='master_secret')
|
||||
def declarative_base(domain_name: str, master_secret: bytes, metadata: dict | None = None, **kwargs) -> type[DeclarativeBase]:
|
||||
"""
|
||||
Drop-in replacement for sqlalchemy.orm.declarative_base()
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ def yesno(x: str | int | bool | None) -> bool:
|
|||
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')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue