From b175dabe7729ea4bac2d4c8eff164682afb3000a Mon Sep 17 00:00:00 2001 From: Aweryc Date: Mon, 14 Jul 2025 13:00:09 +0300 Subject: [PATCH 01/14] Convenience Functionality for BusinessOpeningHours -check if the business is open at a given time -get the opening hours for a given day --- changes/unreleased/48XX._.toml | 5 + pyproject.toml | 5 + src/telegram/__main__.py | 4 +- src/telegram/_business.py | 89 ++++++++- .../_passport/encryptedpassportelement.py | 1 - src/telegram/_payment/stars/staramount.py | 1 - src/telegram/_utils/datetime.py | 25 +++ src/telegram/_utils/enum.py | 2 +- tests/_utils/test_datetime.py | 54 +++++- tests/test_business_classes.py | 177 ++++++++++++++++++ 10 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 changes/unreleased/48XX._.toml diff --git a/changes/unreleased/48XX._.toml b/changes/unreleased/48XX._.toml new file mode 100644 index 00000000000..fa083bd43a0 --- /dev/null +++ b/changes/unreleased/48XX._.toml @@ -0,0 +1,5 @@ +features = "Added a two methods for BusinessOpeningHours" +[[pull_requests]] +uid = "4326" +author_uid = "Aweryc" +closes_threads = ["4194, 4326"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 66589c25b0e..25d18172d19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,12 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ + "black>=25.1.0", "httpx >=0.27,<0.29", + "pre-commit>=4.2.0", + "pytest>=8.3.5", + "pytz>=2025.2", + "ruff==0.11.9", ] [project.urls] diff --git a/src/telegram/__main__.py b/src/telegram/__main__.py index 7d291b2ae1e..c685c0a389a 100644 --- a/src/telegram/__main__.py +++ b/src/telegram/__main__.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. # pylint: disable=missing-module-docstring -# ruff: noqa: T201, D100, S603, S607 +# ruff: noqa: T201, D100, S607 import subprocess import sys from typing import Optional @@ -28,7 +28,7 @@ def _git_revision() -> Optional[str]: try: - output = subprocess.check_output( + output = subprocess.check_output( # noqa: S603 ["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT ) except (subprocess.SubprocessError, OSError): diff --git a/src/telegram/_business.py b/src/telegram/_business.py index dd055426654..7846955af7e 100644 --- a/src/telegram/_business.py +++ b/src/telegram/_business.py @@ -21,6 +21,7 @@ import datetime as dtm from collections.abc import Sequence from typing import TYPE_CHECKING, Optional +from zoneinfo import ZoneInfo from telegram._chat import Chat from telegram._files.location import Location @@ -28,7 +29,7 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg -from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp, verify_timezone from telegram._utils.types import JSONDict from telegram._utils.warnings import warn from telegram._utils.warnings_transition import ( @@ -494,7 +495,7 @@ class BusinessOpeningHoursInterval(TelegramObject): Examples: A day has (24 * 60 =) 1440 minutes, a week has (7 * 1440 =) 10080 minutes. - Starting the the minute's sequence from Monday, example values of + Starting the minute's sequence from Monday, example values of :attr:`opening_minute`, :attr:`closing_minute` will map to the following day times: * Monday - 8am to 8:30pm: @@ -616,6 +617,90 @@ def __init__( self._freeze() + def get_opening_hours_for_day( + self, date: dtm.date, time_zone: Optional[ZoneInfo] = None + ) -> tuple[tuple[dtm.datetime, dtm.datetime], ...]: + """Returns the opening hours intervals for a specific day as datetime objects. + + .. versionadded:: NEXT.VERSION + + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`time_zone_name` and :attr:`opening_hours` are equal. + Args: + date (:obj:`datetime.date`): The date to get opening hours for. + Only the weekday component + is used to determine matching opening intervals. + time_zone (:obj:`zoneinfo.ZoneInfo`, optional): Timezone to use for the returned + datetime objects. If not specified, the returned datetime objects + will be timezone-naive. + + Returns: + tuple[tuple[:obj:`datetime.datetime`, :obj:`datetime.datetime`], ...]: + A tuple of datetime pairs representing opening and closing times for the specified day. + Each pair consists of (opening_time, closing_time). Returns an empty tuple if there are + no opening hours for the given day. + """ + + week_day = date.weekday() + res = [] + + for interval in self.opening_hours: + int_open = interval.opening_time + int_close = interval.closing_time + if int_open[0] == week_day: + res.append( + ( + dtm.datetime( + year=date.year, + month=date.month, + day=date.day, + hour=int_open[1], + minute=int_open[2], + tzinfo=verify_timezone(time_zone), + ), + dtm.datetime( + year=date.year, + month=date.month, + day=date.day, + hour=int_close[1], + minute=int_close[2], + tzinfo=verify_timezone(time_zone), + ), + ) + ) + + return tuple(res) + + def is_open(self, dt: dtm.datetime) -> bool: + """Check if the business is open at the specified datetime. + + .. versionadded:: NEXT.VERSION + + Args: + dt (:obj:`datetime.datetime`): The datetime to check. + If timezone-aware, the check will be performed in that timezone. + If timezone-naive, the check will be performed in the + timezone specified by :attr:`time_zone_name`. + Returns: + :obj:`bool`: True if the business is open at the specified time, False otherwise. + """ + + if dt.tzinfo is None: + dt_utc = dt + else: + dt_utc = dt.astimezone(verify_timezone(ZoneInfo(self.time_zone_name))) + + weekday = dt_utc.weekday() + minute_of_week = weekday * 1440 + dt_utc.hour * 60 + dt_utc.minute + + for interval in self.opening_hours: + if interval.opening_minute <= minute_of_week < interval.closing_minute: + return True + + return False + @classmethod def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessOpeningHours": """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/src/telegram/_passport/encryptedpassportelement.py b/src/telegram/_passport/encryptedpassportelement.py index c231c51640b..65f88e7a69b 100644 --- a/src/telegram/_passport/encryptedpassportelement.py +++ b/src/telegram/_passport/encryptedpassportelement.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# flake8: noqa: E501 # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2025 # Leandro Toledo de Souza diff --git a/src/telegram/_payment/stars/staramount.py b/src/telegram/_payment/stars/staramount.py index a8d61b2a118..c78a4aa9aba 100644 --- a/src/telegram/_payment/stars/staramount.py +++ b/src/telegram/_payment/stars/staramount.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains an object that represents a Telegram StarAmount.""" diff --git a/src/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py index 8e6ebdda1b4..bd941b2a5b6 100644 --- a/src/telegram/_utils/datetime.py +++ b/src/telegram/_utils/datetime.py @@ -30,8 +30,11 @@ import contextlib import datetime as dtm import time +import zoneinfo from typing import TYPE_CHECKING, Optional, Union +from telegram.error import TelegramError + if TYPE_CHECKING: from telegram import Bot @@ -224,3 +227,25 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float: if dt_obj.tzinfo is None: dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc) return dt_obj.timestamp() + + +def verify_timezone( + tz: Optional[Union[dtm.tzinfo, zoneinfo.ZoneInfo]], +) -> Optional[Union[zoneinfo.ZoneInfo, dtm.tzinfo]]: + """ + Verifies that the given timezone is a valid timezone. + """ + + if tz is None: + return None + if isinstance(tz, (dtm.tzinfo, zoneinfo.ZoneInfo)): + return tz + + try: + return zoneinfo.ZoneInfo(tz) + except zoneinfo.ZoneInfoNotFoundError as err: + raise TelegramError( + f"No time zone found with key {tz}. " + f"Make sure to use a valid time zone name and " + f"correct install tzdata (https://pypi.org/project/tzdata/)" + ) from err diff --git a/src/telegram/_utils/enum.py b/src/telegram/_utils/enum.py index 58362870f7e..52e21eb46f5 100644 --- a/src/telegram/_utils/enum.py +++ b/src/telegram/_utils/enum.py @@ -60,7 +60,7 @@ def __str__(self) -> str: # Apply the __repr__ modification and __str__ fix to IntEnum -class IntEnum(_enum.IntEnum): # pylint: disable=invalid-slots +class IntEnum(_enum.IntEnum): """Helper class for int enums where ``str(member)`` prints the value, but ``repr(member)`` gives ``EnumName.MEMBER_NAME``. """ diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py index dfcaca67587..d1b9aaea91d 100644 --- a/tests/_utils/test_datetime.py +++ b/tests/_utils/test_datetime.py @@ -23,6 +23,8 @@ import pytest from telegram._utils import datetime as tg_dtm +from telegram._utils.datetime import verify_timezone +from telegram.error import TelegramError from telegram.ext import Defaults # sample time specification values categorised into absolute / delta / time-of-day @@ -168,7 +170,7 @@ def test_to_timestamp(self): assert tg_dtm.to_timestamp(i) == int(tg_dtm.to_float_timestamp(i)), f"Failed for {i}" def test_to_timestamp_none(self): - # this 'convenience' behaviour has been left left for backwards compatibility + # this 'convenience' behaviour has been left for backwards compatibility assert tg_dtm.to_timestamp(None) is None def test_from_timestamp_none(self): @@ -192,3 +194,53 @@ def test_extract_tzinfo_from_defaults(self, tz_bot, bot, raw_bot): assert tg_dtm.extract_tzinfo_from_defaults(tz_bot) == tz_bot.defaults.tzinfo assert tg_dtm.extract_tzinfo_from_defaults(bot) is None assert tg_dtm.extract_tzinfo_from_defaults(raw_bot) is None + + def test_with_zoneinfo_object(self): + """Test with a valid zoneinfo.ZoneInfo object.""" + tz = zoneinfo.ZoneInfo("Europe/Paris") + result = verify_timezone(tz) + assert result == tz + + def test_with_datetime_tzinfo(self): + """Test with a datetime.tzinfo object.""" + + class CustomTZ(dtm.tzinfo): + def utcoffset(self, dt): + return dtm.timedelta(hours=2) + + def dst(self, dt): + return dtm.timedelta(0) + + tz = CustomTZ() + result = verify_timezone(tz) + assert result == tz + + def test_with_valid_timezone_string(self): + """Test with a valid timezone string.""" + tz = "Asia/Tokyo" + result = verify_timezone(tz) + assert isinstance(result, zoneinfo.ZoneInfo) + assert str(result) == "Asia/Tokyo" + + def test_with_none(self): + """Test with None input.""" + assert verify_timezone(None) is None + + def test_with_invalid_timezone_string(self): + """Test with an invalid timezone string.""" + with pytest.raises(TelegramError, match="No time zone found"): + verify_timezone("Invalid/Timezone") + + def test_with_empty_string(self): + """Test with empty string input.""" + with pytest.raises(TelegramError, match="No time zone found"): + verify_timezone("") + + def test_with_non_timezone_object(self): + """Test with an object that isn't a timezone.""" + with pytest.raises(TelegramError, match="No time zone found"): + verify_timezone(123) # integer + with pytest.raises(TelegramError, match="No time zone found"): + verify_timezone({"key": "value"}) # dict + with pytest.raises(TelegramError, match="No time zone found"): + verify_timezone([]) # empty list diff --git a/tests/test_business_classes.py b/tests/test_business_classes.py index aabf60064c6..afb8f60b765 100644 --- a/tests/test_business_classes.py +++ b/tests/test_business_classes.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import datetime as dtm +from zoneinfo import ZoneInfo import pytest @@ -589,3 +590,179 @@ def test_equality(self): assert boh1 != boh3 assert hash(boh1) != hash(boh3) + + +class TestBusinessOpeningHoursGetOpeningHoursForDayWithoutRequest: + @pytest.fixture + def sample_opening_hours(self): + # Monday 8am-8:30pm (480-1230) + # Tuesday 24 hours (1440-2879) + # Sunday 12am-11:58pm (8640-10078) + intervals = [ + BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm + BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours + BusinessOpeningHoursInterval(8640, 10078), # Sunday 12am-11:58pm + ] + return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals) + + def test_monday_opening_hours(self, sample_opening_hours): + # Test for Monday + test_date = dtm.date(2023, 11, 6) # Monday + time_zone = ZoneInfo("UTC") + result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) + + expected = ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ) + + assert result == expected + + def test_tuesday_24_hours(self, sample_opening_hours): + # Test for Tuesday (24 hours) + test_date = dtm.date(2023, 11, 7) # Tuesday + time_zone = ZoneInfo("UTC") + result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) + + expected = ( + dtm.datetime(2023, 11, 7, 0, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 7, 23, 59, tzinfo=time_zone), + ) + + assert result == expected + + def test_sunday_opening_hours(self, sample_opening_hours): + # Test for Sunday + test_date = dtm.date(2023, 11, 12) # Sunday + time_zone = ZoneInfo("UTC") + result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) + + expected = ( + dtm.datetime(2023, 11, 12, 0, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 12, 23, 58, tzinfo=time_zone), + ) + + assert result == expected + + def test_day_with_no_opening_hours(self, sample_opening_hours): + # Test for Wednesday (no opening hours defined) + test_date = dtm.date(2023, 11, 8) # Wednesday + time_zone = ZoneInfo("UTC") + result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) + + assert result == () + + def test_multiple_intervals_same_day(self): + # Test with multiple intervals on the same day + intervals = [ + BusinessOpeningHoursInterval(480, 720), # Monday 8am-12pm + BusinessOpeningHoursInterval(900, 1230), # Monday 3pm-8:30pm + ] + opening_hours = BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals) + + test_date = dtm.date(2023, 11, 6) # Monday + time_zone = ZoneInfo("UTC") + result = opening_hours.get_opening_hours_for_day(test_date, time_zone) + + expected = ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 12, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 15, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ) + + assert result == expected + + def test_timezone_conversion(self, sample_opening_hours): + # Test that timezone is properly applied + test_date = dtm.date(2023, 11, 6) # Monday + time_zone = ZoneInfo("America/New_York") + result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) + + expected = ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ) + + assert result == expected + assert result[0].tzinfo == time_zone + assert result[1].tzinfo == time_zone + + def test_no_timezone_provided(self, sample_opening_hours): + # Test when no timezone is provided + test_date = dtm.date(2023, 11, 6) # Monday + result = sample_opening_hours.get_opening_hours_for_day(test_date) + + expected = ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=None), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=None), + ) + + assert result == expected + + +class TestBusinessOpeningHoursIsOpenWithoutRequest: + @pytest.fixture + def sample_opening_hours(self): + # Monday 8am-8:30pm (480-1230) + # Tuesday 24 hours (1440-2879) + # Sunday 12am-11:58pm (8640-10078) + intervals = [ + BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm UTC + BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours UTC + BusinessOpeningHoursInterval(8640, 10078), # Sunday 12am-11:58pm UTC + ] + return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals) + + def test_is_open_during_business_hours(self, sample_opening_hours): + # Monday 10am UTC (within 8am-8:30pm) + dt = dtm.datetime(2023, 11, 6, 10, 0, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is True + + def test_is_open_at_opening_time(self, sample_opening_hours): + # Monday exactly 8am UTC + dt = dtm.datetime(2023, 11, 6, 8, 0, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is True + + def test_is_closed_at_closing_time(self, sample_opening_hours): + # Monday exactly 8:30pm UTC (closing time is exclusive) + dt = dtm.datetime(2023, 11, 6, 20, 30, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is False + + def test_is_closed_outside_business_hours(self, sample_opening_hours): + # Monday 7am UTC (before opening) + dt = dtm.datetime(2023, 11, 6, 7, 0, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is False + + def test_is_open_24h_day(self, sample_opening_hours): + # Tuesday 3am UTC (24h opening) + dt = dtm.datetime(2023, 11, 7, 3, 0, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is True + + def test_is_closed_on_day_with_no_hours(self, sample_opening_hours): + # Wednesday (no opening hours) + dt = dtm.datetime(2023, 11, 8, 12, 0, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is False + + def test_timezone_conversion(self, sample_opening_hours): + # Monday 10am UTC is 6am EDT (should be closed) + dt = dtm.datetime(2023, 11, 6, 6, 0, tzinfo=ZoneInfo("America/New_York")) + assert sample_opening_hours.is_open(dt) is False + + # Monday 10am EDT is 2pm UTC (should be open) + dt = dtm.datetime(2023, 11, 6, 10, 0, tzinfo=ZoneInfo("America/New_York")) + assert sample_opening_hours.is_open(dt) is True + + def test_naive_datetime_uses_business_timezone(self, sample_opening_hours): + # Naive datetime - should be interpreted as UTC (business timezone) + dt = dtm.datetime(2023, 11, 6, 10, 0) # 10am naive + assert sample_opening_hours.is_open(dt) is True + + def test_boundary_conditions(self, sample_opening_hours): + # Sunday 11:58pm UTC (should be open) + dt = dtm.datetime(2023, 11, 12, 23, 57, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is True + + # Sunday 11:59pm UTC (should be closed) + dt = dtm.datetime(2023, 11, 12, 23, 59, tzinfo=ZoneInfo("UTC")) + assert sample_opening_hours.is_open(dt) is False From e3132e983d09e1e5d40dcf5063252b86b8ef5465 Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:56:17 +0300 Subject: [PATCH 02/14] Update changes/unreleased/48XX._.toml Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- changes/unreleased/48XX._.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/unreleased/48XX._.toml b/changes/unreleased/48XX._.toml index fa083bd43a0..b511be65416 100644 --- a/changes/unreleased/48XX._.toml +++ b/changes/unreleased/48XX._.toml @@ -2,4 +2,4 @@ features = "Added a two methods for BusinessOpeningHours" [[pull_requests]] uid = "4326" author_uid = "Aweryc" -closes_threads = ["4194, 4326"] \ No newline at end of file +closes_threads = ["4194"] \ No newline at end of file From fa4e6cd733f2e974c402aeefef4466fb9c69f537 Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:58:41 +0300 Subject: [PATCH 03/14] Update src/telegram/_utils/datetime.py Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- src/telegram/_utils/datetime.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py index 8ea9850d5cb..a4a64143beb 100644 --- a/src/telegram/_utils/datetime.py +++ b/src/telegram/_utils/datetime.py @@ -34,7 +34,6 @@ import zoneinfo from typing import TYPE_CHECKING, Optional, Union -from telegram.error import TelegramError from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning From 9c183e7bc6d2a7df40aaaba8b0554b126821340e Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:00:12 +0300 Subject: [PATCH 04/14] Update src/telegram/_utils/datetime.py Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- src/telegram/_utils/datetime.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py index a4a64143beb..bbfb943211a 100644 --- a/src/telegram/_utils/datetime.py +++ b/src/telegram/_utils/datetime.py @@ -295,4 +295,3 @@ def get_timedelta_value( if (seconds := value.total_seconds()).is_integer() else seconds # type: ignore[return-value] ) - From 0b836cf0f8a94853fa3afd2d6d58c56c183663bc Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:00:27 +0300 Subject: [PATCH 05/14] Update tests/_utils/test_datetime.py Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- tests/_utils/test_datetime.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py index 7e5706482b3..8be5254cb9e 100644 --- a/tests/_utils/test_datetime.py +++ b/tests/_utils/test_datetime.py @@ -263,4 +263,3 @@ def test_get_timedelta_value(self, PTB_TIMEDELTA, arg, timedelta_result, number_ else: assert result == number_result assert type(result) is type(number_result) - From 5a91e66e36dc1bbeee06221467dbe4cc80762e66 Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:01:48 +0300 Subject: [PATCH 06/14] Rename 48XX._.toml to 4861.HEoGVs2mYXWzqMahi6SEhV.toml --- .../{48XX._.toml => 4861.HEoGVs2mYXWzqMahi6SEhV.toml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename changes/unreleased/{48XX._.toml => 4861.HEoGVs2mYXWzqMahi6SEhV.toml} (81%) diff --git a/changes/unreleased/48XX._.toml b/changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml similarity index 81% rename from changes/unreleased/48XX._.toml rename to changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml index b511be65416..c3ccdcf6061 100644 --- a/changes/unreleased/48XX._.toml +++ b/changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml @@ -2,4 +2,4 @@ features = "Added a two methods for BusinessOpeningHours" [[pull_requests]] uid = "4326" author_uid = "Aweryc" -closes_threads = ["4194"] \ No newline at end of file +closes_threads = ["4194"] From 83cb615f8ba899d01caf94df5c227b65b411e3e3 Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:48:35 +0300 Subject: [PATCH 07/14] Update pyproject.toml --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d70eaa4bda6..181fb0e0ed3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,12 +39,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "black>=25.1.0", "httpx >=0.27,<0.29", - "pre-commit>=4.2.0", - "pytest>=8.3.5", - "pytz>=2025.2", - "ruff==0.11.9", ] [project.urls] From 73def0ae6d805584349a3f1b45304c0340c41ee8 Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:53:47 +0300 Subject: [PATCH 08/14] Revert enum.py --- src/telegram/_utils/enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telegram/_utils/enum.py b/src/telegram/_utils/enum.py index 52e21eb46f5..58362870f7e 100644 --- a/src/telegram/_utils/enum.py +++ b/src/telegram/_utils/enum.py @@ -60,7 +60,7 @@ def __str__(self) -> str: # Apply the __repr__ modification and __str__ fix to IntEnum -class IntEnum(_enum.IntEnum): +class IntEnum(_enum.IntEnum): # pylint: disable=invalid-slots """Helper class for int enums where ``str(member)`` prints the value, but ``repr(member)`` gives ``EnumName.MEMBER_NAME``. """ From 0a70348d9442e9f50881c45c3ee47059b2023d4d Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:56:02 +0300 Subject: [PATCH 09/14] Proper re-raise zoneinfo.ZoneInfoNotFoundError --- src/telegram/_utils/datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py index bd941b2a5b6..fb2f0724ba6 100644 --- a/src/telegram/_utils/datetime.py +++ b/src/telegram/_utils/datetime.py @@ -244,7 +244,7 @@ def verify_timezone( try: return zoneinfo.ZoneInfo(tz) except zoneinfo.ZoneInfoNotFoundError as err: - raise TelegramError( + raise zoneinfo.ZoneInfoNotFoundError( f"No time zone found with key {tz}. " f"Make sure to use a valid time zone name and " f"correct install tzdata (https://pypi.org/project/tzdata/)" From 99634d090ea74b6e3124d4228d18aee9a9fb2c31 Mon Sep 17 00:00:00 2001 From: Aweryc <93672316+Aweryc@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:57:09 +0300 Subject: [PATCH 10/14] Update datetime.py --- src/telegram/_utils/datetime.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py index fb2f0724ba6..57aa683c977 100644 --- a/src/telegram/_utils/datetime.py +++ b/src/telegram/_utils/datetime.py @@ -33,8 +33,6 @@ import zoneinfo from typing import TYPE_CHECKING, Optional, Union -from telegram.error import TelegramError - if TYPE_CHECKING: from telegram import Bot From 86540bfcb244f56e8b98383bee7e8367b6d5cc39 Mon Sep 17 00:00:00 2001 From: Aweryc Date: Thu, 17 Jul 2025 12:01:52 +0300 Subject: [PATCH 11/14] Rename change file .toml --- .../unreleased/{48XX._.toml => 4861.HEoGVs2mYXWzqMahi6SEhV.toml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/unreleased/{48XX._.toml => 4861.HEoGVs2mYXWzqMahi6SEhV.toml} (100%) diff --git a/changes/unreleased/48XX._.toml b/changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml similarity index 100% rename from changes/unreleased/48XX._.toml rename to changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml From 6ff094d7baa8e444828b2f60a027161d1151c607 Mon Sep 17 00:00:00 2001 From: Aweryc Date: Thu, 17 Jul 2025 12:06:09 +0300 Subject: [PATCH 12/14] revert pyproject.toml edit 4861.HEoGVs2mYXWzqMahi6SEhV.toml --- changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml | 2 +- pyproject.toml | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml b/changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml index fa083bd43a0..b511be65416 100644 --- a/changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml +++ b/changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml @@ -2,4 +2,4 @@ features = "Added a two methods for BusinessOpeningHours" [[pull_requests]] uid = "4326" author_uid = "Aweryc" -closes_threads = ["4194, 4326"] \ No newline at end of file +closes_threads = ["4194"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 25d18172d19..66589c25b0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,12 +38,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ - "black>=25.1.0", "httpx >=0.27,<0.29", - "pre-commit>=4.2.0", - "pytest>=8.3.5", - "pytz>=2025.2", - "ruff==0.11.9", ] [project.urls] From c63f1ebedeece4666a8c05a02a79622aad4cdb7c Mon Sep 17 00:00:00 2001 From: Aweryc Date: Fri, 18 Jul 2025 16:19:48 +0300 Subject: [PATCH 13/14] remove TelegramError --- tests/_utils/test_datetime.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py index d1b9aaea91d..039b1c81bad 100644 --- a/tests/_utils/test_datetime.py +++ b/tests/_utils/test_datetime.py @@ -228,7 +228,7 @@ def test_with_none(self): def test_with_invalid_timezone_string(self): """Test with an invalid timezone string.""" - with pytest.raises(TelegramError, match="No time zone found"): + with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"): verify_timezone("Invalid/Timezone") def test_with_empty_string(self): @@ -238,9 +238,9 @@ def test_with_empty_string(self): def test_with_non_timezone_object(self): """Test with an object that isn't a timezone.""" - with pytest.raises(TelegramError, match="No time zone found"): + with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"): verify_timezone(123) # integer - with pytest.raises(TelegramError, match="No time zone found"): + with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"): verify_timezone({"key": "value"}) # dict - with pytest.raises(TelegramError, match="No time zone found"): + with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"): verify_timezone([]) # empty list From a40a5739959efb24b7d0d20b5ee9e97848782417 Mon Sep 17 00:00:00 2001 From: Aweryc Date: Sat, 19 Jul 2025 21:12:06 +0300 Subject: [PATCH 14/14] Fix a code and tests --- src/telegram/_business.py | 4 +-- src/telegram/_utils/datetime.py | 10 ++++--- tests/_utils/test_datetime.py | 6 ++--- tests/test_business_classes.py | 48 +++++++++++++++++++++------------ 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/telegram/_business.py b/src/telegram/_business.py index 7846955af7e..e235460a813 100644 --- a/src/telegram/_business.py +++ b/src/telegram/_business.py @@ -658,7 +658,7 @@ def get_opening_hours_for_day( day=date.day, hour=int_open[1], minute=int_open[2], - tzinfo=verify_timezone(time_zone), + tzinfo=verify_timezone(time_zone) if time_zone else None, ), dtm.datetime( year=date.year, @@ -666,7 +666,7 @@ def get_opening_hours_for_day( day=date.day, hour=int_close[1], minute=int_close[2], - tzinfo=verify_timezone(time_zone), + tzinfo=verify_timezone(time_zone) if time_zone else None, ), ) ) diff --git a/src/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py index 57aa683c977..bfc9bcf0605 100644 --- a/src/telegram/_utils/datetime.py +++ b/src/telegram/_utils/datetime.py @@ -228,19 +228,23 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float: def verify_timezone( - tz: Optional[Union[dtm.tzinfo, zoneinfo.ZoneInfo]], + tz: Union[dtm.tzinfo, zoneinfo.ZoneInfo], ) -> Optional[Union[zoneinfo.ZoneInfo, dtm.tzinfo]]: """ Verifies that the given timezone is a valid timezone. """ - if tz is None: - return None if isinstance(tz, (dtm.tzinfo, zoneinfo.ZoneInfo)): return tz try: return zoneinfo.ZoneInfo(tz) + except (TypeError, ValueError) as e: + raise zoneinfo.ZoneInfoNotFoundError( + f"No time zone found with key {tz}. " + f"Make sure to use a valid time zone name and " + f"correct install tzdata (https://pypi.org/project/tzdata/)" + ) from e except zoneinfo.ZoneInfoNotFoundError as err: raise zoneinfo.ZoneInfoNotFoundError( f"No time zone found with key {tz}. " diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py index 039b1c81bad..9cb0309e5c3 100644 --- a/tests/_utils/test_datetime.py +++ b/tests/_utils/test_datetime.py @@ -24,7 +24,6 @@ from telegram._utils import datetime as tg_dtm from telegram._utils.datetime import verify_timezone -from telegram.error import TelegramError from telegram.ext import Defaults # sample time specification values categorised into absolute / delta / time-of-day @@ -224,7 +223,8 @@ def test_with_valid_timezone_string(self): def test_with_none(self): """Test with None input.""" - assert verify_timezone(None) is None + with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"): + verify_timezone(None) def test_with_invalid_timezone_string(self): """Test with an invalid timezone string.""" @@ -233,7 +233,7 @@ def test_with_invalid_timezone_string(self): def test_with_empty_string(self): """Test with empty string input.""" - with pytest.raises(TelegramError, match="No time zone found"): + with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"): verify_timezone("") def test_with_non_timezone_object(self): diff --git a/tests/test_business_classes.py b/tests/test_business_classes.py index afb8f60b765..02266d05919 100644 --- a/tests/test_business_classes.py +++ b/tests/test_business_classes.py @@ -612,8 +612,10 @@ def test_monday_opening_hours(self, sample_opening_hours): result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( - dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), - dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ), ) assert result == expected @@ -625,8 +627,10 @@ def test_tuesday_24_hours(self, sample_opening_hours): result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( - dtm.datetime(2023, 11, 7, 0, 0, tzinfo=time_zone), - dtm.datetime(2023, 11, 7, 23, 59, tzinfo=time_zone), + ( + dtm.datetime(2023, 11, 7, 0, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 7, 23, 59, tzinfo=time_zone), + ), ) assert result == expected @@ -638,8 +642,10 @@ def test_sunday_opening_hours(self, sample_opening_hours): result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( - dtm.datetime(2023, 11, 12, 0, 0, tzinfo=time_zone), - dtm.datetime(2023, 11, 12, 23, 58, tzinfo=time_zone), + ( + dtm.datetime(2023, 11, 12, 0, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 12, 23, 58, tzinfo=time_zone), + ), ) assert result == expected @@ -665,10 +671,14 @@ def test_multiple_intervals_same_day(self): result = opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( - dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), - dtm.datetime(2023, 11, 6, 12, 0, tzinfo=time_zone), - dtm.datetime(2023, 11, 6, 15, 0, tzinfo=time_zone), - dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 12, 0, tzinfo=time_zone), + ), + ( + dtm.datetime(2023, 11, 6, 15, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ), ) assert result == expected @@ -680,13 +690,15 @@ def test_timezone_conversion(self, sample_opening_hours): result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( - dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), - dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), + ), ) assert result == expected - assert result[0].tzinfo == time_zone - assert result[1].tzinfo == time_zone + assert result[0][0].tzinfo == time_zone + assert result[0][1].tzinfo == time_zone def test_no_timezone_provided(self, sample_opening_hours): # Test when no timezone is provided @@ -694,8 +706,10 @@ def test_no_timezone_provided(self, sample_opening_hours): result = sample_opening_hours.get_opening_hours_for_day(test_date) expected = ( - dtm.datetime(2023, 11, 6, 8, 0, tzinfo=None), - dtm.datetime(2023, 11, 6, 20, 30, tzinfo=None), + ( + dtm.datetime(2023, 11, 6, 8, 0, tzinfo=None), + dtm.datetime(2023, 11, 6, 20, 30, tzinfo=None), + ), ) assert result == expected @@ -746,7 +760,7 @@ def test_is_closed_on_day_with_no_hours(self, sample_opening_hours): def test_timezone_conversion(self, sample_opening_hours): # Monday 10am UTC is 6am EDT (should be closed) - dt = dtm.datetime(2023, 11, 6, 6, 0, tzinfo=ZoneInfo("America/New_York")) + dt = dtm.datetime(2023, 11, 6, 2, 30, tzinfo=ZoneInfo("America/New_York")) assert sample_opening_hours.is_open(dt) is False # Monday 10am EDT is 2pm UTC (should be open) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy