Skip to content

Convenience Functionality for BusinessOpeningHours #4861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changes/unreleased/4861.HEoGVs2mYXWzqMahi6SEhV.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
features = "Added a two methods for BusinessOpeningHours"
[[pull_requests]]
uid = "4326"
author_uid = "Aweryc"
closes_threads = ["4194"]
2 changes: 1 addition & 1 deletion src/telegram/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
89 changes: 87 additions & 2 deletions src/telegram/_business.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
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
from telegram._files.sticker import Sticker
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

if TYPE_CHECKING:
Expand Down Expand Up @@ -448,7 +449,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:
Expand Down Expand Up @@ -570,6 +571,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) if time_zone else None,
),
dtm.datetime(
year=date.year,
month=date.month,
day=date.day,
hour=int_close[1],
minute=int_close[2],
tzinfo=verify_timezone(time_zone) if time_zone else None,
),
)
)

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`."""
Expand Down
29 changes: 27 additions & 2 deletions src/telegram/_utils/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@
import datetime as dtm
import os
import time
import zoneinfo
from typing import TYPE_CHECKING, Optional, Union

from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning

if TYPE_CHECKING:
from telegram import Bot
Expand Down Expand Up @@ -230,6 +229,32 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
return dt_obj.timestamp()


def verify_timezone(
tz: Union[dtm.tzinfo, zoneinfo.ZoneInfo],
) -> Optional[Union[zoneinfo.ZoneInfo, dtm.tzinfo]]:
"""
Verifies that the given timezone is a valid timezone.
"""

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}. "
f"Make sure to use a valid time zone name and "
f"correct install tzdata (https://pypi.org/project/tzdata/)"
) from err


def get_timedelta_value(
value: Optional[dtm.timedelta], attribute: str
) -> Optional[Union[int, dtm.timedelta]]:
Expand Down
56 changes: 55 additions & 1 deletion tests/_utils/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import pytest

from telegram._utils import datetime as tg_dtm
from telegram._utils.datetime import verify_timezone
from telegram.ext import Defaults

# sample time specification values categorised into absolute / delta / time-of-day
Expand Down Expand Up @@ -168,7 +169,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):
Expand All @@ -193,6 +194,59 @@ def test_extract_tzinfo_from_defaults(self, tz_bot, bot, raw_bot):
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."""
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."""
with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"):
verify_timezone("Invalid/Timezone")

def test_with_empty_string(self):
"""Test with empty string input."""
with pytest.raises(zoneinfo.ZoneInfoNotFoundError, 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(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"):
verify_timezone(123) # integer
with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"):
verify_timezone({"key": "value"}) # dict
with pytest.raises(zoneinfo.ZoneInfoNotFoundError, match="No time zone found"):
verify_timezone([]) # empty list


@pytest.mark.parametrize(
("arg", "timedelta_result", "number_result"),
[
Expand Down
Loading
Loading
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