Repository: attzonko/mmpy_bot (language: python)

Issue:
Disable httpx logging?
The logs for my bot are most filled with `INFO` level messages like
```
[03/08/2024 14:32:48][httpx][INFO] HTTP Request: GET https://mattermost.example/api/v4/channels/hdjdsjkjsyhyhiiid932ls7jcz/posts "HTTP/1.1 200 OK"
```

Is there a way to log only errors?



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 075d3a32..c25c201d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,7 +3,7 @@
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
+    rev: v4.6.0
     hooks:
       - id: end-of-file-fixer
       - id: fix-byte-order-marker
@@ -22,16 +22,22 @@ repos:
           - "--filter-files"
   - repo: https://github.com/psf/black
     # Code style formatting
-    rev: 24.2.0
+    rev: 24.8.0
     hooks:
       - id: black
         exclude: (.*/)*snapshots/
   - repo: https://github.com/PyCQA/flake8
     # Checks the code for PEP8 violations and common pitfals
-    rev: 7.0.0
+    rev: 7.1.1
     hooks:
       - id: flake8
         exclude: (.*/)*snapshots/
+  # Ruff is a "fastar than flake8" linter/formatter
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.6.9
+    hooks:
+      # Run the linter.
+      - id: ruff
   - repo: https://github.com/mattseymour/pre-commit-pytype
     rev: '2023.5.8'
     hooks:
@@ -54,6 +60,6 @@ repos:
           - "88"
   - repo: https://github.com/pycqa/doc8
     # sphinx rst style checker
-    rev: v1.1.1
+    rev: v1.1.2
     hooks:
       - id: doc8
diff --git a/docker-compose.yml b/compose.yaml
similarity index 98%
rename from docker-compose.yml
rename to compose.yaml
index 31cb48c6..bcb0ce4f 100644
--- a/docker-compose.yml
+++ b/compose.yaml
@@ -1,5 +1,3 @@
-version: "3.7"
-
 services:
   app:
     # This docker-compose file specifies a mmpy_bot
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 9e608516..9ca0ad24 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,9 +1,9 @@
-black==24.2.0
+black==24.4.2
 docformatter==1.7.5
-filelock==3.13.1
+filelock==3.15.4
 isort==5.13.2
-flake8==7.0.0
-pytest==8.0.1
-pytest-xdist==3.5.0
-pytype==2024.2.13
+flake8==7.1.0
+pytest==8.2.2
+pytest-xdist==3.6.1
+pytype==2024.4.11
 snapshottest==0.6.0
diff --git a/mmpy_bot/bot.py b/mmpy_bot/bot.py
index 6a5b5f69..e9d917bf 100644
--- a/mmpy_bot/bot.py
+++ b/mmpy_bot/bot.py
@@ -80,7 +80,9 @@ def _register_logger(self):
             **{
                 "format": self.settings.LOG_FORMAT,
                 "datefmt": self.settings.LOG_DATE_FORMAT,
-                "level": logging.DEBUG if self.settings.DEBUG else logging.INFO,
+                "level": (
+                    logging.DEBUG if self.settings.DEBUG else self.settings.LOG_LEVEL
+                ),
                 "filename": self.settings.LOG_FILE,
                 "filemode": "w",
             }
diff --git a/mmpy_bot/driver.py b/mmpy_bot/driver.py
index cb78bc17..fa5f7eec 100644
--- a/mmpy_bot/driver.py
+++ b/mmpy_bot/driver.py
@@ -43,7 +43,7 @@ def create_post(
         message: str,
         file_paths: Optional[Sequence[str]] = None,
         root_id: str = "",
-        props: Dict = {},
+        props: Optional[Dict] = None,
         ephemeral_user_id: Optional[str] = None,
     ):
         """Create a post in the specified channel with the specified text.
@@ -52,8 +52,8 @@ def create_post(
         paths are specified, those files will be uploaded to mattermost first and then
         attached.
         """
