This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch v2-8-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 18cbdf6364a7a8c19d85c031f06d8c4059e44ff2
Author: Andrey Anshin <[email protected]>
AuthorDate: Fri Jan 12 14:24:12 2024 +0400

    Add support of Pendulum 3 (#36281)
    
    * Add support of Pendulum 3
    
    * Add backcompat to pendulum 2
    
    * Update airflow/serialization/serialized_objects.py
    
    Co-authored-by: Tzu-ping Chung <[email protected]>
    
    * Add newsfragments
    
    ---------
    
    Co-authored-by: Tzu-ping Chung <[email protected]>
    (cherry picked from commit 2ffa6e4c4c9dc129daa54491d5af8f535cd0d479)
---
 .github/workflows/ci.yml                           |  92 ++++++++++++-
 Dockerfile.ci                                      |  13 ++
 airflow/models/dag.py                              |   6 +-
 .../cncf/kubernetes/pod_launcher_deprecated.py     |   6 +-
 airflow/serialization/serialized_objects.py        |   9 +-
 airflow/serialization/serializers/datetime.py      |  14 +-
 airflow/serialization/serializers/timezone.py      |   7 +-
 airflow/settings.py                                |  11 +-
 airflow/timetables/_cron.py                        |   9 +-
 airflow/timetables/trigger.py                      |  18 ++-
 airflow/utils/sqlalchemy.py                        |   5 +-
 airflow/utils/timezone.py                          |  50 +++++--
 .../src/airflow_breeze/commands/common_options.py  |   6 +
 .../airflow_breeze/commands/developer_commands.py  |   4 +
 .../commands/developer_commands_config.py          |   1 +
 .../airflow_breeze/commands/testing_commands.py    |   6 +
 .../commands/testing_commands_config.py            |   3 +
 .../src/airflow_breeze/params/shell_params.py      |   2 +
 images/breeze/output_shell.svg                     |  44 +++---
 images/breeze/output_shell.txt                     |   2 +-
 images/breeze/output_testing_db-tests.svg          |  26 ++--
 images/breeze/output_testing_db-tests.txt          |   2 +-
 images/breeze/output_testing_non-db-tests.svg      |  26 ++--
 images/breeze/output_testing_non-db-tests.txt      |   2 +-
 images/breeze/output_testing_tests.svg             |  26 ++--
 images/breeze/output_testing_tests.txt             |   2 +-
 kubernetes_tests/test_kubernetes_pod_operator.py   |   5 +-
 newsfragments/36281.significant.rst                |   4 +
 pyproject.toml                                     |   4 +-
 scripts/ci/docker-compose/devcontainer.env         |   1 +
 scripts/docker/entrypoint_ci.sh                    |  14 ++
 tests/api_connexion/endpoints/test_dag_endpoint.py |  12 +-
 tests/api_connexion/schemas/test_dag_schema.py     |   5 +-
 tests/cli/commands/test_dag_command.py             |  13 +-
 tests/models/test_dag.py                           |   9 +-
 tests/providers/openlineage/plugins/test_utils.py  |   6 +-
 tests/sensors/test_time_sensor.py                  |  19 ++-
 .../serialization/serializers/test_serializers.py  | 152 +++++++++++++++++++++
 tests/serialization/test_serialized_objects.py     |   4 +-
 tests/triggers/test_temporal.py                    |   6 +-
 tests/utils/test_timezone.py                       |  47 ++++++-
 41 files changed, 540 insertions(+), 153 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6ab4213153..922efd9854 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1169,11 +1169,61 @@ jobs:
           breeze testing db-tests
           --parallel-test-types 
"${{needs.build-info.outputs.parallel-test-types-list-as-string}}"
       - name: >
-          Post Tests success: 
${{needs.build-info.outputs.default-python-version}}:Boto"
+          Post Tests success: 
${{needs.build-info.outputs.default-python-version}}:MinSQLAlchemy"
         uses: ./.github/actions/post_tests_success
         if: success()
       - name: >
-          Post Tests failure: 
${{needs.build-info.outputs.default-python-version}}:Boto"
+          Post Tests failure: 
${{needs.build-info.outputs.default-python-version}}:MinSQLAlchemy"
+        uses: ./.github/actions/post_tests_failure
+        if: failure()
+
+  tests-postgres-pendulum-2:
+    timeout-minutes: 130
+    name: >
+      DB:Postgres${{needs.build-info.outputs.default-postgres-version}},
+      Pendulum2,Py${{needs.build-info.outputs.default-python-version}}:
+      ${{needs.build-info.outputs.parallel-test-types-list-as-string}}
+    runs-on: ${{fromJSON(needs.build-info.outputs.runs-on)}}
+    needs: [build-info, wait-for-ci-images]
+    env:
+      RUNS_ON: "${{needs.build-info.outputs.runs-on}}"
+      PARALLEL_TEST_TYPES: 
"${{needs.build-info.outputs.parallel-test-types-list-as-string}}"
+      PR_LABELS: "${{needs.build-info.outputs.pull-request-labels}}"
+      FULL_TESTS_NEEDED: "${{needs.build-info.outputs.full-tests-needed}}"
+      DEBUG_RESOURCES: "${{needs.build-info.outputs.debug-resources}}"
+      BACKEND: "postgres"
+      ENABLE_COVERAGE: "${{needs.build-info.outputs.run-coverage}}"
+      PYTHON_MAJOR_MINOR_VERSION: 
"${{needs.build-info.outputs.default-python-version}}"
+      PYTHON_VERSION: "${needs.build-info.outputs.default-python-version}}"
+      POSTGRES_VERSION: 
"${{needs.build-info.outputs.default-postgres-version}}"
+      BACKEND_VERSION: "${{needs.build-info.outputs.default-postgres-version}}"
+      DOWNGRADE_PENDULUM: "true"
+      JOB_ID: >
+        
postgres-pendulum-2-${{needs.build-info.outputs.default-python-version}}-
+        ${{needs.build-info.outputs.default-postgres-version}}
+    if: needs.build-info.outputs.run-tests == 'true'
+    steps:
+      - name: Cleanup repo
+        shell: bash
+        run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm 
-rf /workspace/*"
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: false
+      - name: >
+          Prepare breeze & CI image: 
${{needs.build-info.outputs.default-python-version}}:${{env.IMAGE_TAG}}
+        uses: ./.github/actions/prepare_breeze_and_image
+      - name: >
+          Tests: 
${{matrix.python-version}}:${{needs.build-info.outputs.parallel-test-types-list-as-string}}
+        run: >
+          breeze testing db-tests
+          --parallel-test-types 
"${{needs.build-info.outputs.parallel-test-types-list-as-string}}"
+      - name: >
+          Post Tests success: 
${{needs.build-info.outputs.default-python-version}}:Pendulum2"
+        uses: ./.github/actions/post_tests_success
+        if: success()
+      - name: >
+          Post Tests failure: 
${{needs.build-info.outputs.default-python-version}}:Pendulum2"
         uses: ./.github/actions/post_tests_failure
         if: failure()
 
@@ -1616,6 +1666,44 @@ jobs:
         uses: ./.github/actions/post_tests_failure
         if: failure()
 
+  tests-no-db-pendulum-2:
+    timeout-minutes: 60
+    name: >
+      Non-DB: Pendulum2, 
Py${{needs.build-info.outputs.default-python-version}}:
+      ${{needs.build-info.outputs.parallel-test-types-list-as-string}}
+    runs-on: ${{fromJSON(needs.build-info.outputs.runs-on)}}
+    needs: [build-info, wait-for-ci-images]
+    env:
+      RUNS_ON: "${{needs.build-info.outputs.runs-on}}"
+      PR_LABELS: "${{needs.build-info.outputs.pull-request-labels}}"
+      PYTHON_MAJOR_MINOR_VERSION: 
"${{needs.build-info.outputs.default-python-version}}"
+      DEBUG_RESOURCES: "${{needs.build-info.outputs.debug-resources}}"
+      JOB_ID: 
"quarantined-${{needs.build-info.outputs.default-python-version}}"
+      ENABLE_COVERAGE: "${{needs.build-info.outputs.run-coverage}}"
+      DOWNGRADE_PENDULUM: "true"
+    if: needs.build-info.outputs.run-tests == 'true'
+    steps:
+      - name: Cleanup repo
+        shell: bash
+        run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm 
-rf /workspace/*"
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: false
+      - name: >
+          Prepare breeze & CI image: 
${{needs.build-info.outputs.default-python-version}}:${{env.IMAGE_TAG}}
+        uses: ./.github/actions/prepare_breeze_and_image
+      - name: "Tests: ${{matrix.python-version}}:Non-DB-Pendulum2"
+        run: >
+          breeze testing non-db-tests
+          --parallel-test-types 
"${{needs.build-info.outputs.parallel-test-types-list-as-string}}"
+      - name: "Post Tests success: Non-DB-Pendulum2"
+        uses: ./.github/actions/post_tests_success
+        if: success()
+      - name: "Post Tests failure: Non-DB-Pendulum2"
+        uses: ./.github/actions/post_tests_failure
+        if: failure()
+
   summarize-warnings:
     timeout-minutes: 15
     name: "Summarize warnings"
diff --git a/Dockerfile.ci b/Dockerfile.ci
index 0f10757de5..5487e32af0 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -908,6 +908,18 @@ function check_download_sqlalchemy() {
     pip check
 }
 
+function check_download_pendulum() {
+    if [[ ${DOWNGRADE_PENDULUM=} != "true" ]]; then
+        return
+    fi
+    min_pendulum_version=$(grep "\"pendulum>=" pyproject.toml | sed 
"s/.*>=\([0-9\.]*\).*/\1/" | xargs)
+    echo
+    echo "${COLOR_BLUE}Downgrading pendulum to minimum supported version: 
${min_pendulum_version}${COLOR_RESET}"
+    echo
+    pip install --root-user-action ignore "pendulum==${min_pendulum_version}"
+    pip check
+}
+
 function check_run_tests() {
     if [[ ${RUN_TESTS=} != "true" ]]; then
         return
@@ -937,6 +949,7 @@ determine_airflow_to_use
 environment_initialization
 check_boto_upgrade
 check_download_sqlalchemy
+check_download_pendulum
 check_run_tests "${@}"
 
 exec /bin/bash "${@}"
diff --git a/airflow/models/dag.py b/airflow/models/dag.py
index d0f46feed2..c0abadf339 100644
--- a/airflow/models/dag.py
+++ b/airflow/models/dag.py
@@ -138,7 +138,7 @@ from airflow.utils.types import NOTSET, ArgNotSet, 
DagRunType, EdgeInfoType
 if TYPE_CHECKING:
     from types import ModuleType
 
-    from pendulum.tz.timezone import Timezone
+    from pendulum.tz.timezone import FixedTimezone, Timezone
     from sqlalchemy.orm.query import Query
     from sqlalchemy.orm.session import Session
 
@@ -213,7 +213,7 @@ def _get_model_data_interval(
     return DataInterval(start, end)
 
 
-def create_timetable(interval: ScheduleIntervalArg, timezone: Timezone) -> 
Timetable:
+def create_timetable(interval: ScheduleIntervalArg, timezone: Timezone | 
FixedTimezone) -> Timetable:
     """Create a Timetable instance from a ``schedule_interval`` argument."""
     if interval is NOTSET:
         return DeltaDataIntervalTimetable(DEFAULT_SCHEDULE_INTERVAL)
@@ -529,7 +529,7 @@ class DAG(LoggingMixin):
 
             tzinfo = None if date.tzinfo else settings.TIMEZONE
             tz = pendulum.instance(date, tz=tzinfo).timezone
-        self.timezone: Timezone = tz or settings.TIMEZONE
+        self.timezone: Timezone | FixedTimezone = tz or settings.TIMEZONE
 
         # Apply the timezone we settled on to end_date if it wasn't supplied
         if "end_date" in self.default_args and self.default_args["end_date"]:
diff --git a/airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py 
b/airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py
index 18799ed920..6c5f038b0a 100644
--- a/airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py
+++ b/airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py
@@ -21,7 +21,7 @@ import json
 import math
 import time
 import warnings
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, cast
 
 import pendulum
 import tenacity
@@ -148,13 +148,13 @@ class PodLauncher(LoggingMixin):
         """
         if get_logs:
             read_logs_since_sec = None
-            last_log_time = None
+            last_log_time: pendulum.DateTime | None = None
             while True:
                 logs = self.read_pod_logs(pod, timestamps=True, 
since_seconds=read_logs_since_sec)
                 for line in logs:
                     timestamp, message = 
self.parse_log_line(line.decode("utf-8"))
                     if timestamp:
-                        last_log_time = pendulum.parse(timestamp)
+                        last_log_time = cast(pendulum.DateTime, 
pendulum.parse(timestamp))
                     self.log.info(message)
                 time.sleep(1)
 
diff --git a/airflow/serialization/serialized_objects.py 
b/airflow/serialization/serialized_objects.py
index 48aa595933..87ee4d4a73 100644
--- a/airflow/serialization/serialized_objects.py
+++ b/airflow/serialization/serialized_objects.py
@@ -65,6 +65,7 @@ from airflow.utils.docs import get_docs_url
 from airflow.utils.module_loading import import_string, qualname
 from airflow.utils.operator_resources import Resources
 from airflow.utils.task_group import MappedTaskGroup, TaskGroup
+from airflow.utils.timezone import parse_timezone
 from airflow.utils.types import NOTSET, ArgNotSet
 
 if TYPE_CHECKING:
@@ -144,7 +145,7 @@ def decode_relativedelta(var: dict[str, Any]) -> 
relativedelta.relativedelta:
     return relativedelta.relativedelta(**var)
 
 
-def encode_timezone(var: Timezone) -> str | int:
+def encode_timezone(var: Timezone | FixedTimezone) -> str | int:
     """
     Encode a Pendulum Timezone for serialization.
 
@@ -167,9 +168,9 @@ def encode_timezone(var: Timezone) -> str | int:
     )
 
 
-def decode_timezone(var: str | int) -> Timezone:
+def decode_timezone(var: str | int) -> Timezone | FixedTimezone:
     """Decode a previously serialized Pendulum Timezone."""
-    return pendulum.tz.timezone(var)
+    return parse_timezone(var)
 
 
 def _get_registered_timetable(importable_string: str) -> type[Timetable] | 
None:
@@ -607,7 +608,7 @@ class BaseSerialization:
             raise TypeError(f"Invalid type {type_!s} in deserialization.")
 
     _deserialize_datetime = pendulum.from_timestamp
-    _deserialize_timezone = pendulum.tz.timezone
+    _deserialize_timezone = parse_timezone
 
     @classmethod
     def _deserialize_timedelta(cls, seconds: int) -> datetime.timedelta:
diff --git a/airflow/serialization/serializers/datetime.py 
b/airflow/serialization/serializers/datetime.py
index d32dd8897b..69058b8c02 100644
--- a/airflow/serialization/serializers/datetime.py
+++ b/airflow/serialization/serializers/datetime.py
@@ -24,6 +24,7 @@ from airflow.serialization.serializers.timezone import (
     serialize as serialize_timezone,
 )
 from airflow.utils.module_loading import qualname
+from airflow.utils.timezone import parse_timezone
 
 if TYPE_CHECKING:
     import datetime
@@ -62,23 +63,22 @@ def deserialize(classname: str, version: int, data: dict | 
str) -> datetime.date
     import datetime
 
     from pendulum import DateTime
-    from pendulum.tz import fixed_timezone, timezone
 
     tz: datetime.tzinfo | None = None
     if isinstance(data, dict) and TIMEZONE in data:
         if version == 1:
             # try to deserialize unsupported timezones
             timezone_mapping = {
-                "EDT": fixed_timezone(-4 * 3600),
-                "CDT": fixed_timezone(-5 * 3600),
-                "MDT": fixed_timezone(-6 * 3600),
-                "PDT": fixed_timezone(-7 * 3600),
-                "CEST": timezone("CET"),
+                "EDT": parse_timezone(-4 * 3600),
+                "CDT": parse_timezone(-5 * 3600),
+                "MDT": parse_timezone(-6 * 3600),
+                "PDT": parse_timezone(-7 * 3600),
+                "CEST": parse_timezone("CET"),
             }
             if data[TIMEZONE] in timezone_mapping:
                 tz = timezone_mapping[data[TIMEZONE]]
             else:
-                tz = timezone(data[TIMEZONE])
+                tz = parse_timezone(data[TIMEZONE])
         else:
             tz = (
                 deserialize_timezone(data[TIMEZONE][1], data[TIMEZONE][2], 
data[TIMEZONE][0])
diff --git a/airflow/serialization/serializers/timezone.py 
b/airflow/serialization/serializers/timezone.py
index 23901b9d44..0f580adef8 100644
--- a/airflow/serialization/serializers/timezone.py
+++ b/airflow/serialization/serializers/timezone.py
@@ -74,7 +74,7 @@ def serialize(o: object) -> tuple[U, str, int, bool]:
 
 
 def deserialize(classname: str, version: int, data: object) -> Any:
-    from pendulum.tz import fixed_timezone, timezone
+    from airflow.utils.timezone import parse_timezone
 
     if not isinstance(data, (str, int)):
         raise TypeError(f"{data} is not of type int or str but of 
{type(data)}")
@@ -82,9 +82,6 @@ def deserialize(classname: str, version: int, data: object) 
-> Any:
     if version > __version__:
         raise TypeError(f"serialized {version} of {classname} > {__version__}")
 
-    if isinstance(data, int):
-        return fixed_timezone(data)
-
     if "zoneinfo.ZoneInfo" in classname:
         try:
             from zoneinfo import ZoneInfo
@@ -93,7 +90,7 @@ def deserialize(classname: str, version: int, data: object) 
-> Any:
 
         return ZoneInfo(data)
 
-    return timezone(data)
+    return parse_timezone(data)
 
 
 # ported from pendulum.tz.timezone._get_tzinfo_name
diff --git a/airflow/settings.py b/airflow/settings.py
index 1a38a59ed3..53c5cc6aa4 100644
--- a/airflow/settings.py
+++ b/airflow/settings.py
@@ -26,7 +26,6 @@ import sys
 import warnings
 from typing import TYPE_CHECKING, Any, Callable
 
-import pendulum
 import pluggy
 import sqlalchemy
 from sqlalchemy import create_engine, exc, text
@@ -40,6 +39,7 @@ from airflow.executors import executor_constants
 from airflow.logging_config import configure_logging
 from airflow.utils.orm_event_handlers import setup_event_handlers
 from airflow.utils.state import State
+from airflow.utils.timezone import local_timezone, parse_timezone, utc
 
 if TYPE_CHECKING:
     from sqlalchemy.engine import Engine
@@ -50,13 +50,12 @@ if TYPE_CHECKING:
 log = logging.getLogger(__name__)
 
 try:
-    tz = conf.get_mandatory_value("core", "default_timezone")
-    if tz == "system":
-        TIMEZONE = pendulum.tz.local_timezone()
+    if (tz := conf.get_mandatory_value("core", "default_timezone")) != 
"system":
+        TIMEZONE = parse_timezone(tz)
     else:
-        TIMEZONE = pendulum.tz.timezone(tz)
+        TIMEZONE = local_timezone()
 except Exception:
-    TIMEZONE = pendulum.tz.timezone("UTC")
+    TIMEZONE = utc
 
 log.info("Configured default timezone %s", TIMEZONE)
 
diff --git a/airflow/timetables/_cron.py b/airflow/timetables/_cron.py
index b0e6e256ee..fa2fb1266f 100644
--- a/airflow/timetables/_cron.py
+++ b/airflow/timetables/_cron.py
@@ -19,17 +19,16 @@ from __future__ import annotations
 import datetime
 from typing import TYPE_CHECKING, Any
 
-import pendulum
 from cron_descriptor import CasingTypeEnum, ExpressionDescriptor, 
FormatException, MissingFieldException
 from croniter import CroniterBadCronError, CroniterBadDateError, croniter
 
 from airflow.exceptions import AirflowTimetableInvalid
 from airflow.utils.dates import cron_presets
-from airflow.utils.timezone import convert_to_utc, make_aware, make_naive
+from airflow.utils.timezone import convert_to_utc, make_aware, make_naive, 
parse_timezone
 
 if TYPE_CHECKING:
     from pendulum import DateTime
-    from pendulum.tz.timezone import Timezone
+    from pendulum.tz.timezone import FixedTimezone, Timezone
 
 
 def _covers_every_hour(cron: croniter) -> bool:
@@ -63,11 +62,11 @@ def _covers_every_hour(cron: croniter) -> bool:
 class CronMixin:
     """Mixin to provide interface to work with croniter."""
 
-    def __init__(self, cron: str, timezone: str | Timezone) -> None:
+    def __init__(self, cron: str, timezone: str | Timezone | FixedTimezone) -> 
None:
         self._expression = cron_presets.get(cron, cron)
 
         if isinstance(timezone, str):
-            timezone = pendulum.tz.timezone(timezone)
+            timezone = parse_timezone(timezone)
         self._timezone = timezone
 
         try:
diff --git a/airflow/timetables/trigger.py b/airflow/timetables/trigger.py
index 95d2923803..2a0df645da 100644
--- a/airflow/timetables/trigger.py
+++ b/airflow/timetables/trigger.py
@@ -26,7 +26,7 @@ from airflow.timetables.base import DagRunInfo, DataInterval, 
Timetable
 
 if TYPE_CHECKING:
     from dateutil.relativedelta import relativedelta
-    from pendulum.tz.timezone import Timezone
+    from pendulum.tz.timezone import FixedTimezone, Timezone
 
     from airflow.timetables.base import TimeRestriction
 
@@ -48,7 +48,7 @@ class CronTriggerTimetable(CronMixin, Timetable):
         self,
         cron: str,
         *,
-        timezone: str | Timezone,
+        timezone: str | Timezone | FixedTimezone,
         interval: datetime.timedelta | relativedelta = datetime.timedelta(),
     ) -> None:
         super().__init__(cron, timezone)
@@ -77,7 +77,12 @@ class CronTriggerTimetable(CronMixin, Timetable):
         return {"expression": self._expression, "timezone": timezone, 
"interval": interval}
 
     def infer_manual_data_interval(self, *, run_after: DateTime) -> 
DataInterval:
-        return DataInterval(run_after - self._interval, run_after)
+        return DataInterval(
+            # pendulum.Datetime ± timedelta should return pendulum.Datetime
+            # however mypy decide that output would be datetime.datetime
+            run_after - self._interval,  # type: ignore[arg-type]
+            run_after,
+        )
 
     def next_dagrun_info(
         self,
@@ -101,4 +106,9 @@ class CronTriggerTimetable(CronMixin, Timetable):
             next_start_time = max(start_time_candidates)
         if restriction.latest is not None and restriction.latest < 
next_start_time:
             return None
-        return DagRunInfo.interval(next_start_time - self._interval, 
next_start_time)
+        return DagRunInfo.interval(
+            # pendulum.Datetime ± timedelta should return pendulum.Datetime
+            # however mypy decide that output would be datetime.datetime
+            next_start_time - self._interval,  # type: ignore[arg-type]
+            next_start_time,
+        )
diff --git a/airflow/utils/sqlalchemy.py b/airflow/utils/sqlalchemy.py
index a042d4e902..fb241f482f 100644
--- a/airflow/utils/sqlalchemy.py
+++ b/airflow/utils/sqlalchemy.py
@@ -24,7 +24,6 @@ import json
 import logging
 from typing import TYPE_CHECKING, Any, Generator, Iterable, overload
 
-import pendulum
 from dateutil import relativedelta
 from sqlalchemy import TIMESTAMP, PickleType, and_, event, false, nullsfirst, 
or_, true, tuple_
 from sqlalchemy.dialects import mssql, mysql
@@ -34,7 +33,7 @@ from sqlalchemy.types import JSON, Text, TypeDecorator, 
UnicodeText
 from airflow import settings
 from airflow.configuration import conf
 from airflow.serialization.enums import Encoding
-from airflow.utils.timezone import make_naive
+from airflow.utils.timezone import make_naive, utc
 
 if TYPE_CHECKING:
     from kubernetes.client.models.v1_pod import V1Pod
@@ -46,8 +45,6 @@ if TYPE_CHECKING:
 
 log = logging.getLogger(__name__)
 
-utc = pendulum.tz.timezone("UTC")
-
 
 class UtcDateTime(TypeDecorator):
     """
diff --git a/airflow/utils/timezone.py b/airflow/utils/timezone.py
index 12c75bef59..8ac9a49e0e 100644
--- a/airflow/utils/timezone.py
+++ b/airflow/utils/timezone.py
@@ -18,14 +18,20 @@
 from __future__ import annotations
 
 import datetime as dt
-from typing import overload
+from typing import TYPE_CHECKING, overload
 
 import pendulum
 from dateutil.relativedelta import relativedelta
 from pendulum.datetime import DateTime
 
-# UTC time zone as a tzinfo instance.
-utc = pendulum.tz.timezone("UTC")
+if TYPE_CHECKING:
+    from pendulum.tz.timezone import FixedTimezone, Timezone
+
+_PENDULUM3 = pendulum.__version__.startswith("3")
+# UTC Timezone as a tzinfo instance. Actual value depends on pendulum version:
+# - Timezone("UTC") in pendulum 3
+# - FixedTimezone(0, "UTC") in pendulum 2
+utc = pendulum.UTC
 
 
 def is_localized(value):
@@ -135,12 +141,10 @@ def make_aware(value: dt.datetime | None, timezone: 
dt.tzinfo | None = None) ->
     # Check that we won't overwrite the timezone of an aware datetime.
     if is_localized(value):
         raise ValueError(f"make_aware expects a naive datetime, got {value}")
-    if hasattr(value, "fold"):
-        # In case of python 3.6 we want to do the same that pendulum does for 
python3.5
-        # i.e in case we move clock back we want to schedule the run at the 
time of the second
-        # instance of the same clock time rather than the first one.
-        # Fold parameter has no impact in other cases so we can safely set it 
to 1 here
-        value = value.replace(fold=1)
+    # In case we move clock back we want to schedule the run at the time of 
the second
+    # instance of the same clock time rather than the first one.
+    # Fold parameter has no impact in other cases, so we can safely set it to 
1 here
+    value = value.replace(fold=1)
     localized = getattr(timezone, "localize", None)
     if localized is not None:
         # This method is available for pytz time zones
@@ -273,3 +277,31 @@ def td_format(td_object: None | dt.timedelta | float | 
int) -> str | None:
     if not joined:
         return "<1s"
     return joined
+
+
+def parse_timezone(name: str | int) -> FixedTimezone | Timezone:
+    """
+    Parse timezone and return one of the pendulum Timezone.
+
+    Provide the same interface as ``pendulum.timezone(name)``
+
+    :param name: Either IANA timezone or offset to UTC in seconds.
+
+    :meta private:
+    """
+    if _PENDULUM3:
+        # This only presented in pendulum 3 and code do not reached into the 
pendulum 2
+        return pendulum.timezone(name)  # type: ignore[operator]
+    # In pendulum 2 this refers to the function, in pendulum 3 refers to the 
module
+    return pendulum.tz.timezone(name)  # type: ignore[operator]
+
+
+def local_timezone() -> FixedTimezone | Timezone:
+    """
+    Return local timezone.
+
+    Provide the same interface as ``pendulum.tz.local_timezone()``
+
+    :meta private:
+    """
+    return pendulum.tz.local_timezone()
diff --git a/dev/breeze/src/airflow_breeze/commands/common_options.py 
b/dev/breeze/src/airflow_breeze/commands/common_options.py
index 12d0ee77b8..a280db4e2c 100644
--- a/dev/breeze/src/airflow_breeze/commands/common_options.py
+++ b/dev/breeze/src/airflow_breeze/commands/common_options.py
@@ -151,6 +151,12 @@ option_downgrade_sqlalchemy = click.option(
     is_flag=True,
     envvar="DOWNGRADE_SQLALCHEMY",
 )
+option_downgrade_pendulum = click.option(
+    "--downgrade-pendulum",
+    help="Downgrade Pendulum to minimum supported version.",
+    is_flag=True,
+    envvar="DOWNGRADE_PENDULUM",
+)
 option_dry_run = click.option(
     "-D",
     "--dry-run",
diff --git a/dev/breeze/src/airflow_breeze/commands/developer_commands.py 
b/dev/breeze/src/airflow_breeze/commands/developer_commands.py
index ede6dfd933..27c43fc11b 100644
--- a/dev/breeze/src/airflow_breeze/commands/developer_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/developer_commands.py
@@ -39,6 +39,7 @@ from airflow_breeze.commands.common_options import (
     option_database_isolation,
     option_db_reset,
     option_docker_host,
+    option_downgrade_pendulum,
     option_downgrade_sqlalchemy,
     option_dry_run,
     option_forward_credentials,
@@ -248,6 +249,7 @@ option_warn_image_upgrade_needed = click.option(
 @option_db_reset
 @option_docker_host
 @option_downgrade_sqlalchemy
+@option_downgrade_pendulum
 @option_dry_run
 @option_executor_shell
 @option_force_build
@@ -294,6 +296,7 @@ def shell(
     database_isolation: bool,
     db_reset: bool,
     downgrade_sqlalchemy: bool,
+    downgrade_pendulum: bool,
     docker_host: str | None,
     executor: str,
     extra_args: tuple,
@@ -354,6 +357,7 @@ def shell(
         database_isolation=database_isolation,
         db_reset=db_reset,
         downgrade_sqlalchemy=downgrade_sqlalchemy,
+        downgrade_pendulum=downgrade_pendulum,
         docker_host=docker_host,
         executor=executor,
         extra_args=extra_args if not max_time else ["exit"],
diff --git 
a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
index 88b734f513..911ed9ebb5 100644
--- a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
@@ -159,6 +159,7 @@ DEVELOPER_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
             "options": [
                 "--upgrade-boto",
                 "--downgrade-sqlalchemy",
+                "--downgrade-pendulum",
             ],
         },
         {
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py 
b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index c4aff8797e..f826d9bdff 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -29,6 +29,7 @@ from airflow_breeze.commands.common_options import (
     option_backend,
     option_db_reset,
     option_debug_resources,
+    option_downgrade_pendulum,
     option_downgrade_sqlalchemy,
     option_dry_run,
     option_forward_credentials,
@@ -471,6 +472,7 @@ option_remove_arm_packages = click.option(
 @option_excluded_parallel_test_types
 @option_upgrade_boto
 @option_downgrade_sqlalchemy
+@option_downgrade_pendulum
 @option_collect_only
 @option_remove_arm_packages
 @option_skip_docker_compose_down
@@ -513,6 +515,7 @@ def command_for_tests(**kwargs):
 @option_excluded_parallel_test_types
 @option_upgrade_boto
 @option_downgrade_sqlalchemy
+@option_downgrade_pendulum
 @option_collect_only
 @option_remove_arm_packages
 @option_skip_docker_compose_down
@@ -548,6 +551,7 @@ def command_for_db_tests(**kwargs):
 @option_collect_only
 @option_debug_resources
 @option_downgrade_sqlalchemy
+@option_downgrade_pendulum
 @option_dry_run
 @option_enable_coverage
 @option_excluded_parallel_test_types
@@ -589,6 +593,7 @@ def _run_test_command(
     db_reset: bool,
     debug_resources: bool,
     downgrade_sqlalchemy: bool,
+    downgrade_pendulum: bool,
     enable_coverage: bool,
     excluded_parallel_test_types: str,
     extra_pytest_args: tuple,
@@ -632,6 +637,7 @@ def _run_test_command(
         backend=backend,
         collect_only=collect_only,
         downgrade_sqlalchemy=downgrade_sqlalchemy,
+        downgrade_pendulum=downgrade_pendulum,
         enable_coverage=enable_coverage,
         forward_credentials=forward_credentials,
         forward_ports=False,
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
index 404e0cabe0..370cdad91f 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
@@ -79,6 +79,7 @@ TESTING_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
                 "--mount-sources",
                 "--upgrade-boto",
                 "--downgrade-sqlalchemy",
+                "--downgrade-pendulum",
                 "--remove-arm-packages",
                 "--skip-docker-compose-down",
             ],
@@ -126,6 +127,7 @@ TESTING_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
                 "--mount-sources",
                 "--upgrade-boto",
                 "--downgrade-sqlalchemy",
+                "--downgrade-pendulum",
                 "--remove-arm-packages",
                 "--skip-docker-compose-down",
             ],
@@ -177,6 +179,7 @@ TESTING_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
                 "--mount-sources",
                 "--upgrade-boto",
                 "--downgrade-sqlalchemy",
+                "--downgrade-pendulum",
                 "--remove-arm-packages",
                 "--skip-docker-compose-down",
             ],
diff --git a/dev/breeze/src/airflow_breeze/params/shell_params.py 
b/dev/breeze/src/airflow_breeze/params/shell_params.py
index 85fbecd6ca..fbc02b7922 100644
--- a/dev/breeze/src/airflow_breeze/params/shell_params.py
+++ b/dev/breeze/src/airflow_breeze/params/shell_params.py
@@ -148,6 +148,7 @@ class ShellParams:
     dev_mode: bool = False
     docker_host: str | None = os.environ.get("DOCKER_HOST")
     downgrade_sqlalchemy: bool = False
+    downgrade_pendulum: bool = False
     dry_run: bool = False
     enable_coverage: bool = False
     executor: str = START_AIRFLOW_DEFAULT_ALLOWED_EXECUTOR
@@ -516,6 +517,7 @@ class ShellParams:
         _set_var(_env, "DEV_MODE", self.dev_mode)
         _set_var(_env, "DOCKER_IS_ROOTLESS", self.rootless_docker)
         _set_var(_env, "DOWNGRADE_SQLALCHEMY", self.downgrade_sqlalchemy)
+        _set_var(_env, "DOWNGRADE_PENDULUM", self.downgrade_pendulum)
         _set_var(_env, "ENABLED_SYSTEMS", None, "")
         _set_var(_env, "FLOWER_HOST_PORT", None, FLOWER_HOST_PORT)
         _set_var(_env, "GITHUB_ACTIONS", self.github_actions)
diff --git a/images/breeze/output_shell.svg b/images/breeze/output_shell.svg
index 8d9d7350d5..84151382c7 100644
--- a/images/breeze/output_shell.svg
+++ b/images/breeze/output_shell.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 3026.7999999999997" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 3051.2" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-shell-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="2975.7999999999997" />
+      <rect x="0" y="0" width="1463.0" height="3000.2" />
     </clipPath>
     <clipPath id="breeze-shell-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -408,9 +408,12 @@
 <clipPath id="breeze-shell-line-120">
     <rect x="0" y="2929.5" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-shell-line-121">
+    <rect x="0" y="2953.9" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="3024.8" rx="8"/><text 
class="breeze-shell-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;shell</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="3049.2" rx="8"/><text 
class="breeze-shell-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;shell</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -525,23 +528,24 @@
 </text><text class="breeze-shell-r5" x="0" y="2508.8" textLength="24.4" 
clip-path="url(#breeze-shell-line-102)">╭─</text><text class="breeze-shell-r5" 
x="24.4" y="2508.8" textLength="500.2" 
clip-path="url(#breeze-shell-line-102)">&#160;Upgrading/downgrading&#160;selected&#160;packages&#160;</text><text
 class="breeze-shell-r5" x="524.6" y="2508.8" textLength="915" 
clip-path="url(#breeze-shell-line-102)">───────────────────────────────────────────────────────────────────────────</text><tex
 [...]
 </text><text class="breeze-shell-r5" x="0" y="2533.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-103)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2533.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-103)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2533.2" textLength="97.6" 
clip-path="url(#breeze-shell-line-103)">-upgrade</text><text 
class="breeze-shell-r4" x="134.2" y="2533.2" textLength="61" 
clip-path="url(#breeze-shell-line-103)">-boto</text><text class="b [...]
 </text><text class="breeze-shell-r5" x="0" y="2557.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-104)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2557.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-104)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2557.6" textLength="122" 
clip-path="url(#breeze-shell-line-104)">-downgrade</text><text 
class="breeze-shell-r4" x="158.6" y="2557.6" textLength="134.2" 
clip-path="url(#breeze-shell-line-104)">-sqlalchemy</text><tex [...]
-</text><text class="breeze-shell-r5" x="0" y="2582" textLength="1464" 
clip-path="url(#breeze-shell-line-105)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2582" textLength="12.2" 
clip-path="url(#breeze-shell-line-105)">
-</text><text class="breeze-shell-r5" x="0" y="2606.4" textLength="24.4" 
clip-path="url(#breeze-shell-line-106)">╭─</text><text class="breeze-shell-r5" 
x="24.4" y="2606.4" textLength="183" 
clip-path="url(#breeze-shell-line-106)">&#160;DB&#160;test&#160;flags&#160;</text><text
 class="breeze-shell-r5" x="207.4" y="2606.4" textLength="1232.2" 
clip-path="url(#breeze-shell-line-106)">─────────────────────────────────────────────────────────────────────────────────────────────────────</text><te
 [...]
-</text><text class="breeze-shell-r5" x="0" y="2630.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-107)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2630.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-107)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2630.8" textLength="48.8" 
clip-path="url(#breeze-shell-line-107)">-run</text><text 
class="breeze-shell-r4" x="85.4" y="2630.8" textLength="170.8" 
clip-path="url(#breeze-shell-line-107)">-db-tests-only</text><text c [...]
-</text><text class="breeze-shell-r5" x="0" y="2655.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-108)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2655.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-108)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2655.2" textLength="61" 
clip-path="url(#breeze-shell-line-108)">-skip</text><text 
class="breeze-shell-r4" x="97.6" y="2655.2" textLength="109.8" 
clip-path="url(#breeze-shell-line-108)">-db-tests</text><text class=" [...]
-</text><text class="breeze-shell-r5" x="0" y="2679.6" textLength="1464" 
clip-path="url(#breeze-shell-line-109)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2679.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-109)">
-</text><text class="breeze-shell-r5" x="0" y="2704" textLength="24.4" 
clip-path="url(#breeze-shell-line-110)">╭─</text><text class="breeze-shell-r5" 
x="24.4" y="2704" textLength="183" 
clip-path="url(#breeze-shell-line-110)">&#160;Other&#160;options&#160;</text><text
 class="breeze-shell-r5" x="207.4" y="2704" textLength="1232.2" 
clip-path="url(#breeze-shell-line-110)">─────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="b [...]
-</text><text class="breeze-shell-r5" x="0" y="2728.4" textLength="12.2" 
clip-path="url(#breeze-shell-line-111)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2728.4" textLength="12.2" 
clip-path="url(#breeze-shell-line-111)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2728.4" textLength="97.6" 
clip-path="url(#breeze-shell-line-111)">-forward</text><text 
class="breeze-shell-r4" x="134.2" y="2728.4" textLength="146.4" 
clip-path="url(#breeze-shell-line-111)">-credentials</text><tex [...]
-</text><text class="breeze-shell-r5" x="0" y="2752.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-112)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2752.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-112)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2752.8" textLength="48.8" 
clip-path="url(#breeze-shell-line-112)">-max</text><text 
class="breeze-shell-r4" x="85.4" y="2752.8" textLength="61" 
clip-path="url(#breeze-shell-line-112)">-time</text><text class="breeze [...]
-</text><text class="breeze-shell-r5" x="0" y="2777.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-113)">│</text><text class="breeze-shell-r7" 
x="353.8" y="2777.2" textLength="1049.2" 
clip-path="url(#breeze-shell-line-113)">(INTEGER&#160;RANGE)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
-</text><text class="breeze-shell-r5" x="0" y="2801.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-114)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2801.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-114)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2801.6" textLength="97.6" 
clip-path="url(#breeze-shell-line-114)">-verbose</text><text 
class="breeze-shell-r4" x="134.2" y="2801.6" textLength="109.8" 
clip-path="url(#breeze-shell-line-114)">-commands</text><text c [...]
-</text><text class="breeze-shell-r5" x="0" y="2826" textLength="1464" 
clip-path="url(#breeze-shell-line-115)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2826" textLength="12.2" 
clip-path="url(#breeze-shell-line-115)">
-</text><text class="breeze-shell-r5" x="0" y="2850.4" textLength="24.4" 
clip-path="url(#breeze-shell-line-116)">╭─</text><text class="breeze-shell-r5" 
x="24.4" y="2850.4" textLength="195.2" 
clip-path="url(#breeze-shell-line-116)">&#160;Common&#160;options&#160;</text><text
 class="breeze-shell-r5" x="219.6" y="2850.4" textLength="1220" 
clip-path="url(#breeze-shell-line-116)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 cl [...]
-</text><text class="breeze-shell-r5" x="0" y="2874.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-117)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2874.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-117)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2874.8" textLength="85.4" 
clip-path="url(#breeze-shell-line-117)">-answer</text><text 
class="breeze-shell-r6" x="158.6" y="2874.8" textLength="24.4" 
clip-path="url(#breeze-shell-line-117)">-a</text><text class="bre [...]
-</text><text class="breeze-shell-r5" x="0" y="2899.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-118)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2899.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-118)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2899.2" textLength="48.8" 
clip-path="url(#breeze-shell-line-118)">-dry</text><text 
class="breeze-shell-r4" x="85.4" y="2899.2" textLength="48.8" 
clip-path="url(#breeze-shell-line-118)">-run</text><text class="breez [...]
-</text><text class="breeze-shell-r5" x="0" y="2923.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-119)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2923.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-119)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2923.6" textLength="97.6" 
clip-path="url(#breeze-shell-line-119)">-verbose</text><text 
class="breeze-shell-r6" x="158.6" y="2923.6" textLength="24.4" 
clip-path="url(#breeze-shell-line-119)">-v</text><text class="br [...]
-</text><text class="breeze-shell-r5" x="0" y="2948" textLength="12.2" 
clip-path="url(#breeze-shell-line-120)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2948" textLength="12.2" 
clip-path="url(#breeze-shell-line-120)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2948" textLength="61" 
clip-path="url(#breeze-shell-line-120)">-help</text><text 
class="breeze-shell-r6" x="158.6" y="2948" textLength="24.4" 
clip-path="url(#breeze-shell-line-120)">-h</text><text class="breeze-shell-r1 
[...]
-</text><text class="breeze-shell-r5" x="0" y="2972.4" textLength="1464" 
clip-path="url(#breeze-shell-line-121)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2972.4" textLength="12.2" 
clip-path="url(#breeze-shell-line-121)">
+</text><text class="breeze-shell-r5" x="0" y="2582" textLength="12.2" 
clip-path="url(#breeze-shell-line-105)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2582" textLength="12.2" 
clip-path="url(#breeze-shell-line-105)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2582" textLength="122" 
clip-path="url(#breeze-shell-line-105)">-downgrade</text><text 
class="breeze-shell-r4" x="158.6" y="2582" textLength="109.8" 
clip-path="url(#breeze-shell-line-105)">-pendulum</text><text class="b [...]
+</text><text class="breeze-shell-r5" x="0" y="2606.4" textLength="1464" 
clip-path="url(#breeze-shell-line-106)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2606.4" textLength="12.2" 
clip-path="url(#breeze-shell-line-106)">
+</text><text class="breeze-shell-r5" x="0" y="2630.8" textLength="24.4" 
clip-path="url(#breeze-shell-line-107)">╭─</text><text class="breeze-shell-r5" 
x="24.4" y="2630.8" textLength="183" 
clip-path="url(#breeze-shell-line-107)">&#160;DB&#160;test&#160;flags&#160;</text><text
 class="breeze-shell-r5" x="207.4" y="2630.8" textLength="1232.2" 
clip-path="url(#breeze-shell-line-107)">─────────────────────────────────────────────────────────────────────────────────────────────────────</text><te
 [...]
+</text><text class="breeze-shell-r5" x="0" y="2655.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-108)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2655.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-108)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2655.2" textLength="48.8" 
clip-path="url(#breeze-shell-line-108)">-run</text><text 
class="breeze-shell-r4" x="85.4" y="2655.2" textLength="170.8" 
clip-path="url(#breeze-shell-line-108)">-db-tests-only</text><text c [...]
+</text><text class="breeze-shell-r5" x="0" y="2679.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-109)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2679.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-109)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2679.6" textLength="61" 
clip-path="url(#breeze-shell-line-109)">-skip</text><text 
class="breeze-shell-r4" x="97.6" y="2679.6" textLength="109.8" 
clip-path="url(#breeze-shell-line-109)">-db-tests</text><text class=" [...]
+</text><text class="breeze-shell-r5" x="0" y="2704" textLength="1464" 
clip-path="url(#breeze-shell-line-110)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2704" textLength="12.2" 
clip-path="url(#breeze-shell-line-110)">
+</text><text class="breeze-shell-r5" x="0" y="2728.4" textLength="24.4" 
clip-path="url(#breeze-shell-line-111)">╭─</text><text class="breeze-shell-r5" 
x="24.4" y="2728.4" textLength="183" 
clip-path="url(#breeze-shell-line-111)">&#160;Other&#160;options&#160;</text><text
 class="breeze-shell-r5" x="207.4" y="2728.4" textLength="1232.2" 
clip-path="url(#breeze-shell-line-111)">─────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 cl [...]
+</text><text class="breeze-shell-r5" x="0" y="2752.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-112)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2752.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-112)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2752.8" textLength="97.6" 
clip-path="url(#breeze-shell-line-112)">-forward</text><text 
class="breeze-shell-r4" x="134.2" y="2752.8" textLength="146.4" 
clip-path="url(#breeze-shell-line-112)">-credentials</text><tex [...]
+</text><text class="breeze-shell-r5" x="0" y="2777.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-113)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2777.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-113)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2777.2" textLength="48.8" 
clip-path="url(#breeze-shell-line-113)">-max</text><text 
class="breeze-shell-r4" x="85.4" y="2777.2" textLength="61" 
clip-path="url(#breeze-shell-line-113)">-time</text><text class="breeze [...]
+</text><text class="breeze-shell-r5" x="0" y="2801.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-114)">│</text><text class="breeze-shell-r7" 
x="353.8" y="2801.6" textLength="1049.2" 
clip-path="url(#breeze-shell-line-114)">(INTEGER&#160;RANGE)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
+</text><text class="breeze-shell-r5" x="0" y="2826" textLength="12.2" 
clip-path="url(#breeze-shell-line-115)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2826" textLength="12.2" 
clip-path="url(#breeze-shell-line-115)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2826" textLength="97.6" 
clip-path="url(#breeze-shell-line-115)">-verbose</text><text 
class="breeze-shell-r4" x="134.2" y="2826" textLength="109.8" 
clip-path="url(#breeze-shell-line-115)">-commands</text><text class="br [...]
+</text><text class="breeze-shell-r5" x="0" y="2850.4" textLength="1464" 
clip-path="url(#breeze-shell-line-116)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2850.4" textLength="12.2" 
clip-path="url(#breeze-shell-line-116)">
+</text><text class="breeze-shell-r5" x="0" y="2874.8" textLength="24.4" 
clip-path="url(#breeze-shell-line-117)">╭─</text><text class="breeze-shell-r5" 
x="24.4" y="2874.8" textLength="195.2" 
clip-path="url(#breeze-shell-line-117)">&#160;Common&#160;options&#160;</text><text
 class="breeze-shell-r5" x="219.6" y="2874.8" textLength="1220" 
clip-path="url(#breeze-shell-line-117)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 cl [...]
+</text><text class="breeze-shell-r5" x="0" y="2899.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-118)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2899.2" textLength="12.2" 
clip-path="url(#breeze-shell-line-118)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2899.2" textLength="85.4" 
clip-path="url(#breeze-shell-line-118)">-answer</text><text 
class="breeze-shell-r6" x="158.6" y="2899.2" textLength="24.4" 
clip-path="url(#breeze-shell-line-118)">-a</text><text class="bre [...]
+</text><text class="breeze-shell-r5" x="0" y="2923.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-119)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2923.6" textLength="12.2" 
clip-path="url(#breeze-shell-line-119)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2923.6" textLength="48.8" 
clip-path="url(#breeze-shell-line-119)">-dry</text><text 
class="breeze-shell-r4" x="85.4" y="2923.6" textLength="48.8" 
clip-path="url(#breeze-shell-line-119)">-run</text><text class="breez [...]
+</text><text class="breeze-shell-r5" x="0" y="2948" textLength="12.2" 
clip-path="url(#breeze-shell-line-120)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2948" textLength="12.2" 
clip-path="url(#breeze-shell-line-120)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2948" textLength="97.6" 
clip-path="url(#breeze-shell-line-120)">-verbose</text><text 
class="breeze-shell-r6" x="158.6" y="2948" textLength="24.4" 
clip-path="url(#breeze-shell-line-120)">-v</text><text class="breeze-she [...]
+</text><text class="breeze-shell-r5" x="0" y="2972.4" textLength="12.2" 
clip-path="url(#breeze-shell-line-121)">│</text><text class="breeze-shell-r4" 
x="24.4" y="2972.4" textLength="12.2" 
clip-path="url(#breeze-shell-line-121)">-</text><text class="breeze-shell-r4" 
x="36.6" y="2972.4" textLength="61" 
clip-path="url(#breeze-shell-line-121)">-help</text><text 
class="breeze-shell-r6" x="158.6" y="2972.4" textLength="24.4" 
clip-path="url(#breeze-shell-line-121)">-h</text><text class="breeze- [...]
+</text><text class="breeze-shell-r5" x="0" y="2996.8" textLength="1464" 
clip-path="url(#breeze-shell-line-122)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-shell-r1" x="1464" y="2996.8" textLength="12.2" 
clip-path="url(#breeze-shell-line-122)">
 </text>
     </g>
     </g>
diff --git a/images/breeze/output_shell.txt b/images/breeze/output_shell.txt
index e71507684a..0702a3041e 100644
--- a/images/breeze/output_shell.txt
+++ b/images/breeze/output_shell.txt
@@ -1 +1 @@
-0ceaf6dd335e09cc9ecb42cedd18c064
+35d2198ba2086d6f11da105483505fe1
diff --git a/images/breeze/output_testing_db-tests.svg 
b/images/breeze/output_testing_db-tests.svg
index a4c191f601..9a8c28fceb 100644
--- a/images/breeze/output_testing_db-tests.svg
+++ b/images/breeze/output_testing_db-tests.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1831.1999999999998" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 1855.6" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-testing-db-tests-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="1780.1999999999998" />
+      <rect x="0" y="0" width="1463.0" height="1804.6" />
     </clipPath>
     <clipPath id="breeze-testing-db-tests-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -261,9 +261,12 @@
 <clipPath id="breeze-testing-db-tests-line-71">
     <rect x="0" y="1733.9" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-testing-db-tests-line-72">
+    <rect x="0" y="1758.3" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1829.2" rx="8"/><text 
class="breeze-testing-db-tests-title" fill="#c5c8c6" text-anchor="middle" 
x="740" y="27">Command:&#160;testing&#160;db-tests</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1853.6" rx="8"/><text 
class="breeze-testing-db-tests-title" fill="#c5c8c6" text-anchor="middle" 
x="740" y="27">Command:&#160;testing&#160;db-tests</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -338,14 +341,15 @@
 </text><text class="breeze-testing-db-tests-r5" x="0" y="1532.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-62)">│</text><text 
class="breeze-testing-db-tests-r5" x="414.8" y="1532.8" textLength="1024.8" 
clip-path="url(#breeze-testing-db-tests-line-62)">[default:&#160;selected]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#1
 [...]
 </text><text class="breeze-testing-db-tests-r5" x="0" y="1557.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-63)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1557.2" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-63)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1557.2" textLength="97.6" 
clip-path="url(#breeze-testing-db-tests-line-63)">-upgrade</text><text 
class="breeze-testing-db-tests-r4" x="134.2" y="1557.2" textLeng [...]
 </text><text class="breeze-testing-db-tests-r5" x="0" y="1581.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-64)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1581.6" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-64)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1581.6" textLength="122" 
clip-path="url(#breeze-testing-db-tests-line-64)">-downgrade</text><text 
class="breeze-testing-db-tests-r4" x="158.6" y="1581.6" textLen [...]
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1606" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-65)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1606" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-65)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1606" textLength="85.4" 
clip-path="url(#breeze-testing-db-tests-line-65)">-remove</text><text 
class="breeze-testing-db-tests-r4" x="122" y="1606" textLength="158.6"  [...]
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1630.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-66)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1630.4" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-66)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1630.4" textLength="61" 
clip-path="url(#breeze-testing-db-tests-line-66)">-skip</text><text 
class="breeze-testing-db-tests-r4" x="97.6" y="1630.4" textLength="24 [...]
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1654.8" 
textLength="1464" 
clip-path="url(#breeze-testing-db-tests-line-67)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-db-tests-r1" x="1464" y="1654.8" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-67)">
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1679.2" 
textLength="24.4" 
clip-path="url(#breeze-testing-db-tests-line-68)">╭─</text><text 
class="breeze-testing-db-tests-r5" x="24.4" y="1679.2" textLength="195.2" 
clip-path="url(#breeze-testing-db-tests-line-68)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-db-tests-r5" x="219.6" y="1679.2" textLength="1220" 
clip-path="url(#breeze-testing-db-tests-line-68)">────────────────────────────────────────────────────
 [...]
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1703.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-69)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1703.6" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-69)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1703.6" textLength="97.6" 
clip-path="url(#breeze-testing-db-tests-line-69)">-verbose</text><text 
class="breeze-testing-db-tests-r7" x="158.6" y="1703.6" textLeng [...]
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1728" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-70)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1728" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-70)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1728" textLength="48.8" 
clip-path="url(#breeze-testing-db-tests-line-70)">-dry</text><text 
class="breeze-testing-db-tests-r4" x="85.4" y="1728" textLength="48.8" cli [...]
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1752.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-71)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1752.4" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-71)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1752.4" textLength="61" 
clip-path="url(#breeze-testing-db-tests-line-71)">-help</text><text 
class="breeze-testing-db-tests-r7" x="158.6" y="1752.4" textLength="2 [...]
-</text><text class="breeze-testing-db-tests-r5" x="0" y="1776.8" 
textLength="1464" 
clip-path="url(#breeze-testing-db-tests-line-72)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-db-tests-r1" x="1464" y="1776.8" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-72)">
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1606" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-65)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1606" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-65)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1606" textLength="122" 
clip-path="url(#breeze-testing-db-tests-line-65)">-downgrade</text><text 
class="breeze-testing-db-tests-r4" x="158.6" y="1606" textLength="109 [...]
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1630.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-66)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1630.4" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-66)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1630.4" textLength="85.4" 
clip-path="url(#breeze-testing-db-tests-line-66)">-remove</text><text 
class="breeze-testing-db-tests-r4" x="122" y="1630.4" textLength= [...]
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1654.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-67)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1654.8" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-67)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1654.8" textLength="61" 
clip-path="url(#breeze-testing-db-tests-line-67)">-skip</text><text 
class="breeze-testing-db-tests-r4" x="97.6" y="1654.8" textLength="24 [...]
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1679.2" 
textLength="1464" 
clip-path="url(#breeze-testing-db-tests-line-68)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-db-tests-r1" x="1464" y="1679.2" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-68)">
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1703.6" 
textLength="24.4" 
clip-path="url(#breeze-testing-db-tests-line-69)">╭─</text><text 
class="breeze-testing-db-tests-r5" x="24.4" y="1703.6" textLength="195.2" 
clip-path="url(#breeze-testing-db-tests-line-69)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-db-tests-r5" x="219.6" y="1703.6" textLength="1220" 
clip-path="url(#breeze-testing-db-tests-line-69)">────────────────────────────────────────────────────
 [...]
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1728" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-70)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1728" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-70)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1728" textLength="97.6" 
clip-path="url(#breeze-testing-db-tests-line-70)">-verbose</text><text 
class="breeze-testing-db-tests-r7" x="158.6" y="1728" textLength="24.4 [...]
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1752.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-71)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1752.4" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-71)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1752.4" textLength="48.8" 
clip-path="url(#breeze-testing-db-tests-line-71)">-dry</text><text 
class="breeze-testing-db-tests-r4" x="85.4" y="1752.4" textLength="4 [...]
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1776.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-72)">│</text><text 
class="breeze-testing-db-tests-r4" x="24.4" y="1776.8" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-72)">-</text><text 
class="breeze-testing-db-tests-r4" x="36.6" y="1776.8" textLength="61" 
clip-path="url(#breeze-testing-db-tests-line-72)">-help</text><text 
class="breeze-testing-db-tests-r7" x="158.6" y="1776.8" textLength="2 [...]
+</text><text class="breeze-testing-db-tests-r5" x="0" y="1801.2" 
textLength="1464" 
clip-path="url(#breeze-testing-db-tests-line-73)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-db-tests-r1" x="1464" y="1801.2" textLength="12.2" 
clip-path="url(#breeze-testing-db-tests-line-73)">
 </text>
     </g>
     </g>
diff --git a/images/breeze/output_testing_db-tests.txt 
b/images/breeze/output_testing_db-tests.txt
index 24c7f336cf..092397ae05 100644
--- a/images/breeze/output_testing_db-tests.txt
+++ b/images/breeze/output_testing_db-tests.txt
@@ -1 +1 @@
-825d30b396fe55ffd0862c0d85441f36
+bbff9b2f8394bf13cb4916c15c0ba967
diff --git a/images/breeze/output_testing_non-db-tests.svg 
b/images/breeze/output_testing_non-db-tests.svg
index e7686cd36c..3d64e0989c 100644
--- a/images/breeze/output_testing_non-db-tests.svg
+++ b/images/breeze/output_testing_non-db-tests.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1636.0" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 1660.3999999999999" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-testing-non-db-tests-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="1585.0" />
+      <rect x="0" y="0" width="1463.0" height="1609.3999999999999" />
     </clipPath>
     <clipPath id="breeze-testing-non-db-tests-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -237,9 +237,12 @@
 <clipPath id="breeze-testing-non-db-tests-line-63">
     <rect x="0" y="1538.7" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-testing-non-db-tests-line-64">
+    <rect x="0" y="1563.1" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1634" rx="8"/><text 
class="breeze-testing-non-db-tests-title" fill="#c5c8c6" text-anchor="middle" 
x="740" y="27">Command:&#160;testing&#160;non-db-tests</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1658.4" rx="8"/><text 
class="breeze-testing-non-db-tests-title" fill="#c5c8c6" text-anchor="middle" 
x="740" y="27">Command:&#160;testing&#160;non-db-tests</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -306,14 +309,15 @@
 </text><text class="breeze-testing-non-db-tests-r5" x="0" y="1337.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-54)">│</text><text 
class="breeze-testing-non-db-tests-r5" x="414.8" y="1337.6" textLength="1024.8" 
clip-path="url(#breeze-testing-non-db-tests-line-54)">[default:&#160;selected]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160
 [...]
 </text><text class="breeze-testing-non-db-tests-r5" x="0" y="1362" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-55)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1362" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-55)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1362" textLength="97.6" 
clip-path="url(#breeze-testing-non-db-tests-line-55)">-upgrade</text><text 
class="breeze-testing-non-db-tests-r4" x="134. [...]
 </text><text class="breeze-testing-non-db-tests-r5" x="0" y="1386.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-56)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1386.4" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-56)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1386.4" textLength="122" 
clip-path="url(#breeze-testing-non-db-tests-line-56)">-downgrade</text><text 
class="breeze-testing-non-db-tests-r4"  [...]
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1410.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-57)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1410.8" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-57)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1410.8" textLength="85.4" 
clip-path="url(#breeze-testing-non-db-tests-line-57)">-remove</text><text 
class="breeze-testing-non-db-tests-r4" x= [...]
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1435.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-58)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1435.2" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-58)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1435.2" textLength="61" 
clip-path="url(#breeze-testing-non-db-tests-line-58)">-skip</text><text 
class="breeze-testing-non-db-tests-r4" x="97. [...]
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1459.6" 
textLength="1464" 
clip-path="url(#breeze-testing-non-db-tests-line-59)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-non-db-tests-r1" x="1464" y="1459.6" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-59)">
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1484" 
textLength="24.4" 
clip-path="url(#breeze-testing-non-db-tests-line-60)">╭─</text><text 
class="breeze-testing-non-db-tests-r5" x="24.4" y="1484" textLength="195.2" 
clip-path="url(#breeze-testing-non-db-tests-line-60)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-non-db-tests-r5" x="219.6" y="1484" textLength="1220" 
clip-path="url(#breeze-testing-non-db-tests-line-60)">──────────────────────────────────
 [...]
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1508.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-61)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1508.4" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-61)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1508.4" textLength="48.8" 
clip-path="url(#breeze-testing-non-db-tests-line-61)">-dry</text><text 
class="breeze-testing-non-db-tests-r4" x="85 [...]
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1532.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-62)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1532.8" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-62)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1532.8" textLength="97.6" 
clip-path="url(#breeze-testing-non-db-tests-line-62)">-verbose</text><text 
class="breeze-testing-non-db-tests-r7" x [...]
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1557.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-63)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1557.2" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-63)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1557.2" textLength="61" 
clip-path="url(#breeze-testing-non-db-tests-line-63)">-help</text><text 
class="breeze-testing-non-db-tests-r7" x="158 [...]
-</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1581.6" 
textLength="1464" 
clip-path="url(#breeze-testing-non-db-tests-line-64)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-non-db-tests-r1" x="1464" y="1581.6" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-64)">
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1410.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-57)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1410.8" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-57)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1410.8" textLength="122" 
clip-path="url(#breeze-testing-non-db-tests-line-57)">-downgrade</text><text 
class="breeze-testing-non-db-tests-r4"  [...]
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1435.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-58)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1435.2" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-58)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1435.2" textLength="85.4" 
clip-path="url(#breeze-testing-non-db-tests-line-58)">-remove</text><text 
class="breeze-testing-non-db-tests-r4" x= [...]
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1459.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-59)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1459.6" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-59)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1459.6" textLength="61" 
clip-path="url(#breeze-testing-non-db-tests-line-59)">-skip</text><text 
class="breeze-testing-non-db-tests-r4" x="97. [...]
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1484" 
textLength="1464" 
clip-path="url(#breeze-testing-non-db-tests-line-60)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-non-db-tests-r1" x="1464" y="1484" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-60)">
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1508.4" 
textLength="24.4" 
clip-path="url(#breeze-testing-non-db-tests-line-61)">╭─</text><text 
class="breeze-testing-non-db-tests-r5" x="24.4" y="1508.4" textLength="195.2" 
clip-path="url(#breeze-testing-non-db-tests-line-61)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-non-db-tests-r5" x="219.6" y="1508.4" textLength="1220" 
clip-path="url(#breeze-testing-non-db-tests-line-61)">────────────────────────────
 [...]
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1532.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-62)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1532.8" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-62)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1532.8" textLength="48.8" 
clip-path="url(#breeze-testing-non-db-tests-line-62)">-dry</text><text 
class="breeze-testing-non-db-tests-r4" x="85 [...]
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1557.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-63)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1557.2" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-63)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1557.2" textLength="97.6" 
clip-path="url(#breeze-testing-non-db-tests-line-63)">-verbose</text><text 
class="breeze-testing-non-db-tests-r7" x [...]
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1581.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-64)">│</text><text 
class="breeze-testing-non-db-tests-r4" x="24.4" y="1581.6" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-64)">-</text><text 
class="breeze-testing-non-db-tests-r4" x="36.6" y="1581.6" textLength="61" 
clip-path="url(#breeze-testing-non-db-tests-line-64)">-help</text><text 
class="breeze-testing-non-db-tests-r7" x="158 [...]
+</text><text class="breeze-testing-non-db-tests-r5" x="0" y="1606" 
textLength="1464" 
clip-path="url(#breeze-testing-non-db-tests-line-65)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-non-db-tests-r1" x="1464" y="1606" textLength="12.2" 
clip-path="url(#breeze-testing-non-db-tests-line-65)">
 </text>
     </g>
     </g>
diff --git a/images/breeze/output_testing_non-db-tests.txt 
b/images/breeze/output_testing_non-db-tests.txt
index 6bc1946e1a..7d9ef31069 100644
--- a/images/breeze/output_testing_non-db-tests.txt
+++ b/images/breeze/output_testing_non-db-tests.txt
@@ -1 +1 @@
-70b970c5c754371d9d5d80234d130615
+8aa7803c0bbff622175c01d94d22290b
diff --git a/images/breeze/output_testing_tests.svg 
b/images/breeze/output_testing_tests.svg
index 9e06eefda1..ed7c062406 100644
--- a/images/breeze/output_testing_tests.svg
+++ b/images/breeze/output_testing_tests.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 2246.0" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 2270.4" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-testing-tests-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="2195.0" />
+      <rect x="0" y="0" width="1463.0" height="2219.4" />
     </clipPath>
     <clipPath id="breeze-testing-tests-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -312,9 +312,12 @@
 <clipPath id="breeze-testing-tests-line-88">
     <rect x="0" y="2148.7" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-testing-tests-line-89">
+    <rect x="0" y="2173.1" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="2244" rx="8"/><text 
class="breeze-testing-tests-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;testing&#160;tests</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="2268.4" rx="8"/><text 
class="breeze-testing-tests-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;testing&#160;tests</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -406,14 +409,15 @@
 </text><text class="breeze-testing-tests-r5" x="0" y="1947.6" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-79)">│</text><text 
class="breeze-testing-tests-r5" x="414.8" y="1947.6" textLength="1024.8" 
clip-path="url(#breeze-testing-tests-line-79)">[default:&#160;selected]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#1
 [...]
 </text><text class="breeze-testing-tests-r5" x="0" y="1972" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-80)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="1972" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-80)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="1972" textLength="97.6" 
clip-path="url(#breeze-testing-tests-line-80)">-upgrade</text><text 
class="breeze-testing-tests-r4" x="134.2" y="1972" textLength="61" 
clip-path="url(#breez [...]
 </text><text class="breeze-testing-tests-r5" x="0" y="1996.4" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-81)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="1996.4" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-81)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="1996.4" textLength="122" 
clip-path="url(#breeze-testing-tests-line-81)">-downgrade</text><text 
class="breeze-testing-tests-r4" x="158.6" y="1996.4" textLength="134.2" 
clip-path [...]
-</text><text class="breeze-testing-tests-r5" x="0" y="2020.8" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-82)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2020.8" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-82)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2020.8" textLength="85.4" 
clip-path="url(#breeze-testing-tests-line-82)">-remove</text><text 
class="breeze-testing-tests-r4" x="122" y="2020.8" textLength="158.6" 
clip-path="ur [...]
-</text><text class="breeze-testing-tests-r5" x="0" y="2045.2" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-83)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2045.2" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-83)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2045.2" textLength="61" 
clip-path="url(#breeze-testing-tests-line-83)">-skip</text><text 
class="breeze-testing-tests-r4" x="97.6" y="2045.2" textLength="244" 
clip-path="url(#br [...]
-</text><text class="breeze-testing-tests-r5" x="0" y="2069.6" 
textLength="1464" 
clip-path="url(#breeze-testing-tests-line-84)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-tests-r1" x="1464" y="2069.6" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-84)">
-</text><text class="breeze-testing-tests-r5" x="0" y="2094" textLength="24.4" 
clip-path="url(#breeze-testing-tests-line-85)">╭─</text><text 
class="breeze-testing-tests-r5" x="24.4" y="2094" textLength="195.2" 
clip-path="url(#breeze-testing-tests-line-85)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-tests-r5" x="219.6" y="2094" textLength="1220" 
clip-path="url(#breeze-testing-tests-line-85)">────────────────────────────────────────────────────────────────────────────
 [...]
-</text><text class="breeze-testing-tests-r5" x="0" y="2118.4" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-86)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2118.4" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-86)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2118.4" textLength="97.6" 
clip-path="url(#breeze-testing-tests-line-86)">-verbose</text><text 
class="breeze-testing-tests-r6" x="158.6" y="2118.4" textLength="24.4" 
clip-path=" [...]
-</text><text class="breeze-testing-tests-r5" x="0" y="2142.8" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-87)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2142.8" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-87)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2142.8" textLength="48.8" 
clip-path="url(#breeze-testing-tests-line-87)">-dry</text><text 
class="breeze-testing-tests-r4" x="85.4" y="2142.8" textLength="48.8" 
clip-path="url(# [...]
-</text><text class="breeze-testing-tests-r5" x="0" y="2167.2" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-88)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2167.2" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-88)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2167.2" textLength="61" 
clip-path="url(#breeze-testing-tests-line-88)">-help</text><text 
class="breeze-testing-tests-r6" x="158.6" y="2167.2" textLength="24.4" 
clip-path="url(# [...]
-</text><text class="breeze-testing-tests-r5" x="0" y="2191.6" 
textLength="1464" 
clip-path="url(#breeze-testing-tests-line-89)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-tests-r1" x="1464" y="2191.6" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-89)">
+</text><text class="breeze-testing-tests-r5" x="0" y="2020.8" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-82)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2020.8" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-82)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2020.8" textLength="122" 
clip-path="url(#breeze-testing-tests-line-82)">-downgrade</text><text 
class="breeze-testing-tests-r4" x="158.6" y="2020.8" textLength="109.8" 
clip-path [...]
+</text><text class="breeze-testing-tests-r5" x="0" y="2045.2" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-83)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2045.2" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-83)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2045.2" textLength="85.4" 
clip-path="url(#breeze-testing-tests-line-83)">-remove</text><text 
class="breeze-testing-tests-r4" x="122" y="2045.2" textLength="158.6" 
clip-path="ur [...]
+</text><text class="breeze-testing-tests-r5" x="0" y="2069.6" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-84)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2069.6" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-84)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2069.6" textLength="61" 
clip-path="url(#breeze-testing-tests-line-84)">-skip</text><text 
class="breeze-testing-tests-r4" x="97.6" y="2069.6" textLength="244" 
clip-path="url(#br [...]
+</text><text class="breeze-testing-tests-r5" x="0" y="2094" textLength="1464" 
clip-path="url(#breeze-testing-tests-line-85)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-tests-r1" x="1464" y="2094" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-85)">
+</text><text class="breeze-testing-tests-r5" x="0" y="2118.4" 
textLength="24.4" clip-path="url(#breeze-testing-tests-line-86)">╭─</text><text 
class="breeze-testing-tests-r5" x="24.4" y="2118.4" textLength="195.2" 
clip-path="url(#breeze-testing-tests-line-86)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-tests-r5" x="219.6" y="2118.4" textLength="1220" 
clip-path="url(#breeze-testing-tests-line-86)">──────────────────────────────────────────────────────────────────────
 [...]
+</text><text class="breeze-testing-tests-r5" x="0" y="2142.8" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-87)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2142.8" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-87)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2142.8" textLength="97.6" 
clip-path="url(#breeze-testing-tests-line-87)">-verbose</text><text 
class="breeze-testing-tests-r6" x="158.6" y="2142.8" textLength="24.4" 
clip-path=" [...]
+</text><text class="breeze-testing-tests-r5" x="0" y="2167.2" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-88)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2167.2" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-88)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2167.2" textLength="48.8" 
clip-path="url(#breeze-testing-tests-line-88)">-dry</text><text 
class="breeze-testing-tests-r4" x="85.4" y="2167.2" textLength="48.8" 
clip-path="url(# [...]
+</text><text class="breeze-testing-tests-r5" x="0" y="2191.6" 
textLength="12.2" clip-path="url(#breeze-testing-tests-line-89)">│</text><text 
class="breeze-testing-tests-r4" x="24.4" y="2191.6" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-89)">-</text><text 
class="breeze-testing-tests-r4" x="36.6" y="2191.6" textLength="61" 
clip-path="url(#breeze-testing-tests-line-89)">-help</text><text 
class="breeze-testing-tests-r6" x="158.6" y="2191.6" textLength="24.4" 
clip-path="url(# [...]
+</text><text class="breeze-testing-tests-r5" x="0" y="2216" textLength="1464" 
clip-path="url(#breeze-testing-tests-line-90)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-tests-r1" x="1464" y="2216" textLength="12.2" 
clip-path="url(#breeze-testing-tests-line-90)">
 </text>
     </g>
     </g>
diff --git a/images/breeze/output_testing_tests.txt 
b/images/breeze/output_testing_tests.txt
index b243afade7..6b81ec51d0 100644
--- a/images/breeze/output_testing_tests.txt
+++ b/images/breeze/output_testing_tests.txt
@@ -1 +1 @@
-4c38b2815f6a7606d05fe58fd9a8550a
+f63db76413092aa6f822957edbf3fc8c
diff --git a/kubernetes_tests/test_kubernetes_pod_operator.py 
b/kubernetes_tests/test_kubernetes_pod_operator.py
index 249cf667e1..8d7dad9d12 100644
--- a/kubernetes_tests/test_kubernetes_pod_operator.py
+++ b/kubernetes_tests/test_kubernetes_pod_operator.py
@@ -26,7 +26,6 @@ from unittest import mock
 from unittest.mock import ANY, MagicMock
 from uuid import uuid4
 
-import pendulum
 import pytest
 from kubernetes import client
 from kubernetes.client import V1EnvVar, V1PodSecurityContext, 
V1SecurityContext, models as k8s
@@ -53,7 +52,9 @@ POD_MANAGER_CLASS = 
"airflow.providers.cncf.kubernetes.utils.pod_manager.PodMana
 
 def create_context(task) -> Context:
     dag = DAG(dag_id="dag")
-    execution_date = timezone.datetime(2016, 1, 1, 1, 0, 0, 
tzinfo=pendulum.tz.timezone("Europe/Amsterdam"))
+    execution_date = timezone.datetime(
+        2016, 1, 1, 1, 0, 0, tzinfo=timezone.parse_timezone("Europe/Amsterdam")
+    )
     dag_run = DagRun(
         dag_id=dag.dag_id,
         execution_date=execution_date,
diff --git a/newsfragments/36281.significant.rst 
b/newsfragments/36281.significant.rst
new file mode 100644
index 0000000000..1207be0782
--- /dev/null
+++ b/newsfragments/36281.significant.rst
@@ -0,0 +1,4 @@
+Target version for core dependency ``pendulum`` package set to 3
+
+Support for pendulum 2.1.2 will be saved for a while, presumably until the 
next feature version of Airflow.
+It is advised to upgrade user code to use pendulum 3 as soon as possible.
diff --git a/pyproject.toml b/pyproject.toml
index b1c40d4b40..be12fcba93 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -123,9 +123,7 @@ dependencies = [
     "opentelemetry-exporter-otlp",
     "packaging>=14.0",
     "pathspec>=0.9.0",
-    # When (if) pendulum 3 released it would introduce changes in 
module/objects imports,
-    # since we are tightly coupled with pendulum library internally it will 
breaks Airflow functionality.
-    "pendulum>=2.0,<3.0",
+    "pendulum>=2.1.2,<4.0",
     "pluggy>=1.0",
     "psutil>=4.2.0",
     "pydantic>=2.3.0",
diff --git a/scripts/ci/docker-compose/devcontainer.env 
b/scripts/ci/docker-compose/devcontainer.env
index 4ef3fd0459..5c627c9ebf 100644
--- a/scripts/ci/docker-compose/devcontainer.env
+++ b/scripts/ci/docker-compose/devcontainer.env
@@ -68,6 +68,7 @@ START_AIRFLOW="false"
 SUSPENDED_PROVIDERS_FOLDERS=""
 TEST_TYPE=
 UPGRADE_BOTO="false"
+DOWNGRADE_PENDULUM="false"
 DOWNGRADE_SQLALCHEMY="false"
 UPGRADE_TO_NEWER_DEPENDENCIES="false"
 VERBOSE="false"
diff --git a/scripts/docker/entrypoint_ci.sh b/scripts/docker/entrypoint_ci.sh
index 52d79f4154..b5bbe777ac 100755
--- a/scripts/docker/entrypoint_ci.sh
+++ b/scripts/docker/entrypoint_ci.sh
@@ -239,6 +239,19 @@ function check_download_sqlalchemy() {
     pip check
 }
 
+# Download minimum supported version of pendulum to run tests with it
+function check_download_pendulum() {
+    if [[ ${DOWNGRADE_PENDULUM=} != "true" ]]; then
+        return
+    fi
+    min_pendulum_version=$(grep "\"pendulum>=" pyproject.toml | sed 
"s/.*>=\([0-9\.]*\).*/\1/" | xargs)
+    echo
+    echo "${COLOR_BLUE}Downgrading pendulum to minimum supported version: 
${min_pendulum_version}${COLOR_RESET}"
+    echo
+    pip install --root-user-action ignore "pendulum==${min_pendulum_version}"
+    pip check
+}
+
 # Check if we should run tests and run them if needed
 function check_run_tests() {
     if [[ ${RUN_TESTS=} != "true" ]]; then
@@ -269,6 +282,7 @@ determine_airflow_to_use
 environment_initialization
 check_boto_upgrade
 check_download_sqlalchemy
+check_download_pendulum
 check_run_tests "${@}"
 
 # If we are not running tests - just exec to bash shell
diff --git a/tests/api_connexion/endpoints/test_dag_endpoint.py 
b/tests/api_connexion/endpoints/test_dag_endpoint.py
index c02e8b0ff3..a29b86f4a6 100644
--- a/tests/api_connexion/endpoints/test_dag_endpoint.py
+++ b/tests/api_connexion/endpoints/test_dag_endpoint.py
@@ -20,6 +20,7 @@ import os
 import unittest.mock
 from datetime import datetime
 
+import pendulum
 import pytest
 
 from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
@@ -46,6 +47,7 @@ DAG_ID = "test_dag"
 TASK_ID = "op1"
 DAG2_ID = "test_dag2"
 DAG3_ID = "test_dag3"
+UTC_JSON_REPR = "UTC" if pendulum.__version__.startswith("3") else 
"Timezone('UTC')"
 
 
 @pytest.fixture(scope="module")
@@ -316,7 +318,7 @@ class TestGetDagDetails(TestDagEndpoint):
             "tags": [],
             "template_searchpath": None,
             "timetable_description": None,
-            "timezone": "Timezone('UTC')",
+            "timezone": UTC_JSON_REPR,
         }
         assert response.json == expected
 
@@ -367,7 +369,7 @@ class TestGetDagDetails(TestDagEndpoint):
             "tags": [],
             "template_searchpath": None,
             "timetable_description": None,
-            "timezone": "Timezone('UTC')",
+            "timezone": UTC_JSON_REPR,
         }
         assert response.json == expected
 
@@ -418,7 +420,7 @@ class TestGetDagDetails(TestDagEndpoint):
             "tags": [],
             "template_searchpath": None,
             "timetable_description": None,
-            "timezone": "Timezone('UTC')",
+            "timezone": UTC_JSON_REPR,
         }
         assert response.json == expected
 
@@ -478,7 +480,7 @@ class TestGetDagDetails(TestDagEndpoint):
             "tags": [],
             "template_searchpath": None,
             "timetable_description": None,
-            "timezone": "Timezone('UTC')",
+            "timezone": UTC_JSON_REPR,
         }
         response = self.client.get(
             f"/api/v1/dags/{self.dag_id}/details", 
environ_overrides={"REMOTE_USER": "test"}
@@ -539,7 +541,7 @@ class TestGetDagDetails(TestDagEndpoint):
             "tags": [],
             "template_searchpath": None,
             "timetable_description": None,
-            "timezone": "Timezone('UTC')",
+            "timezone": UTC_JSON_REPR,
         }
         expected.update({"last_parsed": response.json["last_parsed"]})
         assert response.json == expected
diff --git a/tests/api_connexion/schemas/test_dag_schema.py 
b/tests/api_connexion/schemas/test_dag_schema.py
index f3e54c0a96..df227eb5c6 100644
--- a/tests/api_connexion/schemas/test_dag_schema.py
+++ b/tests/api_connexion/schemas/test_dag_schema.py
@@ -18,6 +18,7 @@ from __future__ import annotations
 
 from datetime import datetime
 
+import pendulum
 import pytest
 
 from airflow.api_connexion.schemas.dag_schema import (
@@ -29,6 +30,8 @@ from airflow.api_connexion.schemas.dag_schema import (
 from airflow.models import DagModel, DagTag
 from airflow.models.dag import DAG
 
+UTC_JSON_REPR = "UTC" if pendulum.__version__.startswith("3") else 
"Timezone('UTC')"
+
 
 def test_serialize_test_dag_schema(url_safe_serializer):
     dag_model = DagModel(
@@ -184,7 +187,7 @@ def 
test_serialize_test_dag_detail_schema(url_safe_serializer):
         "start_date": "2020-06-19T00:00:00+00:00",
         "tags": [{"name": "example1"}, {"name": "example2"}],
         "template_searchpath": None,
-        "timezone": "Timezone('UTC')",
+        "timezone": UTC_JSON_REPR,
         "max_active_runs": 16,
         "pickle_id": None,
         "end_date": None,
diff --git a/tests/cli/commands/test_dag_command.py 
b/tests/cli/commands/test_dag_command.py
index 4f16c381ad..1c1a7ff650 100644
--- a/tests/cli/commands/test_dag_command.py
+++ b/tests/cli/commands/test_dag_command.py
@@ -50,6 +50,10 @@ from tests.test_utils.config import conf_vars
 from tests.test_utils.db import clear_db_dags, clear_db_runs
 
 DEFAULT_DATE = timezone.make_aware(datetime(2015, 1, 1), timezone=timezone.utc)
+if pendulum.__version__.startswith("3"):
+    DEFAULT_DATE_REPR = DEFAULT_DATE.isoformat(sep=" ")
+else:
+    DEFAULT_DATE_REPR = DEFAULT_DATE.isoformat()
 
 # TODO: Check if tests needs side effects - locally there's missing DAG
 
@@ -162,7 +166,7 @@ class TestCliDags:
             )
 
         output = stdout.getvalue()
-        assert f"Dry run of DAG example_bash_operator on 
{DEFAULT_DATE.isoformat()}\n" in output
+        assert f"Dry run of DAG example_bash_operator on 
{DEFAULT_DATE_REPR}\n" in output
         assert "Task runme_0 located in DAG example_bash_operator\n" in output
 
         mock_run.assert_not_called()  # Dry run shouldn't run the backfill
@@ -235,12 +239,9 @@ class TestCliDags:
 
         output = stdout.getvalue()
 
-        assert (
-            f"Dry run of DAG example_branch_python_operator_decorator on "
-            f"{DEFAULT_DATE.isoformat()}\n" in output
-        )
+        assert f"Dry run of DAG example_branch_python_operator_decorator on 
{DEFAULT_DATE_REPR}\n" in output
         assert "Task run_this_first located in DAG 
example_branch_python_operator_decorator\n" in output
-        assert f"Dry run of DAG example_branch_operator on 
{DEFAULT_DATE.isoformat()}\n" in output
+        assert f"Dry run of DAG example_branch_operator on 
{DEFAULT_DATE_REPR}\n" in output
         assert "Task run_this_first located in DAG example_branch_operator\n" 
in output
 
     @mock.patch("airflow.cli.commands.dag_command.get_dag")
diff --git a/tests/models/test_dag.py b/tests/models/test_dag.py
index f7bf1ad6d0..de7fa87d5a 100644
--- a/tests/models/test_dag.py
+++ b/tests/models/test_dag.py
@@ -37,6 +37,7 @@ import pendulum
 import pytest
 import time_machine
 from dateutil.relativedelta import relativedelta
+from pendulum.tz.timezone import Timezone
 from sqlalchemy import inspect
 
 from airflow import settings
@@ -676,8 +677,8 @@ class TestDag:
         """
         Make sure DST transitions are properly observed
         """
-        local_tz = pendulum.timezone("Europe/Zurich")
-        start = local_tz.convert(datetime.datetime(2018, 10, 28, 2, 55), 
dst_rule=pendulum.PRE_TRANSITION)
+        local_tz = Timezone("Europe/Zurich")
+        start = local_tz.convert(datetime.datetime(2018, 10, 28, 2, 55, 
fold=0))
         assert start.isoformat() == "2018-10-28T02:55:00+02:00", 
"Pre-condition: start date is in DST"
 
         utc = timezone.convert_to_utc(start)
@@ -706,7 +707,7 @@ class TestDag:
         Make sure DST transitions are properly observed
         """
         local_tz = pendulum.timezone("Europe/Zurich")
-        start = local_tz.convert(datetime.datetime(2018, 10, 27, 3), 
dst_rule=pendulum.PRE_TRANSITION)
+        start = local_tz.convert(datetime.datetime(2018, 10, 27, 3, fold=0))
 
         utc = timezone.convert_to_utc(start)
 
@@ -735,7 +736,7 @@ class TestDag:
         Make sure DST transitions are properly observed
         """
         local_tz = pendulum.timezone("Europe/Zurich")
-        start = local_tz.convert(datetime.datetime(2018, 3, 25, 2), 
dst_rule=pendulum.PRE_TRANSITION)
+        start = local_tz.convert(datetime.datetime(2018, 3, 25, 2, fold=0))
 
         utc = timezone.convert_to_utc(start)
 
diff --git a/tests/providers/openlineage/plugins/test_utils.py 
b/tests/providers/openlineage/plugins/test_utils.py
index 54710bcd9e..b7ced7a37c 100644
--- a/tests/providers/openlineage/plugins/test_utils.py
+++ b/tests/providers/openlineage/plugins/test_utils.py
@@ -23,7 +23,6 @@ import uuid
 from json import JSONEncoder
 from typing import Any
 
-import pendulum
 import pytest
 from attrs import define
 from openlineage.client.utils import RedactMixin
@@ -39,6 +38,7 @@ from airflow.providers.openlineage.utils.utils import (
     to_json_encodable,
     url_to_https,
 )
+from airflow.utils import timezone
 from airflow.utils.log.secrets_masker import _secrets_masker
 from airflow.utils.state import State
 
@@ -86,8 +86,8 @@ def test_get_dagrun_start_end():
         state=State.NONE, run_id=run_id, 
data_interval=dag.get_next_data_interval(dag_model)
     )
     assert dagrun.data_interval_start is not None
-    start_date_tz = datetime.datetime(2022, 1, 1, 
tzinfo=pendulum.tz.timezone("UTC"))
-    end_date_tz = datetime.datetime(2022, 1, 1, hour=2, 
tzinfo=pendulum.tz.timezone("UTC"))
+    start_date_tz = datetime.datetime(2022, 1, 1, tzinfo=timezone.utc)
+    end_date_tz = datetime.datetime(2022, 1, 1, hour=2, tzinfo=timezone.utc)
     assert dagrun.data_interval_start, dagrun.data_interval_end == 
(start_date_tz, end_date_tz)
 
 
diff --git a/tests/sensors/test_time_sensor.py 
b/tests/sensors/test_time_sensor.py
index 935d1cb128..54a0212a24 100644
--- a/tests/sensors/test_time_sensor.py
+++ b/tests/sensors/test_time_sensor.py
@@ -18,12 +18,10 @@
 from __future__ import annotations
 
 from datetime import datetime, time
-from unittest.mock import patch
 
 import pendulum
 import pytest
 import time_machine
-from pendulum.tz.timezone import UTC
 
 from airflow.exceptions import TaskDeferred
 from airflow.models.dag import DAG
@@ -33,7 +31,7 @@ from airflow.utils import timezone
 
 DEFAULT_TIMEZONE = "Asia/Singapore"  # UTC+08:00
 DEFAULT_DATE_WO_TZ = datetime(2015, 1, 1)
-DEFAULT_DATE_WITH_TZ = datetime(2015, 1, 1, 
tzinfo=pendulum.tz.timezone(DEFAULT_TIMEZONE))
+DEFAULT_DATE_WITH_TZ = datetime(2015, 1, 1, 
tzinfo=timezone.parse_timezone(DEFAULT_TIMEZONE))
 
 
 class TestTimeSensor:
@@ -46,11 +44,11 @@ class TestTimeSensor:
         ],
     )
     @time_machine.travel(timezone.datetime(2020, 1, 1, 23, 
0).replace(tzinfo=timezone.utc))
-    def test_timezone(self, default_timezone, start_date, expected):
-        with patch("airflow.settings.TIMEZONE", 
pendulum.timezone(default_timezone)):
-            dag = DAG("test", default_args={"start_date": start_date})
-            op = TimeSensor(task_id="test", target_time=time(10, 0), dag=dag)
-            assert op.poke(None) == expected
+    def test_timezone(self, default_timezone, start_date, expected, 
monkeypatch):
+        monkeypatch.setattr("airflow.settings.TIMEZONE", 
timezone.parse_timezone(default_timezone))
+        dag = DAG("test", default_args={"start_date": start_date})
+        op = TimeSensor(task_id="test", target_time=time(10, 0), dag=dag)
+        assert op.poke(None) == expected
 
 
 class TestTimeSensorAsync:
@@ -72,8 +70,7 @@ class TestTimeSensorAsync:
         with DAG("test_target_time_aware", start_date=timezone.datetime(2020, 
1, 1, 23, 0)):
             aware_time = time(0, 1).replace(tzinfo=pendulum.local_timezone())
             op = TimeSensorAsync(task_id="test", target_time=aware_time)
-            assert hasattr(op.target_datetime.tzinfo, "offset")
-            assert op.target_datetime.tzinfo.offset == 0
+            assert op.target_datetime.tzinfo == timezone.utc
 
     def test_target_time_naive_dag_timezone(self):
         """
@@ -85,4 +82,4 @@ class TestTimeSensorAsync:
         ):
             op = TimeSensorAsync(task_id="test", target_time=pendulum.time(9, 
0))
             assert op.target_datetime.time() == pendulum.time(1, 0)
-            assert op.target_datetime.tzinfo == UTC
+            assert op.target_datetime.tzinfo == timezone.utc
diff --git a/tests/serialization/serializers/test_serializers.py 
b/tests/serialization/serializers/test_serializers.py
index 32e9787ccf..26027fdbf0 100644
--- a/tests/serialization/serializers/test_serializers.py
+++ b/tests/serialization/serializers/test_serializers.py
@@ -21,11 +21,13 @@ import decimal
 from unittest.mock import patch
 
 import numpy as np
+import pendulum
 import pendulum.tz
 import pytest
 from dateutil.tz import tzutc
 from deltalake import DeltaTable
 from pendulum import DateTime
+from pendulum.tz.timezone import FixedTimezone, Timezone
 from pyiceberg.catalog import Catalog
 from pyiceberg.io import FileIO
 from pyiceberg.table import Table
@@ -39,6 +41,8 @@ if PY39:
 else:
     from backports.zoneinfo import ZoneInfo
 
+PENDULUM3 = pendulum.__version__.startswith("3")
+
 
 class TestSerializers:
     def test_datetime(self):
@@ -227,3 +231,151 @@ class TestSerializers:
         assert i.version() == d.version()
         assert i._storage_options == d._storage_options
         assert d._storage_options is None
+
+    @pytest.mark.skipif(not PENDULUM3, reason="Test case for pendulum~=3")
+    @pytest.mark.parametrize(
+        "ser_value, expected",
+        [
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680307200.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": ["UTC", 
"pendulum.tz.timezone.FixedTimezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=Timezone("UTC")),
+                id="in-utc-timezone",
+            ),
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680292800.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": ["Asia/Tbilisi", 
"pendulum.tz.timezone.Timezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=Timezone("Asia/Tbilisi")),
+                id="non-dts-timezone",
+            ),
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680303600.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": ["Europe/London", 
"pendulum.tz.timezone.Timezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=Timezone("Europe/London")),
+                id="dts-timezone",
+            ),
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680310800.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": [-3600, 
"pendulum.tz.timezone.FixedTimezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=FixedTimezone(-3600)),
+                id="offset-timezone",
+            ),
+        ],
+    )
+    def test_pendulum_2_to_3(self, ser_value, expected):
+        """Test deserialize objects in pendulum 3 which serialised in pendulum 
2."""
+        assert deserialize(ser_value) == expected
+
+    @pytest.mark.skipif(PENDULUM3, reason="Test case for pendulum~=2")
+    @pytest.mark.parametrize(
+        "ser_value, expected",
+        [
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680307200.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": ["UTC", 
"pendulum.tz.timezone.Timezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=Timezone("UTC")),
+                id="in-utc-timezone",
+            ),
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680292800.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": ["Asia/Tbilisi", 
"pendulum.tz.timezone.Timezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=Timezone("Asia/Tbilisi")),
+                id="non-dts-timezone",
+            ),
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680303600.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": ["Europe/London", 
"pendulum.tz.timezone.Timezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=Timezone("Europe/London")),
+                id="dts-timezone",
+            ),
+            pytest.param(
+                {
+                    "__classname__": "pendulum.datetime.DateTime",
+                    "__version__": 2,
+                    "__data__": {
+                        "timestamp": 1680310800.0,
+                        "tz": {
+                            "__classname__": "builtins.tuple",
+                            "__version__": 1,
+                            "__data__": [-3600, 
"pendulum.tz.timezone.FixedTimezone", 1, True],
+                        },
+                    },
+                },
+                pendulum.datetime(2023, 4, 1, tz=FixedTimezone(-3600)),
+                id="offset-timezone",
+            ),
+        ],
+    )
+    def test_pendulum_3_to_2(self, ser_value, expected):
+        """Test deserialize objects in pendulum 2 which serialised in pendulum 
3."""
+        assert deserialize(ser_value) == expected
diff --git a/tests/serialization/test_serialized_objects.py 
b/tests/serialization/test_serialized_objects.py
index c059a8d236..a40e0d01ea 100644
--- a/tests/serialization/test_serialized_objects.py
+++ b/tests/serialization/test_serialized_objects.py
@@ -20,10 +20,10 @@ from __future__ import annotations
 import json
 from datetime import datetime, timedelta
 
