<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Персональный блог Адиля Хаштамова</title><link>http://khashtamov.com/ru/</link><description>программист-прагматик</description><atom:link href="http://khashtamov.com/ru/feed/" rel="self"/><language>ru</language><lastBuildDate>Tue, 14 Apr 2026 22:45:59 +0000</lastBuildDate><item><title>Сравнение Gemini 3 и Claude Opus 4.5</title><link>http://khashtamov.com/ru/gemini-vs-claude-opus-45/</link><description>&lt;p&gt;Наступил 2026 год,&amp;nbsp;а значит необходимо обновить мой код,&amp;nbsp;который вычисляет является ли текущий день праздничным в Казахстане или нет. У нас в стране есть сайт электронного правительства, где ежегодно публикуется календарь праздников -&amp;nbsp;&lt;a href="https://egov.kz/cms/ru/articles/employment_relations/holidays-calend"&gt;Праздничные и выходные дни в Республике Казахстан&lt;/a&gt;. Уже второй год подряд я использую &lt;strong&gt;Agentic Coding&lt;/strong&gt; для написания необходимой мне функции на Python. И&amp;nbsp;второй год подряд лучше всех справляется LLM модель от Google - Gemini 3.&lt;/p&gt;
&lt;p&gt;В начале прошлого года у меня появилась необходимость учитывать является ли текущий день праздничным или нет,&amp;nbsp;и тогда мне в голову пришла идея скормить изображение календаря LLM моделям и на базе этой картинке получить готовую Python-функцию. На тот момент рабочий вариант функции мне удалось получить только от Gemini, причем совсем с незначительными правками с моей стороны. Модели от Anthropic выдавали нерабочий код, была даже ситуация,&amp;nbsp;когда Sonnet&amp;nbsp;выплюнула мне огромный словарь,&amp;nbsp;где ключ это конкретная дата,&amp;nbsp;а значение типа bool определяет выходной это или нет. С&amp;nbsp;тех пор прошел почти год,&amp;nbsp;мир увидел самую продвинутую модель для кодинга от Anthropic Opus 4.5. В связи с наступлением нового года снова возникла идея скормить изображение и получить на выходе готовый код.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Вот промт,&amp;nbsp;который я использую для двух моделей&lt;/p&gt;
&lt;blockquote&gt;Based on the provided image calendar for 2026 year, write a python function which returns whether the particular day is a day off or not. Working day cells have light background, day offs are red/orange filled cells.&lt;/blockquote&gt;
&lt;p&gt;Простой промт, описывающий что необходимо сделать с небольшими подсказками,&amp;nbsp;чтобы модельке было легче понять где рабочий день,&amp;nbsp;а где выходной.&lt;/p&gt;
&lt;h2&gt;Результат&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claude Sonnet 4.5&lt;/strong&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_day_off(day: int, month: int, year: int = 2026) -&amp;gt; bool:
    """
    Check if a particular day is a day off in Kazakhstan for 2026.
    
    Args:
        day: Day of the month (1-31)
        month: Month (1-12)
        year: Year (default: 2026)
    
    Returns:
        True if the day is a day off (weekend or public holiday), False otherwise
    """
    
    if year != 2026:
        raise ValueError("This function only works for year 2026")
    
    # Define all days off for each month in 2026
    # Based on the calendar: red/orange cells are days off
    days_off_by_month = {
        1: [1, 2, 3, 4, 10, 11, 17, 18, 24, 25, 31],  # January
        2: [1, 7, 8, 14, 15, 21, 22, 28],  # February
        3: [1, 7, 8, 14, 15, 21, 22, 23, 24, 28, 29],  # March
        4: [4, 5, 11, 12, 18, 19, 25, 26],  # April
        5: [1, 2, 3, 7, 9, 10, 11, 16, 17, 23, 24, 27, 30, 31],  # May
        6: [6, 7, 13, 14, 20, 21, 27, 28],  # June
        7: [4, 5, 6, 11, 12, 18, 19, 25, 26],  # July
        8: [1, 2, 8, 9, 15, 16, 22, 23, 29, 30, 31],  # August
        9: [5, 6, 12, 13, 19, 20, 26, 27],  # September
        10: [3, 4, 10, 11, 17, 18, 24, 25, 26, 31],  # October
        11: [1, 7, 8, 14, 15, 21, 22, 28, 29],  # November
        12: [5, 6, 12, 13, 16, 19, 20, 26, 27],  # December
    }
    
    # Validate input
    if month &amp;lt; 1 or month &amp;gt; 12:
        raise ValueError(f"Invalid month: {month}. Must be between 1 and 12")
    
    if day &amp;lt; 1 or day &amp;gt; 31:
        raise ValueError(f"Invalid day: {day}. Must be between 1 and 31")
    
    # Check if the day exists in the given month
    try:
        date(year, month, day)
    except ValueError:
        raise ValueError(f"Invalid date: {year}-{month:02d}-{day:02d}")
    
    return day in days_off_by_month.get(month, [])&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Claude Opus 4.5&lt;/strong&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datetime import date, timedelta
