add doc, implement UlidSiqMigrator, 0.2.0

This commit is contained in:
Yusur 2025-05-27 01:29:52 +02:00
parent 91c6e9c1f9
commit 5b74d0d4c4
6 changed files with 61 additions and 16 deletions

View file

@ -6,6 +6,7 @@
- Add `i18n`, `itertools` - Add `i18n`, `itertools`
- Add `toml` as a hard dependency - Add `toml` as a hard dependency
- Add support for Python dicts as `ConfigSource` - Add support for Python dicts as `ConfigSource`
- Implement ULID -> SIQ migrator (with flaws)
- First release on pip under name `sakuragasaki46-suou` - First release on pip under name `sakuragasaki46-suou`
- Improve sqlalchemy support - Improve sqlalchemy support

View file

@ -17,18 +17,19 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
""" """
from .iding import Siq, SiqCache, SiqType, SiqGen from .iding import Siq, SiqCache, SiqType, SiqGen
from .codecs import StringCase from .codecs import StringCase, cb32encode, cb32decode
from .configparse import MissingConfigError, MissingConfigWarning, ConfigOptions, ConfigParserConfigSource, ConfigSource, ConfigValue, EnvConfigSource from .bits import count_ones, mask_shift
from .configparse import MissingConfigError, MissingConfigWarning, ConfigOptions, ConfigParserConfigSource, ConfigSource, DictConfigSource, ConfigValue, EnvConfigSource
from .functools import deprecated, not_implemented from .functools import deprecated, not_implemented
from .classtools import Wanted, Incomplete from .classtools import Wanted, Incomplete
from .itertools import makelist, kwargs_prefix from .itertools import makelist, kwargs_prefix
from .i18n import I18n, JsonI18n, TomlI18n from .i18n import I18n, JsonI18n, TomlI18n
__version__ = "0.2.0-dev21" __version__ = "0.2.0"
__all__ = ( __all__ = (
'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase',
'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', 'EnvConfigSource', 'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue', 'EnvConfigSource', 'DictConfigSource',
'deprecated', 'not_implemented', 'Wanted', 'Incomplete', 'deprecated', 'not_implemented', 'Wanted', 'Incomplete',
'makelist', 'kwargs_prefix', 'I18n', 'JsonI18n', 'TomlI18n' 'makelist', 'kwargs_prefix', 'I18n', 'JsonI18n', 'TomlI18n', 'cb32encode', 'cb32decode', 'count_ones', 'mask_shift'
) )

View file

@ -98,7 +98,7 @@ class ConfigParserConfigSource(ConfigSource):
class DictConfigSource(ConfigSource): class DictConfigSource(ConfigSource):
''' '''
Config source from Python mappings Config source from Python mappings. Useful with JSON/TOML config
''' '''
__slots__ = ('_d',) __slots__ = ('_d',)
@ -126,6 +126,12 @@ class ConfigValue:
You can specify further sources, if the parent ConfigOptions class You can specify further sources, if the parent ConfigOptions class
supports them. 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 for https://stackoverflow.com/questions/45864273/slots-conflicts-with-a-class-variable-in-a-generic-class # XXX disabled for https://stackoverflow.com/questions/45864273/slots-conflicts-with-a-class-variable-in-a-generic-class
#__slots__ = ('_srcs', '_val', '_default', '_cast', '_required', '_preserve_case') #__slots__ = ('_srcs', '_val', '_default', '_cast', '_required', '_preserve_case')
@ -215,6 +221,8 @@ class ConfigOptions:
def expose(self, public_name: str, attr_name: str | None = None) -> None: def expose(self, public_name: str, attr_name: str | None = None) -> None:
''' '''
Mark a config value as public. Mark a config value as public.
Called automatically by ConfigValue.__set_name__().
''' '''
attr_name = attr_name or public_name attr_name = attr_name or public_name
self._pub[public_name] = attr_name self._pub[public_name] = attr_name
@ -227,7 +235,7 @@ class ConfigOptions:
__all__ = ( __all__ = (
'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'EnvConfigSource', 'ConfigParserConfigSource', 'ConfigSource', 'ConfigValue' 'MissingConfigError', 'MissingConfigWarning', 'ConfigOptions', 'EnvConfigSource', 'ConfigParserConfigSource', 'DictConfigSource', 'ConfigSource', 'ConfigValue'
) )

View file