-import pendulum
 import pytest
 from dateutil import relativedelta
 from kubernetes.client import models as k8s
+from pendulum.tz.timezone import Timezone
 
 from airflow.datasets import Dataset
 from airflow.exceptions import SerializationError
@@ -142,7 +142,7 @@ def equal_time(a: datetime, b: datetime) -> bool:
         (1, None, equals),
         (datetime.utcnow(), DAT.DATETIME, equal_time),
         (timedelta(minutes=2), DAT.TIMEDELTA, equals),
-        (pendulum.tz.timezone("UTC"), DAT.TIMEZONE, lambda a, b: a.name == 
b.name),
+        (Timezone("UTC"), DAT.TIMEZONE, lambda a, b: a.name == b.name),
         (relativedelta.relativedelta(hours=+1), DAT.RELATIVEDELTA, lambda a, 
b: a.hours == b.hours),
         ({"test": "dict", "test-1": 1}, None, equals),
         (["array_item", 2], None, equals),
diff --git a/tests/triggers/test_temporal.py b/tests/triggers/test_temporal.py
index 655910394f..52cc2c64f6 100644
--- a/tests/triggers/test_temporal.py
+++ b/tests/triggers/test_temporal.py
@@ -64,9 +64,9 @@ def test_timedelta_trigger_serialization():
 @pytest.mark.parametrize(
     "tz",
     [
-        pendulum.tz.timezone("UTC"),
-        pendulum.tz.timezone("Europe/Paris"),
-        pendulum.tz.timezone("America/Toronto"),
+        timezone.parse_timezone("UTC"),
+        timezone.parse_timezone("Europe/Paris"),
+        timezone.parse_timezone("America/Toronto"),
     ],
 )
 @pytest.mark.asyncio
