Repository: python-telegram-bot/python-telegram-bot

Issue:
Adjust weekday behavior of `JobQueue.run_daily`
# Current situation

Currently, 0 correspondigs to monday and 6 to sunday. This does not match the the behavior of [cron](https://en.wikipedia.org/wiki/Cron), which is the de-facto standard for many scheduling related things in IT. In cron, we have 0=sunday, 6=saturday. Moreover, the APScheduler library, which we currently use as backend for the `JobQueue` will also adapt the sun-sat approach in the next major release 4.0 - cf https://github.com/agronholm/apscheduler/issues/495

# Proposed change

We should make the transition to a sun-sat based numbering scheme. This has the advantages of
* complying with cron
* having one less thing to worry about for users when we switch to APS 4.0 at some point in the future

An implementation could for example use a mapping like [here](https://github.com/agronholm/apscheduler/issues/286#issuecomment-449273964) to pass the correct values to APS.
Accepting the weekdays as `mon, tue` etc instead of integers could also be added as additional support, but is not mandatory IMO.

# A PR for this issue should

* Implement a conversion of the input such that we accept 0=sun, 6=sat, but pass the correct values to APS
* add a `.. versionchanged:: 20.0` directive to the docstring of `run_daily` and the `days` parameter to explain the changed behavior
* Add a unit test to `test_jobqueue.py` that ensures that this works as expected
* Make `JobQueue.run_daily` issue a warning that tells users about the changed behavior and recommends them to double check that they pass the correct days. This warning should be issued via the `warn` function from `telegram._utils.warnings`. The unit test should ensure that this warning is issued correctly. See [here](https://github.com/python-telegram-bot/python-telegram-bot/blob/298c5fab3b8655ebae4416341335fd9c05f2ce5a/tests/test_telegramobject.py#L103-L114) for an example on how warnings can be checked in unit tests.

# Additional Info

We have a [contribution guide](https://python-telegram-bot.readthedocs.io/en/v20.0a0/contributing.html)


Discussion:
Hi, 

So does the proposed change above mean if I passed in `days=[0,1,2,3]`  in `JobQueue.run_daily` it should resolve to `"sun,mon,tue,wed"` as being passed below?

https://github.com/python-telegram-bot/python-telegram-bot/blob/1f7efe4519e1ee82da684d390b7b108e61067877/telegram/ext/_jobqueue.py#L461
Yes, that would be the idea.

Resolving patch:
diff --git a/telegram/ext/_jobqueue.py b/telegram/ext/_jobqueue.py
--- a/telegram/ext/_jobqueue.py
+++ b/telegram/ext/_jobqueue.py
@@ -28,6 +28,7 @@
 from apscheduler.schedulers.asyncio import AsyncIOScheduler
 
 from telegram._utils.types import JSONDict
+from telegram._utils.warnings import warn
 from telegram.ext._extbot import ExtBot
 from telegram.ext._utils.types import JobCallback
 
@@ -50,6 +51,7 @@ class JobQueue:
     """
 
     __slots__ = ("_application", "scheduler", "_executor")
+    _CRON_MAPPING = ("sun", "mon", "tue", "wed", "thu", "fri", "sat")
 
     def __init__(self) -> None:
         self._application: "Optional[weakref.ReferenceType[Application]]" = None
@@ -419,7 +421,11 @@ async def callback(context: CallbackContext)
                 (:obj:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will
                 be used.
             days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
-                run (where ``0-6`` correspond to monday - sunday). Defaults to ``EVERY_DAY``
+                run (where ``0-6`` correspond to sunday - saturday). By default, the job will run
+                every day.
+
+                .. versionchanged:: 20.0
+                    Changed day of the week mapping of 0-6 from monday-sunday to sunday-saturday.
             data (:obj:`object`, optional): Additional data needed for the callback function.
                 Can be accessed through :attr:`Job.data` in the callback. Defaults to
                 :obj:`None`.
@@ -447,6 +453,12 @@ async def callback(context: CallbackContext)
             queue.
 
         """
+        # TODO: After v20.0, we should remove the this warning.
+        warn(
+            "Prior to v20.0 the `days` parameter was not aligned to that of cron's weekday scheme."
+            "We recommend double checking if the passed value is correct.",
+            stacklevel=2,
+        )
         if not job_kwargs:
             job_kwargs = {}
 
@@ -458,7 +470,7 @@ async def callback(context: CallbackContext)
             name=name,
             args=(self.application,),
             trigger="cron",
-            day_of_week=",".join([str(d) for d in days]),
+            day_of_week=",".join([self._CRON_MAPPING[d] for d in days]),
             hour=time.hour,
             minute=time.minute,
             second=time.second,


Test patch:
diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py
--- a/tests/test_jobqueue.py
+++ b/tests/test_jobqueue.py
@@ -303,7 +303,11 @@ async def test_time_unit_dt_time_tomorrow(self, job_queue):
         scheduled_time = job_queue.jobs()[0].next_t.timestamp()
         assert scheduled_time == pytest.approx(expected_time)
 
-    async def test_run_daily(self, job_queue):
+    async def test_run_daily(self, job_queue, recwarn):
+        expected_warning = (
+            "Prior to v20.0 the `days` parameter was not aligned to that of cron's weekday scheme."
+            "We recommend double checking if the passed value is correct."
+        )
         delta, now = 1, dtm.datetime.now(pytz.utc)
         time_of_day = (now + dtm.timedelta(seconds=delta)).time()
         expected_reschedule_time = (now + dtm.timedelta(seconds=delta, days=1)).timestamp()
@@ -313,6 +317,30 @@ async def test_run_daily(self, job_queue):
         assert self.result == 1
         scheduled_time = job_queue.jobs()[0].next_t.timestamp()
         assert scheduled_time == pytest.approx(expected_reschedule_time)
+        assert len(recwarn) == 1
+        assert str(recwarn[0].message) == expected_warning
+        assert recwarn[0].filename == __file__, "wrong stacklevel"
+
+    @pytest.mark.parametrize("weekday", (0, 1, 2, 3, 4, 5, 6))
+    async def test_run_daily_days_of_week(self, job_queue, recwarn, weekday):
+        expected_warning = (
+            "Prior to v20.0 the `days` parameter was not aligned to that of cron's weekday scheme."
+            "We recommend double checking if the passed value is correct."
+        )
+        delta, now = 1, dtm.datetime.now(pytz.utc)
+        time_of_day = (now + dtm.timedelta(seconds=delta)).time()
+        # offset in days until next weekday
+        offset = (weekday + 6 - now.weekday()) % 7
+        offset = offset if offset > 0 else 7
+        expected_reschedule_time = (now + dtm.timedelta(seconds=delta, days=offset)).timestamp()
+
+        job_queue.run_daily(self.job_run_once, time_of_day, days=[weekday])
+        await asyncio.sleep(delta + 0.1)
+        scheduled_time = job_queue.jobs()[0].next_t.timestamp()
+        assert scheduled_time == pytest.approx(expected_reschedule_time)
+        assert len(recwarn) == 1
+        assert str(recwarn[0].message) == expected_warning
+        assert recwarn[0].filename == __file__, "wrong stacklevel"
 
     async def test_run_monthly(self, job_queue, timezone):
         delta, now = 1, dtm.datetime.now(timezone)