Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-django-health-check for 
openSUSE:Factory checked in at 2026-04-16 17:25:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-django-health-check (Old)
 and      /work/SRC/openSUSE:Factory/.python-django-health-check.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-django-health-check"

Thu Apr 16 17:25:53 2026 rev:15 rq:1347168 version:4.3.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-django-health-check/python-django-health-check.changes
    2026-04-11 22:32:36.395684835 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-django-health-check.new.11940/python-django-health-check.changes
 2026-04-16 17:26:18.269788546 +0200
@@ -1,0 +2,7 @@
+Wed Apr 15 20:22:23 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.3.0:
+  * Ref #701 -- Add support for a custom executor for synchronous
+    checks
+
+-------------------------------------------------------------------

Old:
----
  django-health-check-4.2.2.tar.gz

New:
----
  django-health-check-4.3.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-django-health-check.spec ++++++
--- /var/tmp/diff_new_pack.DqSd3K/_old  2026-04-16 17:26:18.949816530 +0200
+++ /var/tmp/diff_new_pack.DqSd3K/_new  2026-04-16 17:26:18.949816530 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-django-health-check
-Version:        4.2.2
+Version:        4.3.0
 Release:        0
 Summary:        Run checks on Django and is dependent services
 License:        MIT

++++++ django-health-check-4.2.2.tar.gz -> django-health-check-4.3.0.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/django-health-check-4.2.2/.github/workflows/release.yml 
new/django-health-check-4.3.0/.github/workflows/release.yml
--- old/django-health-check-4.2.2/.github/workflows/release.yml 2026-04-09 
18:03:15.000000000 +0200
+++ new/django-health-check-4.3.0/.github/workflows/release.yml 2026-04-15 
19:08:10.000000000 +0200
@@ -11,7 +11,7 @@
       - uses: actions/checkout@v6
       - uses: astral-sh/setup-uv@v7
       - run: uv run mkdocs build
-      - uses: actions/upload-pages-artifact@v4
+      - uses: actions/upload-pages-artifact@v5
         with:
           path: site
   gh-page-deploy:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/.pre-commit-config.yaml 
new/django-health-check-4.3.0/.pre-commit-config.yaml
--- old/django-health-check-4.2.2/.pre-commit-config.yaml       2026-04-09 
18:03:15.000000000 +0200
+++ new/django-health-check-4.3.0/.pre-commit-config.yaml       2026-04-15 
19:08:10.000000000 +0200
@@ -36,7 +36,7 @@
           - mdformat-gfm-alerts
         exclude: '.github/agents/'
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.15.9
+    rev: v0.15.10
     hooks:
       - id: ruff-check
         args: [--fix, --exit-non-zero-on-fix]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/docs/usage.md 
new/django-health-check-4.3.0/docs/usage.md
--- old/django-health-check-4.2.2/docs/usage.md 2026-04-09 18:03:15.000000000 
+0200
+++ new/django-health-check-4.3.0/docs/usage.md 2026-04-15 19:08:10.000000000 
+0200
@@ -141,3 +141,37 @@
 
 Similar to the http version, a critical error will cause the command to
 quit with the exit code `1`.
+
+## Performance tweaks
+
+All checks are executed asynchronously, either via `asyncio` or via a thread 
pool,
+depending on the implementation of the individual checks.
+This allows for concurrent execution of the IO-bound checks,
+which reduces the response time.
+
+The event loop's default executor is used to run synchronous checks
+(e.g. [Database][health_check.checks.Database], 
[Mail][health_check.checks.Mail],
+or [Storage][health_check.checks.Storage]) in a thread pool.
+This pool is usually persisted across requests. This may lead to high 
performance while
+permanently allocating more memory. This may be undesirable for some 
applications,
+especially with `S3Storage`, which uses thread-local connections.
+
+This can be mitigated by using a custom executor that creates a new
+thread pool for each request, which is then cleaned up after the checks
+are completed. This can be achieved by subclassing `HealthCheckView`
+and overriding the `get_executor` method to return a context manager
+providing a new `ThreadPoolExecutor` instance for each request.
+
+```python
+from concurrent.futures import ThreadPoolExecutor
+from health_check.views import HealthCheckView
+
+
+class CustomHealthCheckView(HealthCheckView):
+    def get_executor(self):
+        return ThreadPoolExecutor(max_workers=len(self.checks))
+```
+
+This approach ensures that each request gets a fresh thread pool,
+which can help manage memory usage more effectively
+while still providing the benefits of concurrent execution for synchronous 
checks.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/health_check/base.py 
new/django-health-check-4.3.0/health_check/base.py
--- old/django-health-check-4.2.2/health_check/base.py  2026-04-09 
18:03:15.000000000 +0200
+++ new/django-health-check-4.3.0/health_check/base.py  2026-04-15 
19:08:10.000000000 +0200
@@ -6,6 +6,7 @@
 import inspect
 import logging
 import timeit
+from concurrent.futures import Executor
 
 from health_check.exceptions import HealthCheckException
 
@@ -70,16 +71,16 @@
         ...
 
     def pretty_status(self) -> str:
-        """Return human-readable status string, always 'OK' for the check 
itself."""
+        """Return a human-readable status string, always 'OK' for the check 
itself."""
         return "OK"
 
-    async def get_result(self: HealthCheck) -> HealthCheckResult:
+    async def get_result(self, executor: Executor | None = None) -> 
HealthCheckResult:
         loop = asyncio.get_running_loop()
         start = timeit.default_timer()
         try:
             await self.run() if inspect.iscoroutinefunction(
                 self.run
-            ) else await loop.run_in_executor(None, self.run)
+            ) else await loop.run_in_executor(executor, self.run)
         except HealthCheckException as e:
             error = e
         except BaseException:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/health_check/checks.py 
new/django-health-check-4.3.0/health_check/checks.py
--- old/django-health-check-4.2.2/health_check/checks.py        2026-04-09 
18:03:15.000000000 +0200
+++ new/django-health-check-4.3.0/health_check/checks.py        2026-04-15 
19:08:10.000000000 +0200
@@ -194,11 +194,11 @@
 @dataclasses.dataclass
 class Mail(HealthCheck):
     """
-    Check that mail backend is able to open and close connection.
+    Check that an email backend is able to open and close the connection.
 
     Args:
         backend: The email backend to test against.
-        timeout: Timeout for connection to mail server in seconds.
+        timeout: Timeout for connection to an email server in seconds.
 
     """
 
@@ -231,7 +231,7 @@
     """
     Check file storage backends by saving, reading, and deleting a test file.
 
-    It can be setup multiple times for different storage backends if needed.
+    It can be set up multiple times for different storage backends if needed.
 
     Args:
         alias: The alias of the storage backend to check.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/health_check/views.py 
new/django-health-check-4.3.0/health_check/views.py
--- old/django-health-check-4.2.2/health_check/views.py 2026-04-09 
18:03:15.000000000 +0200
+++ new/django-health-check-4.3.0/health_check/views.py 2026-04-15 
19:08:10.000000000 +0200
@@ -1,7 +1,9 @@
 import asyncio
+import contextlib
 import datetime
 import re
 import typing
+from concurrent.futures import Executor
 
 from django.db import transaction
 from django.http import HttpResponse, JsonResponse
@@ -111,9 +113,10 @@
 
     @method_decorator(never_cache)
     async def get(self, request, *args, **kwargs):
-        self.results = await asyncio.gather(
-            *(check.get_result() for check in self.get_checks())
-        )
+        with self.get_executor() as executor:
+            self.results = await asyncio.gather(
+                *(check.get_result(executor) for check in self.get_checks())
+            )
         has_errors = any(result.error for result in self.results)
         status_code = 500 if has_errors else 200
         format_override = request.GET.get("format")
@@ -159,6 +162,20 @@
             "errors": any(result.error for result in self.results),
         }
 
+    def get_executor(self) -> contextlib.AbstractContextManager[Executor | 
None]:
+        """
+        Return a context manager providing an executor for synchronous checks.
+
+        Return a context manager that yields ``None`` to use the event loop's
+        default executor.
+
+        Example:
+            def get_executor(self):
+                return ThreadPoolExecutor(max_workers=5)
+
+        """
+        return contextlib.nullcontext(None)
+
     def render_to_response_json(self, status):
         """Return JSON response with health check results."""
         return JsonResponse(
@@ -170,7 +187,7 @@
         )
 
     def render_to_response_text(self, status):
-        """Return plain text response with health check results."""
+        """Return a plain text response with health check results."""
         lines = (
             f"{repr(result.check)}: {'OK' if not result.error else 
str(result.error)}"
             for result in self.results
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/mkdocs.yml 
new/django-health-check-4.3.0/mkdocs.yml
--- old/django-health-check-4.2.2/mkdocs.yml    2026-04-09 18:03:15.000000000 
+0200
+++ new/django-health-check-4.3.0/mkdocs.yml    2026-04-15 19:08:10.000000000 
+0200
@@ -30,8 +30,8 @@
             - https://www.psycopg.org/psycopg3/docs/objects.inv
             - https://docs.celeryq.dev/en/stable/objects.inv
             - https://psutil.readthedocs.io/en/stable/objects.inv
-            - https://django-storages.readthedocs.io/en/latest/objects.inv
             - https://redis.readthedocs.io/en/stable/objects.inv
+            - https://django-storages.readthedocs.io/en/stable/objects.inv
 theme:
   name: material
   logo: images/icon.svg
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/tests/test_base.py 
new/django-health-check-4.3.0/tests/test_base.py
--- old/django-health-check-4.2.2/tests/test_base.py    2026-04-09 
18:03:15.000000000 +0200
+++ new/django-health-check-4.3.0/tests/test_base.py    2026-04-15 
19:08:10.000000000 +0200
@@ -1,3 +1,6 @@
+import asyncio
+from unittest.mock import MagicMock, patch
+
 import pytest
 
 from health_check.base import HealthCheck, HealthCheckResult
@@ -47,6 +50,36 @@
         assert isinstance(result, HealthCheckResult)
 
     @pytest.mark.asyncio
+    async def test_run__sync_check_uses_custom_executor(self):
+        """Pass custom executor to run_in_executor for synchronous checks."""
+
+        class SyncCheck(HealthCheck):
+            def run(self):
+                pass
+
+        check = SyncCheck()
+        custom_executor = MagicMock()
+        loop = asyncio.get_running_loop()
+        with patch.object(loop, "run_in_executor", wraps=loop.run_in_executor) 
as mock:
+            await check.get_result(executor=custom_executor)
+        mock.assert_called_once_with(custom_executor, check.run)
+
+    @pytest.mark.asyncio
+    async def test_run__sync_check_default_executor(self):
+        """Use default executor (None) for synchronous checks when none is 
supplied."""
+
+        class SyncCheck(HealthCheck):
+            def run(self):
+                pass
+
+        check = SyncCheck()
+        loop = asyncio.get_running_loop()
+        with patch.object(loop, "run_in_executor", wraps=loop.run_in_executor) 
as mock:
+            result = await check.get_result()
+        mock.assert_called_once_with(None, check.run)
+        assert result.error is None
+
+    @pytest.mark.asyncio
     async def test_result__timing(self):
         """Result includes execution time."""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/django-health-check-4.2.2/tests/test_views.py 
new/django-health-check-4.3.0/tests/test_views.py
--- old/django-health-check-4.2.2/tests/test_views.py   2026-04-09 
18:03:15.000000000 +0200
+++ new/django-health-check-4.3.0/tests/test_views.py   2026-04-15 
19:08:10.000000000 +0200
@@ -1038,3 +1038,36 @@
         assert ": OK" in content
         assert "FailingBackend" in content
         assert "Failed" in content
+
+    def test_get_executor__returns_null_context(self):
+        """Default get_executor returns a nullcontext yielding None."""
+        import contextlib
+
+        view = HealthCheckView()
+        ctx = view.get_executor()
+        assert isinstance(ctx, contextlib.AbstractContextManager)
+        with ctx as executor:
+            assert executor is None
+
+    @pytest.mark.asyncio
+    async def test_get_executor__custom_thread_pool(self):
+        """Custom get_executor returning a ThreadPoolExecutor is used for sync 
checks."""
+        from concurrent.futures import ThreadPoolExecutor
+
+        from django.test import AsyncRequestFactory
+
+        class CustomHealthCheckView(HealthCheckView):
+            def get_executor(self):
+                return ThreadPoolExecutor(max_workers=1)
+
+        class SyncCheck(HealthCheck):
+            def run(self):
+                pass
+
+        factory = AsyncRequestFactory()
+        request = factory.get("/")
+        view = CustomHealthCheckView.as_view(checks=[SyncCheck])
+        response = await view(request)
+        if hasattr(response, "render"):
+            response.render()
+        assert response.status_code == 200

Reply via email to