tools: renamed module sat.tools.datetime to date.tools.xmpp_datetime to avoid conflict with Python's standard lib
This commit is contained in:
parent
ed3660c4a7
commit
672174fb23
3 changed files with 13 additions and 10 deletions
|
|
@ -23,7 +23,7 @@
|
|||
from sat.core.constants import Const as C
|
||||
from sat.core.i18n import D_
|
||||
from sat.core.sat_main import SAT
|
||||
from sat.tools import datetime
|
||||
from sat.tools import xmpp_datetime
|
||||
|
||||
|
||||
__all__ = [ # pylint: disable=unused-variable
|
||||
|
|
@ -49,7 +49,7 @@ class XEP_0082: # pylint: disable=invalid-name
|
|||
"""
|
||||
Implementation of the date and time profiles specified in XEP-0082 using Python's
|
||||
datetime module. The legacy format described in XEP-0082 section "4. Migration" is not
|
||||
supported. Reexports of the functions in :mod:`sat.tools.datetime`.
|
||||
supported. Reexports of the functions in :mod:`sat.tools.xmpp_datetime`.
|
||||
|
||||
This is a passive plugin, i.e. it doesn't hook into any triggers to process stanzas
|
||||
actively, but offers API for other plugins to use.
|
||||
|
|
@ -60,9 +60,9 @@ class XEP_0082: # pylint: disable=invalid-name
|
|||
@param sat: The SAT instance.
|
||||
"""
|
||||
|
||||
format_date = staticmethod(datetime.format_date)
|
||||
parse_date = staticmethod(datetime.parse_date)
|
||||
format_datetime = staticmethod(datetime.format_datetime)
|
||||
parse_datetime = staticmethod(datetime.parse_datetime)
|
||||
format_time = staticmethod(datetime.format_time)
|
||||
parse_time = staticmethod(datetime.parse_time)
|
||||
format_date = staticmethod(xmpp_datetime.format_date)
|
||||
parse_date = staticmethod(xmpp_datetime.parse_date)
|
||||
format_datetime = staticmethod(xmpp_datetime.format_datetime)
|
||||
parse_datetime = staticmethod(xmpp_datetime.parse_datetime)
|
||||
format_time = staticmethod(xmpp_datetime.format_time)
|
||||
parse_time = staticmethod(xmpp_datetime.parse_time)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from twisted.python import procutils, failure
|
|||
from twisted.internet import defer
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core.log import getLogger
|
||||
from sat.tools.datetime import format_date, format_datetime
|
||||
from sat.tools import xmpp_datetime
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
|
@ -157,7 +157,10 @@ def xmpp_date(
|
|||
datetime.timezone.utc
|
||||
)
|
||||
|
||||
return format_datetime(dtime) if with_time else format_date(dtime.date())
|
||||
return (
|
||||
xmpp_datetime.format_datetime(dtime) if with_time
|
||||
else xmpp_datetime.format_date(dtime.date())
|
||||
)
|
||||
|
||||
|
||||
def generatePassword(vocabulary=None, size=20):
|
||||
|
|
|
|||
187
sat/tools/xmpp_datetime.py
Normal file
187
sat/tools/xmpp_datetime.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Libervia: XMPP Date and Time profiles as per XEP-0082
|
||||
# Copyright (C) 2022-2022 Tim Henkes (me@syndace.dev)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Type-check with `mypy --strict`
|
||||
# Lint with `pylint`
|
||||
|
||||
from sat.core import exceptions
|
||||
from datetime import date, datetime, time, timezone
|
||||
import re
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
__all__ = [ # pylint: disable=unused-variable
|
||||
"format_date",
|
||||
"parse_date",
|
||||
"format_datetime",
|
||||
"parse_datetime",
|
||||
"format_time",
|
||||
"parse_time"
|
||||
]
|
||||
|
||||
|
||||
def __parse_fraction_of_a_second(value: str) -> Tuple[str, Optional[int]]:
|
||||
"""
|
||||
datetime's strptime only supports up to six digits of the fraction of a seconds, while
|
||||
the XEP-0082 specification allows for any number of digits. This function parses and
|
||||
removes the optional fraction of a second from the input string.
|
||||
|
||||
@param value: The input string, containing a section of the format [.sss].
|
||||
@return: The input string with the fraction of a second removed, and the fraction of a
|
||||
second parsed with microsecond resolution. Returns the unaltered input string and
|
||||
``None`` if no fraction of a second was found in the input string.
|
||||
"""
|
||||
|
||||
# The following regex matches the optional fraction of a seconds for manual
|
||||
# processing.
|
||||
match = re.search(r"\.(\d*)", value)
|
||||
microsecond: Optional[int] = None
|
||||
if match is not None:
|
||||
# Remove the fraction of a second from the input string
|
||||
value = value[:match.start()] + value[match.end():]
|
||||
|
||||
# datetime supports microsecond resolution for the fraction of a second, thus
|
||||
# limit/pad the parsed fraction of a second to six digits
|
||||
microsecond = int(match.group(1)[:6].ljust(6, '0'))
|
||||
|
||||
return value, microsecond
|
||||
|
||||
|
||||
def format_date(value: Optional[date] = None) -> str:
|
||||
"""
|
||||
@param value: The date for format. Defaults to the current date in the UTC timezone.
|
||||
@return: The date formatted according to the Date profile specified in XEP-0082.
|
||||
|
||||
@warning: Formatting of the current date in the local timezone may leak geographical
|
||||
information of the sender. Thus, it is advised to only format the current date in
|
||||
UTC.
|
||||
"""
|
||||
# CCYY-MM-DD
|
||||
|
||||
# The Date profile of XEP-0082 is equal to the ISO 8601 format.
|
||||
return (datetime.now(timezone.utc).date() if value is None else value).isoformat()
|
||||
|
||||
|
||||
def parse_date(value: str) -> date:
|
||||
"""
|
||||
@param value: A string containing date information formatted according to the Date
|
||||
profile specified in XEP-0082.
|
||||
@return: The date parsed from the input string.
|
||||
@raise ValueError: if the input string is not correctly formatted.
|
||||
"""
|
||||
# CCYY-MM-DD
|
||||
|
||||
# The Date profile of XEP-0082 is equal to the ISO 8601 format.
|
||||
return date.fromisoformat(value)
|
||||
|
||||
|
||||
def format_datetime(
|
||||
value: Optional[datetime] = None,
|
||||
include_microsecond: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
@param value: The datetime to format. Defaults to the current datetime.
|
||||
must be an aware datetime object (timezone must be specified)
|
||||
@param include_microsecond: Include the microsecond of the datetime in the output.
|
||||
@return: The datetime formatted according to the DateTime profile specified in
|
||||
XEP-0082. The datetime is always converted to UTC before formatting to avoid
|
||||
leaking geographical information of the sender.
|
||||
"""
|
||||
# CCYY-MM-DDThh:mm:ss[.sss]TZD
|
||||
|
||||
# We format the time in UTC, since the %z formatter of strftime doesn't include colons
|
||||
# to separate hours and minutes which is required by XEP-0082. UTC allows us to put a
|
||||
# simple letter 'Z' as the time zone definition.
|
||||
if value is not None:
|
||||
if value.tzinfo is None:
|
||||
raise exceptions.InternalError(
|
||||
"an aware datetime object must be used, but a naive one has been provided"
|
||||
)
|
||||
value = value.astimezone(timezone.utc) # pylint: disable=no-member
|
||||
else:
|
||||
value = datetime.now(timezone.utc)
|
||||
|
||||
if include_microsecond:
|
||||
return value.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
|
||||
return value.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def parse_datetime(value: str) -> datetime:
|
||||
"""
|
||||
@param value: A string containing datetime information formatted according to the
|
||||
DateTime profile specified in XEP-0082.
|
||||
@return: The datetime parsed from the input string.
|
||||
@raise ValueError: if the input string is not correctly formatted.
|
||||
"""
|
||||
# CCYY-MM-DDThh:mm:ss[.sss]TZD
|
||||
|
||||
value, microsecond = __parse_fraction_of_a_second(value)
|
||||
|
||||
result = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S%z")
|
||||
|
||||
if microsecond is not None:
|
||||
result = result.replace(microsecond=microsecond)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def format_time(value: Optional[time] = None, include_microsecond: bool = False) -> str:
|
||||
"""
|
||||
@param value: The time to format. Defaults to the current time in the UTC timezone.
|
||||
@param include_microsecond: Include the microsecond of the time in the output.
|
||||
@return: The time formatted according to the Time profile specified in XEP-0082.
|
||||
|
||||
@warning: Since accurate timezone conversion requires the date to be known, this
|
||||
function cannot convert input times to UTC before formatting. This means that
|
||||
geographical information of the sender may be leaked if a time in local timezone
|
||||
is formatted. Thus, when passing a time to format, it is advised to pass the time
|
||||
in UTC if possible.
|
||||
"""
|
||||
# hh:mm:ss[.sss][TZD]
|
||||
|
||||
if value is None:
|
||||
# There is no time.now() method as one might expect, but the current time can be
|
||||
# extracted from a datetime object including time zone information.
|
||||
value = datetime.now(timezone.utc).timetz()
|
||||
|
||||
# The format created by time.isoformat complies with the XEP-0082 Time profile.
|
||||
return value.isoformat("auto" if include_microsecond else "seconds")
|
||||
|
||||
|
||||
def parse_time(value: str) -> time:
|
||||
"""
|
||||
@param value: A string containing time information formatted according to the Time
|
||||
profile specified in XEP-0082.
|
||||
@return: The time parsed from the input string.
|
||||
@raise ValueError: if the input string is not correctly formatted.
|
||||
"""
|
||||
# hh:mm:ss[.sss][TZD]
|
||||
|
||||
value, microsecond = __parse_fraction_of_a_second(value)
|
||||
|
||||
# The format parsed by time.fromisoformat mostly complies with the XEP-0082 Time
|
||||
# profile, except that it doesn't handle the letter Z as time zone information for
|
||||
# UTC. This can be fixed with a simple string replacement of 'Z' with "+00:00", which
|
||||
# is another way to represent UTC.
|
||||
result = time.fromisoformat(value.replace('Z', "+00:00"))
|
||||
|
||||
if microsecond is not None:
|
||||
result = result.replace(microsecond=microsecond)
|
||||
|
||||
return result
|
||||
Loading…
Reference in a new issue