-        if file_paths is None:
-            file_paths = []
+        file_paths = file_paths or []
+        props = props or {}
 
         file_ids = (
             self.upload_files(file_paths, channel_id) if len(file_paths) > 0 else []
@@ -88,9 +88,9 @@ def get_post_thread(self, post_id: str):
         duplicate and wrongly ordered entries in the ordered list."""
         thread_info = self.posts.get_post_thread(post_id)
 
-        id_stamps = []
-        for id, post in thread_info["posts"].items():
-            id_stamps.append((id, int(post["create_at"])))
+        id_stamps = (
+            (id, int(post["create_at"])) for id, post in thread_info["posts"].items()
+        )
         # Sort the posts by their timestamps
         sorted_stamps = sorted(id_stamps, key=lambda x: x[-1])
         # Overwrite the order with the sorted list
@@ -116,7 +116,7 @@ def reply_to(
         message: Message,
         response: str,
         file_paths: Optional[Sequence[str]] = None,
-        props: Dict = {},
+        props: Optional[Dict] = None,
         ephemeral: bool = False,
         direct: bool = False,
     ):
@@ -127,8 +127,8 @@ def reply_to(
 
         Also supports replying privately by setting direct=True.
         """
-        if file_paths is None:
-            file_paths = []
+        file_paths = file_paths or []
+        props = props or {}
 
         if direct and not message.is_direct_message:
             # NOTE we explicitly don't pass root_id as it would refer to a
@@ -160,7 +160,7 @@ def direct_message(
         message: str,
         file_paths: Optional[Sequence[str]] = None,
         root_id: str = "",
-        props: Dict = {},
+        props: Optional[Dict] = None,
         ephemeral_user_id: Optional[str] = None,
     ):
         # Private/direct messages are sent to a special channel that
@@ -168,6 +168,7 @@ def direct_message(
         direct_id = self.channels.create_direct_channel([self.user_id, receiver_id])[
             "id"
         ]
+        props = props or {}
 
         return self.create_post(
             channel_id=direct_id,
@@ -199,22 +200,20 @@ def upload_files(
     ) -> List[str]:
         """Given a list of file paths and the channel id, uploads the corresponding
         files and returns a list their internal file IDs."""
-        file_list = []
-        for path in file_paths:
-            path = Path(path)
-            # Note: 'files' should be a name of an expected attribute in the body
-            # but seems to be ignored when simply uploading files to mattermost
-            file_list.append(
+        # Note: 'files' should be a name of an expected attribute in the body
+        # but seems to be ignored when simply uploading files to mattermost
+        file_list = [
+            (
+                "files",
                 (
-                    "files",
-                    (
-                        path.name,
-                        Path(path).read_bytes(),
-                    ),
-                )
+                    Path(path).name,
+                    Path(path).read_bytes(),
+                ),
             )
+            for path in file_paths
+        ]
 
         result = self.files.upload_file(
             files=file_list, data={"channel_id": channel_id}
         )
-        return list(info["id"] for info in result["file_infos"])
+        return [info["id"] for info in result["file_infos"]]
diff --git a/mmpy_bot/event_handler.py b/mmpy_bot/event_handler.py
index 7d70466a..e719932c 100644
--- a/mmpy_bot/event_handler.py
+++ b/mmpy_bot/event_handler.py
@@ -13,7 +13,7 @@
 log = logging.getLogger("mmpy.event_handler")
 
 
-class EventHandler(object):
+class EventHandler:
     def __init__(
         self,
         driver: Driver,
@@ -37,10 +37,8 @@ def start(self):
     def _should_ignore(self, message: Message):
         # Ignore message from senders specified in settings, and maybe from ourself
         return (
-            True
-            if message.sender_name.lower()
+            message.sender_name.lower()
             in (name.lower() for name in self.settings.IGNORE_USERS)
-            else False
         ) or (self.ignore_own_messages and message.sender_name == self.driver.username)
 
     async def _check_queue_loop(self, webhook_queue: queue.Queue):
@@ -75,37 +73,34 @@ async def _handle_post(self, post):
 
         # Find all the listeners that match this message, and have their plugins handle
         # the rest.
-        tasks = []
-        for matcher, functions in self.plugin_manager.message_listeners.items():
-            match = matcher.search(message.text)
-            if match:
-                groups = list([group for group in match.groups() if group != ""])
-                for function in functions:
-                    # Create an asyncio task to handle this callback
-                    tasks.append(
-                        asyncio.create_task(
-                            function.plugin.call_function(
-                                function, message, groups=groups
-                            )
-                        )
-                    )
+        tasks = [
+            asyncio.create_task(
+                function.plugin.call_function(
+                    function,
+                    message,
+                    groups=[group for group in match.groups() if group != ""],
+                )
+            )
+            for matcher, functions in self.plugin_manager.message_listeners.items()
+            if (match := matcher.search(message.text))
+            for function in functions
+        ]
+
         # Execute the callbacks in parallel
         asyncio.gather(*tasks)
 
     async def _handle_webhook(self, event: WebHookEvent):
         # Find all the listeners that match this webhook id, and have their plugins
         # handle the rest.
-        tasks = []
-        for matcher, functions in self.plugin_manager.webhook_listeners.items():
-            match = matcher.search(event.webhook_id)
-            if match:
-                for function in functions:
-                    # Create an asyncio task to handle this callback
-                    tasks.append(
-                        asyncio.create_task(
-                            function.plugin.call_function(function, event)
-                        )
-                    )
+
+        tasks = [
+            # Create an asyncio task to handle this callback
+            asyncio.create_task(function.plugin.call_function(function, event))
+            for matcher, functions in self.plugin_manager.webhook_listeners.items()
+            if matcher.search(event.webhook_id)
+            for function in functions
+        ]
+
         # If this webhook doesn't correspond to any listeners, signal the WebHookServer
         # to not wait for any response
         if len(tasks) == 0:
diff --git a/mmpy_bot/function.py b/mmpy_bot/function.py
index 1149d526..c738937e 100644
--- a/mmpy_bot/function.py
+++ b/mmpy_bot/function.py
@@ -43,7 +43,7 @@ def __init__(
             )
 
         self.function = function
-        self.is_coroutine = asyncio.iscoroutinefunction(function)
+        self.is_coroutine = inspect.iscoroutinefunction(function)
         self.is_click_function: bool = False
         self.matcher = matcher
         self.metadata = metadata
@@ -100,7 +100,7 @@ def __init__(
 
         if self.is_click_function:
             _function = self.function.callback
-            if asyncio.iscoroutinefunction(_function):
+            if inspect.iscoroutinefunction(_function):
                 raise ValueError(
                     "Combining click functions and coroutines is currently not supported!"
                     " Consider using a regular function, which will be threaded by default."
@@ -155,10 +155,10 @@ def __call__(self, message: Message, *args):
             assert len(args) <= 1  # There is only one group, (.*)?
             if len(args) == 1:
                 # Turn space-separated string into list
-                args = tuple(shlex.split(args[0]))
+                args = shlex.split(args[0])
             try:
                 ctx = self.function.make_context(
-                    info_name=self.plugin.__class__.__name__, args=list(args)
+                    info_name=self.plugin.__class__.__name__, args=args
                 )
                 ctx.params.update({"self": self.plugin, "message": message})
                 return self.function.invoke(ctx)
diff --git a/mmpy_bot/settings.py b/mmpy_bot/settings.py
index 7ce9aa24..628157d8 100644
--- a/mmpy_bot/settings.py
+++ b/mmpy_bot/settings.py
@@ -1,4 +1,5 @@
 import collections
+import logging
 import os
 import warnings
 from dataclasses import dataclass, field, fields
@@ -8,9 +9,7 @@
 def _get_comma_separated_list(string: str, type=str):
     values = string.split(",")
     # Convert to the specified type if necessary.
-    if type is not str:
-        values = list([type(value) for value in values])
-    return values
+    return values if type is str else [type(value) for value in values]
 
 
 def _is_valid_option(_type, valid_types):
@@ -59,6 +58,7 @@ class Settings:
     DEBUG: bool = False
     # Respond to channel message "!help" (without @bot)
     RESPOND_CHANNEL_HELP: bool = False
+    LOG_LEVEL: int = logging.INFO
     LOG_FILE: Optional[str] = None
     LOG_FORMAT: str = "[%(asctime)s][%(name)s][%(levelname)s] %(message)s"
     LOG_DATE_FORMAT: str = "%m/%d/%Y %H:%M:%S"
@@ -91,6 +91,13 @@ def __post_init__(self):
             )
             self.MATTERMOST_API_PATH = self.MATTERMOST_API_PATH[: -len(api_url)]
 
+        if self.DEBUG:
+            warnings.warn(
+                "DEBUG has been deprecated and will be removed in a future release. "
+                "Set LOG_LEVEL to logging.DEBUG to increase verbosity.",
+                DeprecationWarning,
+            )
+
     def _check_environment_variables(self):
         for f in fields(self):
             if f.name in os.environ:
diff --git a/mmpy_bot/wrappers.py b/mmpy_bot/wrappers.py
index 7a66bac7..58a459ee 100644
--- a/mmpy_bot/wrappers.py
+++ b/mmpy_bot/wrappers.py
@@ -38,6 +38,10 @@ def channel_id(self):
     def channel_name(self):
         return self.body["data"]["channel_name"]
 
+    @cached_property
+    def file_ids(self):
+        return self.body["data"]["post"].get("file_ids", [])
+
     @cached_property
     def is_direct_message(self):
         return self.body["data"]["channel_type"] == "D"
diff --git a/requirements.txt b/requirements.txt
index 63651b5b..f3f34574 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
-aiohttp>=3.7.4.post0
-click>=7.0
-mattermostautodriver~=1.3.0
-schedule>=0.6.0
+httpx>=0.27.0,<0.28.0
+aiohttp>=3.9.5
+click>=8.1.7
+mattermostautodriver>=2.0.0
+schedule>=1.2.2


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 28655ec4..25bff94a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -58,9 +58,9 @@ jobs:
         run: pip install -e .[dev]
       - name: Launch test server
         working-directory: tests/integration_tests
-        run: docker-compose up -d && sleep 30
-      - name: Print docker info
-        run: docker ps -a
+        run: docker compose up -d && sleep 30
+      - name: Print docker services info
+        run: docker compose ps -a
       - name: Run integration tests
         working-directory: tests/integration_tests
         run: pytest . -vv -n auto
diff --git a/tests/integration_tests/docker-compose.yml b/tests/integration_tests/compose.yaml
similarity index 91%
rename from tests/integration_tests/docker-compose.yml
rename to tests/integration_tests/compose.yaml
index 111a3d88..e12285c3 100644
--- a/tests/integration_tests/docker-compose.yml
+++ b/tests/integration_tests/compose.yaml
@@ -1,5 +1,3 @@
-version: "3.7"
-
 services:
   app:
     container_name: "mattermost-bot-test"
diff --git a/tests/integration_tests/test_direct_message_plugin.py b/tests/integration_tests/test_direct_message_plugin.py
index db73f5f6..c6e548b0 100644
--- a/tests/integration_tests/test_direct_message_plugin.py
+++ b/tests/integration_tests/test_direct_message_plugin.py
@@ -2,7 +2,7 @@
 import time
 from string import ascii_letters
 
-from .utils import start_bot  # noqa, only imported so that the bot is started
+from .utils import start_bot  # noqa: F401, only imported so that the bot is started
 from .utils import MAIN_BOT_ID, OFF_TOPIC_ID, RESPONSE_TIMEOUT, TEAM_ID
 from .utils import driver as driver_fixture
 from .utils import expect_reply
diff --git a/tests/integration_tests/test_example_plugin.py b/tests/integration_tests/test_example_plugin.py
index 7ebf8891..e731ff9d 100644
--- a/tests/integration_tests/test_example_plugin.py
+++ b/tests/integration_tests/test_example_plugin.py
@@ -1,6 +1,6 @@
 import time
 
-from .utils import start_bot  # noqa, only imported so that the bot is started
+from .utils import start_bot  # noqa: F401, only imported so that the bot is started
 from .utils import MAIN_BOT_ID, OFF_TOPIC_ID, RESPONSE_TIMEOUT, TEAM_ID
 from .utils import driver as driver_fixture
 from .utils import expect_reply
diff --git a/tests/integration_tests/test_webhook_example.py b/tests/integration_tests/test_webhook_example.py
index ebb0c4d8..338d3c34 100644
--- a/tests/integration_tests/test_webhook_example.py
+++ b/tests/integration_tests/test_webhook_example.py
@@ -1,7 +1,7 @@
 import asyncio
 import time
 
-from .utils import start_bot  # noqa, only imported so that the bot is started
+from .utils import start_bot  # noqa: F401, only imported so that the bot is started
 from .utils import OFF_TOPIC_ID, RESPONSE_TIMEOUT
 from .utils import driver as driver_fixture
 from .utils import expect_reply
diff --git a/tests/integration_tests/utils.py b/tests/integration_tests/utils.py
index 589bce96..fe8a589c 100644
--- a/tests/integration_tests/utils.py
+++ b/tests/integration_tests/utils.py
@@ -34,7 +34,7 @@ def expect_reply(driver: Driver, post: Dict, wait=RESPONSE_TIMEOUT, retries=1):
             reply = thread_info["posts"][reply_id]
             break
 
-    if not reply:
+    if reply is None:
         raise ValueError("Expected a response, but didn't get any!")
 
     return reply
diff --git a/tests/unit_tests/bot_test.py b/tests/unit_tests/bot_test.py
index 7d2f076f..97a1d59d 100644
--- a/tests/unit_tests/bot_test.py
+++ b/tests/unit_tests/bot_test.py
@@ -1,3 +1,4 @@
+import logging
 from unittest import mock
 
 import pytest
@@ -12,7 +13,7 @@
 def bot():
     # Patch login to avoid sending requests to the internet
     with mock.patch("mmpy_bot.driver.Driver.login") as login:
-        bot = Bot(plugins=[ExamplePlugin()], settings=Settings(DEBUG=True))
+        bot = Bot(plugins=[ExamplePlugin()], settings=Settings(LOG_LEVEL=logging.DEBUG))
         login.assert_called_once()
         yield bot
         bot.stop()  # if the bot was started, stop it