from typing import Union


def is_day_off(check_date: Union[date, str]) -&amp;gt; bool:
    """
    Check if a given date is a day off in Kazakhstan for 2026.
    
    Args:
        check_date: Either a date object or a string in 'YYYY-MM-DD' format
        
    Returns:
        True if the date is a day off, False if it's a working day
        
    Raises:
        ValueError: If the date is not in 2026 or invalid format
    """
    if isinstance(check_date, str):
        check_date = date.fromisoformat(check_date)
    
    if check_date.year != 2026:
        raise ValueError("This calendar is only valid for 2026")
    
    # Set of all days off in 2026 (month, day)
    # Extracted from the Kazakhstan 2026 holiday calendar
    
    days_off = {
        # January - New Year holidays
        (1, 1), (1, 2), (1, 3), (1, 4),  # Thu-Sun (New Year)
        (1, 7),  # Wed - Orthodox Christmas
        (1, 10), (1, 11),  # Sat-Sun
        (1, 17), (1, 18),  # Sat-Sun
        (1, 24), (1, 25),  # Sat-Sun
        (1, 31),  # Sat
        
        # February
        (2, 1),  # Sun
        (2, 7), (2, 8),  # Sat-Sun
        (2, 14), (2, 15),  # Sat-Sun
        (2, 21), (2, 22),  # Sat-Sun
        (2, 28),  # Sat
        
        # March
        (3, 1),  # Sun
        (3, 7), (3, 8),  # Sat-Sun (March 8 - International Women's Day)
        (3, 14), (3, 15),  # Sat-Sun
        (3, 21), (3, 22),  # Sat-Sun (March 21-23 - Nauryz)
        (3, 23),  # Mon - Nauryz holiday
        (3, 28), (3, 29),  # Sat-Sun
        
        # April
        (4, 4), (4, 5),  # Sat-Sun
        (4, 11), (4, 12),  # Sat-Sun
        (4, 18), (4, 19),  # Sat-Sun
        (4, 25), (4, 26),  # Sat-Sun
        
        # May
        (5, 1),  # Fri - Kazakhstan People's Unity Day
        (5, 2), (5, 3),  # Sat-Sun
        (5, 7),  # Thu - Defender of the Fatherland Day
        (5, 9), (5, 10),  # Sat-Sun (May 9 - Victory Day)
        (5, 16), (5, 17),  # Sat-Sun
        (5, 23), (5, 24),  # Sat-Sun
        (5, 27),  # Wed - Eid al-Fitr (Oraza Ait) - variable Islamic holiday
        (5, 30), (5, 31),  # Sat-Sun
        
        # June
        (6, 6), (6, 7),  # Sat-Sun
        (6, 13), (6, 14),  # Sat-Sun
        (6, 20), (6, 21),  # Sat-Sun
        (6, 27), (6, 28),  # Sat-Sun
        
        # July
        (7, 4), (7, 5),  # Sat-Sun
        (7, 6),  # Mon - Capital City Day
        (7, 11), (7, 12),  # Sat-Sun
        (7, 18), (7, 19),  # Sat-Sun
        (7, 25), (7, 26),  # Sat-Sun
        
        # August
        (8, 1), (8, 2),  # Sat-Sun
        (8, 3),  # Mon - Eid al-Adha (Kurban Ait) - variable Islamic holiday
        (8, 8), (8, 9),  # Sat-Sun
        (8, 15), (8, 16),  # Sat-Sun
        (8, 22), (8, 23),  # Sat-Sun
        (8, 29), (8, 30),  # Sat-Sun (Aug 30 - Constitution Day)
        
        # September
        (9, 5), (9, 6),  # Sat-Sun
        (9, 12), (9, 13),  # Sat-Sun
        (9, 19), (9, 20),  # Sat-Sun
        (9, 26), (9, 27),  # Sat-Sun
        
        # October
        (10, 3), (10, 4),  # Sat-Sun
        (10, 10), (10, 11),  # Sat-Sun
        (10, 17), (10, 18),  # Sat-Sun
        (10, 24), (10, 25),  # Sat-Sun (Oct 25 - Republic Day)
        (10, 31),  # Sat
        
        # November
        (11, 1),  # Sun
        (11, 7), (11, 8),  # Sat-Sun
        (11, 14), (11, 15),  # Sat-Sun
        (11, 21), (11, 22),  # Sat-Sun
        (11, 28), (11, 29),  # Sat-Sun
        
        # December
        (12, 5), (12, 6),  # Sat-Sun
        (12, 12), (12, 13),  # Sat-Sun
        (12, 16),  # Wed - Independence Day
        (12, 19), (12, 20),  # Sat-Sun
        (12, 26), (12, 27),  # Sat-Sun
    }
    
    return (check_date.month, check_date.day) in days_off
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Радует,&amp;nbsp;что обе модели выдали рабочий код в отличии от прошлогоднего результата,&amp;nbsp;где у меня не получилось с первого раза добиться желаемого. Но обе функции повторяют один и тот же паттерн - создают массивы выходных дней из-за чего код получается более громоздким. Что же на это скажет LLM от Google?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gemini 3&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datetime import date

