Зелёный CI с устаревшим lock-файлом
Вы добавили зависимость в
Чтобы заметить такое расхождение, нужен
«Актуальный» здесь означает согласованный с метаданными проекта. Выход новой версии пакета на PyPI сам по себе не делает lock-файл устаревшим. А
Вы добавили зависимость в
pyproject.toml, а обновлённый uv.lock забыли закоммитить. Флаг --frozen может показаться строгой проверкой, но у него другой контракт: использовать существующий lock-файл, не проверяя его соответствие проекту.Чтобы заметить такое расхождение, нужен
--locked. По документации uv, команда завершится ошибкой, если lock-файл требует обновления. Например, uv sync --locked проверит его перед синхронизацией окружения. Отдельная проверка без установки зависимостей: uv lock --check.«Актуальный» здесь означает согласованный с метаданными проекта. Выход новой версии пакета на PyPI сам по себе не делает lock-файл устаревшим. А
--no-sync решает третью задачу: отключает проверку и обновление окружения перед запуском. Эти флаги отвечают за разные условия воспроизводимости.Forwarded from Нейроканал
На PyPI снова нашли заражённый LiteLLM, популярный шлюз к API разных LLM: версия 1.107.0 провисела около двух часов и ставила стилер. Он срабатывает при старте любого Python-процесса в окружении, даже если litellm никто не импортирует. Заражены были и Docker-образы с тегами latest, main-latest и main-stable. Сейчас версия снята с PyPI, а BerriAI подтвердила удаление образов.
Стилер забирает SSH-ключи, доступы к AWS, GCP, Azure и Kubernetes, пароли из браузеров, секреты CI, токены Claude Code, Cursor, Codex и других ИИ-инструментов, а при доступном
В марте проект уже взламывали через украденные учётки публикации, у нынешней атаки другие код и инфраструктура, связь между ними не установлена. Если ставились wheels 1.107.0 или запускались заражённые образы, доступные им секреты считаются скомпрометированными. До запуска pip из shell удаляют
@neuro_channel
Стилер забирает SSH-ключи, доступы к AWS, GCP, Azure и Kubernetes, пароли из браузеров, секреты CI, токены Claude Code, Cursor, Codex и других ИИ-инструментов, а при доступном
DATABASE_URL пытается выгрузить из базы LiteLLM-прокси ключи провайдеров. На Linux и macOS отдельный демон раз в час готов скачать и выполнить новый код, хотя вторая стадия так и не была замечена. По provenance образов исследователи Upwind нашли, что релиз собрал официальный конвейер BerriAI из репозитория-двойника Berri-AI/litellm. Как атакующий это провернул, пока неизвестно.В марте проект уже взламывали через украденные учётки публикации, у нынешней атаки другие код и инфраструктура, связь между ними не установлена. Если ставились wheels 1.107.0 или запускались заражённые образы, доступные им секреты считаются скомпрометированными. До запуска pip из shell удаляют
cook.pth, cook.py, sitecustomize.py и их файлы в __pycache__, останавливают фоновый Python-демон, пересобирают окружения с litellm==1.106.2 и меняют секреты. Индикаторы заражения собраны в issue проекта.@neuro_channel
🤯4
Decimal начинается до первого float
Две почти одинаковые записи дают разное округление. На Python 3.14.4 этот пример выводит сначала
Если вход изначально десятичный, сохраняйте строку до создания
Две почти одинаковые записи дают разное округление. На Python 3.14.4 этот пример выводит сначала
2.68, затем 2.67:from decimal import Decimal, ROUND_HALF_EVEN
cent = Decimal(".01")
print(Decimal("2.675").quantize(cent, rounding=ROUND_HALF_EVEN))
print(Decimal(2.675).quantize(cent, rounding=ROUND_HALF_EVEN))
Decimal из строки получает ровно заданную десятичную дробь. Из float он точно переносит уже существующее двоичное значение, которое чуть меньше 2.675. Конструктор не может восстановить исходное намерение по округлённому представлению числа.Если вход изначально десятичный, сохраняйте строку до создания
Decimal. Метод quantize задаёт нужный показатель степени, а режим округления выбирайте по правилам расчёта. ROUND_HALF_EVEN в примере округляет точную середину к чётной последней цифре. Это конкретная политика, а не универсальное правило для любых сумм.Задача в очереди, а строки в базе ещё нет
Внутри
В Django 5.2 для такого сценария есть transaction.on_commit. Зарегистрируйте функцию отправки задачи внутри
У этой границы есть предел: база уже закоммичена, когда callback обращается к брокеру. Ошибка отправки не откатит заказ и не гарантирует повторную доставку задачи.
Внутри
transaction.atomic() вы создаёте заказ и сразу отправляете его ID фоновому воркеру. Воркер может успеть обратиться к базе раньше коммита. Или транзакция откатится, а задача уже останется в очереди.В Django 5.2 для такого сценария есть transaction.on_commit. Зарегистрируйте функцию отправки задачи внутри
atomic. Она будет вызвана после успешного коммита, а при откате отброшена. Callback из вложенного блока ждёт внешнего коммита и не выполняется, если произошёл откат к его savepoint. В обычном режиме autocommit без открытой транзакции вызов происходит сразу.У этой границы есть предел: база уже закоммичена, когда callback обращается к брокеру. Ошибка отправки не откатит заказ и не гарантирует повторную доставку задачи.
on_commit устраняет преждевременный запуск, но надёжность передачи между базой и очередью нужно проектировать отдельно.В строке ABA у groupby три группы
Две буквы A не объединятся, потому что между ними стоит B.
Результат:
Есть и ловушка со сроком жизни: итераторы групп используют общий исходный итератор. Если группа понадобится позже, прочитайте её в список до перехода к следующей, как в примере. Эти детали описаны в контракте groupby. Сортировка и сохранённые списки требуют памяти, поэтому для подсчётов по несортированному потоку может лучше подойти словарь.
Две буквы A не объединятся, потому что между ними стоит B.
itertools.groupby начинает новую группу каждый раз, когда меняется ключ соседнего элемента:from itertools import groupby
print([(key, list(group)) for key, group in groupby("ABA")])
Результат:
[('A', ['A']), ('B', ['B']), ('A', ['A'])]. Это удобно для серий одинаковых событий в потоке. Для объединения всех записей с одним ключом данные обычно сначала сортируют по тому же key. Получить поведение SQL GROUP BY одним названием функции не выйдет.Есть и ловушка со сроком жизни: итераторы групп используют общий исходный итератор. Если группа понадобится позже, прочитайте её в список до перехода к следующей, как в примере. Эти детали описаны в контракте groupby. Сортировка и сохранённые списки требуют памяти, поэтому для подсчётов по несортированному потоку может лучше подойти словарь.
❤2
Pydantic может создать модель с неверными данными
Наличие объекта
Метод не отключает вообще всё: значения полей по умолчанию по-прежнему подставляются. Зато
В документации метода его предлагают для уже проверенных или заведомо доверенных данных. Выбирать этот путь только ради скорости тоже рано: в V2 разрыв сократился, а на простых моделях обычная валидация может оказаться быстрее. Прежде чем убрать проверку, измерьте свой сценарий и определите, кто теперь отвечает за корректность вложенных объектов.
Наличие объекта
BaseModel ещё не доказывает, что данные прошли проверку. В Pydantic V2 model_construct() создаёт модель без валидации. Если поле объявлено вложенной моделью, переданный туда словарь сам в неё не превратится.Метод не отключает вообще всё: значения полей по умолчанию по-прежнему подставляются. Зато
extra="forbid" при таком создании не даст привычной ошибки за лишнее поле. Оно будет проигнорировано. Поэтому контракт обычного конструктора нельзя механически переносить на model_construct.В документации метода его предлагают для уже проверенных или заведомо доверенных данных. Выбирать этот путь только ради скорости тоже рано: в V2 разрыв сократился, а на простых моделях обычная валидация может оказаться быстрее. Прежде чем убрать проверку, измерьте свой сценарий и определите, кто теперь отвечает за корректность вложенных объектов.
bind принимает строку в параметр int
Первый вывод:
Так можно разбирать вызовы в декораторе или диспетчере и получать
inspect.Signature.bind проверяет, можно ли разложить аргументы по параметрам функции. Аннотация int при этом не превращается в проверку типа:from inspect import signature
def ship(item: int, *, retries=2):
return item, retries
bound = signature(ship).bind("text")
print(bound.arguments)
bound.apply_defaults()
print(bound.arguments)
Первый вывод:
{'item': 'text'}. Второй добавляет 'retries': 2. bind не вставляет пропущенные значения по умолчанию в arguments, для этого нужен apply_defaults(). Саму ship этот код не вызывает.Так можно разбирать вызовы в декораторе или диспетчере и получать
TypeError при несовместимых аргументах. Но проверку значений нужно организовать отдельно. В документации bind есть и bind_partial, который разрешает пропустить обязательные параметры. Для проверки полного вызова это уже слишком мягкий контракт.Подмена в pytest может закончиться раньше теста
Тест сначала проверяет код с подменённой зависимостью, а затем хочет вызвать настоящий объект. Если все изменения сделаны через обычную фикстуру
Сузить срок жизни можно через
В примере pytest так ограничивают подмену
Тест сначала проверяет код с подменённой зависимостью, а затем хочет вызвать настоящий объект. Если все изменения сделаны через обычную фикстуру
monkeypatch, автоматический откат произойдёт лишь после завершения теста или использующей её фикстуры.Сузить срок жизни можно через
with monkeypatch.context() as m. Подмены, зарегистрированные через m.setattr и другие методы этого контекста, откатятся при выходе из блока. После него можно проверять поведение с восстановленной зависимостью, не завершая тест.В примере pytest так ограничивают подмену
functools.partial. Это важно: стандартной библиотекой пользуется и сам pytest, поэтому глобальная подмена может сломать тестовый раннер. Короткий контекст уменьшает время воздействия, но не делает любую подмену безопасной. Для своего кода документация рекомендует явную передачу зависимостей, если это возможно.bisect ждёт ключ, insort ждёт запись
У двух функций из одного модуля разные требования к искомому значению. Если записи отсортированы по числовому полю, в
Поиск вернёт
Параметр
У двух функций из одного модуля разные требования к искомому значению. Если записи отсортированы по числовому полю, в
bisect_left передайте число, а в insort саму запись:from bisect import bisect_left, insort
rows = [("Ada", 10), ("Lin", 30)]
print(bisect_left(rows, 20, key=lambda row: row[1]))
insort(rows, ("Sam", 20), key=lambda row: row[1])
print(rows)
Поиск вернёт
1, а вставка поставит ("Sam", 20) между Ada и Lin. У bisect_left функция key применяется только к элементам списка. У insort она применяется и к вставляемому объекту для поиска позиции, но в список попадает исходный объект.Параметр
key доступен с Python 3.10. Список должен быть уже отсортирован по тому же ключу. И ещё одно ограничение из документации bisect: логарифмический поиск не делает вставку дешёвой. Сдвиг элементов списка по-прежнему занимает O(n).urljoin спокойно заменит ваш домен
Базовый адрес в первом аргументе не удерживает результат внутри вашего сайта. Второй аргумент может содержать собственный домен даже без схемы:
Получится
Если собираете ссылку из пользовательского ввода для перенаправления или серверного запроса, нужна отдельная политика допустимых адресов.
Базовый адрес в первом аргументе не удерживает результат внутри вашего сайта. Второй аргумент может содержать собственный домен даже без схемы:
from urllib.parse import urljoin
print(urljoin(
"https://safe.example/users/",
"//other.example/admin",
))
Получится
https://other.example/admin. Сетевой путь //host берёт схему из базы, а домен задаёт свой. Полный абсолютный URL во втором аргументе тоже может заменить адрес. Это штатное поведение, о котором прямо предупреждает документация urljoin.Если собираете ссылку из пользовательского ввода для перенаправления или серверного запроса, нужна отдельная политика допустимых адресов.
urlsplit поможет выделить схему, хост и путь, но сам по себе не проверит, можно ли им доверять. Успешно разобранный URL ещё не означает, что по нему безопасно переходить.❤1
Цикл в графе: независимые задачи ещё можно выполнить
Задача
У
Важно различать «готов к запуску» и «выполнен». Именно
Задача
a ждёт b, а b ждёт a. Полностью упорядочить такой граф нельзя. Но если рядом есть независимая задача free, ошибка цикла не обязана останавливать и её.У
graphlib.TopologicalSorter вызов prepare() поднимет CycleError. После обработки этого исключения можно продолжить через get_ready(): он выдаст узлы, которым не мешает цикл. В примере с a ↔ b и отдельным free доступен только free. Завершив его обработку, вызовите done("free").Важно различать «готов к запуску» и «выполнен». Именно
done разблокирует зависящие узлы. Если после независимой части is_active() вернул False, это не доказывает завершение всего графа: цикл остался. Такой частичный прогресс разрешён контрактом TopologicalSorter с Python 3.9. Сам CycleError сообщает один обнаруженный цикл, а не все сразу.ChainMap читает все слои, а пишет только в первый
Для конфигурации удобно читать переопределения поверх значений по умолчанию.
Вывод:
Это живое представление исходных словарей, а не снимок: изменения в них видны через
Для конфигурации удобно читать переопределения поверх значений по умолчанию.
ChainMap делает это без предварительного слияния словарей:from collections import ChainMap
defaults = {"timeout": 30}
overrides = {}
config = ChainMap(overrides, defaults)
config["timeout"] = 5
print(overrides, defaults)
Вывод:
{'timeout': 5} {'timeout': 30}. Чтение ищет ключ в слоях по очереди, а присваивание меняет только первый словарь. Туда же направлены update и удаление. Ключ, который есть лишь в нижнем слое, удалить через цепочку не получится.Это живое представление исходных словарей, а не снимок: изменения в них видны через
ChainMap. И копирования вложенных объектов здесь тоже нет. Если прочитанное значение оказалось общим списком, его изменение затронет этот список в исходном слое. Правило записи в первый словарь не означает изоляцию всех доступных через него данных.❤1🌚1
Кто закроет async-генератор после break
Вы читаете поток до первого подходящего элемента и выходите через
В Python 3.10+ для этого есть
Очистка пройдёт в том же контексте, что и перебор. Это существенно, если она использует контекстные переменные или зависит от ещё работающей задачи. В примере из документации очистка выполняется при выходе из блока, без ожидания сборки мусора.
Вы читаете поток до первого подходящего элемента и выходите через
break. Цикл закончился, но сам по себе этот выход не гарантирует немедленное выполнение finally у async-генератора. Если там освобождается ресурс, его срок жизни лучше задать явно.В Python 3.10+ для этого есть
contextlib.aclosing. Оберните генератор в async with, как на карточке. При выходе из блока менеджер вызовет await generator.aclose(), в том числе после раннего выхода или исключения.Очистка пройдёт в том же контексте, что и перебор. Это существенно, если она использует контекстные переменные или зависит от ещё работающей задачи. В примере из документации очистка выполняется при выходе из блока, без ожидания сборки мусора.
🔥1
NewType различает ID без новых объектов
Идентификаторы пользователя и заказа могут быть обычными
Функция с параметром
Во время выполнения вызов возвращает тот же объект. Это отдельный тип для проверяющего, а не валидатор входных данных и не runtime-подкласс. Использовать его вторым аргументом
Идентификаторы пользователя и заказа могут быть обычными
int, но случайно поменять их местами всё равно ошибка. UserId = NewType("UserId", int) позволяет описать это различие для статического анализатора.Функция с параметром
UserId принимает UserId(42), а на голое 42 проверяющий должен пожаловаться. В обратную сторону преобразование не нужно: UserId допустим там, где ожидается int. После арифметики вроде UserId(5) + 1 статический тип результата снова int.Во время выполнения вызов возвращает тот же объект. Это отдельный тип для проверяющего, а не валидатор входных данных и не runtime-подкласс. Использовать его вторым аргументом
isinstance нельзя. Проверку внешнего ID всё ещё придётся написать.