Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ on:
jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version:
Expand All @@ -22,7 +21,9 @@ jobs:
- "3.10"
- "3.11"
- "3.12"

- "3.13"
- "3.14"
runs-on: "${{ matrix.python-version == '3.7' && 'ubuntu-22.04' || 'ubuntu-latest' }}"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -36,7 +37,7 @@ jobs:
python -m pip install --upgrade pip setuptools
poetry install

- if: ${{ matrix.python-version == '3.11' }}
- if: ${{ matrix.python-version == '3.14' }}
name: Lint
run: |
poetry run make lint
Expand All @@ -45,7 +46,12 @@ jobs:
run: |
poetry run make test-all

- if: ${{ matrix.python-version == '3.11' }}
- if: ${{ matrix.python-version == '3.14' }}
name: Create coverage report
run: |
poetry run make coverage

- if: ${{ matrix.python-version == '3.14' }}
name: Code Coverage Summary Report
uses: irongut/CodeCoverageSummary@v1.0.2
with:
Expand Down
9 changes: 8 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
History
=======

1.0.3 (2026-02-12)
------------------

* Add support for Django 5.1, Django 5.2 and Django 6.0
* Fix support for django-health-check

1.0.2 (2024-01-08)
------------------

* Add support for django 5
* Add support for Django 5

1.0.1 (2023-01-13)
------------------
Expand Down
2,644 changes: 2,230 additions & 414 deletions poetry.lock

Large diffs are not rendered by default.

14 changes: 6 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "tg-utils"
version = "1.0.2"
version = "1.0.3"
description = "Common utils for Django-based projects."
authors = ["Thorgate <code@thorgate.eu>"]
license = "ISCL"
Expand Down Expand Up @@ -33,7 +33,7 @@ python = ">=3.7.2,<4"

django = ">=2.2"

django-health-check = { version = "*", optional = true }
django-health-check = { version = "<4", optional = true }
hashids = { version = "*", optional = true }
psutil = { version = "*", optional = true }
python-redis-lock = { version = "*", optional = true }
Expand All @@ -48,17 +48,15 @@ coveralls = "*"
pytest-cov = "*"
pytest-django = "*"
pytest-xdist = "*"
black = "==22.8.0"
prospector = "^1.7.0"
# pylint 2.15 is inconsitently crashing for some reason
# Ref: https://github.com/PyCQA/pylint-django/issues/370
pylint = "==2.14.*"
black = "*"
prospector = "*"
pylint = "*"
sphinx = "==3.*"
tox = "*"
tox-gh-actions = "*"

django-compressor = "*"
django-health-check = "*"
django-health-check = "<4"
psutil = "*"
hashids = "*"
python-redis-lock = "*"
Expand Down
3 changes: 3 additions & 0 deletions tests/test_importing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ def test_imports():
from tg_utils import signals
from tg_utils import uuid
from tg_utils import decorators
from tg_utils.health_check.checks.celery_beat import apps
from tg_utils.health_check.checks.elvis import apps
from tg_utils.health_check.checks.phantomjs import apps
1 change: 0 additions & 1 deletion tg_utils/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from django.contrib.admin.options import InlineModelAdmin
from django.forms.models import BaseInlineFormSet


# Select correct has_add_permission signature based on Django version
if django.VERSION >= (2, 1):

Expand Down
2 changes: 1 addition & 1 deletion tg_utils/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from django.core.mail import EmailMessage
from django.template.loader import render_to_string


logger = logging.getLogger("tg_utils.email")


# pylint: disable-next=too-many-positional-arguments
def send_email(
rcpt_email, email_subject, template_name, template_vars, from_email=None, html=True
):
Expand Down
10 changes: 7 additions & 3 deletions tg_utils/health_check/base_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import requests
from django.conf import settings
from health_check.backends import BaseHealthCheckBackend
from health_check.exceptions import ServiceReturnedUnexpectedResult, ServiceUnavailable

try:
from health_check.backends import HealthCheck
except ImportError:
from health_check.backends import BaseHealthCheckBackend as HealthCheck

from health_check.exceptions import ServiceReturnedUnexpectedResult, ServiceUnavailable

logger = logging.getLogger("health")

Expand All @@ -22,7 +26,7 @@ def requests_timeout(self):
return self.settings.get("REQUESTS_TIMEOUT", 5)


class HTTPBasedHealthCheck(BaseHealthCheckBackend, HealthCheckSettingsMixin):
class HTTPBasedHealthCheck(HealthCheck, HealthCheckSettingsMixin):
"""Helper class for any health check that check status of HTTP based service"""

expected_status_code = 200
Expand Down
35 changes: 18 additions & 17 deletions tg_utils/health_check/checks/celery_beat/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,23 @@ def ready(self):

# Set initial timestamp not to fail the health-check before it runs for the first time
# Don't use apply_async - otherwise, the whole initialisation will fail if celery fails
try:
timestamp_task()

getattr(app_module, class_name).add_periodic_task(
TIMEOUT,
timestamp_task.signature((), **signature_extra_kwargs),
name="Celery health check beat",
)
except Exception:
# This is likely issue with the cache or with celery broker connection. Handle any exception not to let the
# whole app go down - depending on cache and broker backend, different exceptions can be encountered here
#
# Don't try to recover from this error, health-check will be failing unless the issue is resolved.
#
# Don't log the exception since sometimes it is expected to fail here - i.e. when running some management
# command that does not expect all the infrastructure to work
pass
if timestamp_task is not None:
try:
timestamp_task()

getattr(app_module, class_name).add_periodic_task(
TIMEOUT,
timestamp_task.signature((), **signature_extra_kwargs),
name="Celery health check beat",
)
except Exception:
# This is likely issue with the cache or with celery broker connection. Handle any exception not to let the
# whole app go down - depending on cache and broker backend, different exceptions can be encountered here
#
# Don't try to recover from this error, health-check will be failing unless the issue is resolved.
#
# Don't log the exception since sometimes it is expected to fail here - i.e. when running some management
# command that does not expect all the infrastructure to work
pass

plugin_dir.register(CeleryBeatHealthCheck)
6 changes: 2 additions & 4 deletions tg_utils/health_check/checks/celery_beat/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
from django.core.cache import cache
from django.utils import timezone

from health_check.backends import BaseHealthCheckBackend
from health_check.exceptions import ServiceUnavailable
from tg_utils.health_check.base_checks import HealthCheckSettingsMixin

from tg_utils.health_check.base_checks import HealthCheckSettingsMixin, HealthCheck

CACHE_KEY = "CELERY_BEAT_HEALTH_CHECK_TIME_STAMP"
TIMEOUT = getattr(settings, "HEALTH_CHECK", {}).get("CELERY_BEAT_CHECK_INTERVAL", 60)
Expand All @@ -14,7 +12,7 @@
)


class CeleryBeatHealthCheck(BaseHealthCheckBackend, HealthCheckSettingsMixin):
class CeleryBeatHealthCheck(HealthCheck, HealthCheckSettingsMixin):
def check_status(self):
last_beat_time = cache.get(CACHE_KEY, None)
if last_beat_time is None:
Expand Down
23 changes: 22 additions & 1 deletion tg_utils/health_check/checks/celery_beat/tasks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
from django.core.cache import cache
from django.utils import timezone

from celery import shared_task # pylint: disable=import-error
import warnings
import functools

try:
from celery import shared_task # pylint: disable=import-error
except ImportError:

def shared_task(*args, **kwargs):
def decorator(func):
@functools.wraps(func)
def fake_task():
warnings.warn(
"It seems that you do not have celery installed. "
"Celery beat health check will always report error.",
RuntimeWarning,
)

return fake_task

return decorator


from tg_utils.health_check.checks.celery_beat.backends import CACHE_KEY


Expand Down
1 change: 0 additions & 1 deletion tg_utils/health_check/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from tg_utils.health_check.views import HealthCheckViewProtected, HealthCheckViewMinimal


urlpatterns = [
path(
"health/detail/", HealthCheckViewProtected.as_view(), name="health-check-detail"
Expand Down
1 change: 0 additions & 1 deletion tg_utils/health_check/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.http import JsonResponse, HttpResponseNotFound, HttpResponseForbidden
from health_check.views import MainView


logger = logging.getLogger("health")


Expand Down
4 changes: 2 additions & 2 deletions tg_utils/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import redis_lock
from redis import StrictRedis


REDIS_LOCK_URL = getattr(settings, "REDIS_LOCK_URL", None)

DEFAULT_PREFIX = getattr(settings, "REDIS_LOCK_DEFAULT_PREFIX", "acquires_lock")
Expand All @@ -39,6 +38,7 @@ def get_lock(resource, expires):
return redis_lock.Lock(get_redis_connection(), resource, expire=expires)


# pylint: disable-next=too-many-positional-arguments
def acquires_lock(
expires,
should_fail=True,
Expand Down Expand Up @@ -103,7 +103,7 @@ def wrapper(*args, **kwargs):

# Get default lock blocking mode
# Copying to local variable so original variable would not be touched
nonlocal should_wait
nonlocal should_wait # noqa: F999
is_blocking = should_wait

should_execute_if_lock_fails = False
Expand Down
1 change: 0 additions & 1 deletion tg_utils/profiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import logging
import time


profiling_results = []


Expand Down
1 change: 0 additions & 1 deletion tg_utils/signals.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.dispatch import Signal


post_modify = Signal()
13 changes: 10 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ isolated_build = True
envlist =
py37-django{22,30,31,32}
py{38,39}-django{22,30,31,32,40}
py{310,311}-django{32,40,41,42,50}
py{312}-django{42,50}
py{310,311}-django{32,40,41,42,50,51,52}
py{312}-django{42,50,51,52,60}
py{313}-django{42,51,52,60}
py{314}-django{52,60}

[gh-actions]
python =
Expand All @@ -14,6 +16,8 @@ python =
3.10: py310
3.11: py311
3.12: py312
3.13: py313
3.14: py314

[testenv]
setenv =
Expand All @@ -30,6 +34,9 @@ deps=
django41: Django>=4.1,<4.2
django42: Django>=4.2,<4.3
django50: Django>=5.0,<5.1
django51: Django>=5.1,<5.2
django52: Django>=5.2,<5.3
django60: Django>=6.0,<6.1

[testenv:py311-django50]
[testenv:py311-django52]
commands = make test-full