mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-12-31 16:40:53 +01:00
Defaults.tzinfo (#2042)
This commit is contained in:
parent
b07e42ef33
commit
103b115486
12 changed files with 194 additions and 55 deletions
|
@ -1766,6 +1766,8 @@ class Bot(TelegramObject):
|
||||||
until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will
|
until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will
|
||||||
be unbanned, unix time. If user is banned for more than 366 days or less than 30
|
be unbanned, unix time. If user is banned for more than 366 days or less than 30
|
||||||
seconds from the current time they are considered to be banned forever.
|
seconds from the current time they are considered to be banned forever.
|
||||||
|
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
|
||||||
|
bot will be used.
|
||||||
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
|
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
|
||||||
Telegram API.
|
Telegram API.
|
||||||
|
|
||||||
|
@ -1780,7 +1782,8 @@ class Bot(TelegramObject):
|
||||||
|
|
||||||
if until_date is not None:
|
if until_date is not None:
|
||||||
if isinstance(until_date, datetime):
|
if isinstance(until_date, datetime):
|
||||||
until_date = to_timestamp(until_date)
|
until_date = to_timestamp(until_date,
|
||||||
|
tzinfo=self.defaults.tzinfo if self.defaults else None)
|
||||||
data['until_date'] = until_date
|
data['until_date'] = until_date
|
||||||
|
|
||||||
result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
|
result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
|
||||||
|
@ -2866,6 +2869,8 @@ class Bot(TelegramObject):
|
||||||
will be lifted for the user, unix time. If user is restricted for more than 366
|
will be lifted for the user, unix time. If user is restricted for more than 366
|
||||||
days or less than 30 seconds from the current time, they are considered to be
|
days or less than 30 seconds from the current time, they are considered to be
|
||||||
restricted forever.
|
restricted forever.
|
||||||
|
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
|
||||||
|
bot will be used.
|
||||||
permissions (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user
|
permissions (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user
|
||||||
permissions.
|
permissions.
|
||||||
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
|
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
|
||||||
|
@ -2884,7 +2889,8 @@ class Bot(TelegramObject):
|
||||||
|
|
||||||
if until_date is not None:
|
if until_date is not None:
|
||||||
if isinstance(until_date, datetime):
|
if isinstance(until_date, datetime):
|
||||||
until_date = to_timestamp(until_date)
|
until_date = to_timestamp(until_date,
|
||||||
|
tzinfo=self.defaults.tzinfo if self.defaults else None)
|
||||||
data['until_date'] = until_date
|
data['until_date'] = until_date
|
||||||
|
|
||||||
result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
|
result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
|
||||||
|
@ -3630,6 +3636,8 @@ class Bot(TelegramObject):
|
||||||
timestamp) when the poll will be automatically closed. Must be at least 5 and no
|
timestamp) when the poll will be automatically closed. Must be at least 5 and no
|
||||||
more than 600 seconds in the future. Can't be used together with
|
more than 600 seconds in the future. Can't be used together with
|
||||||
:attr:`open_period`.
|
:attr:`open_period`.
|
||||||
|
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
|
||||||
|
bot will be used.
|
||||||
is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be
|
is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be
|
||||||
immediately closed. This can be useful for poll preview.
|
immediately closed. This can be useful for poll preview.
|
||||||
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
|
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
|
||||||
|
@ -3682,7 +3690,8 @@ class Bot(TelegramObject):
|
||||||
data['open_period'] = open_period
|
data['open_period'] = open_period
|
||||||
if close_date:
|
if close_date:
|
||||||
if isinstance(close_date, datetime):
|
if isinstance(close_date, datetime):
|
||||||
close_date = to_timestamp(close_date)
|
close_date = to_timestamp(close_date,
|
||||||
|
tzinfo=self.defaults.tzinfo if self.defaults else None)
|
||||||
data['close_date'] = close_date
|
data['close_date'] = close_date
|
||||||
|
|
||||||
return self._message('sendPoll', data, timeout=timeout,
|
return self._message('sendPoll', data, timeout=timeout,
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
# You should have received a copy of the GNU Lesser Public License
|
# You should have received a copy of the GNU Lesser Public License
|
||||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||||
"""This module contains the class Defaults, which allows to pass default values to Updater."""
|
"""This module contains the class Defaults, which allows to pass default values to Updater."""
|
||||||
|
import pytz
|
||||||
|
|
||||||
from telegram.utils.helpers import DEFAULT_NONE
|
from telegram.utils.helpers import DEFAULT_NONE
|
||||||
|
|
||||||
|
@ -37,6 +38,8 @@ class Defaults:
|
||||||
quote (:obj:`bool`): Optional. If set to :obj:`True`, the reply is sent as an actual reply
|
quote (:obj:`bool`): Optional. If set to :obj:`True`, the reply is sent as an actual reply
|
||||||
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
|
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
|
||||||
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
|
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
|
||||||
|
tzinfo (:obj:`tzinfo`): A timezone to be used for all date(time) objects appearing
|
||||||
|
throughout PTB.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
|
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
|
||||||
|
@ -51,6 +54,10 @@ class Defaults:
|
||||||
quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply
|
quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply
|
||||||
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
|
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
|
||||||
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
|
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
|
||||||
|
tzinfo (:obj:`tzinfo`, optional): A timezone to be used for all date(time) inputs
|
||||||
|
appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
|
||||||
|
somewhere, it will be assumed to be in ``tzinfo``. Must be a timezone provided by the
|
||||||
|
``pytz`` module. Defaults to UTC.
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
parse_mode=None,
|
parse_mode=None,
|
||||||
|
@ -59,12 +66,14 @@ class Defaults:
|
||||||
# Timeout needs special treatment, since the bot methods have two different
|
# Timeout needs special treatment, since the bot methods have two different
|
||||||
# default values for timeout (None and 20s)
|
# default values for timeout (None and 20s)
|
||||||
timeout=DEFAULT_NONE,
|
timeout=DEFAULT_NONE,
|
||||||
quote=None):
|
quote=None,
|
||||||
|
tzinfo=pytz.utc):
|
||||||
self._parse_mode = parse_mode
|
self._parse_mode = parse_mode
|
||||||
self._disable_notification = disable_notification
|
self._disable_notification = disable_notification
|
||||||
self._disable_web_page_preview = disable_web_page_preview
|
self._disable_web_page_preview = disable_web_page_preview
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._quote = quote
|
self._quote = quote
|
||||||
|
self._tzinfo = tzinfo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parse_mode(self):
|
def parse_mode(self):
|
||||||
|
@ -111,12 +120,22 @@ class Defaults:
|
||||||
raise AttributeError("You can not assign a new value to defaults after because it would "
|
raise AttributeError("You can not assign a new value to defaults after because it would "
|
||||||
"not have any effect.")
|
"not have any effect.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tzinfo(self):
|
||||||
|
return self._tzinfo
|
||||||
|
|
||||||
|
@tzinfo.setter
|
||||||
|
def tzinfo(self, value):
|
||||||
|
raise AttributeError("You can not assign a new value to defaults after because it would "
|
||||||
|
"not have any effect.")
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash((self._parse_mode,
|
return hash((self._parse_mode,
|
||||||
self._disable_notification,
|
self._disable_notification,
|
||||||
self._disable_web_page_preview,
|
self._disable_web_page_preview,
|
||||||
self._timeout,
|
self._timeout,
|
||||||
self._quote))
|
self._quote,
|
||||||
|
self._tzinfo))
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, Defaults):
|
if isinstance(other, Defaults):
|
||||||
|
|
|
@ -89,8 +89,9 @@ class JobQueue:
|
||||||
return self._tz_now() + time
|
return self._tz_now() + time
|
||||||
if isinstance(time, datetime.time):
|
if isinstance(time, datetime.time):
|
||||||
dt = datetime.datetime.combine(
|
dt = datetime.datetime.combine(
|
||||||
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time,
|
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time)
|
||||||
tzinfo=time.tzinfo or self.scheduler.timezone)
|
if dt.tzinfo is None:
|
||||||
|
dt = self.scheduler.timezone.localize(dt)
|
||||||
if shift_day and dt <= datetime.datetime.now(pytz.utc):
|
if shift_day and dt <= datetime.datetime.now(pytz.utc):
|
||||||
dt += datetime.timedelta(days=1)
|
dt += datetime.timedelta(days=1)
|
||||||
return dt
|
return dt
|
||||||
|
@ -106,6 +107,9 @@ class JobQueue:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._dispatcher = dispatcher
|
self._dispatcher = dispatcher
|
||||||
|
if dispatcher.bot.defaults:
|
||||||
|
if dispatcher.bot.defaults:
|
||||||
|
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
|
||||||
|
|
||||||
def run_once(self, callback, when, context=None, name=None, job_kwargs=None):
|
def run_once(self, callback, when, context=None, name=None, job_kwargs=None):
|
||||||
"""Creates a new ``Job`` that runs once and adds it to the queue.
|
"""Creates a new ``Job`` that runs once and adds it to the queue.
|
||||||
|
@ -129,13 +133,11 @@ class JobQueue:
|
||||||
job should run.
|
job should run.
|
||||||
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
|
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
|
||||||
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
|
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
|
||||||
UTC will be assumed.
|
the default timezone of the bot will be used.
|
||||||
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
|
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
|
||||||
job should run. This could be either today or, if the time has already passed,
|
job should run. This could be either today or, if the time has already passed,
|
||||||
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, UTC will be assumed.
|
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
|
||||||
|
default timezone of the bot will be used.
|
||||||
If ``when`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
|
|
||||||
then ``when.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
|
|
||||||
|
|
||||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||||
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
|
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
|
||||||
|
@ -193,13 +195,11 @@ class JobQueue:
|
||||||
job should run.
|
job should run.
|
||||||
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
|
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
|
||||||
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
|
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
|
||||||
UTC will be assumed.
|
the default timezone of the bot will be used.
|
||||||
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
|
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
|
||||||
job should run. This could be either today or, if the time has already passed,
|
job should run. This could be either today or, if the time has already passed,
|
||||||
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, UTC will be assumed.
|
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
|
||||||
|
default timezone of the bot will be used.
|
||||||
If ``first`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
|
|
||||||
then ``first.tzinfo`` will define ``Job.tzinfo``. Otherwise UTC will be assumed.
|
|
||||||
|
|
||||||
Defaults to ``interval``
|
Defaults to ``interval``
|
||||||
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||||
|
@ -208,7 +208,8 @@ class JobQueue:
|
||||||
depending on its type. See ``first`` for details.
|
depending on its type. See ``first`` for details.
|
||||||
|
|
||||||
If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
|
If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
|
||||||
and ``last.tzinfo`` is :obj:`None`, UTC will be assumed.
|
and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be
|
||||||
|
assumed.
|
||||||
|
|
||||||
Defaults to :obj:`None`.
|
Defaults to :obj:`None`.
|
||||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||||
|
@ -268,8 +269,7 @@ class JobQueue:
|
||||||
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
|
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
|
||||||
its ``job.context`` or change it to a repeating job.
|
its ``job.context`` or change it to a repeating job.
|
||||||
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
||||||
(``when.tzinfo``) is :obj:`None`, UTC will be assumed. This will also implicitly
|
(``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
|
||||||
define ``Job.tzinfo``.
|
|
||||||
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
|
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
|
||||||
be within the range of 1 and 31, inclusive.
|
be within the range of 1 and 31, inclusive.
|
||||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||||
|
@ -338,8 +338,7 @@ class JobQueue:
|
||||||
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
|
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
|
||||||
its ``job.context`` or change it to a repeating job.
|
its ``job.context`` or change it to a repeating job.
|
||||||
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
||||||
(``time.tzinfo``) is :obj:`None`, UTC will be assumed.
|
(``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
|
||||||
``time.tzinfo`` will implicitly define ``Job.tzinfo``.
|
|
||||||
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
|
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
|
||||||
run. Defaults to ``EVERY_DAY``
|
run. Defaults to ``EVERY_DAY``
|
||||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||||
|
|
|
@ -26,6 +26,8 @@ from collections import defaultdict
|
||||||
from html import escape
|
from html import escape
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ujson as json
|
import ujson as json
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -72,8 +74,6 @@ def escape_markdown(text, version=1, entity_type=None):
|
||||||
|
|
||||||
|
|
||||||
# -------- date/time related helpers --------
|
# -------- date/time related helpers --------
|
||||||
# TODO: add generic specification of UTC for naive datetimes to docs
|
|
||||||
|
|
||||||
def _datetime_to_float_timestamp(dt_obj):
|
def _datetime_to_float_timestamp(dt_obj):
|
||||||
"""
|
"""
|
||||||
Converts a datetime object to a float timestamp (with sub-second precision).
|
Converts a datetime object to a float timestamp (with sub-second precision).
|
||||||
|
@ -85,13 +85,13 @@ def _datetime_to_float_timestamp(dt_obj):
|
||||||
return dt_obj.timestamp()
|
return dt_obj.timestamp()
|
||||||
|
|
||||||
|
|
||||||
def to_float_timestamp(t, reference_timestamp=None):
|
def to_float_timestamp(t, reference_timestamp=None, tzinfo=None):
|
||||||
"""
|
"""
|
||||||
Converts a given time object to a float POSIX timestamp.
|
Converts a given time object to a float POSIX timestamp.
|
||||||
Used to convert different time specifications to a common format. The time object
|
Used to convert different time specifications to a common format. The time object
|
||||||
can be relative (i.e. indicate a time increment, or a time of day) or absolute.
|
can be relative (i.e. indicate a time increment, or a time of day) or absolute.
|
||||||
Any objects from the :class:`datetime` module that are timezone-naive will be assumed
|
Any objects from the :class:`datetime` module that are timezone-naive will be assumed
|
||||||
to be in UTC.
|
to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`.
|
||||||
|
|
||||||
:obj:`None` s are left alone (i.e. ``to_float_timestamp(None)`` is :obj:`None`).
|
:obj:`None` s are left alone (i.e. ``to_float_timestamp(None)`` is :obj:`None`).
|
||||||
|
|
||||||
|
@ -113,6 +113,9 @@ def to_float_timestamp(t, reference_timestamp=None):
|
||||||
If ``t`` is given as an absolute representation of date & time (i.e. a
|
If ``t`` is given as an absolute representation of date & time (i.e. a
|
||||||
``datetime.datetime`` object), ``reference_timestamp`` is not relevant and so its
|
``datetime.datetime`` object), ``reference_timestamp`` is not relevant and so its
|
||||||
value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised.
|
value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised.
|
||||||
|
tzinfo (:obj:`datetime.tzinfo`, optional): If ``t`` is a naive object from the
|
||||||
|
:class:`datetime` module, it will be interpreted as this timezone. Defaults to
|
||||||
|
``pytz.utc``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(float | None) The return value depends on the type of argument ``t``. If ``t`` is
|
(float | None) The return value depends on the type of argument ``t``. If ``t`` is
|
||||||
|
@ -138,33 +141,43 @@ def to_float_timestamp(t, reference_timestamp=None):
|
||||||
return reference_timestamp + t.total_seconds()
|
return reference_timestamp + t.total_seconds()
|
||||||
elif isinstance(t, Number):
|
elif isinstance(t, Number):
|
||||||
return reference_timestamp + t
|
return reference_timestamp + t
|
||||||
elif isinstance(t, dtm.time):
|
|
||||||
if t.tzinfo is not None:
|
if tzinfo is None:
|
||||||
reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo)
|
tzinfo = pytz.utc
|
||||||
else:
|
|
||||||
reference_dt = dtm.datetime.utcfromtimestamp(reference_timestamp) # assume UTC
|
if isinstance(t, dtm.time):
|
||||||
|
reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo or tzinfo)
|
||||||
reference_date = reference_dt.date()
|
reference_date = reference_dt.date()
|
||||||
reference_time = reference_dt.timetz()
|
reference_time = reference_dt.timetz()
|
||||||
if reference_time > t: # if the time of day has passed today, use tomorrow
|
|
||||||
reference_date += dtm.timedelta(days=1)
|
aware_datetime = dtm.datetime.combine(reference_date, t)
|
||||||
return _datetime_to_float_timestamp(dtm.datetime.combine(reference_date, t))
|
if aware_datetime.tzinfo is None:
|
||||||
|
aware_datetime = tzinfo.localize(aware_datetime)
|
||||||
|
|
||||||
|
# if the time of day has passed today, use tomorrow
|
||||||
|
if reference_time > aware_datetime.timetz():
|
||||||
|
aware_datetime += dtm.timedelta(days=1)
|
||||||
|
return _datetime_to_float_timestamp(aware_datetime)
|
||||||
elif isinstance(t, dtm.datetime):
|
elif isinstance(t, dtm.datetime):
|
||||||
|
if t.tzinfo is None:
|
||||||
|
t = tzinfo.localize(t)
|
||||||
return _datetime_to_float_timestamp(t)
|
return _datetime_to_float_timestamp(t)
|
||||||
|
|
||||||
raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__))
|
raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__))
|
||||||
|
|
||||||
|
|
||||||
def to_timestamp(dt_obj, reference_timestamp=None):
|
def to_timestamp(dt_obj, reference_timestamp=None, tzinfo=pytz.utc):
|
||||||
"""
|
"""
|
||||||
Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated
|
Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated
|
||||||
down to the nearest integer).
|
down to the nearest integer).
|
||||||
|
|
||||||
See the documentation for :func:`to_float_timestamp` for more details.
|
See the documentation for :func:`to_float_timestamp` for more details.
|
||||||
"""
|
"""
|
||||||
return int(to_float_timestamp(dt_obj, reference_timestamp)) if dt_obj is not None else None
|
return (int(to_float_timestamp(dt_obj, reference_timestamp, tzinfo))
|
||||||
|
if dt_obj is not None else None)
|
||||||
|
|
||||||
|
|
||||||
def from_timestamp(unixtime, tzinfo=dtm.timezone.utc):
|
def from_timestamp(unixtime, tzinfo=pytz.utc):
|
||||||
"""
|
"""
|
||||||
Converts an (integer) unix timestamp to a timezone aware datetime object.
|
Converts an (integer) unix timestamp to a timezone aware datetime object.
|
||||||
:obj:`None`s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`).
|
:obj:`None`s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`).
|
||||||
|
|
|
@ -69,6 +69,18 @@ def default_bot(request, bot_info):
|
||||||
return default_bot
|
return default_bot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def tz_bot(timezone, bot_info):
|
||||||
|
defaults = Defaults(tzinfo=timezone)
|
||||||
|
default_bot = DEFAULT_BOTS.get(defaults)
|
||||||
|
if default_bot:
|
||||||
|
return default_bot
|
||||||
|
else:
|
||||||
|
default_bot = make_bot(bot_info, **{'defaults': defaults})
|
||||||
|
DEFAULT_BOTS[defaults] = default_bot
|
||||||
|
return default_bot
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def chat_id(bot_info):
|
def chat_id(bot_info):
|
||||||
return bot_info['chat_id']
|
return bot_info['chat_id']
|
||||||
|
|
|
@ -29,7 +29,7 @@ from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboa
|
||||||
InlineQueryResultDocument, Dice, MessageEntity, ParseMode)
|
InlineQueryResultDocument, Dice, MessageEntity, ParseMode)
|
||||||
from telegram.constants import MAX_INLINE_QUERY_RESULTS
|
from telegram.constants import MAX_INLINE_QUERY_RESULTS
|
||||||
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
|
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
|
||||||
from telegram.utils.helpers import from_timestamp, escape_markdown
|
from telegram.utils.helpers import from_timestamp, escape_markdown, to_timestamp
|
||||||
from tests.conftest import expect_bad_request
|
from tests.conftest import expect_bad_request
|
||||||
|
|
||||||
BASE_TIME = time.time()
|
BASE_TIME = time.time()
|
||||||
|
@ -272,6 +272,29 @@ class TestBot:
|
||||||
assert new_message.poll.id == message.poll.id
|
assert new_message.poll.id == message.poll.id
|
||||||
assert new_message.poll.is_closed
|
assert new_message.poll.is_closed
|
||||||
|
|
||||||
|
@flaky(5, 1)
|
||||||
|
@pytest.mark.timeout(10)
|
||||||
|
def test_send_close_date_default_tz(self, tz_bot, super_group_id):
|
||||||
|
question = 'Is this a test?'
|
||||||
|
answers = ['Yes', 'No', 'Maybe']
|
||||||
|
reply_markup = InlineKeyboardMarkup.from_button(
|
||||||
|
InlineKeyboardButton(text='text', callback_data='data'))
|
||||||
|
|
||||||
|
aware_close_date = dtm.datetime.now(tz=tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=5)
|
||||||
|
close_date = aware_close_date.replace(tzinfo=None)
|
||||||
|
|
||||||
|
message = tz_bot.send_poll(chat_id=super_group_id, question=question, options=answers,
|
||||||
|
close_date=close_date, timeout=60)
|
||||||
|
assert message.poll.close_date == aware_close_date.replace(microsecond=0)
|
||||||
|
|
||||||
|
time.sleep(5.1)
|
||||||
|
|
||||||
|
new_message = tz_bot.edit_message_reply_markup(chat_id=super_group_id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
reply_markup=reply_markup, timeout=60)
|
||||||
|
assert new_message.poll.id == message.poll.id
|
||||||
|
assert new_message.poll.is_closed
|
||||||
|
|
||||||
@flaky(3, 1)
|
@flaky(3, 1)
|
||||||
@pytest.mark.timeout(10)
|
@pytest.mark.timeout(10)
|
||||||
@pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True)
|
@pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True)
|
||||||
|
@ -518,6 +541,22 @@ class TestBot:
|
||||||
assert bot.kick_chat_member(2, 32, until_date=until)
|
assert bot.kick_chat_member(2, 32, until_date=until)
|
||||||
assert bot.kick_chat_member(2, 32, until_date=1577887200)
|
assert bot.kick_chat_member(2, 32, until_date=1577887200)
|
||||||
|
|
||||||
|
def test_kick_chat_member_default_tz(self, monkeypatch, tz_bot):
|
||||||
|
until = dtm.datetime(2020, 1, 11, 16, 13)
|
||||||
|
until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo)
|
||||||
|
|
||||||
|
def test(url, data, *args, **kwargs):
|
||||||
|
chat_id = data['chat_id'] == 2
|
||||||
|
user_id = data['user_id'] == 32
|
||||||
|
until_date = data.get('until_date', until_timestamp) == until_timestamp
|
||||||
|
return chat_id and user_id and until_date
|
||||||
|
|
||||||
|
monkeypatch.setattr(tz_bot.request, 'post', test)
|
||||||
|
|
||||||
|
assert tz_bot.kick_chat_member(2, 32)
|
||||||
|
assert tz_bot.kick_chat_member(2, 32, until_date=until)
|
||||||
|
assert tz_bot.kick_chat_member(2, 32, until_date=until_timestamp)
|
||||||
|
|
||||||
# TODO: Needs improvement.
|
# TODO: Needs improvement.
|
||||||
def test_unban_chat_member(self, monkeypatch, bot):
|
def test_unban_chat_member(self, monkeypatch, bot):
|
||||||
def test(url, data, *args, **kwargs):
|
def test(url, data, *args, **kwargs):
|
||||||
|
@ -951,6 +990,28 @@ class TestBot:
|
||||||
chat_permissions,
|
chat_permissions,
|
||||||
until_date=dtm.datetime.utcnow())
|
until_date=dtm.datetime.utcnow())
|
||||||
|
|
||||||
|
def test_restrict_chat_member_default_tz(self, monkeypatch, tz_bot, channel_id,
|
||||||
|
chat_permissions):
|
||||||
|
until = dtm.datetime(2020, 1, 11, 16, 13)
|
||||||
|
until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo)
|
||||||
|
|
||||||
|
def test(url, data, *args, **kwargs):
|
||||||
|
return data.get('until_date', until_timestamp) == until_timestamp
|
||||||
|
|
||||||
|
monkeypatch.setattr(tz_bot.request, 'post', test)
|
||||||
|
|
||||||
|
assert tz_bot.restrict_chat_member(channel_id,
|
||||||
|
95205500,
|
||||||
|
chat_permissions)
|
||||||
|
assert tz_bot.restrict_chat_member(channel_id,
|
||||||
|
95205500,
|
||||||
|
chat_permissions,
|
||||||
|
until_date=until)
|
||||||
|
assert tz_bot.restrict_chat_member(channel_id,
|
||||||
|
95205500,
|
||||||
|
chat_permissions,
|
||||||
|
until_date=until_timestamp)
|
||||||
|
|
||||||
@flaky(3, 1)
|
@flaky(3, 1)
|
||||||
@pytest.mark.timeout(10)
|
@pytest.mark.timeout(10)
|
||||||
def test_promote_chat_member(self, bot, channel_id):
|
def test_promote_chat_member(self, bot, channel_id):
|
||||||
|
|
|
@ -37,6 +37,8 @@ class TestDefault:
|
||||||
defaults.timeout = True
|
defaults.timeout = True
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
defaults.quote = True
|
defaults.quote = True
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
defaults.tzinfo = True
|
||||||
|
|
||||||
def test_equality(self):
|
def test_equality(self):
|
||||||
a = Defaults(parse_mode='HTML', quote=True)
|
a = Defaults(parse_mode='HTML', quote=True)
|
||||||
|
|
|
@ -190,7 +190,7 @@ class TestFilters:
|
||||||
matches = result['matches']
|
matches = result['matches']
|
||||||
assert isinstance(matches, list)
|
assert isinstance(matches, list)
|
||||||
assert all([type(res) == SRE_TYPE for res in matches])
|
assert all([type(res) == SRE_TYPE for res in matches])
|
||||||
update.message.forward_date = False
|
update.message.forward_date = None
|
||||||
result = filter(update)
|
result = filter(update)
|
||||||
assert not result
|
assert not result
|
||||||
update.message.text = 'test it out'
|
update.message.text = 'test it out'
|
||||||
|
@ -926,7 +926,7 @@ class TestFilters:
|
||||||
update.message.text = 'test'
|
update.message.text = 'test'
|
||||||
update.message.forward_date = datetime.datetime.utcnow()
|
update.message.forward_date = datetime.datetime.utcnow()
|
||||||
assert (Filters.text & (Filters.status_update | Filters.forwarded))(update)
|
assert (Filters.text & (Filters.status_update | Filters.forwarded))(update)
|
||||||
update.message.forward_date = False
|
update.message.forward_date = None
|
||||||
assert not (Filters.text & (Filters.forwarded | Filters.status_update))(update)
|
assert not (Filters.text & (Filters.forwarded | Filters.status_update))(update)
|
||||||
update.message.pinned_message = True
|
update.message.pinned_message = True
|
||||||
assert (Filters.text & (Filters.forwarded | Filters.status_update)(update))
|
assert (Filters.text & (Filters.forwarded | Filters.status_update)(update))
|
||||||
|
|
|
@ -25,6 +25,7 @@ from telegram import Sticker
|
||||||
from telegram import Update
|
from telegram import Update
|
||||||
from telegram import User
|
from telegram import User
|
||||||
from telegram import MessageEntity
|
from telegram import MessageEntity
|
||||||
|
from telegram.ext import Defaults
|
||||||
from telegram.message import Message
|
from telegram.message import Message
|
||||||
from telegram.utils import helpers
|
from telegram.utils import helpers
|
||||||
from telegram.utils.helpers import _datetime_to_float_timestamp
|
from telegram.utils.helpers import _datetime_to_float_timestamp
|
||||||
|
@ -135,6 +136,10 @@ class TestHelpers:
|
||||||
assert (helpers.to_float_timestamp(time_spec)
|
assert (helpers.to_float_timestamp(time_spec)
|
||||||
== pytest.approx(helpers.to_float_timestamp(time_spec, reference_timestamp=now)))
|
== pytest.approx(helpers.to_float_timestamp(time_spec, reference_timestamp=now)))
|
||||||
|
|
||||||
|
def test_to_float_timestamp_error(self):
|
||||||
|
with pytest.raises(TypeError, match='Defaults'):
|
||||||
|
helpers.to_float_timestamp(Defaults())
|
||||||
|
|
||||||
@pytest.mark.parametrize('time_spec', TIME_SPECS, ids=str)
|
@pytest.mark.parametrize('time_spec', TIME_SPECS, ids=str)
|
||||||
def test_to_timestamp(self, time_spec):
|
def test_to_timestamp(self, time_spec):
|
||||||
# delegate tests to `to_float_timestamp`
|
# delegate tests to `to_float_timestamp`
|
||||||
|
@ -144,6 +149,9 @@ class TestHelpers:
|
||||||
# this 'convenience' behaviour has been left left for backwards compatibility
|
# this 'convenience' behaviour has been left left for backwards compatibility
|
||||||
assert helpers.to_timestamp(None) is None
|
assert helpers.to_timestamp(None) is None
|
||||||
|
|
||||||
|
def test_from_timestamp_none(self):
|
||||||
|
assert helpers.from_timestamp(None) is None
|
||||||
|
|
||||||
def test_from_timestamp_naive(self):
|
def test_from_timestamp_naive(self):
|
||||||
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None)
|
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None)
|
||||||
assert helpers.from_timestamp(1573431976, tzinfo=None) == datetime
|
assert helpers.from_timestamp(1573431976, tzinfo=None) == datetime
|
||||||
|
|
|
@ -121,7 +121,7 @@ class TestJobQueue:
|
||||||
sleep(0.07)
|
sleep(0.07)
|
||||||
assert self.result == 1
|
assert self.result == 1
|
||||||
|
|
||||||
def test_run_repeating_first_timezone(self, job_queue, timezone):
|
def test_run_repeating_last_timezone(self, job_queue, timezone):
|
||||||
"""Test correct scheduling of job when passing a timezone-aware datetime as ``first``"""
|
"""Test correct scheduling of job when passing a timezone-aware datetime as ``first``"""
|
||||||
job_queue.run_repeating(self.job_run_once, 0.1,
|
job_queue.run_repeating(self.job_run_once, 0.1,
|
||||||
first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.05))
|
first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.05))
|
||||||
|
@ -135,15 +135,6 @@ class TestJobQueue:
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
assert self.result == 1
|
assert self.result == 1
|
||||||
|
|
||||||
def test_run_repeating_last_timezone(self, job_queue, timezone):
|
|
||||||
"""Test correct scheduling of job when passing a timezone-aware datetime as ``first``"""
|
|
||||||
job_queue.run_repeating(self.job_run_once, 0.05,
|
|
||||||
last=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.06))
|
|
||||||
sleep(0.1)
|
|
||||||
assert self.result == 1
|
|
||||||
sleep(0.1)
|
|
||||||
assert self.result == 1
|
|
||||||
|
|
||||||
def test_run_repeating_last_before_first(self, job_queue):
|
def test_run_repeating_last_before_first(self, job_queue):
|
||||||
with pytest.raises(ValueError, match="'last' must not be before 'first'!"):
|
with pytest.raises(ValueError, match="'last' must not be before 'first'!"):
|
||||||
job_queue.run_repeating(self.job_run_once, 0.05, first=1, last=0.5)
|
job_queue.run_repeating(self.job_run_once, 0.05, first=1, last=0.5)
|
||||||
|
@ -300,7 +291,11 @@ class TestJobQueue:
|
||||||
time_of_day = expected_reschedule_time.time().replace(tzinfo=timezone)
|
time_of_day = expected_reschedule_time.time().replace(tzinfo=timezone)
|
||||||
|
|
||||||
day = now.day
|
day = now.day
|
||||||
expected_reschedule_time += dtm.timedelta(calendar.monthrange(now.year, now.month)[1])
|
expected_reschedule_time = timezone.normalize(
|
||||||
|
expected_reschedule_time + dtm.timedelta(calendar.monthrange(now.year, now.month)[1]))
|
||||||
|
# Adjust the hour for the special case that between now and next month a DST switch happens
|
||||||
|
expected_reschedule_time += dtm.timedelta(
|
||||||
|
hours=time_of_day.hour - expected_reschedule_time.hour)
|
||||||
expected_reschedule_time = expected_reschedule_time.timestamp()
|
expected_reschedule_time = expected_reschedule_time.timestamp()
|
||||||
|
|
||||||
job_queue.run_monthly(self.job_run_once, time_of_day, day)
|
job_queue.run_monthly(self.job_run_once, time_of_day, day)
|
||||||
|
@ -326,6 +321,25 @@ class TestJobQueue:
|
||||||
scheduled_time = job_queue.jobs()[0].next_t.timestamp()
|
scheduled_time = job_queue.jobs()[0].next_t.timestamp()
|
||||||
assert scheduled_time == pytest.approx(expected_reschedule_time)
|
assert scheduled_time == pytest.approx(expected_reschedule_time)
|
||||||
|
|
||||||
|
def test_default_tzinfo(self, _dp, tz_bot):
|
||||||
|
# we're parametrizing this with two different UTC offsets to exclude the possibility
|
||||||
|
# of an xpass when the test is run in a timezone with the same UTC offset
|
||||||
|
jq = JobQueue()
|
||||||
|
original_bot = _dp.bot
|
||||||
|
_dp.bot = tz_bot
|
||||||
|
jq.set_dispatcher(_dp)
|
||||||
|
try:
|
||||||
|
jq.start()
|
||||||
|
|
||||||
|
when = dtm.datetime.now(tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=0.0005)
|
||||||
|
jq.run_once(self.job_run_once, when.time())
|
||||||
|
sleep(0.001)
|
||||||
|
assert self.result == 1
|
||||||
|
|
||||||
|
jq.stop()
|
||||||
|
finally:
|
||||||
|
_dp.bot = original_bot
|
||||||
|
|
||||||
@pytest.mark.parametrize('use_context', [True, False])
|
@pytest.mark.parametrize('use_context', [True, False])
|
||||||
def test_get_jobs(self, job_queue, use_context):
|
def test_get_jobs(self, job_queue, use_context):
|
||||||
job_queue._dispatcher.use_context = use_context
|
job_queue._dispatcher.use_context = use_context
|
||||||
|
|
|
@ -169,13 +169,13 @@ class TestMessage:
|
||||||
MessageEntity(**e) for e in test_entities_v2
|
MessageEntity(**e) for e in test_entities_v2
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_all_posibilities_de_json_and_to_dict(self, bot, message_params):
|
def test_all_possibilities_de_json_and_to_dict(self, bot, message_params):
|
||||||
new = Message.de_json(message_params.to_dict(), bot)
|
new = Message.de_json(message_params.to_dict(), bot)
|
||||||
|
|
||||||
assert new.to_dict() == message_params.to_dict()
|
assert new.to_dict() == message_params.to_dict()
|
||||||
|
|
||||||
def test_dict_approach(self, message):
|
def test_dict_approach(self, message):
|
||||||
assert message['date'] == message.date
|
assert message['text'] == message.text
|
||||||
assert message['chat_id'] == message.chat_id
|
assert message['chat_id'] == message.chat_id
|
||||||
assert message['no_key'] is None
|
assert message['no_key'] is None
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
from telegram import Poll, PollOption, PollAnswer, User, MessageEntity
|
from telegram import Poll, PollOption, PollAnswer, User, MessageEntity
|
||||||
from telegram.utils.helpers import to_timestamp
|
from telegram.utils.helpers import to_timestamp
|
||||||
|
|
||||||
|
@ -154,7 +156,7 @@ class TestPoll:
|
||||||
open_period = 42
|
open_period = 42
|
||||||
close_date = datetime.utcnow()
|
close_date = datetime.utcnow()
|
||||||
|
|
||||||
def test_de_json(self):
|
def test_de_json(self, bot):
|
||||||
json_dict = {
|
json_dict = {
|
||||||
'id': self.id_,
|
'id': self.id_,
|
||||||
'question': self.question,
|
'question': self.question,
|
||||||
|
@ -169,7 +171,7 @@ class TestPoll:
|
||||||
'open_period': self.open_period,
|
'open_period': self.open_period,
|
||||||
'close_date': to_timestamp(self.close_date)
|
'close_date': to_timestamp(self.close_date)
|
||||||
}
|
}
|
||||||
poll = Poll.de_json(json_dict, None)
|
poll = Poll.de_json(json_dict, bot)
|
||||||
|
|
||||||
assert poll.id == self.id_
|
assert poll.id == self.id_
|
||||||
assert poll.question == self.question
|
assert poll.question == self.question
|
||||||
|
|
Loading…
Reference in a new issue