Repository: aiogram/aiogram

Issue:
Add sentry exceptions repr support
### Problem

Sentry logging module uses `__repr__` to print an exception message.

aioram 2.x behavior:
```python
from aiogram.utils.exceptions import TelegramAPIError

exc = TelegramAPIError(message="reason")

print(str(exc))
# Reason

print(repr(exc))
# TelegramAPIError('Reason')

```

aiogram 3.x behavior:
```python
from aiogram.exceptions import DetailedAiogramError

exc = DetailedAiogramError(message="reason")

print(str(exc))
# reason

print(repr(exc))
# DetailedAiogramError()

```

So with 3.x all Sentry records are displayed without `message` :(

```python
logger.error("%s: %s", type(exc).__name__, exc)
# Sentry: `TelegramForbiddenError: TelegramForbiddenError()`
```

### Possible solution

Make 3.x repr behaviour equal to 2.x


Discussion:
github-actions[bot]: # :x: Changelog is required!

You need to add a brief description of the changes to the `CHANGES` directory.

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments)
codecov[bot]: # [Codecov](https://codecov.io/gh/aiogram/aiogram/pull/989?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#989](https://codecov.io/gh/aiogram/aiogram/pull/989?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (027b375) into [dev-3.x](https://codecov.io/gh/aiogram/aiogram/commit/a812a40fd5979422ddebb9b95612833527a2a199?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (a812a40) will **decrease** coverage by `0.01%`.
> The diff coverage is `50.00%`.

> :exclamation: Current head 027b375 differs from pull request most recent head 7e3ff9a. Consider uploading reports for the commit 7e3ff9a to get more accurate results

