diff --git a/CHANGELOG.md b/CHANGELOG.md index ce764a9..6b70a17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ + \[BREAKING] Changed the behavior of `makelist()`: now it's also a decorator, converting its return type to a list (revertable with `wrap=False`) + New module `lex` with functions `symbol_table()` and `lex()` — make tokenization more affordable + Add `dorks` module and `flask.harden()` -+ Added `addattr()` ++ Add `sqlalchemy.bool_column()`: make making flags painless ++ Added `addattr()`, `PrefixIdentifier()` ## 0.3.6 diff --git a/pyproject.toml b/pyproject.toml index 58766fa..9ef670d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,7 @@ sqlalchemy = [ ] flask = [ "Flask>=2.0.0", - "Flask-RestX", - "Quart", - "Quart-Schema" + "Flask-RestX" ] flask_sqlalchemy = [ "Flask-SqlAlchemy", @@ -50,6 +48,21 @@ peewee = [ markdown = [ "markdown>=3.0.0" ] +quart = [ + "Flask>=2.0.0", + "Quart", + "Quart-Schema", + "uvloop; os_name=='posix'" +] + +full = [ + "sakuragasaki46-suou[sqlalchemy]", + "sakuragasaki46-suou[flask]", + "sakuragasaki46-suou[quart]", + "sakuragasaki46-suou[peewee]", + "sakuragasaki46-suou[markdown]" +] + [tool.setuptools.dynamic] version = { attr = "suou.__version__" } diff --git a/src/suou/sqlalchemy.py b/src/suou/sqlalchemy.py index edd1b02..b16cae7 100644 --- a/src/suou/sqlalchemy.py +++ b/src/suou/sqlalchemy.py @@ -20,7 +20,7 @@ from abc import ABCMeta, abstractmethod from functools import wraps from typing import Callable, Iterable, Never, TypeVar import warnings -from sqlalchemy import BigInteger, CheckConstraint, Date, Dialect, ForeignKey, LargeBinary, Column, MetaData, SmallInteger, String, create_engine, select, text +from sqlalchemy import BigInteger, Boolean, CheckConstraint, Date, Dialect, ForeignKey, LargeBinary, Column, MetaData, SmallInteger, String, create_engine, select, text from sqlalchemy.orm import DeclarativeBase, Session, declarative_base as _declarative_base, relationship from .snowflake import SnowflakeGen @@ -120,7 +120,17 @@ def match_column(length: int, regex: str, /, case: StringCase = StringCase.AS_IS constraint_name=constraint_name or f'{x.__tablename__}_{n}_valid')), *args, **kwargs) -def declarative_base(domain_name: str, master_secret: bytes, metadata: dict | None = None, **kwargs): +def bool_column(value: bool = False, nullable: bool = False, **kwargs): + """ + Column for a single boolean value. + + NEW in 0.4.0 + """ + def_val = text('true') if value else text('false') + return Column(Boolean, server_default=def_val, nullable=nullable, **kwargs) + + +def declarative_base(domain_name: str, master_secret: bytes, metadata: dict | None = None, **kwargs) -> DeclarativeBase: """ Drop-in replacement for sqlalchemy.orm.declarative_base() taking in account requirements for SIQ generation (i.e. domain name). diff --git a/src/suou/strtools.py b/src/suou/strtools.py new file mode 100644 index 0000000..2381953 --- /dev/null +++ b/src/suou/strtools.py @@ -0,0 +1,44 @@ +""" +Utilities for string manipulation. + +Why `strtools`? Why not `string`? I just~ happen to not like it + +--- + +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 typing import Callable, Iterable + +from .itertools import makelist + +class PrefixIdentifier: + _prefix: str + + def __init__(self, prefix: str | None, validators: Iterable[Callable[[str], bool]] | Callable[[str], bool] | None = None): + prefix = '' if prefix is None else prefix + if not isinstance(prefix, str): + raise TypeError + validators = makelist(validators, wrap=False) + for validator in validators: + if not validator(prefix): + raise ValueError('invalid prefix') + self._prefix = prefix + + def __getattr__(self, key: str): + return f'{self._prefix}{key}' + + def __getitem__(self, key: str) -> str: + return f'{self._prefix}{key}' + +__all__ = ('PrefixIdentifier',) + diff --git a/tests/test_codecs.py b/tests/test_codecs.py new file mode 100644 index 0000000..7ac70d6 --- /dev/null +++ b/tests/test_codecs.py @@ -0,0 +1,50 @@ + + +import binascii +import unittest +from suou.codecs import b64encode, b64decode + +B1 = b'N\xf0\xb4\xc3\x85\n\xf9\xb6\x9a\x0f\x82\xa6\x99G\x07#' +B2 = b'\xbcXiF,@|{\xbe\xe3\x0cz\xa8\xcbQ\x82' +B3 = b"\xe9\x18)\xcb'\xc2\x96\xae\xde\x86" +B4 = B1[-2:] + B2[:-2] +B5 = b'\xff\xf8\xa7\x8a\xdf\xff' + + +class TestCodecs(unittest.TestCase): + def setUp(self) -> None: + ... + def tearDown(self) -> None: + ... + + #def runTest(self): + # self.test_b64encode() + # self.test_b64decode() + + def test_b64encode(self): + self.assertEqual(b64encode(B1), 'TvC0w4UK-baaD4KmmUcHIw') + self.assertEqual(b64encode(B2), 'vFhpRixAfHu-4wx6qMtRgg') + self.assertEqual(b64encode(B3), '6RgpyyfClq7ehg') + self.assertEqual(b64encode(B4), 'ByO8WGlGLEB8e77jDHqoyw') + self.assertEqual(b64encode(B5), '__init__') + self.assertEqual(b64encode(B1[:4]), 'TvC0ww') + self.assertEqual(b64encode(b'\0' + B1[:4]), 'AE7wtMM') + self.assertEqual(b64encode(b'\0\0\0\0\0' + B1[:4]), 'AAAAAABO8LTD') + self.assertEqual(b64encode(b'\xff'), '_w') + self.assertEqual(b64encode(b''), '') + + def test_b64decode(self): + self.assertEqual(b64decode('TvC0w4UK-baaD4KmmUcHIw'), B1) + self.assertEqual(b64decode('vFhpRixAfHu-4wx6qMtRgg'), B2) + self.assertEqual(b64decode('6RgpyyfClq7ehg'), B3) + self.assertEqual(b64decode('ByO8WGlGLEB8e77jDHqoyw'), B4) + self.assertEqual(b64decode('__init__'), B5) + self.assertEqual(b64decode('TvC0ww'), B1[:4]) + self.assertEqual(b64decode('AE7wtMM'), b'\0' + B1[:4]) + self.assertEqual(b64decode('AAAAAABO8LTD'), b'\0\0\0\0\0' + B1[:4]) + self.assertEqual(b64decode('_w'), b'\xff') + self.assertEqual(b64decode(''), b'') + + self.assertRaises(binascii.Error, b64decode, 'C') + + diff --git a/tests/test_strtools.py b/tests/test_strtools.py new file mode 100644 index 0000000..d07ed88 --- /dev/null +++ b/tests/test_strtools.py @@ -0,0 +1,38 @@ + + + +import unittest + +from suou.strtools import PrefixIdentifier + +class TestStrtools(unittest.TestCase): + def setUp(self) -> None: + ... + + def tearDown(self) -> None: + ... + + def test_PrefixIdentifier_empty(self): + pi = PrefixIdentifier(None) + self.assertEqual(pi.hello, 'hello') + self.assertEqual(pi['with spaces'], 'with spaces') + self.assertEqual(pi['\x1b\x00'], '\x1b\0') + self.assertEqual(pi.same_thing, pi['same_thing']) + + with self.assertRaises(TypeError): + pi[0] + + self.assertEqual(PrefixIdentifier(None), PrefixIdentifier('')) + + def test_PrefixIdentifier_invalid(self): + with self.assertRaises(TypeError): + pi = PrefixIdentifier(1) + pi.hello + + with self.assertRaises(TypeError): + PrefixIdentifier([99182]) + + with self.assertRaises(TypeError): + PrefixIdentifier(b'alpha_') + + \ No newline at end of file