diff --git a/docs/source/api_ref/impl/parser/builtins.rst b/docs/source/api_ref/impl/parser/builtins.rst new file mode 100644 index 0000000..4bcac8b --- /dev/null +++ b/docs/source/api_ref/impl/parser/builtins.rst @@ -0,0 +1,35 @@ +.. currentmodule:: disnake.ext.components.impl + +Builtins Parser Implementations +=============================== + +.. automodule:: components.impl.parser.builtins + + +Classes +------- + +.. attributetable:: components.impl.parser.builtins.NoneParser + +.. autoclass:: components.impl.parser.builtins.NoneParser + :members: + +.. attributetable:: components.impl.parser.builtins.FloatParser + +.. autoclass:: components.impl.parser.builtins.FloatParser + :members: + +.. attributetable:: components.impl.parser.builtins.IntParser + +.. autoclass:: components.impl.parser.builtins.IntParser + :members: + +.. attributetable:: components.impl.parser.builtins.BoolParser + +.. autoclass:: components.impl.parser.builtins.BoolParser + :members: + +.. attributetable:: components.impl.parser.builtins.StringParser + +.. autoclass:: components.impl.parser.builtins.StringParser + :members: diff --git a/docs/source/api_ref/impl/parser/datetime.rst b/docs/source/api_ref/impl/parser/datetime.rst new file mode 100644 index 0000000..7cce98d --- /dev/null +++ b/docs/source/api_ref/impl/parser/datetime.rst @@ -0,0 +1,41 @@ +.. currentmodule:: disnake.ext.components.impl + +Datetime Parser Implementations +=============================== + +.. automodule:: components.impl.parser.datetime + + +Enumerations +------------ + +.. autoenum:: components.impl.parser.datetime.Resolution + :members: + +Classes +------- + +.. attributetable:: components.impl.parser.datetime.DatetimeParser + +.. autoclass:: components.impl.parser.datetime.DatetimeParser + :members: + +.. attributetable:: components.impl.parser.datetime.TimedeltaParser + +.. autoclass:: components.impl.parser.datetime.TimedeltaParser + :members: + +.. attributetable:: components.impl.parser.datetime.DateParser + +.. autoclass:: components.impl.parser.datetime.DateParser + :members: + +.. attributetable:: components.impl.parser.datetime.TimeParser + +.. autoclass:: components.impl.parser.datetime.TimeParser + :members: + +.. attributetable:: components.impl.parser.datetime.TimezoneParser + +.. autoclass:: components.impl.parser.datetime.TimezoneParser + :members: diff --git a/docs/source/api_ref/impl/parser/index.rst b/docs/source/api_ref/impl/parser/index.rst index 1eec5aa..1e0e627 100644 --- a/docs/source/api_ref/impl/parser/index.rst +++ b/docs/source/api_ref/impl/parser/index.rst @@ -10,4 +10,5 @@ Submodules :maxdepth: 1 base - stdlib + builtins + datetime diff --git a/docs/source/api_ref/impl/parser/stdlib.rst b/docs/source/api_ref/impl/parser/stdlib.rst deleted file mode 100644 index 69a1da0..0000000 --- a/docs/source/api_ref/impl/parser/stdlib.rst +++ /dev/null @@ -1,66 +0,0 @@ -.. currentmodule:: disnake.ext.components.impl - -Standard Library Parser Implementation -====================================== - -.. automodule:: components.impl.parser.stdlib - - -Enumerations ------------- - -.. autoenum:: components.impl.parser.stdlib.Resolution - :members: - -Classes -------- - -.. attributetable:: components.impl.parser.stdlib.NoneParser - -.. autoclass:: components.impl.parser.stdlib.NoneParser - :members: - -.. attributetable:: components.impl.parser.stdlib.FloatParser - -.. autoclass:: components.impl.parser.stdlib.FloatParser - :members: - -.. attributetable:: components.impl.parser.stdlib.IntParser - -.. autoclass:: components.impl.parser.stdlib.IntParser - :members: - -.. attributetable:: components.impl.parser.stdlib.BoolParser - -.. autoclass:: components.impl.parser.stdlib.BoolParser - :members: - -.. attributetable:: components.impl.parser.stdlib.StringParser - -.. autoclass:: components.impl.parser.stdlib.StringParser - :members: - -.. attributetable:: components.impl.parser.stdlib.DatetimeParser - -.. autoclass:: components.impl.parser.stdlib.DatetimeParser - :members: - -.. attributetable:: components.impl.parser.stdlib.TimedeltaParser - -.. autoclass:: components.impl.parser.stdlib.TimedeltaParser - :members: - -.. attributetable:: components.impl.parser.stdlib.DateParser - -.. autoclass:: components.impl.parser.stdlib.DateParser - :members: - -.. attributetable:: components.impl.parser.stdlib.TimeParser - -.. autoclass:: components.impl.parser.stdlib.TimeParser - :members: - -.. attributetable:: components.impl.parser.stdlib.TimezoneParser - -.. autoclass:: components.impl.parser.stdlib.TimezoneParser - :members: diff --git a/src/disnake/ext/components/impl/parser/__init__.py b/src/disnake/ext/components/impl/parser/__init__.py index ad21932..02521d0 100644 --- a/src/disnake/ext/components/impl/parser/__init__.py +++ b/src/disnake/ext/components/impl/parser/__init__.py @@ -5,7 +5,9 @@ """Implementations for all kinds of parser classes.""" from disnake.ext.components.impl.parser.base import * +from disnake.ext.components.impl.parser.builtins import * from disnake.ext.components.impl.parser.channel import * +from disnake.ext.components.impl.parser.datetime import * from disnake.ext.components.impl.parser.emoji import * from disnake.ext.components.impl.parser.enum import * from disnake.ext.components.impl.parser.guild import * @@ -13,5 +15,4 @@ from disnake.ext.components.impl.parser.message import * from disnake.ext.components.impl.parser.role import * from disnake.ext.components.impl.parser.snowflake import * -from disnake.ext.components.impl.parser.stdlib import * from disnake.ext.components.impl.parser.user import * diff --git a/src/disnake/ext/components/impl/parser/stdlib.py b/src/disnake/ext/components/impl/parser/builtins.py similarity index 51% rename from src/disnake/ext/components/impl/parser/stdlib.py rename to src/disnake/ext/components/impl/parser/builtins.py index bd41095..18cefdc 100644 --- a/src/disnake/ext/components/impl/parser/stdlib.py +++ b/src/disnake/ext/components/impl/parser/builtins.py @@ -1,10 +1,8 @@ -"""Parser implementations for types provided in the python standard library.""" +"""Parser implementations for (mostly) builtin types.""" from __future__ import annotations import contextlib -import datetime -import enum import inspect import string import typing @@ -22,11 +20,6 @@ "IntParser", "BoolParser", "StringParser", - "DatetimeParser", - "DateParser", - "TimeParser", - "TimedeltaParser", - "TimezoneParser", "CollectionParser", "TupleParser", "UnionParser", @@ -377,533 +370,6 @@ def dumps(self, argument: str) -> str: return argument -# DATETIME - - -class Resolution(float, enum.Enum): - r"""The resolution with which :class:`datetime.datetime`\s are stored.""" - - MICROS = 1e-6 - """Microsecond resolution. - - This is the default for the datetime module, but often more than required. - """ - MILLIS = 1e-3 - """Millisecond resolution. - - Rounds the datetime **down** to the nearest microsecond. - """ - SECONDS = 1 - """Second resolution. - - Rounds the datetime **down** to the nearest second. - """ - MINUTES = 60 * SECONDS - """Minute resolution. - - Rounds the datetime **down** to the nearest minute. - """ - HOURS = 60 * MINUTES - """Hour resolution. - - Rounds the datetime **down** to the nearest hour. - """ - DAYS = 24 * HOURS - """Day resolution. - - Rounds the datetime **down** to the nearest day. - """ - - -_VALID_BASE_10 = frozenset([10**i for i in range(-6, 0)]) - - -# TODO: Is forcing the use of timezones on users really a parser_based move? -# Probably. -@parser_base.register_parser_for(datetime.datetime) -class DatetimeParser(parser_base.Parser[datetime.datetime]): - r"""Parser type with support for datetimes. - - Parameters - ---------- - resolution: - The resolution with which to store :class:`~datetime.datetime`\s in custom ids. - Defaults to :obj:`Resolution.SECONDS`. - timezone: - The timezone to use for parsing. - Defaults to :obj:`datetime.timezone.utc`. - strict: - Whether this parser is in strict mode. - Defaults to ``True``. - int_parser: - The :class:`IntParser` to use internally for this parser. - - """ - - resolution: typing.Union[int, float] - r"""The resolution with which to store :class:`~datetime.datetime`\s in seconds. - - .. warning:: - The resolution must be greater than ``1e-6``, and if the resolution is - smaller than 1, it **must** be a power of 10. If the resolution is - greater than 1, it is coerced into an integer. - - .. note:: - Python datetime objects have microsecond accuracy. For most - applications, this is much more precise than necessary. - Since custom id space is limited, seconds was chosen as the default. - """ - - timezone: datetime.timezone - """The timezone to use for parsing. - Datetimes returned by :meth:`loads` will always be of this timezone. - - This is *not* stored in the custom id. - """ - - strict: bool - """Whether the parser is in strict mode. - - If the parser is in strict mode, :meth:`loads` requires the provided - datetime object to be of the correct :attr:`timezone`. - """ - - int_parser: IntParser - """The :class:`IntParser` to use internally for this parser. - - Since the default integer parser uses base-36 to "compress" numbers, the - default datetime parser will also return compressed results. - """ - - def __init__( - self, - *, - resolution: typing.Union[int, float] = Resolution.SECONDS, - timezone: datetime.timezone = datetime.timezone.utc, - strict: bool = True, - int_parser: typing.Optional[IntParser] = None, - ): - if resolution < 1e-6: - msg = f"Resolution must be greater than 1e-6, got {resolution}." - raise ValueError(msg) - - if resolution < 1 and resolution not in _VALID_BASE_10: - # TODO: Verify whether this doesn't false-negative - msg = f"Resolutions smaller than 1 must be a power of 10, got {resolution}." - raise ValueError(msg) - - self.resolution = resolution - self.timezone = timezone - self.strict = strict - self.int_parser = int_parser or IntParser.default() - - def loads(self, argument: str) -> datetime.datetime: - """Load a datetime from a string. - - This uses the underlying :attr:`int_parser`. - - The returned datetime is always of the specified :attr:`timezone`. - - Parameters - ---------- - argument: - The string that is to be converted into a datetime. - - """ - return datetime.datetime.fromtimestamp( - self.int_parser.loads(argument) * self.resolution, - tz=self.timezone, - ) - - def dumps(self, argument: datetime.datetime) -> str: - """Dump a datetime into a string. - - This uses the underlying :attr:`int_parser`. - - If :attr:`strict` is set to ``True``, this will fail if the provided - ``argument`` does not have a timezone set. Otherwise, a timezone-naive - datetime will automatically get its timezone set to :attr:`timezone`. - - Parameters - ---------- - argument: - The value that is to be dumped. - - Raises - ------ - :class:`ValueError`: - Either the parser is set to strict and the provided datetime was - timezone-naive, or the provided datetime's timezone does not match - that of the parser. - - """ - if self.strict: - if not argument.tzinfo: - msg = "Strict DatetimeParsers can only load timezone-aware datetimes." - raise ValueError(msg) - else: - argument = argument.replace(tzinfo=self.timezone) - - if argument.tzinfo != self.timezone: - msg = ( - "Cannot dump the provided datetime object due to a mismatch in" - f" timezones. (expected: {self.timezone}, got: {argument.tzinfo})" - ) - raise ValueError(msg) - - timestamp = argument.timestamp() - if self.resolution != 0: - timestamp //= self.resolution - - return self.int_parser.dumps(int(timestamp)) - - -@parser_base.register_parser_for(datetime.timedelta) -class TimedeltaParser(parser_base.Parser[datetime.timedelta]): - r"""Parser type with support for :class:`datetime.timedelta`\s. - - Parameters - ---------- - resolution: - The resolution with which to store :class:`~datetime.timedelta`\s in custom ids. - Defaults to :obj:`Resolution.SECONDS`. - timezone: - The timezone to use for parsing. - Defaults to :obj:`datetime.timezone.utc`. - strict: - Whether this parser is in strict mode. - Defaults to ``True``. - int_parser: - The :class:`IntParser` to use internally for this parser. - - """ - - resolution: typing.Union[int, float] - r"""The resolution with which to store :class:`~datetime.timedelta`\s in seconds. - - .. warning:: - The resolution must be greater than ``1e-6``, and if the resolution is - smaller than 1, it **must** be a power of 10. If the resolution is - greater than 1, it is coerced into an integer. - - .. note:: - Python datetime objects have microsecond accuracy. For most - applications, this is much more precise than necessary. - Since custom id space is limited, seconds was chosen as the default. - """ - - int_parser: IntParser - """The :class:`IntParser` to use internally for this parser. - - Since the default integer parser uses base-36 to "compress" numbers, the - default datetime parser will also return compressed results. - """ - - def __init__( - self, - *, - resolution: typing.Union[int, float] = Resolution.SECONDS, - int_parser: typing.Optional[IntParser] = None, - ): - if resolution < 1e-6: - msg = f"Resolution must be greater than 1e-6, got {resolution}." - raise ValueError(msg) - - if resolution < 1 and resolution not in _VALID_BASE_10: - # TODO: Verify whether this doesn't false-negative - msg = f"Resolutions smaller than 1 must be a power of 10, got {resolution}." - raise ValueError(msg) - - self.resolution = resolution - self.int_parser = int_parser or IntParser.default() - - def loads(self, argument: str) -> datetime.timedelta: - """Load a timedelta from a string. - - This uses the underlying :attr:`int_parser`. - - Parameters - ---------- - argument: - The string that is to be converted into a timedelta. - - """ - seconds = self.int_parser.loads(argument) * self.resolution - return datetime.timedelta(seconds=seconds) - - def dumps(self, argument: datetime.timedelta) -> str: - """Dump a timedelta into a string. - - This uses the underlying :attr:`int_parser`. - - Parameters - ---------- - argument: - The value that is to be dumped. - - """ - return self.int_parser.dumps(int(argument.total_seconds() // self.resolution)) - - -@parser_base.register_parser_for(datetime.date) -class DateParser(parser_base.Parser[datetime.date]): - """Parser type with support for dates. - - Parameters - ---------- - int_parser: - The :class:`IntParser` to use internally for this parser. - - """ - - int_parser: IntParser - """The :class:`IntParser` to use internally for this parser. - - Since the default integer parser uses base-36 to "compress" numbers, the - default date parser will also return compressed results. - """ - - def __init__(self, *, int_parser: typing.Optional[IntParser]): - self.int_parser = int_parser or IntParser.default() - - def loads(self, argument: str) -> datetime.date: - """Load a date from a string. - - This uses the underlying :attr:`int_parser`. - - Parameters - ---------- - argument: - The string that is to be converted into a date. - - """ - return datetime.date.fromordinal(self.int_parser.loads(argument)) - - def dumps(self, argument: datetime.date) -> str: - """Dump a datetime into a string. - - This uses the underlying :attr:`int_parser`. - - Parameters - ---------- - argument: - The value that is to be dumped. - - """ - return self.int_parser.dumps(datetime.date.toordinal(argument)) - - -@parser_base.register_parser_for(datetime.time) -class TimeParser(parser_base.Parser[datetime.time]): - r"""Parser type with support for times. - - .. important:: - Unlike :class:`DatetimeParser` etc., resolution for this class is set - via the underlying :attr:`timedelta_parser`. Note that this class *does* - proxy it through the :attr:`precision` property, which supports both - getting and setting. - - Parameters - ---------- - timezone: - The timezone to use for parsing. - Defaults to :obj:`datetime.timezone.utc`. - strict: - Whether this parser is in strict mode. - Defaults to ``True``. - timedelta_parser: - The :class:`TimedeltaParser` to use internally for this parser. - - """ - - timezone: datetime.timezone - """The timezone to use for parsing. - Times returned by :meth:`loads` will always be of this timezone. - - This is *not* stored in the custom id. - """ - - strict: bool - """Whether the parser is in strict mode. - - If the parser is in strict mode, :meth:`loads` requires the provided - datetime object to be of the correct :attr:`timezone`. - """ - - timedelta_parser: TimedeltaParser - """The :class:`TimedeltaParser` to use internally for this parser. - - Since the default timedelta parser uses base-36 to "compress" numbers, the - default datetime parser will also return compressed results. - """ - - def __init__( - self, - *, - timezone: datetime.timezone = datetime.timezone.utc, - timedelta_parser: typing.Optional[TimedeltaParser] = None, - strict: bool = True, - ): - self.timezone = timezone - self.timedelta_parser = timedelta_parser or TimedeltaParser.default() - self.strict = strict - - @property - def resolution(self) -> typing.Union[int, float]: - r"""The resolution with which to store :class:`~datetime.time`\s in seconds. - - .. warning:: - The resolution must be greater than ``1e-6``, and if the resolution is - smaller than 1, it **must** be a power of 10. If the resolution is - greater than 1, it is coerced into an integer. - - .. note:: - Python time objects have microsecond accuracy. For most - applications, this is much more precise than necessary. - Since custom id space is limited, seconds was chosen as the default. - """ - return self.timedelta_parser.resolution - - @resolution.setter - def resolution(self, resolution: typing.Union[int, float]) -> None: - self.timedelta_parser.resolution = resolution - - def loads(self, argument: str) -> datetime.time: - """Load a time from a string. - - This uses the underlying :attr:`timedelta_parser`. - - The returned time is always of the specified :attr:`timezone`. - - Parameters - ---------- - argument: - The string that is to be converted into a time. - - """ - dt = datetime.datetime.min + self.timedelta_parser.loads(argument) - return dt.time().replace(tzinfo=self.timezone) - - def dumps(self, argument: datetime.time) -> str: - """Dump a time into a string. - - This uses the underlying :attr:`timedelta_parser`. - - If :attr:`strict` is set to ``True``, this will fail if the provided - ``argument`` does not have a timezone set. Otherwise, a timezone-naive - time will automatically get its timezone set to :attr:`timezone`. - - Parameters - ---------- - argument: - The value that is to be dumped. - - Raises - ------ - :class:`ValueError`: - Either the parser is set to strict and the provided time was - timezone-naive, or the provided time's timezone does not match - that of the parser. - - """ - if self.strict: - if not argument.tzinfo: - msg = "Strict TimeParsers can only load timezone-aware times." - raise ValueError(msg) - else: - argument = argument.replace(tzinfo=self.timezone) - - if argument.tzinfo != self.timezone: - msg = ( - "Cannot dump the provided time object due to a mismatch in" - f" timezones. (expected: {self.timezone}, got: {argument.tzinfo})" - ) - raise ValueError(msg) - - return self.timedelta_parser.dumps( - datetime.timedelta( - hours=argument.hour, - minutes=argument.minute, - seconds=argument.second, - microseconds=argument.microsecond, - ) - ) - - -@parser_base.register_parser_for(datetime.timezone) -class TimezoneParser(parser_base.Parser[datetime.timezone]): - r"""Parser type with support for :class:`~datetime.timezone`\s. - - .. important:: - Unlike :class:`DatetimeParser` etc., resolution for this class is set - via the underlying :attr:`timedelta_parser`. Note that this class *does* - proxy it through the :attr:`precision` property, which supports both - getting and setting. - - Parameters - ---------- - timedelta_parser: - The :class:`TimedeltaParser` to use internally for this parser. - - """ - - timedelta_parser: TimedeltaParser - """The :class:`TimedeltaParser` to use internally for this parser. - - Since the default timedelta parser uses base-36 to "compress" numbers, the - default datetime parser will also return compressed results. - """ - - def __init__(self, *, timedelta_parser: typing.Optional[TimedeltaParser] = None): - self.timedelta_parser = timedelta_parser or TimedeltaParser() - - @property - def resolution(self) -> typing.Union[int, float]: - r"""The resolution with which to store :class:`~datetime.time`\s in seconds. - - .. warning:: - The resolution must be greater than ``1e-6``, and if the resolution is - smaller than 1, it **must** be a power of 10. If the resolution is - greater than 1, it is coerced into an integer. - - .. note:: - Python time objects have microsecond accuracy. For most - applications, this is much more precise than necessary. - Since custom id space is limited, seconds was chosen as the default. - """ - return self.timedelta_parser.resolution - - @resolution.setter - def resolution(self, resolution: typing.Union[int, float]) -> None: - self.timedelta_parser.resolution = resolution - - def loads(self, argument: str) -> datetime.timezone: - """Load a timezone from a string. - - This uses the underlying :attr:`timedelta_parser`. - - Parameters - ---------- - argument: - The string that is to be converted into a timezone. - - """ - return datetime.timezone(self.timedelta_parser.loads(argument)) - - def dumps(self, argument: datetime.timezone) -> str: - """Dump a timezone into a string. - - This uses the underlying :attr:`timedelta_parser`. - - Parameters - ---------- - argument: - The value that is to be dumped. - - """ - return self.timedelta_parser.dumps(argument.utcoffset(None)) - - def _resolve_collection(type_: typing.Type[_CollectionT]) -> typing.Type[_CollectionT]: # ContainerParser itself does not support tuples. if issubclass(type_, typing.Tuple): diff --git a/src/disnake/ext/components/impl/parser/datetime.py b/src/disnake/ext/components/impl/parser/datetime.py new file mode 100644 index 0000000..2a89319 --- /dev/null +++ b/src/disnake/ext/components/impl/parser/datetime.py @@ -0,0 +1,539 @@ +"""Parser implementations for types provided in the datetime package.""" + +import datetime +import enum +import typing + +from disnake.ext.components.impl.parser import base as parser_base +from disnake.ext.components.impl.parser import builtins as builtins_parsers + +__all__: typing.Sequence[str] = ( + "DatetimeParser", + "DateParser", + "TimeParser", + "TimedeltaParser", + "TimezoneParser", +) + +_VALID_BASE_10 = frozenset([10**i for i in range(-6, 0)]) + + +class Resolution(float, enum.Enum): + r"""The resolution with which :class:`datetime.datetime`\s etc. are stored.""" + + MICROS = 1e-6 + """Microsecond resolution. + + This is the default for the datetime module, but often more than required. + """ + MILLIS = 1e-3 + """Millisecond resolution. + + Rounds the datetime **down** to the nearest microsecond. + """ + SECONDS = 1 + """Second resolution. + + Rounds the datetime **down** to the nearest second. + """ + MINUTES = 60 * SECONDS + """Minute resolution. + + Rounds the datetime **down** to the nearest minute. + """ + HOURS = 60 * MINUTES + """Hour resolution. + + Rounds the datetime **down** to the nearest hour. + """ + DAYS = 24 * HOURS + """Day resolution. + + Rounds the datetime **down** to the nearest day. + """ + + +# TODO: Is forcing the use of timezones on users really a parser_based move? +# Probably. +@parser_base.register_parser_for(datetime.datetime) +class DatetimeParser(parser_base.Parser[datetime.datetime]): + r"""Parser type with support for datetimes. + + Parameters + ---------- + resolution: + The resolution with which to store :class:`~datetime.datetime`\s in custom ids. + Defaults to :obj:`Resolution.SECONDS`. + timezone: + The timezone to use for parsing. + Defaults to :obj:`datetime.timezone.utc`. + strict: + Whether this parser is in strict mode. + Defaults to ``True``. + int_parser: + The :class:`IntParser` to use internally for this parser. + + """ + + resolution: typing.Union[int, float] + r"""The resolution with which to store :class:`~datetime.datetime`\s in seconds. + + .. warning:: + The resolution must be greater than ``1e-6``, and if the resolution is + smaller than 1, it **must** be a power of 10. If the resolution is + greater than 1, it is coerced into an integer. + + .. note:: + Python datetime objects have microsecond accuracy. For most + applications, this is much more precise than necessary. + Since custom id space is limited, seconds was chosen as the default. + """ + + timezone: datetime.timezone + """The timezone to use for parsing. + Datetimes returned by :meth:`loads` will always be of this timezone. + + This is *not* stored in the custom id. + """ + + strict: bool + """Whether the parser is in strict mode. + + If the parser is in strict mode, :meth:`loads` requires the provided + datetime object to be of the correct :attr:`timezone`. + """ + + int_parser: builtins_parsers.IntParser + """The :class:`IntParser` to use internally for this parser. + + Since the default integer parser uses base-36 to "compress" numbers, the + default datetime parser will also return compressed results. + """ + + def __init__( + self, + *, + resolution: typing.Union[int, float] = Resolution.SECONDS, + timezone: datetime.timezone = datetime.timezone.utc, + strict: bool = True, + int_parser: typing.Optional[builtins_parsers.IntParser] = None, + ): + if resolution < 1e-6: + msg = f"Resolution must be greater than 1e-6, got {resolution}." + raise ValueError(msg) + + if resolution < 1 and resolution not in _VALID_BASE_10: + # TODO: Verify whether this doesn't false-negative + msg = f"Resolutions smaller than 1 must be a power of 10, got {resolution}." + raise ValueError(msg) + + self.resolution = resolution + self.timezone = timezone + self.strict = strict + self.int_parser = int_parser or builtins_parsers.IntParser.default() + + def loads(self, argument: str) -> datetime.datetime: + """Load a datetime from a string. + + This uses the underlying :attr:`int_parser`. + + The returned datetime is always of the specified :attr:`timezone`. + + Parameters + ---------- + argument: + The string that is to be converted into a datetime. + + """ + return datetime.datetime.fromtimestamp( + self.int_parser.loads(argument) * self.resolution, + tz=self.timezone, + ) + + def dumps(self, argument: datetime.datetime) -> str: + """Dump a datetime into a string. + + This uses the underlying :attr:`int_parser`. + + If :attr:`strict` is set to ``True``, this will fail if the provided + ``argument`` does not have a timezone set. Otherwise, a timezone-naive + datetime will automatically get its timezone set to :attr:`timezone`. + + Parameters + ---------- + argument: + The value that is to be dumped. + + Raises + ------ + :class:`ValueError`: + Either the parser is set to strict and the provided datetime was + timezone-naive, or the provided datetime's timezone does not match + that of the parser. + + """ + if self.strict: + if not argument.tzinfo: + msg = "Strict DatetimeParsers can only load timezone-aware datetimes." + raise ValueError(msg) + else: + argument = argument.replace(tzinfo=self.timezone) + + if argument.tzinfo != self.timezone: + msg = ( + "Cannot dump the provided datetime object due to a mismatch in" + f" timezones. (expected: {self.timezone}, got: {argument.tzinfo})" + ) + raise ValueError(msg) + + timestamp = argument.timestamp() + if self.resolution != 0: + timestamp //= self.resolution + + return self.int_parser.dumps(int(timestamp)) + + +@parser_base.register_parser_for(datetime.timedelta) +class TimedeltaParser(parser_base.Parser[datetime.timedelta]): + r"""Parser type with support for :class:`datetime.timedelta`\s. + + Parameters + ---------- + resolution: + The resolution with which to store :class:`~datetime.timedelta`\s in custom ids. + Defaults to :obj:`Resolution.SECONDS`. + timezone: + The timezone to use for parsing. + Defaults to :obj:`datetime.timezone.utc`. + strict: + Whether this parser is in strict mode. + Defaults to ``True``. + int_parser: + The :class:`IntParser` to use internally for this parser. + + """ + + resolution: typing.Union[int, float] + r"""The resolution with which to store :class:`~datetime.timedelta`\s in seconds. + + .. warning:: + The resolution must be greater than ``1e-6``, and if the resolution is + smaller than 1, it **must** be a power of 10. If the resolution is + greater than 1, it is coerced into an integer. + + .. note:: + Python datetime objects have microsecond accuracy. For most + applications, this is much more precise than necessary. + Since custom id space is limited, seconds was chosen as the default. + """ + + int_parser: builtins_parsers.IntParser + """The :class:`IntParser` to use internally for this parser. + + Since the default integer parser uses base-36 to "compress" numbers, the + default datetime parser will also return compressed results. + """ + + def __init__( + self, + *, + resolution: typing.Union[int, float] = Resolution.SECONDS, + int_parser: typing.Optional[builtins_parsers.IntParser] = None, + ): + if resolution < 1e-6: + msg = f"Resolution must be greater than 1e-6, got {resolution}." + raise ValueError(msg) + + if resolution < 1 and resolution not in _VALID_BASE_10: + # TODO: Verify whether this doesn't false-negative + msg = f"Resolutions smaller than 1 must be a power of 10, got {resolution}." + raise ValueError(msg) + + self.resolution = resolution + self.int_parser = int_parser or builtins_parsers.IntParser.default() + + def loads(self, argument: str) -> datetime.timedelta: + """Load a timedelta from a string. + + This uses the underlying :attr:`int_parser`. + + Parameters + ---------- + argument: + The string that is to be converted into a timedelta. + + """ + seconds = self.int_parser.loads(argument) * self.resolution + return datetime.timedelta(seconds=seconds) + + def dumps(self, argument: datetime.timedelta) -> str: + """Dump a timedelta into a string. + + This uses the underlying :attr:`int_parser`. + + Parameters + ---------- + argument: + The value that is to be dumped. + + """ + return self.int_parser.dumps(int(argument.total_seconds() // self.resolution)) + + +@parser_base.register_parser_for(datetime.date) +class DateParser(parser_base.Parser[datetime.date]): + """Parser type with support for dates. + + Parameters + ---------- + int_parser: + The :class:`IntParser` to use internally for this parser. + + """ + + int_parser: builtins_parsers.IntParser + """The :class:`IntParser` to use internally for this parser. + + Since the default integer parser uses base-36 to "compress" numbers, the + default date parser will also return compressed results. + """ + + def __init__(self, *, int_parser: typing.Optional[builtins_parsers.IntParser]): + self.int_parser = int_parser or builtins_parsers.IntParser.default() + + def loads(self, argument: str) -> datetime.date: + """Load a date from a string. + + This uses the underlying :attr:`int_parser`. + + Parameters + ---------- + argument: + The string that is to be converted into a date. + + """ + return datetime.date.fromordinal(self.int_parser.loads(argument)) + + def dumps(self, argument: datetime.date) -> str: + """Dump a datetime into a string. + + This uses the underlying :attr:`int_parser`. + + Parameters + ---------- + argument: + The value that is to be dumped. + + """ + return self.int_parser.dumps(datetime.date.toordinal(argument)) + + +@parser_base.register_parser_for(datetime.time) +class TimeParser(parser_base.Parser[datetime.time]): + r"""Parser type with support for times. + + .. important:: + Unlike :class:`DatetimeParser` etc., resolution for this class is set + via the underlying :attr:`timedelta_parser`. Note that this class *does* + proxy it through the :attr:`precision` property, which supports both + getting and setting. + + Parameters + ---------- + timezone: + The timezone to use for parsing. + Defaults to :obj:`datetime.timezone.utc`. + strict: + Whether this parser is in strict mode. + Defaults to ``True``. + timedelta_parser: + The :class:`TimedeltaParser` to use internally for this parser. + + """ + + timezone: datetime.timezone + """The timezone to use for parsing. + Times returned by :meth:`loads` will always be of this timezone. + + This is *not* stored in the custom id. + """ + + strict: bool + """Whether the parser is in strict mode. + + If the parser is in strict mode, :meth:`loads` requires the provided + datetime object to be of the correct :attr:`timezone`. + """ + + timedelta_parser: TimedeltaParser + """The :class:`TimedeltaParser` to use internally for this parser. + + Since the default timedelta parser uses base-36 to "compress" numbers, the + default datetime parser will also return compressed results. + """ + + def __init__( + self, + *, + timezone: datetime.timezone = datetime.timezone.utc, + timedelta_parser: typing.Optional[TimedeltaParser] = None, + strict: bool = True, + ): + self.timezone = timezone + self.timedelta_parser = timedelta_parser or TimedeltaParser.default() + self.strict = strict + + @property + def resolution(self) -> typing.Union[int, float]: + r"""The resolution with which to store :class:`~datetime.time`\s in seconds. + + .. warning:: + The resolution must be greater than ``1e-6``, and if the resolution is + smaller than 1, it **must** be a power of 10. If the resolution is + greater than 1, it is coerced into an integer. + + .. note:: + Python time objects have microsecond accuracy. For most + applications, this is much more precise than necessary. + Since custom id space is limited, seconds was chosen as the default. + """ + return self.timedelta_parser.resolution + + @resolution.setter + def resolution(self, resolution: typing.Union[int, float]) -> None: + self.timedelta_parser.resolution = resolution + + def loads(self, argument: str) -> datetime.time: + """Load a time from a string. + + This uses the underlying :attr:`timedelta_parser`. + + The returned time is always of the specified :attr:`timezone`. + + Parameters + ---------- + argument: + The string that is to be converted into a time. + + """ + dt = datetime.datetime.min + self.timedelta_parser.loads(argument) + return dt.time().replace(tzinfo=self.timezone) + + def dumps(self, argument: datetime.time) -> str: + """Dump a time into a string. + + This uses the underlying :attr:`timedelta_parser`. + + If :attr:`strict` is set to ``True``, this will fail if the provided + ``argument`` does not have a timezone set. Otherwise, a timezone-naive + time will automatically get its timezone set to :attr:`timezone`. + + Parameters + ---------- + argument: + The value that is to be dumped. + + Raises + ------ + :class:`ValueError`: + Either the parser is set to strict and the provided time was + timezone-naive, or the provided time's timezone does not match + that of the parser. + + """ + if self.strict: + if not argument.tzinfo: + msg = "Strict TimeParsers can only load timezone-aware times." + raise ValueError(msg) + else: + argument = argument.replace(tzinfo=self.timezone) + + if argument.tzinfo != self.timezone: + msg = ( + "Cannot dump the provided time object due to a mismatch in" + f" timezones. (expected: {self.timezone}, got: {argument.tzinfo})" + ) + raise ValueError(msg) + + return self.timedelta_parser.dumps( + datetime.timedelta( + hours=argument.hour, + minutes=argument.minute, + seconds=argument.second, + microseconds=argument.microsecond, + ) + ) + + +@parser_base.register_parser_for(datetime.timezone) +class TimezoneParser(parser_base.Parser[datetime.timezone]): + r"""Parser type with support for :class:`~datetime.timezone`\s. + + .. important:: + Unlike :class:`DatetimeParser` etc., resolution for this class is set + via the underlying :attr:`timedelta_parser`. Note that this class *does* + proxy it through the :attr:`precision` property, which supports both + getting and setting. + + Parameters + ---------- + timedelta_parser: + The :class:`TimedeltaParser` to use internally for this parser. + + """ + + timedelta_parser: TimedeltaParser + """The :class:`TimedeltaParser` to use internally for this parser. + + Since the default timedelta parser uses base-36 to "compress" numbers, the + default datetime parser will also return compressed results. + """ + + def __init__(self, *, timedelta_parser: typing.Optional[TimedeltaParser] = None): + self.timedelta_parser = timedelta_parser or TimedeltaParser() + + @property + def resolution(self) -> typing.Union[int, float]: + r"""The resolution with which to store :class:`~datetime.time`\s in seconds. + + .. warning:: + The resolution must be greater than ``1e-6``, and if the resolution is + smaller than 1, it **must** be a power of 10. If the resolution is + greater than 1, it is coerced into an integer. + + .. note:: + Python time objects have microsecond accuracy. For most + applications, this is much more precise than necessary. + Since custom id space is limited, seconds was chosen as the default. + """ + return self.timedelta_parser.resolution + + @resolution.setter + def resolution(self, resolution: typing.Union[int, float]) -> None: + self.timedelta_parser.resolution = resolution + + def loads(self, argument: str) -> datetime.timezone: + """Load a timezone from a string. + + This uses the underlying :attr:`timedelta_parser`. + + Parameters + ---------- + argument: + The string that is to be converted into a timezone. + + """ + return datetime.timezone(self.timedelta_parser.loads(argument)) + + def dumps(self, argument: datetime.timezone) -> str: + """Dump a timezone into a string. + + This uses the underlying :attr:`timedelta_parser`. + + Parameters + ---------- + argument: + The value that is to be dumped. + + """ + return self.timedelta_parser.dumps(argument.utcoffset(None))