Repository: agronholm/apscheduler

Issue:
Infinite loop in CalendarIntervalTrigger with UTC timezone
### Things to check first

- [X] I have checked that my issue does not already have a solution in the [FAQ](https://apscheduler.readthedocs.io/en/master/faq.html)

- [X] I have searched the existing issues and didn't find my bug already reported there

- [X] I have checked that my bug is still present in the latest release


### Version

4.0.0a4

### What happened?

Time with UTC timezone will be always not equal to other time.

```python
from datetime import time
from zoneinfo import ZoneInfo

utctime = time(0, 0, tzinfo=ZoneInfo('UTC'))

a = time(0, 0)
b = time(0, 0, tzinfo=ZoneInfo("Asia/Shanghai"))
c = time(0, 0, tzinfo=ZoneInfo("Asia/Tokyo"))

print(a == utctime)  # False
print(a == b == c)   # True
```

So the condition here will always be `False`

https://github.com/agronholm/apscheduler/blob/3e42a1bd73da947634ea94724e5221a43ad7ce0c/src/apscheduler/triggers/calendarinterval.py#L121

It should be call `.timetz()` to get the time part.

### How can we reproduce the bug?

```python
from apscheduler.triggers.calendarinterval import CalendarIntervalTrigger

trigger = CalendarIntervalTrigger(days=1, timezone="UTC")

print(trigger.next())

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\.venv\Lib\site-packages\apscheduler\triggers\calendarinterval.py", line 109, in next
    next_time = datetime.fromtimestamp(timestamp, self.timezone)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 22] Invalid argument
```

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/67070637/badge)](https://coveralls.io/builds/67070637)

coverage: 92.596%. remained the same
when pulling **efd001320235f5bfa04ad7412b71d9792d0a613e on unights:patch-fix-897**
into **3e42a1bd73da947634ea94724e5221a43ad7ce0c on agronholm:master**.

agronholm: Ok, looks good. If you could also add a changelog note (`docs/versionhistory.rst`), then I'll merge.

Patch:
diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index 4d164a4..22bf8ef 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -20,6 +20,7 @@ APScheduler, see the :doc:`migration section <migration>`.
 - Fixed JSON serialization of triggers that had been used at least once
 - Fixed dialect name checks in the SQLAlchemy job store
 - Fixed JSON and CBOR serializers unable to serialize enums
+- Fixed infinite loop in CalendarIntervalTrigger with UTC timezone (PR by unights)
 
 **4.0.0a4**
 
diff --git a/src/apscheduler/triggers/calendarinterval.py b/src/apscheduler/triggers/calendarinterval.py
index ed38eb6..cec164e 100644
--- a/src/apscheduler/triggers/calendarinterval.py
+++ b/src/apscheduler/triggers/calendarinterval.py
@@ -118,7 +118,7 @@ class CalendarIntervalTrigger(Trigger):
             next_time = datetime.fromtimestamp(timestamp, self.timezone)
 
             # Check if the time is off due to normalization and a forward DST shift
-            if next_time.time() != self._time:
+            if next_time.timetz() != self._time:
                 previous_date = next_time.date()
             else:
                 self._last_fire_date = next_date


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 83263c6..3acc95f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -29,6 +29,11 @@ def timezone() -> ZoneInfo:
     return ZoneInfo("Europe/Berlin")
 
 
+@pytest.fixture(scope="session")
+def utc_timezone() -> ZoneInfo:
+    return ZoneInfo("UTC")
+
+
 @pytest.fixture(
     params=[
         pytest.param(PickleSerializer, id="pickle"),
diff --git a/tests/triggers/test_calendarinterval.py b/tests/triggers/test_calendarinterval.py
index bd5a4e7..21825f4 100644
--- a/tests/triggers/test_calendarinterval.py
+++ b/tests/triggers/test_calendarinterval.py
@@ -100,3 +100,10 @@ def test_repr(timezone, serializer):
         "time='03:00:08', start_date='2016-03-05', end_date='2020-12-25', "
         "timezone='Europe/Berlin')"
     )
+
+
+def test_utc_timezone(utc_timezone):
+    trigger = CalendarIntervalTrigger(
+        days=1, hour=1, start_date=date(2016, 3, 31), timezone=utc_timezone
+    )
+    assert trigger.next() == datetime(2016, 3, 31, 1, tzinfo=utc_timezone)