add calendar module, drop Quart-SQLAlchemy

This commit is contained in:
Yusur 2025-07-25 08:24:46 +02:00
parent 589d4b3b13
commit 38ff59c76a
6 changed files with 118 additions and 16 deletions

View file

@ -3,7 +3,8 @@
## 0.5.0 ## 0.5.0
+ `sqlalchemy`: add `unbound_fk()`, `bound_fk()` + `sqlalchemy`: add `unbound_fk()`, `bound_fk()`
+ Add `timed_cache()`, `TimedDict()` + Add `timed_cache()`, `TimedDict()`, `age_and_days()`
+ Add date conversion utilities
+ Move obsolete stuff to `obsolete` package (includes configparse 0.3 as of now) + Move obsolete stuff to `obsolete` package (includes configparse 0.3 as of now)
+ Add more exceptions: `NotFoundError()` + Add more exceptions: `NotFoundError()`

View file

@ -11,7 +11,8 @@ readme = "README.md"
dependencies = [ dependencies = [
"itsdangerous", "itsdangerous",
"toml", "toml",
"pydantic" "pydantic",
"uvloop; os_name=='posix'"
] ]
# - further devdependencies below - # # - further devdependencies below - #
@ -43,16 +44,17 @@ flask_sqlalchemy = [
"Flask-SqlAlchemy", "Flask-SqlAlchemy",
] ]
peewee = [ peewee = [
"peewee>=3.0.0, <4.0" "peewee>=3.0.0"
] ]
markdown = [ markdown = [
"markdown>=3.0.0" "markdown>=3.0.0"
] ]
quart = [ quart = [
"Flask>=2.0.0",
"Quart", "Quart",
"Quart-Schema", "Quart-Schema"
"uvloop; os_name=='posix'" ]
quart_sqlalchemy = [
"Quart_SQLALchemy>=3.0.0, <4.0"
] ]
full = [ full = [
@ -60,7 +62,10 @@ full = [
"sakuragasaki46-suou[flask]", "sakuragasaki46-suou[flask]",
"sakuragasaki46-suou[quart]", "sakuragasaki46-suou[quart]",
"sakuragasaki46-suou[peewee]", "sakuragasaki46-suou[peewee]",
"sakuragasaki46-suou[markdown]" "sakuragasaki46-suou[markdown]",
"sakuragasaki46-suou[flask-sqlalchemy]"
# disabled: quart-sqlalchemy causes issues with anyone else. WON'T BE IMPLEMENTED
#"sakuragasaki46-suou[quart-sqlalchemy]"
] ]

View file

@ -18,13 +18,14 @@ 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, cb32encode, cb32decode, b32lencode, b32ldecode, b64encode, b64decode, b2048encode, b2048decode, from .codecs import (StringCase, cb32encode, cb32decode, b32lencode, b32ldecode, b64encode, b64decode, b2048encode, b2048decode,
jsonencode, want_bytes, want_str, ssv_list, want_urlsafe) jsonencode, want_bytes, want_str, ssv_list, want_urlsafe, want_urlsafe_bytes)
from .bits import count_ones, mask_shift, split_bits, join_bits, mod_ceil, mod_floor from .bits import count_ones, mask_shift, split_bits, join_bits, mod_ceil, mod_floor
from .calendar import want_datetime, want_isodate, want_timestamp, age_and_days
from .configparse import MissingConfigError, MissingConfigWarning, ConfigOptions, ConfigParserConfigSource, ConfigSource, DictConfigSource, ConfigValue, EnvConfigSource from .configparse import MissingConfigError, MissingConfigWarning, ConfigOptions, ConfigParserConfigSource, ConfigSource, DictConfigSource, ConfigValue, EnvConfigSource
from .collections import TimedDict from .collections import TimedDict
from .functools import deprecated, not_implemented, timed_cache from .functools import deprecated, not_implemented, timed_cache
from .classtools import Wanted, Incomplete from .classtools import Wanted, Incomplete
from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem, addattr
from .i18n import I18n, JsonI18n, TomlI18n from .i18n import I18n, JsonI18n, TomlI18n
from .snowflake import Snowflake, SnowflakeGen from .snowflake import Snowflake, SnowflakeGen
from .lex import symbol_table, lex, ilex from .lex import symbol_table, lex, ilex
@ -37,10 +38,11 @@ __all__ = (
'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n', 'DictConfigSource', 'EnvConfigSource', 'I18n', 'Incomplete', 'JsonI18n',
'MissingConfigError', 'MissingConfigWarning', 'PrefixIdentifier', 'Siq', 'SiqCache', 'SiqGen', 'MissingConfigError', 'MissingConfigWarning', 'PrefixIdentifier', 'Siq', 'SiqCache', 'SiqGen',
'SiqType', 'Snowflake', 'SnowflakeGen', 'StringCase', 'TimedDict', 'TomlI18n', 'Wanted', 'SiqType', 'Snowflake', 'SnowflakeGen', 'StringCase', 'TimedDict', 'TomlI18n', 'Wanted',
'additem', 'b2048decode', 'b2048encode', 'b32ldecode', 'b32lencode', 'addattr', 'additem', 'age_and_days', 'b2048decode', 'b2048encode',
'b64encode', 'b64decode', 'cb32encode', 'cb32decode', 'count_ones', 'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode',
'deprecated', 'ilex', 'join_bits', 'jsonencode', 'kwargs_prefix', 'lex', 'cb32decode', 'count_ones', 'deprecated', 'ilex', 'join_bits',
'ltuple', 'makelist', 'mask_shift', 'mod_ceil', 'mod_floor', 'jsonencode', 'kwargs_prefix', 'lex', 'ltuple', 'makelist', 'mask_shift',
'not_implemented', 'rtuple', 'split_bits', 'ssv_list', 'symbol_table', 'mod_ceil', 'mod_floor', 'not_implemented', 'rtuple', 'split_bits',
'timed_cache', 'want_bytes', 'want_str', 'want_urlsafe' 'ssv_list', 'symbol_table', 'timed_cache', 'want_bytes', 'want_datetime',
'want_isodate', 'want_str', 'want_timestamp', 'want_urlsafe', 'want_urlsafe_bytes'
) )