diff --git a/tests/utils/test_timezone.py b/tests/utils/test_timezone.py
index ff5ad26f5a..df8af04604 100644
--- a/tests/utils/test_timezone.py
+++ b/tests/utils/test_timezone.py
@@ -21,13 +21,14 @@ import datetime
 
 import pendulum
 import pytest
+from pendulum.tz.timezone import Timezone
 
 from airflow.utils import timezone
-from airflow.utils.timezone import coerce_datetime
+from airflow.utils.timezone import coerce_datetime, parse_timezone
 
-CET = pendulum.tz.timezone("Europe/Paris")
-EAT = pendulum.tz.timezone("Africa/Nairobi")  # Africa/Nairobi
-ICT = pendulum.tz.timezone("Asia/Bangkok")  # Asia/Bangkok
+CET = Timezone("Europe/Paris")
+EAT = Timezone("Africa/Nairobi")  # Africa/Nairobi
+ICT = Timezone("Asia/Bangkok")  # Asia/Bangkok
 UTC = timezone.utc
 
 
@@ -117,3 +118,41 @@ class TestTimezone:
 )
 def test_coerce_datetime(input_datetime, output_datetime):
     assert output_datetime == coerce_datetime(input_datetime)
+
+
[email protected](
+    "tz_name",
+    [
+        pytest.param("Europe/Paris", id="CET"),
+        pytest.param("Africa/Nairobi", id="EAT"),
+        pytest.param("Asia/Bangkok", id="ICT"),
+    ],
+)
+def test_parse_timezone_iana(tz_name: str):
+    tz = parse_timezone(tz_name)
+    assert tz.name == tz_name
+    assert parse_timezone(tz_name) is tz
+
+
[email protected]("tz_name", ["utc", "UTC", "uTc"])
+def test_parse_timezone_utc(tz_name):
+    tz = parse_timezone(tz_name)
+    assert tz.name == "UTC"
+    assert parse_timezone(tz_name) is tz
+    assert tz is timezone.utc, "Expected that UTC timezone is same object as 
`airflow.utils.timezone.utc`"
+
+
[email protected](
+    "tz_offset, expected_offset, expected_name",
+    [
+        pytest.param(0, 0, "+00:00", id="zero-offset"),
+        pytest.param(-3600, -3600, "-01:00", id="1-hour-behind"),
+        pytest.param(19800, 19800, "+05:30", id="5.5-hours-ahead"),
+    ],
+)
+def test_parse_timezone_offset(tz_offset: int, expected_offset, expected_name):
+    tz = parse_timezone(tz_offset)
+    assert hasattr(tz, "offset")
+    assert tz.offset == expected_offset
+    assert tz.name == expected_name
+    assert parse_timezone(tz_offset) is tz

Reply via email to