[![Impacted file tree graph](https://codecov.io/gh/aiogram/aiogram/pull/989/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://codecov.io/gh/aiogram/aiogram/pull/989?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@             Coverage Diff             @@
##           dev-3.x     #989      +/-   ##
===========================================
- Coverage   100.00%   99.98%   -0.02%     
===========================================
  Files          303      303              
  Lines         8216     8218       +2     
===========================================
+ Hits          8216     8217       +1     
- Misses           0        1       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.98% <50.00%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/aiogram/aiogram/pull/989?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/exceptions.py](https://codecov.io/gh/aiogram/aiogram/pull/989/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9leGNlcHRpb25zLnB5) | `98.52% <50.00%> (-1.48%)` | :arrow_down: |


Patch:
diff --git a/CHANGES/985.feature b/CHANGES/985.feature
new file mode 100644
index 00000000..d7f8a052
--- /dev/null
+++ b/CHANGES/985.feature
@@ -0,0 +1,1 @@
+Add PyPy support and run tests under PyPy
diff --git a/CHANGES/988.feature b/CHANGES/988.feature
new file mode 100644
index 00000000..2cec018b
--- /dev/null
+++ b/CHANGES/988.feature
@@ -0,0 +1,1 @@
+Added message text to aiogram exceptions representation
diff --git a/CHANGES/995.misc.rst b/CHANGES/995.misc.rst
new file mode 100644
index 00000000..e47bd739
--- /dev/null
+++ b/CHANGES/995.misc.rst
@@ -0,0 +1,1 @@
+Fixed polling crash when Telegram Bot API raises HTTP 429 status-code.
diff --git a/README.rst b/README.rst
index 312cc63f..3be42b83 100644
--- a/README.rst
+++ b/README.rst
@@ -61,6 +61,7 @@ Features
 
 - Asynchronous (`asyncio docs <https://docs.python.org/3/library/asyncio.html>`_, :pep:`492`)
 - Has type hints (:pep:`484`) and can be used with `mypy <http://mypy-lang.org/>`_
+- Supports `PyPy <https://www.pypy.org/>`_
 - Supports `Telegram Bot API 5.3 <https://core.telegram.org/bots/api>`_
 - Telegram Bot API integration code was `autogenerated <https://github.com/aiogram/tg-codegen>`_ and can be easily re-generated when API gets updated
 - Updates router (Blueprints)
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index eafced6e..52acda8e 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -8,7 +8,7 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Union
 
 from .. import loggers
 from ..client.bot import Bot
-from ..exceptions import TelegramAPIError, TelegramNetworkError, TelegramServerError
+from ..exceptions import TelegramAPIError
 from ..fsm.middleware import FSMContextMiddleware
 from ..fsm.storage.base import BaseEventIsolation, BaseStorage
 from ..fsm.storage.memory import DisabledEventIsolation, MemoryStorage
@@ -183,15 +183,15 @@ class Dispatcher(Router):
         get_updates = GetUpdates(timeout=polling_timeout, allowed_updates=allowed_updates)
         kwargs = {}
         if bot.session.timeout:
-            # Request timeout can be lower than session timeout ant that's OK.
+            # Request timeout can be lower than session timeout and that's OK.
             # To prevent false-positive TimeoutError we should wait longer than polling timeout
             kwargs["request_timeout"] = int(bot.session.timeout + polling_timeout)
         while True:
             try:
                 updates = await bot(get_updates, **kwargs)
-            except (TelegramNetworkError, TelegramServerError) as e:
-                # In cases when Telegram Bot API was inaccessible don't need to stop polling process
-                # because some of developers can't make auto-restarting of the script
+            except Exception as e:
+                # In cases when Telegram Bot API was inaccessible don't need to stop polling
+                # process because some developers can't make auto-restarting of the script
                 loggers.dispatcher.error("Failed to fetch updates - %s: %s", type(e).__name__, e)
                 # And also backoff timeout is best practice to retry any network activity
                 loggers.dispatcher.warning(
@@ -211,8 +211,8 @@ class Dispatcher(Router):
                 yield update
                 # The getUpdates method returns the earliest 100 unconfirmed updates.
                 # To confirm an update, use the offset parameter when calling getUpdates
-                # All updates with update_id less than or equal to offset will be marked as confirmed on the server
-                # and will no longer be returned.
+                # All updates with update_id less than or equal to offset will be marked
+                # as confirmed on the server and will no longer be returned.
                 get_updates.offset = update.update_id + 1
 
     async def _listen_update(self, update: Update, **kwargs: Any) -> Any:
diff --git a/aiogram/exceptions.py b/aiogram/exceptions.py
index 514d7382..5a52670c 100644
--- a/aiogram/exceptions.py
+++ b/aiogram/exceptions.py
@@ -23,6 +23,9 @@ class DetailedAiogramError(AiogramError):
             message += f"\n(background on this error at: {self.url})"
         return message
 
+    def __repr__(self) -> str:
+        return f"{type(self).__name__}('{self}')"
+
 
 class TelegramAPIError(DetailedAiogramError):
     def __init__(


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2dfebe63..3ababc4c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -32,7 +32,6 @@ jobs:
   build:
     strategy:
       fail-fast: false
-      max-parallel: 9
       matrix:
         os:
           - ubuntu-latest
@@ -42,6 +41,8 @@ jobs:
           - '3.8'
           - '3.9'
           - '3.10'
+          - 'pypy3.8'
+          - 'pypy3.9'
 
     defaults:
       # Windows sucks. Force use bash instead of PowerShell
@@ -50,8 +51,22 @@ jobs:
 
     runs-on: ${{ matrix.os }}
 
+    env:
+      # We disable some features for PyPy by this environment variable such as:
+      # – Installation of `fast` extras: `uvloop` on PyPy is useless and may be even slower
+      # than the default loop;
+      # – Coverage reports: code introspection disables any optimizations, so tests with
+      # coverage enabled are very slow on PyPy.
+      # More: https://www.pypy.org/performance.html
+      IS_PYPY: ${{ startswith(matrix.python-version, 'pypy') }}
+      # Windows has also some limitations:
+      # – Redis is not supported on GitHub Windows runners;
+      # – Poetry installer doesn't work on Windows with PyPy.
+      IS_WINDOWS: ${{ startswith(matrix.os, 'windows') }}
+
     steps:
-      - uses: actions/checkout@master
+      - name: Checkout code
+        uses: actions/checkout@master
 
       - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
         uses: actions/setup-python@v4
@@ -60,14 +75,24 @@ jobs:
 
       - name: Install and configure Poetry
         uses: snok/install-poetry@v1
+        if: "env.IS_PYPY == 'false' || env.IS_WINDOWS == 'false'"
         with:
           version: 1.1.11
           virtualenvs-create: true
           virtualenvs-in-project: true
           installer-parallel: true
 
+      - name: Install and configure Poetry (PyPy on Windows)
+        if: "env.IS_PYPY == 'true' && env.IS_WINDOWS == 'true'"
+        run: |
+          set -eu
+          pip install "poetry==1.1.11"
+          poetry config virtualenvs.create true
+          poetry config virtualenvs.in-project true
+          poetry config installer.parallel true
+
       - name: Setup redis
-        if: ${{ matrix.os != 'windows-latest' }}
+        if: ${{ env.IS_WINDOWS == 'false' }}
         uses: shogo82148/actions-setup-redis@v1
         with:
           redis-version: 6
@@ -79,32 +104,29 @@ jobs:
           path: .venv
           key: venv-${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}-${{ secrets.CACHE_VERSION }}
 
-      - name: Project dependencies
+      - name: Install project dependencies
         if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
         run: |
-          poetry install --no-interaction -E fast -E redis -E proxy -E i18n -E docs
+          flags=""
+          [[ "$IS_PYPY" == "false" ]] && flags="$flags -E fast"
+          poetry install --no-interaction -E redis -E proxy -E i18n -E docs $flags
 
       - name: Lint code
         run: |
           poetry run flake8 aiogram
           poetry run mypy aiogram
-
-      - name: Check code-style (Black)
-        run: |
           poetry run black --check --diff aiogram tests
 
-      - name: Run tests (with Redis)
-        if: ${{ matrix.os != 'windows-latest' }}
-        run: |
-          poetry run pytest --cov=aiogram --cov-config .coveragerc --cov-report=xml --redis redis://localhost:6379/0
-
-      - name: Run tests (without Redis)
-        # Redis can't be used on GitHub Windows Runners
-        if: ${{ matrix.os == 'windows-latest' }}
+      - name: Run tests
         run: |
-          poetry run pytest --cov=aiogram --cov-config .coveragerc --cov-report=xml
+          flags=""
+          [[ "$IS_PYPY" == "false" ]] && flags="$flags --cov=aiogram --cov-config .coveragerc --cov-report=xml"
+          [[ "$IS_WINDOWS" == "false" ]] && flags="$flags --redis redis://localhost:6379/0"
+          poetry run pytest $flags
 
-      - uses: codecov/codecov-action@v1
+      - name: Upload coverage data
+        if: "env.IS_PYPY == 'false'"
+        uses: codecov/codecov-action@v1
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           file: coverage.xml
diff --git a/tests/test_api/test_types/test_exception.py b/tests/test_api/test_types/test_exception.py
new file mode 100644
index 00000000..93c82ad6
--- /dev/null
+++ b/tests/test_api/test_types/test_exception.py
@@ -0,0 +1,15 @@
+import pytest
+
+from aiogram.exceptions import DetailedAiogramError
+
+
+class TestException:
+    @pytest.mark.parametrize(
+        "message,result",
+        [
+            ["reason", "DetailedAiogramError('reason')"],
+        ],
+    )
+    def test_representation(self, message: str, result: str):
+        exc = DetailedAiogramError(message=message)
+        assert repr(exc) == result