mirror of
https://github.com/OpenVoiceOS/OpenVoiceOS
synced 2025-06-05 22:19:21 +02:00
[WIP] Pushed for backup.
... Do not build this as of yet ...
This commit is contained in:
@ -0,0 +1,234 @@
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
from datetime import time
|
||||
|
||||
from dateutil import parser
|
||||
|
||||
from .exceptions import ParserError
|
||||
|
||||
|
||||
with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1"
|
||||
|
||||
try:
|
||||
if not with_extensions or struct.calcsize("P") == 4:
|
||||
raise ImportError()
|
||||
|
||||
from ._iso8601 import parse_iso8601
|
||||
except ImportError:
|
||||
from .iso8601 import parse_iso8601
|
||||
|
||||
|
||||
COMMON = re.compile(
|
||||
# Date (optional)
|
||||
"^"
|
||||
"(?P<date>"
|
||||
" (?P<classic>" # Classic date (YYYY-MM-DD)
|
||||
r" (?P<year>\d{4})" # Year
|
||||
" (?P<monthday>"
|
||||
r" (?P<monthsep>[/:])?(?P<month>\d{2})" # Month (optional)
|
||||
r" ((?P<daysep>[/:])?(?P<day>\d{2}))" # Day (optional)
|
||||
" )?"
|
||||
" )"
|
||||
")?"
|
||||
# Time (optional)
|
||||
"(?P<time>"
|
||||
r" (?P<timesep>\ )?" # Separator (space)
|
||||
r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?" # HH:mm:ss (optional mm and ss)
|
||||
# Subsecond part (optional)
|
||||
" (?P<subsecondsection>"
|
||||
" (?:[.|,])" # Subsecond separator (optional)
|
||||
r" (?P<subsecond>\d{1,9})" # Subsecond
|
||||
" )?"
|
||||
")?"
|
||||
"$",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
"day_first": False,
|
||||
"year_first": True,
|
||||
"strict": True,
|
||||
"exact": False,
|
||||
"now": None,
|
||||
}
|
||||
|
||||
|
||||
def parse(text, **options):
|
||||
"""
|
||||
Parses a string with the given options.
|
||||
|
||||
:param text: The string to parse.
|
||||
:type text: str
|
||||
|
||||
:rtype: Parsed
|
||||
"""
|
||||
_options = copy.copy(DEFAULT_OPTIONS)
|
||||
_options.update(options)
|
||||
|
||||
return _normalize(_parse(text, **_options), **_options)
|
||||
|
||||
|
||||
def _normalize(parsed, **options):
|
||||
"""
|
||||
Normalizes the parsed element.
|
||||
|
||||
:param parsed: The parsed elements.
|
||||
:type parsed: Parsed
|
||||
|
||||
:rtype: Parsed
|
||||
"""
|
||||
if options.get("exact"):
|
||||
return parsed
|
||||
|
||||
if isinstance(parsed, time):
|
||||
now = options["now"] or datetime.now()
|
||||
|
||||
return datetime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
parsed.hour,
|
||||
parsed.minute,
|
||||
parsed.second,
|
||||
parsed.microsecond,
|
||||
)
|
||||
elif isinstance(parsed, date) and not isinstance(parsed, datetime):
|
||||
return datetime(parsed.year, parsed.month, parsed.day)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def _parse(text, **options):
|
||||
# Trying to parse ISO8601
|
||||
try:
|
||||
return parse_iso8601(text)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return _parse_iso8601_interval(text)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return _parse_common(text, **options)
|
||||
except ParserError:
|
||||
pass
|
||||
|
||||
# We couldn't parse the string
|
||||
# so we fallback on the dateutil parser
|
||||
# If not strict
|
||||
if options.get("strict", True):
|
||||
raise ParserError("Unable to parse string [{}]".format(text))
|
||||
|
||||
try:
|
||||
dt = parser.parse(
|
||||
text, dayfirst=options["day_first"], yearfirst=options["year_first"]
|
||||
)
|
||||
except ValueError:
|
||||
raise ParserError("Invalid date string: {}".format(text))
|
||||
|
||||
return dt
|
||||
|
||||
|
||||
def _parse_common(text, **options):
|
||||
"""
|
||||
Tries to parse the string as a common datetime format.
|
||||
|
||||
:param text: The string to parse.
|
||||
:type text: str
|
||||
|
||||
:rtype: dict or None
|
||||
"""
|
||||
m = COMMON.match(text)
|
||||
has_date = False
|
||||
year = 0
|
||||
month = 1
|
||||
day = 1
|
||||
|
||||
if not m:
|
||||
raise ParserError("Invalid datetime string")
|
||||
|
||||
if m.group("date"):
|
||||
# A date has been specified
|
||||
has_date = True
|
||||
|
||||
year = int(m.group("year"))
|
||||
|
||||
if not m.group("monthday"):
|
||||
# No month and day
|
||||
month = 1
|
||||
day = 1
|
||||
else:
|
||||
if options["day_first"]:
|
||||
month = int(m.group("day"))
|
||||
day = int(m.group("month"))
|
||||
else:
|
||||
month = int(m.group("month"))
|
||||
day = int(m.group("day"))
|
||||
|
||||
if not m.group("time"):
|
||||
return date(year, month, day)
|
||||
|
||||
# Grabbing hh:mm:ss
|
||||
hour = int(m.group("hour"))
|
||||
|
||||
minute = int(m.group("minute"))
|
||||
|
||||
if m.group("second"):
|
||||
second = int(m.group("second"))
|
||||
else:
|
||||
second = 0
|
||||
|
||||
# Grabbing subseconds, if any
|
||||
microsecond = 0
|
||||
if m.group("subsecondsection"):
|
||||
# Limiting to 6 chars
|
||||
subsecond = m.group("subsecond")[:6]
|
||||
|
||||
microsecond = int("{:0<6}".format(subsecond))
|
||||
|
||||
if has_date:
|
||||
return datetime(year, month, day, hour, minute, second, microsecond)
|
||||
|
||||
return time(hour, minute, second, microsecond)
|
||||
|
||||
|
||||
class _Interval:
|
||||
"""
|
||||
Special class to handle ISO 8601 intervals
|
||||
"""
|
||||
|
||||
def __init__(self, start=None, end=None, duration=None):
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.duration = duration
|
||||
|
||||
|
||||
def _parse_iso8601_interval(text):
|
||||
if "/" not in text:
|
||||
raise ParserError("Invalid interval")
|
||||
|
||||
first, last = text.split("/")
|
||||
start = end = duration = None
|
||||
|
||||
if first[0] == "P":
|
||||
# duration/end
|
||||
duration = parse_iso8601(first)
|
||||
end = parse_iso8601(last)
|
||||
elif last[0] == "P":
|
||||
# start/duration
|
||||
start = parse_iso8601(first)
|
||||
duration = parse_iso8601(last)
|
||||
else:
|
||||
# start/end
|
||||
start = parse_iso8601(first)
|
||||
end = parse_iso8601(last)
|
||||
|
||||
return _Interval(start, end, duration)
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
||||
class ParserError(ValueError):
|
||||
|
||||
pass
|
Binary file not shown.
@ -0,0 +1,447 @@
|
||||
from __future__ import division
|
||||
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from ..constants import HOURS_PER_DAY
|
||||
from ..constants import MINUTES_PER_HOUR
|
||||
from ..constants import MONTHS_OFFSETS
|
||||
from ..constants import SECONDS_PER_MINUTE
|
||||
from ..duration import Duration
|
||||
from ..helpers import days_in_year
|
||||
from ..helpers import is_leap
|
||||
from ..helpers import is_long_year
|
||||
from ..helpers import week_day
|
||||
from ..tz.timezone import UTC
|
||||
from ..tz.timezone import FixedTimezone
|
||||
from .exceptions import ParserError
|
||||
|
||||
|
||||
ISO8601_DT = re.compile(
|
||||
# Date (optional)
|
||||
"^"
|
||||
"(?P<date>"
|
||||
" (?P<classic>" # Classic date (YYYY-MM-DD) or ordinal (YYYY-DDD)
|
||||
r" (?P<year>\d{4})" # Year
|
||||
" (?P<monthday>"
|
||||
r" (?P<monthsep>-)?(?P<month>\d{2})" # Month (optional)
|
||||
r" ((?P<daysep>-)?(?P<day>\d{1,2}))?" # Day (optional)
|
||||
" )?"
|
||||
" )"
|
||||
" |"
|
||||
" (?P<isocalendar>" # Calendar date (2016-W05 or 2016-W05-5)
|
||||
r" (?P<isoyear>\d{4})" # Year
|
||||
" (?P<weeksep>-)?" # Separator (optional)
|
||||
" W" # W separator
|
||||
r" (?P<isoweek>\d{2})" # Week number
|
||||
" (?P<weekdaysep>-)?" # Separator (optional)
|
||||
r" (?P<isoweekday>\d)?" # Weekday (optional)
|
||||
" )"
|
||||
")?"
|
||||
# Time (optional)
|
||||
"(?P<time>"
|
||||
r" (?P<timesep>[T\ ])?" # Separator (T or space)
|
||||
r" (?P<hour>\d{1,2})(?P<minsep>:)?(?P<minute>\d{1,2})?(?P<secsep>:)?(?P<second>\d{1,2})?" # HH:mm:ss (optional mm and ss)
|
||||
# Subsecond part (optional)
|
||||
" (?P<subsecondsection>"
|
||||
" (?:[.,])" # Subsecond separator (optional)
|
||||
r" (?P<subsecond>\d{1,9})" # Subsecond
|
||||
" )?"
|
||||
# Timezone offset
|
||||
" (?P<tz>"
|
||||
r" (?:[-+])\d{2}:?(?:\d{2})?|Z" # Offset (+HH:mm or +HHmm or +HH or Z)
|
||||
" )?"
|
||||
")?"
|
||||
"$",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
ISO8601_DURATION = re.compile(
|
||||
"^P" # Duration P indicator
|
||||
# Years, months and days (optional)
|
||||
"(?P<w>"
|
||||
r" (?P<weeks>\d+(?:[.,]\d+)?W)"
|
||||
")?"
|
||||
"(?P<ymd>"
|
||||
r" (?P<years>\d+(?:[.,]\d+)?Y)?"
|
||||
r" (?P<months>\d+(?:[.,]\d+)?M)?"
|
||||
r" (?P<days>\d+(?:[.,]\d+)?D)?"
|
||||
")?"
|
||||
"(?P<hms>"
|
||||
" (?P<timesep>T)" # Separator (T)
|
||||
r" (?P<hours>\d+(?:[.,]\d+)?H)?"
|
||||
r" (?P<minutes>\d+(?:[.,]\d+)?M)?"
|
||||
r" (?P<seconds>\d+(?:[.,]\d+)?S)?"
|
||||
")?"
|
||||
"$",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def parse_iso8601(text):
|
||||
"""
|
||||
ISO 8601 compliant parser.
|
||||
|
||||
:param text: The string to parse
|
||||
:type text: str
|
||||
|
||||
:rtype: datetime.datetime or datetime.time or datetime.date
|
||||
"""
|
||||
parsed = _parse_iso8601_duration(text)
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
|
||||
m = ISO8601_DT.match(text)
|
||||
if not m:
|
||||
raise ParserError("Invalid ISO 8601 string")
|
||||
|
||||
ambiguous_date = False
|
||||
is_date = False
|
||||
is_time = False
|
||||
year = 0
|
||||
month = 1
|
||||
day = 1
|
||||
minute = 0
|
||||
second = 0
|
||||
microsecond = 0
|
||||
tzinfo = None
|
||||
|
||||
if m:
|
||||
if m.group("date"):
|
||||
# A date has been specified
|
||||
is_date = True
|
||||
|
||||
if m.group("isocalendar"):
|
||||
# We have a ISO 8601 string defined
|
||||
# by week number
|
||||
if (
|
||||
m.group("weeksep")
|
||||
and not m.group("weekdaysep")
|
||||
and m.group("isoweekday")
|
||||
):
|
||||
raise ParserError("Invalid date string: {}".format(text))
|
||||
|
||||
if not m.group("weeksep") and m.group("weekdaysep"):
|
||||
raise ParserError("Invalid date string: {}".format(text))
|
||||
|
||||
try:
|
||||
date = _get_iso_8601_week(
|
||||
m.group("isoyear"), m.group("isoweek"), m.group("isoweekday")
|
||||
)
|
||||
except ParserError:
|
||||
raise
|
||||
except ValueError:
|
||||
raise ParserError("Invalid date string: {}".format(text))
|
||||
|
||||
year = date["year"]
|
||||
month = date["month"]
|
||||
day = date["day"]
|
||||
else:
|
||||
# We have a classic date representation
|
||||
year = int(m.group("year"))
|
||||
|
||||
if not m.group("monthday"):
|
||||
# No month and day
|
||||
month = 1
|
||||
day = 1
|
||||
else:
|
||||
if m.group("month") and m.group("day"):
|
||||
# Month and day
|
||||
if not m.group("daysep") and len(m.group("day")) == 1:
|
||||
# Ordinal day
|
||||
ordinal = int(m.group("month") + m.group("day"))
|
||||
leap = is_leap(year)
|
||||
months_offsets = MONTHS_OFFSETS[leap]
|
||||
|
||||
if ordinal > months_offsets[13]:
|
||||
raise ParserError("Ordinal day is out of range")
|
||||
|
||||
for i in range(1, 14):
|
||||
if ordinal <= months_offsets[i]:
|
||||
day = ordinal - months_offsets[i - 1]
|
||||
month = i - 1
|
||||
|
||||
break
|
||||
else:
|
||||
month = int(m.group("month"))
|
||||
day = int(m.group("day"))
|
||||
else:
|
||||
# Only month
|
||||
if not m.group("monthsep"):
|
||||
# The date looks like 201207
|
||||
# which is invalid for a date
|
||||
# But it might be a time in the form hhmmss
|
||||
ambiguous_date = True
|
||||
|
||||
month = int(m.group("month"))
|
||||
day = 1
|
||||
|
||||
if not m.group("time"):
|
||||
# No time has been specified
|
||||
if ambiguous_date:
|
||||
# We can "safely" assume that the ambiguous date
|
||||
# was actually a time in the form hhmmss
|
||||
hhmmss = "{}{:0>2}".format(str(year), str(month))
|
||||
|
||||
return datetime.time(int(hhmmss[:2]), int(hhmmss[2:4]), int(hhmmss[4:]))
|
||||
|
||||
return datetime.date(year, month, day)
|
||||
|
||||
if ambiguous_date:
|
||||
raise ParserError("Invalid date string: {}".format(text))
|
||||
|
||||
if is_date and not m.group("timesep"):
|
||||
raise ParserError("Invalid date string: {}".format(text))
|
||||
|
||||
if not is_date:
|
||||
is_time = True
|
||||
|
||||
# Grabbing hh:mm:ss
|
||||
hour = int(m.group("hour"))
|
||||
minsep = m.group("minsep")
|
||||
|
||||
if m.group("minute"):
|
||||
minute = int(m.group("minute"))
|
||||
elif minsep:
|
||||
raise ParserError("Invalid ISO 8601 time part")
|
||||
|
||||
secsep = m.group("secsep")
|
||||
if secsep and not minsep and m.group("minute"):
|
||||
# minute/second separator but no hour/minute separator
|
||||
raise ParserError("Invalid ISO 8601 time part")
|
||||
|
||||
if m.group("second"):
|
||||
if not secsep and minsep:
|
||||
# No minute/second separator but hour/minute separator
|
||||
raise ParserError("Invalid ISO 8601 time part")
|
||||
|
||||
second = int(m.group("second"))
|
||||
elif secsep:
|
||||
raise ParserError("Invalid ISO 8601 time part")
|
||||
|
||||
# Grabbing subseconds, if any
|
||||
if m.group("subsecondsection"):
|
||||
# Limiting to 6 chars
|
||||
subsecond = m.group("subsecond")[:6]
|
||||
|
||||
microsecond = int("{:0<6}".format(subsecond))
|
||||
|
||||
# Grabbing timezone, if any
|
||||
tz = m.group("tz")
|
||||
if tz:
|
||||
if tz == "Z":
|
||||
tzinfo = UTC
|
||||
else:
|
||||
negative = True if tz.startswith("-") else False
|
||||
tz = tz[1:]
|
||||
if ":" not in tz:
|
||||
if len(tz) == 2:
|
||||
tz = "{}00".format(tz)
|
||||
|
||||
off_hour = tz[0:2]
|
||||
off_minute = tz[2:4]
|
||||
else:
|
||||
off_hour, off_minute = tz.split(":")
|
||||
|
||||
offset = ((int(off_hour) * 60) + int(off_minute)) * 60
|
||||
|
||||
if negative:
|
||||
offset = -1 * offset
|
||||
|
||||
tzinfo = FixedTimezone(offset)
|
||||
|
||||
if is_time:
|
||||
return datetime.time(hour, minute, second, microsecond)
|
||||
|
||||
return datetime.datetime(
|
||||
year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo
|
||||
)
|
||||
|
||||
|
||||
def _parse_iso8601_duration(text, **options):
|
||||
m = ISO8601_DURATION.match(text)
|
||||
if not m:
|
||||
return
|
||||
|
||||
years = 0
|
||||
months = 0
|
||||
weeks = 0
|
||||
days = 0
|
||||
hours = 0
|
||||
minutes = 0
|
||||
seconds = 0
|
||||
microseconds = 0
|
||||
fractional = False
|
||||
|
||||
if m.group("w"):
|
||||
# Weeks
|
||||
if m.group("ymd") or m.group("hms"):
|
||||
# Specifying anything more than weeks is not supported
|
||||
raise ParserError("Invalid duration string")
|
||||
|
||||
_weeks = m.group("weeks")
|
||||
if not _weeks:
|
||||
raise ParserError("Invalid duration string")
|
||||
|
||||
_weeks = _weeks.replace(",", ".").replace("W", "")
|
||||
if "." in _weeks:
|
||||
_weeks, portion = _weeks.split(".")
|
||||
weeks = int(_weeks)
|
||||
_days = int(portion) / 10 * 7
|
||||
days, hours = int(_days // 1), _days % 1 * HOURS_PER_DAY
|
||||
else:
|
||||
weeks = int(_weeks)
|
||||
|
||||
if m.group("ymd"):
|
||||
# Years, months and/or days
|
||||
_years = m.group("years")
|
||||
_months = m.group("months")
|
||||
_days = m.group("days")
|
||||
|
||||
# Checking order
|
||||
years_start = m.start("years") if _years else -3
|
||||
months_start = m.start("months") if _months else years_start + 1
|
||||
days_start = m.start("days") if _days else months_start + 1
|
||||
|
||||
# Check correct order
|
||||
if not (years_start < months_start < days_start):
|
||||
raise ParserError("Invalid duration")
|
||||
|
||||
if _years:
|
||||
_years = _years.replace(",", ".").replace("Y", "")
|
||||
if "." in _years:
|
||||
raise ParserError("Float years in duration are not supported")
|
||||
else:
|
||||
years = int(_years)
|
||||
|
||||
if _months:
|
||||
if fractional:
|
||||
raise ParserError("Invalid duration")
|
||||
|
||||
_months = _months.replace(",", ".").replace("M", "")
|
||||
if "." in _months:
|
||||
raise ParserError("Float months in duration are not supported")
|
||||
else:
|
||||
months = int(_months)
|
||||
|
||||
if _days:
|
||||
if fractional:
|
||||
raise ParserError("Invalid duration")
|
||||
|
||||
_days = _days.replace(",", ".").replace("D", "")
|
||||
|
||||
if "." in _days:
|
||||
fractional = True
|
||||
|
||||
_days, _hours = _days.split(".")
|
||||
days = int(_days)
|
||||
hours = int(_hours) / 10 * HOURS_PER_DAY
|
||||
else:
|
||||
days = int(_days)
|
||||
|
||||
if m.group("hms"):
|
||||
# Hours, minutes and/or seconds
|
||||
_hours = m.group("hours") or 0
|
||||
_minutes = m.group("minutes") or 0
|
||||
_seconds = m.group("seconds") or 0
|
||||
|
||||
# Checking order
|
||||
hours_start = m.start("hours") if _hours else -3
|
||||
minutes_start = m.start("minutes") if _minutes else hours_start + 1
|
||||
seconds_start = m.start("seconds") if _seconds else minutes_start + 1
|
||||
|
||||
# Check correct order
|
||||
if not (hours_start < minutes_start < seconds_start):
|
||||
raise ParserError("Invalid duration")
|
||||
|
||||
if _hours:
|
||||
if fractional:
|
||||
raise ParserError("Invalid duration")
|
||||
|
||||
_hours = _hours.replace(",", ".").replace("H", "")
|
||||
|
||||
if "." in _hours:
|
||||
fractional = True
|
||||
|
||||
_hours, _mins = _hours.split(".")
|
||||
hours += int(_hours)
|
||||
minutes += int(_mins) / 10 * MINUTES_PER_HOUR
|
||||
else:
|
||||
hours += int(_hours)
|
||||
|
||||
if _minutes:
|
||||
if fractional:
|
||||
raise ParserError("Invalid duration")
|
||||
|
||||
_minutes = _minutes.replace(",", ".").replace("M", "")
|
||||
|
||||
if "." in _minutes:
|
||||
fractional = True
|
||||
|
||||
_minutes, _secs = _minutes.split(".")
|
||||
minutes += int(_minutes)
|
||||
seconds += int(_secs) / 10 * SECONDS_PER_MINUTE
|
||||
else:
|
||||
minutes += int(_minutes)
|
||||
|
||||
if _seconds:
|
||||
if fractional:
|
||||
raise ParserError("Invalid duration")
|
||||
|
||||
_seconds = _seconds.replace(",", ".").replace("S", "")
|
||||
|
||||
if "." in _seconds:
|
||||
_seconds, _microseconds = _seconds.split(".")
|
||||
seconds += int(_seconds)
|
||||
microseconds += int("{:0<6}".format(_microseconds[:6]))
|
||||
else:
|
||||
seconds += int(_seconds)
|
||||
|
||||
return Duration(
|
||||
years=years,
|
||||
months=months,
|
||||
weeks=weeks,
|
||||
days=days,
|
||||
hours=hours,
|
||||
minutes=minutes,
|
||||
seconds=seconds,
|
||||
microseconds=microseconds,
|
||||
)
|
||||
|
||||
|
||||
def _get_iso_8601_week(year, week, weekday):
|
||||
if not weekday:
|
||||
weekday = 1
|
||||
else:
|
||||
weekday = int(weekday)
|
||||
|
||||
year = int(year)
|
||||
week = int(week)
|
||||
|
||||
if week > 53 or week > 52 and not is_long_year(year):
|
||||
raise ParserError("Invalid week for week date")
|
||||
|
||||
if weekday > 7:
|
||||
raise ParserError("Invalid weekday for week date")
|
||||
|
||||
# We can't rely on strptime directly here since
|
||||
# it does not support ISO week date
|
||||
ordinal = week * 7 + weekday - (week_day(year, 1, 4) + 3)
|
||||
|
||||
if ordinal < 1:
|
||||
# Previous year
|
||||
ordinal += days_in_year(year - 1)
|
||||
year -= 1
|
||||
|
||||
if ordinal > days_in_year(year):
|
||||
# Next year
|
||||
ordinal -= days_in_year(year)
|
||||
year += 1
|
||||
|
||||
fmt = "%Y-%j"
|
||||
string = "{}-{}".format(year, ordinal)
|
||||
|
||||
dt = datetime.datetime.strptime(string, fmt)
|
||||
|
||||
return {"year": dt.year, "month": dt.month, "day": dt.day}
|
Reference in New Issue
Block a user