add ArgConfigSource(), 3 helpers to .sqlalchemy, add .waiter
This commit is contained in:
parent
97194b2b85
commit
eb8371757d
6 changed files with 177 additions and 4 deletions
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
## 0.6.0
|
||||
|
||||
...
|
||||
+ `.sqlalchemy` has been made a subpackage and split; `sqlalchemy_async` has been deprecated. Update your imports.
|
||||
+ Add `.waiter` module. For now, non-functional.
|
||||
+ Add those new utilities to `.sqlalchemy`: `BitSelector`, `secret_column`, `a_relationship`. Also removed dead batteries.
|
||||
+ Add `ArgConfigSource` to `.configparse`
|
||||
|
||||
## 0.5.3
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import os
|
|||
from typing import Any, Callable, Iterator, override
|
||||
from collections import OrderedDict
|
||||
|
||||
from argparse import Namespace
|
||||
|
||||
from .classtools import ValueSource, ValueProperty
|
||||
from .functools import deprecated
|
||||
from .exceptions import MissingConfigError, MissingConfigWarning
|
||||
|
|
@ -105,6 +107,28 @@ class DictConfigSource(ConfigSource):
|
|||
def __len__(self) -> int:
|
||||
return len(self._d)
|
||||
|
||||
class ArgConfigSource(ValueSource):
|
||||
"""
|
||||
It assumes arguments have already been parsed
|
||||
|
||||
NEW 0.6"""
|
||||
_ns: Namespace
|
||||
def __init__(self, ns: Namespace):
|
||||
super().__init__()
|
||||
self._ns = ns
|
||||
def __getitem__(self, key):
|
||||
return getattr(self._ns, key)
|
||||
def get(self, key, value):
|
||||
return getattr(self._ns, key, value)
|
||||
def __contains__(self, key: str, /) -> bool:
|
||||
return hasattr(self._ns, key)
|
||||
@deprecated('Here for Mapping() implementation. Untested and unused')
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
yield from self._ns._get_args()
|
||||
@deprecated('Here for Mapping() implementation. Untested and unused')
|
||||
def __len__(self) -> int:
|
||||
return len(self._ns._get_args())
|
||||
|
||||
class ConfigValue(ValueProperty):
|
||||
"""
|
||||
A single config property.
|
||||
|
|
@ -205,7 +229,8 @@ class ConfigOptions:
|
|||
|
||||
|
||||
__all__ = (
|
||||
'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'EnvConfigSource', 'ConfigParserConfigSource', 'DictConfigSource', 'ConfigSource', 'ConfigValue'
|
||||
'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'EnvConfigSource', 'ConfigParserConfigSource', 'DictConfigSource', 'ConfigSource', 'ConfigValue',
|
||||
'ArgConfigSource'
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ 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):
|
||||
'''
|
||||
|
||||
|
|
@ -45,6 +47,7 @@ class FlaskAuthSrc(AuthSrc):
|
|||
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.
|
||||
|
|
@ -77,4 +80,4 @@ def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Ca
|
|||
return auth_required
|
||||
|
||||
# Optional dependency: do not import into __init__.py
|
||||
__all__ = ('require_auth', )
|
||||
__all__ = ()
|
||||
|
|
|
|||
|
|
@ -157,13 +157,17 @@ def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str |
|
|||
|
||||
|
||||
from .asyncio import SQLAlchemy, AsyncSelectPagination, async_query
|
||||
from .orm import id_column, snowflake_column, match_column, match_constraint, bool_column, declarative_base, author_pair, age_pair, bound_fk, unbound_fk, want_column
|
||||
from .orm import (
|
||||
id_column, snowflake_column, match_column, match_constraint, bool_column, declarative_base,
|
||||
author_pair, age_pair, bound_fk, unbound_fk, want_column, a_relationship, BitSelector, secret_column
|
||||
)
|
||||
|
||||
# Optional dependency: do not import into __init__.py
|
||||
__all__ = (
|
||||
'IdType', 'id_column', 'snowflake_column', 'entity_base', 'declarative_base', 'token_signer',
|
||||
'match_column', 'match_constraint', 'bool_column', 'parent_children',
|
||||
'author_pair', 'age_pair', 'bound_fk', 'unbound_fk', 'want_column',
|
||||
'a_relationship', 'BitSelector', 'secret_column',
|
||||
# .asyncio
|
||||
'SQLAlchemy', 'AsyncSelectPagination', 'async_query'
|
||||
)
|
||||
|
|
@ -19,6 +19,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
|
||||
|
||||
from binascii import Incomplete
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
import warnings
|
||||
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, Date, ForeignKey, LargeBinary, MetaData, SmallInteger, String, text
|
||||
|
|
@ -167,6 +168,16 @@ def age_pair(*, nullable: bool = False, **ka) -> tuple[Column, Column]:
|
|||
return (date_col, acc_col)
|
||||
|
||||
|
||||
def secret_column(length: int = 64, max_length: int | None = None, gen: Callable[[int], bytes] = os.urandom, nullable=False, **kwargs):
|
||||
"""
|
||||
Column filled in by default with random bits (64 by default). Useful for secrets.
|
||||
|
||||
NEW 0.6.0
|
||||
"""
|
||||
max_length = max_length or length
|
||||
return Column(LargeBinary(max_length), default=lambda: gen(length), nullable=nullable, **kwargs)
|
||||
|
||||
|
||||
|
||||
def parent_children(keyword: str, /, *, lazy='selectin', **kwargs) -> tuple[Incomplete[Relationship[Any]], Incomplete[Relationship[Any]]]:
|
||||
"""
|
||||
|
|
@ -191,6 +202,17 @@ def parent_children(keyword: str, /, *, lazy='selectin', **kwargs) -> tuple[Inco
|
|||
return parent, child
|
||||
|
||||
|
||||
def a_relationship(primary = None, /, j=None, *, lazy='selectin', **kwargs):
|
||||
"""
|
||||
Shorthand for relationship() that sets lazy='selectin' automatically.
|
||||
|
||||
NEW 0.6.0
|
||||
"""
|
||||
if j:
|
||||
kwargs['primaryjoin'] = j
|
||||
return relationship(primary, lazy=lazy, **kwargs) # pyright: ignore[reportArgumentType]
|
||||
|
||||
|
||||
def unbound_fk(target: str | Column | InstrumentedAttribute, typ: _T | None = None, **kwargs) -> Column[_T | IdType]:
|
||||
"""
|
||||
Shorthand for creating a "unbound" foreign key column from a column name, the referenced column.
|
||||
|
|
@ -232,3 +254,62 @@ def bound_fk(target: str | Column | InstrumentedAttribute, typ: _T = None, **kwa
|
|||
|
||||
return Column(typ, ForeignKey(target_name, ondelete='CASCADE'), nullable=False, **kwargs)
|
||||
|
||||
|
||||
class _BitComparator(Comparator):
|
||||
"""
|
||||
Comparator object for BitSelector()
|
||||
|
||||
NEW 0.6.0
|
||||
"""
|
||||
_column: Column
|
||||
_flag: int
|
||||
def __init__(self, col, flag):
|
||||
self._column = col
|
||||
self._flag = flag
|
||||
def _bulk_update_tuples(self, value):
|
||||
return [ (self._column, self._upd_exp(value)) ]
|
||||
def operate(self, op, other, **kwargs):
|
||||
return op(self._sel_exp(), self._flag if other else 0, **kwargs)
|
||||
def __clause_element__(self):
|
||||
return self._column
|
||||
def __str__(self):
|
||||
return self._column
|
||||
def _sel_exp(self):
|
||||
return self._column.op('&')(self._flag)
|
||||
def _upd_exp(self, value):
|
||||
return self._column.op('|')(self._flag) if value else self._column.op('&')(~self._flag)
|
||||
|
||||
class BitSelector:
|
||||
"""
|
||||
"Virtual" column representing a single bit in an integer column (usually a BigInteger).
|
||||
|
||||
Mimicks peewee's 'BitField()' behavior, with SQLAlchemy.
|
||||
|
||||
NEW 0.6.0
|
||||
"""
|
||||
_column: Column
|
||||
_flag: int
|
||||
_name: str
|
||||
def __init__(self, column, flag: int):
|
||||
if bin(flag := int(flag))[2:].rstrip('0') != '1':
|
||||
warnings.warn('using non-powers of 2 as flags may cause errors or undefined behavior', FutureWarning)
|
||||
self._column = column
|
||||
self._flag = flag
|
||||
def __set_name__(self, name, owner=None):
|
||||
self._name = name
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj:
|
||||
return getattr(obj, self._column.name) & self._flag > 0
|
||||
else:
|
||||
return _BitComparator(self._column, self._flag)
|
||||
def __set__(self, obj, val):
|
||||
if obj:
|
||||
orig = getattr(obj, self._column.name)
|
||||
if val:
|
||||
orig |= self._flag
|
||||
else:
|
||||
orig &= ~(self._flag)
|
||||
setattr(obj, self._column.name, orig)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
|
|||
57
src/suou/waiter.py
Normal file
57
src/suou/waiter.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Content serving API over HTTP, based on Starlette.
|
||||
|
||||
NEW 0.6.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.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from starlette.applications import Starlette
|
||||
from starlette.responses import JSONResponse, PlainTextResponse, Response
|
||||
from starlette.routing import Route
|
||||
|
||||
class Waiter():
|
||||
def __init__(self):
|
||||
self.routes: list[Route] = []
|
||||
self.production = False
|
||||
|
||||
def _build_app(self) -> Starlette:
|
||||
return Starlette(
|
||||
debug = not self.production,
|
||||
routes= self.routes
|
||||
)
|
||||
|
||||
## TODO get, post, etc.
|
||||
|
||||
def ok(content = None, **ka):
|
||||
if content is None:
|
||||
return Response(status_code=204, **ka)
|
||||
elif isinstance(content, dict):
|
||||
return JSONResponse(content, **ka)
|
||||
elif isinstance(content, str):
|
||||
return PlainTextResponse(content, **ka)
|
||||
return content
|
||||
|
||||
def ko(status: int, /, content = None, **ka):
|
||||
if status < 400 or status > 599:
|
||||
warnings.warn(f'HTTP {status} is not an error status', UserWarning)
|
||||
if content is None:
|
||||
return Response(status_code=status, **ka)
|
||||
elif isinstance(content, dict):
|
||||
return JSONResponse(content, status_code=status, **ka)
|
||||
elif isinstance(content, str):
|
||||
return PlainTextResponse(content, status_code=status, **ka)
|
||||
return content
|
||||
|
||||
__all__ = ('ko', 'ok', 'Waiter')
|
||||
Loading…
Add table
Add a link
Reference in a new issue