@ -59,6 +59,8 @@ class SnowflakeSiqMigrator(SiqMigrator):
optimization requirements, are based on a different epoch (e.g. optimization requirements, are based on a different epoch (e.g.
Jan 1, 2015 for Discord); epoch is wanted as seconds since Unix epoch Jan 1, 2015 for Discord); epoch is wanted as seconds since Unix epoch
(i.e. midnight of Jan 1, 1970). (i.e. midnight of Jan 1, 1970).
There should be a 1-on-1 correspondence from snowflakes and SIQs.
""" """
def __init__(self, domain: str, epoch: int, *, def __init__(self, domain: str, epoch: int, *,
ts_stop: int = 22, ts_accuracy: int = 1000, ts_stop: int = 22, ts_accuracy: int = 1000,
@ -73,7 +75,7 @@ class SnowflakeSiqMigrator(SiqMigrator):
self.serial_mask = serial_mask self.serial_mask = serial_mask
@override @override
def to_siq(self, orig_id, target_type: SiqType) -> int: def to_siq(self, orig_id: int, target_type: SiqType) -> int:
ts_ms = (orig_id >> self.ts_stop) + self.epoch ts_ms = (orig_id >> self.ts_stop) + self.epoch
ts = int(ts_ms / self.ts_accuracy * (1 << 16)) ts = int(ts_ms / self.ts_accuracy * (1 << 16))
shard = mask_shift(orig_id, self.shard_mask) shard = mask_shift(orig_id, self.shard_mask)
@ -96,15 +98,41 @@ class SnowflakeSiqMigrator(SiqMigrator):
) )
## TODO: UlidSiqMigrator
@not_implemented
class UlidSiqMigrator(SiqMigrator): class UlidSiqMigrator(SiqMigrator):
''' '''
Migrate from ULID's to SIQ. Migrate from ULID's to SIQ.
ULIDs have a timestamp part (expressed in milliseconds) and a randomly generated part. ULIDs are 128-bit identifiers with 48 timestamp bits (expressed in milliseconds) and 80 random bits.
Structure (simplified):
tttttttt tttttttt tttttttt tttttttt tttttttt tttttttt
rrrrrrrr rrrrrrrr rrrrrrrr rrrrrrrr rrrrrrrr rrrrrrrr
rrrrrrrr rrrrrrrr rrrrrrrr rrrrrrrr
For obvious reasons, this makes 1-on-1 correspondence impossible. (Yes, the 16 spare bits.)
It means that, of the 80 random bits, only 24 to 27 bits are preserved:
- 6 bits summed to the timestamp.
- 8 bits as shard ID.
- 10 to 13 bits in the progressive counter.
- The rest is *just discarded*.
''' '''
@override
def to_siq(self, orig_id, target_type: SiqType) -> int:
ts_seq = mask_shift(orig_id, 0xfc000000000000000000)
shard = mask_shift(orig_id, 0x3fc0000000000000000)
seq = mask_shift(orig_id, 0x3fffc000000000000)
ts = ((orig_id >> 80) << 16) // 1000 + ts_seq
return (
(ts << 56)|
((shard % 256) << 48)|
((self.domain_hash % 0xffffffff) << 16)|
(((seq & ~((1 << target_type.n_bits) - 1)) | target_type.prepend(0)) % 0xffff)
)
__all__ = (
'SnowflakeSiqMigrator', 'UlidSiqMigrator'
)

View file

@ -97,6 +97,13 @@ class RegexCharField(CharField):
class SiqField(Field): class SiqField(Field):
'''
Field holding a SIQ.
Stored as varbinary(16).
XXX UNTESTED!
'''
field_type = 'varbinary(16)' field_type = 'varbinary(16)'
def db_value(self, value: int | Siq | bytes) -> bytes: def db_value(self, value: int | Siq | bytes) -> bytes:
@ -111,5 +118,5 @@ class SiqField(Field):
return Siq.from_bytes(value) return Siq.from_bytes(value)
__all__ = ('connect_reconnect', 'RegexCharField') __all__ = ('connect_reconnect', 'RegexCharField', 'SiqField')

View file

@ -149,5 +149,5 @@ def age_pair(*, nullable: bool = False, **ka) -> tuple[Column, Column]:
__all__ = ( __all__ = (
'IdType', 'id_column', 'entity_base', 'declarative_base', 'token_signer', 'match_column', 'match_constraint', 'IdType', 'id_column', 'entity_base', 'declarative_base', 'token_signer', 'match_column', 'match_constraint',
'author_pair' 'author_pair', 'age_pair'
) )