def is_dayoff_kazakhstan_2026(check_date: date):
    """
    Returns True if the date is a day-off in Kazakhstan 2026,
    based on the provided visual calendar.
    """
    if check_date.year != 2026:
        return "This function only supports the year 2026."

    # 1. Check for weekends (Saturday = 5, Sunday = 6)
    if check_date.weekday() &amp;gt;= 5:
        return True

    # 2. List of specific holidays and transferred rest days (Red/Orange cells)
    # Format: (Month, Day)
    holidays_2026 = {
        (1, 1), (1, 2), (1, 7),           # January
        (3, 8), (3, 9), (3, 21), (3, 22), 
        (3, 23), (3, 24), (3, 25),        # March (Nauryz)
        (5, 1), (5, 7), (5, 9), (5, 11),  # May
        (7, 6),                           # July
        (8, 30), (8, 31),                 # August
        (10, 25), (10, 26),               # October
        (12, 16)                          # December
    }

    if (check_date.month, check_date.day) in holidays_2026:
        return True

    return False&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как по мне у Gemini 3 получилось сгенерировать лаконичный и понятный код без лишних данных. Модель показала себя как "думающая" в отличии от Sonnet 4.5 и Opus 4.5. Мне нравится, что код не дублирует выходные дни (субботу и воскресенье), а проверяет их через условие:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if check_date.weekday() &amp;gt;= 5:
        return True&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Вывод&lt;/h2&gt;
&lt;p&gt;В&amp;nbsp;этом ни на что не претендующем тесте более выигрышно явно смотрится Gemini 3. Думаю причина в том,&amp;nbsp;что у Gemini более продвинутые способности по распознаванию изображений чем у моделей Anthropic. Код от&amp;nbsp;Opus и Sonnet&amp;nbsp;показался мне более многословным и менее читабельным,&amp;nbsp;но прогресс в любом случае на лицо. По итогу я взял в продакшен решение от Gemini как самое оптимальное решение как с точки зрения читабельности так и производительности.&lt;/p</description><guid>http://khashtamov.com/ru/gemini-vs-claude-opus-45/</guid></item><item><title>Про Django ORM и SimpleLazyObject</title><link>http://khashtamov.com/ru/django-orm-and-simplelazyobject/</link><description>&lt;p&gt;
    Недавно я захотел создать собственный middleware, чтобы дополнить объект &lt;code&gt;request&lt;/code&gt;, добавив в него дополнительный атрибут. Но я хотел, чтобы этот атрибут вычислялся лениво. Если у вас есть опыт разработки с Django, вы, вероятно, знаете, что он предоставляет ленивые функции, такие как &lt;code&gt;reverse_lazy&lt;/code&gt;. Изучая внутреннюю реализацию функции, я обнаружил, что Django предоставляет модуль &lt;code&gt;django.utils.functional&lt;/code&gt;, который содержит&amp;nbsp;интересные функции и классы.
&lt;/p&gt;
&lt;p&gt;
    Мне понравился &lt;code&gt;SimpleLazyObject&lt;/code&gt;, и я заметил, что он используется в middleware под названием &lt;code&gt;django.contrib.auth.middleware.AuthenticationMiddleware&lt;/code&gt;. Всё казалось&amp;nbsp;просто пока я не&amp;nbsp;стал использовать "ленивый"&amp;nbsp;атрибут&amp;nbsp;в ORM-запросе Django 😁&lt;/p&gt;
&lt;p&gt;
    Мой атрибут возвращает список значений, который должен использоваться для фильтрации в запросе, но основная проблема в том, как Django обрабатывает эти значения внутри ORM. Класс &lt;code&gt;Query&lt;/code&gt; имеет метод &lt;code&gt;resolve_lookup_value&lt;/code&gt;, заглянув внутрь него я увидел такое условие:
&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;elif isinstance(value, (list, tuple)):
    # The items of the iterable may be expressions and therefore need
    # to be resolved independently.
    values = (
        self.resolve_lookup_value(sub_value, can_reuse, allow_joins, summarize)
        for sub_value in value
    )
    type_ = type(value)
    if hasattr(type_, "_make"):  # namedtuple
        return type_(*values)
    return type_(values)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
Этот код означает, что ваш экземпляр &lt;code&gt;SimpleLazyObject&lt;/code&gt; будет преобразован в экземпляр &lt;code&gt;SimpleLazyObject&lt;/code&gt;, содержащий генератор, возвращающий список значений от исходного объекта. И это не работает как я изначально ожидал. Моё решение было довольно простым: поскольку у меня был уникальный набор элементов, я заменил список значений на множество (имейте в виду, что кортеж не подойдёт из-за условия &lt;code&gt;elif&lt;/code&gt;, которое проверяет и список, и кортеж).
&lt;/p&gt;
&lt;p&gt;
    Если у вас более сложный случай, я бы посоветовал расширить класс &lt;code&gt;LazyObject&lt;/code&gt; (SimpleLazyObject является его подклассом) и реализовать свой собственный метод &lt;code&gt;_setup&lt;/code&gt;.
&lt;/p</description><guid>http://khashtamov.com/ru/django-orm-and-simplelazyobject/</guid></item><item><title>Авторизация через Telegram в Django приложении</title><link>http://khashtamov.com/ru/django4-telegram-auth/</link><description>&lt;p&gt;5 лет назад &lt;a href="https://khashtamov.com/ru/telegram-auth-django/"&gt;я написал backend-модуль для авторизации через Telegram&lt;/a&gt; в популярном пакете &lt;code&gt;python-social-auth&lt;/code&gt;. С тех пор я сам регулярно использую эту фичу на своих собственных сайтах, очень удобно и быстро. Но с выходом Django 4.0 модуль авторизации через Telegram перестал работать. Почему? А всё потому что появилась настройка &lt;code&gt;SECURE_CROSS_ORIGIN_OPENER_POLICY&lt;/code&gt; со значением по умолчанию &lt;code&gt;same-origin&lt;/code&gt;. Что это значит? Авторизация в Telegram работает через виджет, который открывает pop-up окно с разрешением на авторизацию и передачу данных вашему веб-приложению. За это отвечает заголовок Cross-Origin-Opener-Policy.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Cross-Origin-Opener-Policy&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Это HTTP заголовок, отвечающий за политику передачи контекста основного документа (откуда вызван pop-up) cross-origin документам. Заголовок может принимать три значения:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;same-origin&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Разрешение на передачу контекста основного окна в рамках одного домена или происхождения (same-origin). Т.е. если у вас есть pop-up окно, открывающееся на том же домене, то эта политика разрешит их взаимодействие (свойство &lt;code&gt;window.opener&lt;/code&gt; не будет пустым). Начиная с версии Django 4 это значение является значением по умолчанию. Это самая безопасная политика.&lt;/p&gt;
&lt;figure&gt;&lt;img style="width:100%;" src="/uploads/redactor/oauth-telegram-headers.png" data-image="oauth-telegram-headers.png"&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;unsafe-none&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Позволяет передавать контекст между cross-origin документами при условии, что у открывающего окна (opener) та же самая политика (не является &lt;code&gt;same-origin&lt;/code&gt; или &lt;code&gt;same-origin-allow-popups&lt;/code&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;same-origin-allow-popups&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Передача контекста документа между same-origin документами либо теми, кто не передаёт заголовок Cross-Origin-Opener-Policy либо его значение равно &lt;code&gt;unsafe-none&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;В контексте авторизации через Telegram значение по умолчанию &lt;code&gt;same-origin&lt;/code&gt; нам не подходит потому что в pop-up окне уже другой origin (oauth.telegram.org). Поэтому необходимо использовать либо &lt;code&gt;unsafe-none&lt;/code&gt; либо &lt;code&gt;same-origin-allow-popups&lt;/code&gt;. Второй вариант предпочтительнее, т.к. OAuth сервис Telegram не передаёт заголовок Cross-Origin-Opener-Policy.&lt;/p&gt;
&lt;h2&gt;Итог&lt;/h2&gt;
&lt;p&gt;Чтобы авторизация через Telegram корректно работала в вашем Django приложении необходимо значение константы &lt;code&gt;SECURE_CROSS_ORIGIN_OPENER_POLICY&lt;/code&gt; в файле &lt;code&gt;settings.py&lt;/code&gt; установить либо в None либо в &lt;code&gt;same-origin-allow-popups&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin-allow-popups"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p</description><guid>http://khashtamov.com/ru/django4-telegram-auth/</guid></item><item><title>Интеграция Trix editor в Django</title><link>http://khashtamov.com/ru/django-trix-editor/</link><description>&lt;p&gt;У ребят из Basecamp (ex-37signals) есть неплохой WYSIWYG редактор &lt;a href="https://trix-editor.org/" target="_blank" rel="nofollow noindex"&gt;Trix Editor&lt;/a&gt;. Я начал использовать его в своих проектах в качестве основного текстового редактора, мне очень нравится. Ранее я везде использовал &lt;a href="https://imperavi.com/redactor/" target="_blank" rel="nofollow noindex"&gt;Redactor.js&lt;/a&gt;. Так как я практически всегда использую веб-фреймворк Django, то я решил сделать &lt;a href="https://github.com/adilkhash/django-trix-editor" target="_blank" rel="nofollow noindex"&gt;reusable django app&lt;/a&gt; для интеграции этого редактора.&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;Установка&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pip install django-trix-editor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Для настройки в Django вам необходимо прописать django app в &lt;code&gt;INSTALLED_APPS&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;INSTALLED_APPS = [
    ...
    'trix_editor',
    ...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Кастомную версию редактора можно указать через константу &lt;code&gt;TRIX_VERSION = '2.0.6'&lt;/code&gt; в файле &lt;code&gt;settings.py&lt;/code&gt;. Для поддержки загрузки файлов в редакторе необходимо добавить в &lt;code&gt;urls.py&lt;/code&gt; следующую строчку:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;from django.urls import include, path

urlpatterns = [
    ...
    path('trix-editor/', include('trix_editor.urls')),
    ...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;По желанию можно настроить права на загрузку в &lt;code&gt;settings.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;TRIX_UPLOAD_PERMISSION = 'your_model.upload_attachment'
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Использование&lt;/h2&gt;
&lt;p&gt;Поле редактора можно передать напрямую при определении модели:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;from django.db import models
from trix_editor.fields import TrixEditorField

class MyModel(models.Model):
    content = TrixEditorField()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Или например использовать в форме в виде виджета:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;from django import forms
from trix_editor.widgets import TrixEditorWidget

class MyForm(forms.Form):
    content = forms.CharField(widget=TrixEditorWidget())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Для кастомизации Django Admin достаточно переопределить форму по умолчанию:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;class ContentForm(forms.ModelForm):
    class Meta:
        model = Content
        fields = ["title", "content", "status"]
        widgets = {
            "content": TrixEditorWidget(),
        }

@admin.register(Content)
class ContentAdmin(admin.ModelAdmin):
    list_display = ("title", "status", "created", "updated")
    form = ContentForm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В случае с Django Admin все необходимые файлы будут автоматически подключены при формировании шаблона. Если же вы планируете использовать редактор в своих собственных шаблонах, то media-файлы нужно задавать явно:&lt;/p</description><guid>http://khashtamov.com/ru/django-trix-editor/</guid></item><item><title>Django, RQ и FakeRedis</title><link>http://khashtamov.com/ru/django-rq-fakeredis/</link><description>&lt;p&gt;&lt;!--StartFragment--&gt;Я часто в своих проектах использую связку &lt;a href="https://khashtamov.com/ru/python-rq-howto/"&gt;Django + RQ&lt;/a&gt; вместо &lt;a href="https://khashtamov.com/ru/celery-best-practices/"&gt;Celery&lt;/a&gt;. RQ удобный и максимально простой инструмент среди популярных Task Queue решений в экосистеме Python. Пару месяцев назад возникла необходимость тестировать код с сигналами в Django. Схема простая: в ответ на какое-то событие (создание объекта в БД, кастомный сигнал и т.д.) вызывался RQ Job через &lt;code&gt;delay&lt;/code&gt;. Дело в том, что такое событие транслировалось ко всем получателям (receivers) как только объект удалялся из базы. Я активный пользователь &lt;code&gt;pytest&lt;/code&gt; и создаю промежуточные объекты через стандартные фикстуры. Одно из решений — патчить/mockать job-функции во всех местах, где такие объекты создаются. Но это неудобно и непрактично, с развитием системы количество получателей может расти. Я нашел выход в подмене connection-класса в зависимости от условий. Условие в моём случае это наличие переменной &lt;code&gt;FAKE_REDIS&lt;/code&gt; в Django &lt;code&gt;settings.py&lt;/code&gt;. Когда &lt;code&gt;FAKE_REDIS=True&lt;/code&gt;, то мы заменяем соединение с redis на инстанс класса &lt;code&gt;FakeRedis&lt;/code&gt; из пакета &lt;a href="https://github.com/cunla/fakeredis-py" target="_blank"&gt;fakeredis&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Чтобы этот подход сносно работал необходимо переписать job-декоратор. Вот что получилось у меня:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;from rq.decorators import job as rq_job_decorator
from django_rq.queues import get_queue

def async_job(queue_name: str, *args: t.Any, **kwargs: t.Any) -&amp;gt; t.Any:
    """
    The same as RQ's job decorator, but it automatically replaces the
    ``connection`` argument with a fake one if ``settings.FAKE_REDIS`` is set to ``True``.
    """

    class LazyAsyncJob:
        def __init__(self, f: t.Callable[..., t.Any]) -&amp;gt; None:
            self.f = f
            self.job: t.Optional[t.Callable[..., t.Any]] = None

        def setup_connection(self) -&amp;gt; t.Callable[..., t.Any]:
            if self.job:
                return self.job
            if settings.FAKE_REDIS:
                from fakeredis import FakeRedis

                queue = get_queue(queue_name, connection=FakeRedis())  # type: ignore
            else:
                queue = get_queue(queue_name)

            RQ = getattr(settings, 'RQ', {})
            default_result_ttl = RQ.get('DEFAULT_RESULT_TTL')
            if default_result_ttl is not None:
                kwargs.setdefault('result_ttl', default_result_ttl)

            return rq_job_decorator(queue, *args, **kwargs)(self.f)

        def delay(self, *args: t.Any, **kwargs: t.Any) -&amp;gt; t.Any:
            self.job = self.setup_connection()
            return self.job.delay(*args, **kwargs)  # type: ignore

        def __call__(self, *args: t.Any, **kwargs: t.Any) -&amp;gt; t.Any:
            self.job = self.setup_connection()
            return self.job(*args, **kwargs)

    return LazyAsyncJob
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И все rq таски необходимо декорировать через &lt;code&gt;async_job&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;@async_job('default')
def process_image():
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В Django тестах я использую фикстуры pytest:&lt;/p&gt;
&lt;pre&gt;&lt;code class="python"&gt;@pytest.fixture(autouse=True)
def fake_redis(settings: t.Any) -&amp;gt; None:
    settings.FAKE_REDIS = True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Параметр &lt;code&gt;autouse=True&lt;/code&gt; нужен для того, чтобы абсолютно во всех тестах использовался &lt;code&gt;fake_redis&lt;/code&gt; без явного на то указания.&lt;/p</description><guid>http://khashtamov.com/ru/django-rq-fakeredis/</guid></item></channel></rss>