66
src/suou/calendar.py Normal file
View file

@ -0,0 +1,66 @@
"""
Calendar utilities (mainly Gregorian oof)
---
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 datetime
from suou.functools import not_implemented
def want_isodate(d: datetime.datetime | str | float | int, *, tz = None) -> str:
"""
Convert a date into ISO timestamp (e.g. 2020-01-01T02:03:04)
"""
if isinstance(d, (int, float)):
d = datetime.datetime.fromtimestamp(d, tz=tz)
if isinstance(d, str):
return d
return d.isoformat()
def want_datetime(d: datetime.datetime | str | float | int, *, tz = None) -> datetime.datetime:
"""
Convert a date into Python datetime.datetime (e.g. datetime.datetime(2020, 1, 1, 2, 3, 4)).
If a string is passed, ISO format is assumed
"""
if isinstance(d, str):
d = datetime.datetime.fromisoformat(d)
elif isinstance(d, (int, float)):
d = datetime.datetime.fromtimestamp(d, tz=tz)
return d
def want_timestamp(d: datetime.datetime | str | float | int, *, tz = None) -> float:
"""
Convert a date into UNIX timestamp (e.g. 1577840584.0). Returned as a float; decimals are milliseconds.
"""
if isinstance(d, str):
d = want_datetime(d, tz=tz)
if isinstance(d, (int, float)):
return d
return d.timestamp()
def age_and_days(date: datetime.datetime, now: datetime.datetime | None = None) -> tuple[int, int]:
"""
Compute age / duration of a timespan in years and days.
"""
if now is None:
now = datetime.date.today()
y = now.year - date.year - ((now.month, now.day) < (date.month, date.day))
d = (now - datetime.date(date.year + y, date.month, date.day)).days
return y, d
__all__ = ('want_datetime', 'want_timestamp', 'want_isodate', 'age_and_days')

View file

@ -57,6 +57,7 @@ def want_urlsafe(s: str | bytes) -> str:
Force a Base64 string into its urlsafe representation. Force a Base64 string into its urlsafe representation.
Behavior is unchecked and undefined with anything else than Base64 strings. Behavior is unchecked and undefined with anything else than Base64 strings.
In particular, this is NOT an URL encoder.
Used by b64encode() and b64decode(). Used by b64encode() and b64decode().
""" """
@ -328,5 +329,5 @@ class StringCase(enum.Enum):
__all__ = ( __all__ = (
'cb32encode', 'cb32decode', 'b32lencode', 'b32ldecode', 'b64encode', 'b64decode', 'jsonencode' 'cb32encode', 'cb32decode', 'b32lencode', 'b32ldecode', 'b64encode', 'b64decode', 'jsonencode'
'StringCase', 'want_bytes', 'want_str', 'jsondecode', 'ssv_list' 'StringCase', 'want_bytes', 'want_str', 'jsondecode', 'ssv_list', 'want_urlsafe', 'want_urlsafe_bytes'
) )

27
tests/test_calendar.py Normal file
View file

@ -0,0 +1,27 @@
from datetime import timezone
import datetime
from suou.calendar import want_datetime, want_isodate
import unittest
class TestCalendar(unittest.TestCase):
def setUp(self) -> None:
...
def tearDown(self) -> None:
...
def test_want_isodate(self):
## if test fails, make sure time zone is set to UTC.
self.assertEqual(want_isodate(0, tz=timezone.utc), '1970-01-01T00:00:00+00:00')
self.assertEqual(want_isodate(86400, tz=timezone.utc), '1970-01-02T00:00:00+00:00')
self.assertEqual(want_isodate(1577840584.0, tz=timezone.utc), '2020-01-01T01:03:04+00:00')
# TODO
def test_want_datetime(self):
self.assertEqual(want_datetime('2017-04-10T19:00:01', tz=timezone.utc) - want_datetime('2017-04-10T18:00:00', tz=timezone.utc), datetime.timedelta(seconds=3601))
# TODO