0.14.0a3 add ascii_column() and runtime check to match_column()

This commit is contained in:
Yusur 2026-07-04 15:37:16 +02:00
parent e2aec01d31
commit b239d0eaca
3 changed files with 50 additions and 8 deletions

View file

@ -4,7 +4,9 @@
* Added `ast` module
* Deprecate `dei_args()` for problems with the typing system. The function is not going away tho
* Module `sqlalchemy`: added `email_column()`
* Module `sqlalchemy`:
* added `email_column()`, `ascii_column()`
* added application level checks for `match_column()`
* Added common values for snowflake epoch in `SnowflakeEpoch` enum
## 0.13.1 and 0.12.7

View file

@ -41,7 +41,7 @@ from .color import OKLabColor, chalk, WebColor, RGBColor, LinearRGBColor, \
from .mat import Matrix
from .argparse import LetterSubparsers
__version__ = "0.14.0a2"
__version__ = "0.14.0a3"
__all__ = (
'ColorFormatter',

View file

@ -20,9 +20,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import os
import re
from typing import Any, Callable, TypeVar
from typing import Any, Callable, TypeAlias, TypeVar
import warnings
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, Date, ForeignKey, LargeBinary, MetaData, SmallInteger, String, text
from sqlalchemy import VARCHAR, BigInteger, Boolean, CheckConstraint, Column, Date, ForeignKey, LargeBinary, MetaData, SmallInteger, String, TypeDecorator, text
from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, Relationship, declarative_base as _declarative_base, relationship
from sqlalchemy.types import TypeEngine
from sqlalchemy.ext.hybrid import Comparator
@ -71,8 +71,6 @@ def id_column(typ: SiqType, *, primary_key: bool = True, **kwargs):
def snowflake_column(*, primary_key: bool = True, **kwargs):
"""
Same as id_column() but with snowflakes.
XXX this is meant ONLY as means of transition; for new stuff, use id_column() and SIQ.
"""
def new_id_factory(owner: DeclarativeBase) -> Callable:
epoch = owner.metadata.info['snowflake_epoch']
@ -100,7 +98,39 @@ match_constraint.TEXT_DIALECTS = {
'mariadb': ':n RLIKE :re'
}
def match_column(length: int, regex: str | re.Pattern, /, case: StringCase = StringCase.AS_IS, *args, constraint_name: str | None = None, **kwargs) -> Incomplete[Column[str]]:
## Type decorators for match_column and ascii_column follow
class MatchString(TypeDecorator):
impl: TypeAlias = String
def __init__(self, length: int, regex: str | re.Pattern, case: StringCase = StringCase.AS_IS, *args, **kwargs):
TypeDecorator.__init__(self, length=length, *args, **kwargs)
self.regex = regex
self.case = case
def process_bind_param(self, value, dialect):
value = self.case.transform(value)
if not re.fullmatch(self.regex, value):
raise ValueError('value does not match regex')
return value
class AsciiString(TypeDecorator):
impl: TypeAlias = String
def process_bind_param(self, value: str, dialect) :
try:
value.encode('ascii')
except Exception:
raise ValueError('only ASCII strings are allowed')
return value
## END type decorators
def match_column(
length: int, regex: str | re.Pattern, /, case: StringCase = StringCase.AS_IS,
*args, constraint_name: str | None = None, **kwargs) -> Incomplete[Column[str]]:
"""
Syntactic sugar to create a String() column with a check constraint matching the given regular expression.
@ -108,10 +138,20 @@ def match_column(length: int, regex: str | re.Pattern, /, case: StringCase = Str
"""
if case != StringCase.AS_IS: # TODO
warnings.warn('case arg is currently not working', FutureWarning)
return Incomplete(Column, String(length), Wanted(lambda x, n: match_constraint(n, regex, #dialect=x.metadata.engine.dialect.name,
return Incomplete(Column, MatchString(length, regex, case), Wanted(lambda x, n: match_constraint(n, regex, #dialect=x.metadata.engine.dialect.name,
constraint_name=constraint_name or f'{x.__tablename__}_{n}_valid')), *args, **kwargs)
def ascii_column(length: int, /, *args, **kwargs):
"""
String() column that admits only ASCII characters.
*New in 0.14.0*
"""
return Column(AsciiString(length), *args, **kwargs)
def username_column(
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]]: