Привет, Хабр!
Если вы собираете прототип на C++, то один файл с main.cpp иногда реально компилируется в рабочую утилиту. Библиотеки либо завозятся пакетным менеджером заранее, либо у вас есть header‑only зависимость и всё взлетает. В Python долгое время это было болью: любой однофайловый скрипт, который требует requests или rich, уже тянет за собой виртуальные окружения, инструкции в README и локальные фичи.
Есть рабочий стандарт для нормальных однофайловых сценариев с зависимостями — PEP 723: вы объявляете зависимости прямо в комментариях, а раннер ставит всё сам и запускает в изолированной среде. В связке с uv получается неплохой такой способ делиться скриптами, в том числе для пвспомогательных задач. И да, у этой красоты есть нюансы безопасности, о них поговорим отдельно.
Что такое PEP 723 и как это выглядит
PEP 723 определяет блок метаданных в комментариях, который парсится внешними инструментами. Формат прост: сверху и снизу маркеры, внутри TOML c полями dependencies и requires-python.
Пример минимального скрипта:
# /// script # requires-python = ">=3.11" # dependencies = [ # "httpx<1.0", # "rich>=13.7", # ] # /// import httpx from rich import print def main() -> None: r = httpx.get("https://httpbin.org/json", timeout=10) r.raise_for_status() print({"status": r.status_code, "len": len(r.text)}) if __name__ == "__main__": main()
Запуск через uv:
uv run example.py
uv создаст изолированную среду в кеше, установит зависимости и выполнит код. Никаких ручных venv.
Добавляем и управляем зависимостями прямо из CLI
PEP 723 — это про формат, но править TOML руками быстро надоедает. uv умеет редактировать блок зависимостей в файле:
uv add --script example.py "pydantic>=2.8" uv remove --script example.py "httpx" uv run example.py
Если нужен исполняемый файл без явного вызова uv run, используем shebang:
#!/usr/bin/env -S uv run --script # /// script # dependencies = ["rich"] # /// from rich import print print("hello")
Ставим права и запускаем из каталога:
chmod +x greet ./greet
Лочим зависимости и делаем скрипт воспроизводимым
Одно дело установить актуальные версии, другое — зафиксировать их. uv поддерживает lock‑файл для скриптов.
uv lock --script example.py # появится example.py.lock # теперь любые операции будут учитывать lock: uv run --script example.py uv add --script example.py "rich" # обновит lock uv export --script example.py -o req.txt
Можно ограничить свежесть пакетов датой, чтобы избежать неожиданных апдейтов через год. В блоке [tool.uv] внутри метаданных:
# /// script # requires-python = ">=3.11" # dependencies = ["httpx<1.0"] # [tool.uv] # exclude-newer = "2025-08-01T00:00:00Z" # ///
exclude-newer заставит резолвер игнорировать релизы, вышедшие после указанной даты.
А если нужно именно pip‑совместимое зафиксированное дерево для длительных CI‑пайплайнов, у uv есть инструменты уровня pip-tools: uv pip compile и uv pip sync. Они позволяют сгенерировать requirements.txt из исходника‑декларации и синхронизировать окружение один‑в‑один.
Контроль версии Python на рантайме
Скрипт может требовать конкретную ветку интерпретатора. В этом случае uv позволяет запросить нужную версию при запуске:
uv run --python 3.10 example.py uv run --python 3.12 example.py
Безопасность
Однофайловые скрипты удобно запускать как есть, и именно поэтому надо быть аккуратнее обычного. Риски:
-
Подмена зависимостей. Тривиальные атаки на цепочку поставок через зарегистрированные пакеты с похожими именами.
-
Непредсказуемые апгрейды. Сегодня всё чисто, завтра новый релиз транзитивной зависимости ломает инварианты.
-
Источники пакетов. Переопределение индексов, отключение TLS‑проверок, внутренние зеркала без политики.
-
Уязвимости в уже выбранных версиях.
Конкретные меры:
Фиксация и аудит. Для скриптов создаём example.py.lock и регулярно прогоняем аудит зависимостей. В экосистеме PyPA для этого есть pip-audit, он использует базу уязвимостей PyPI и может работать поверх вашего requirements или установленной среды. Можно запускать через uvx в изолированном окружении:
uvx pip-audit -r req.txt # или просканировать локальную среду uvx pip-audit --local
Проект поддерживается PyPA, есть GitHub Action.
Хеши и синхронизация. Для больших пайплайнов можно генерировать requirements с хешами, а устанавливать через uv pip sync, чтобы окружение соответствовало файлу один к одному. Управление хешами и параметры компиляции конфигурируются в uv.toml/pyproject.toml для uv pip compile.
Индексы и TLS. Не используем доверенные небезопасные хосты. В справочнике CLI прямо есть предупреждение: флаг --allow-insecure-host отключает верификацию цепочки сертификатов, что делает вас уязвимыми для MITM.
Воспроизводимость по времени. Добавляем exclude-newer к скрипту, держите lock рядом с ним, а CI запускайте с uv run --script или с экспортом в requirements.txt и последующим uv pip sync.
Сценарий: маленький CLI-интеграционный скрипт
Условие: нужен однофайловый инструмент для выгрузки данных из API, с ретраями и логами, без установки проекта и с воспроизводимыми версиями.
fetch_users.py:
# /// script # requires-python = ">=3.11" # dependencies = [ # "httpx==0.27.2", # "tenacity==9.0.0", # "structlog==24.4.0", # ] # [tool.uv] # exclude-newer = "2025-08-01T00:00:00Z" # /// from __future__ import annotations import os import sys import json import time import httpx import structlog from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type log = structlog.get_logger() API_URL = os.environ.get("API_URL", "https://httpbin.org/json") TIMEOUT = httpx.Timeout(10.0, connect=5.0) class FetchError(RuntimeError): pass @retry( reraise=True, stop=stop_after_attempt(4), wait=wait_exponential(multiplier=0.5, min=0.5, max=5), retry=retry_if_exception_type((httpx.HTTPError, FetchError)), ) def fetch_json(client: httpx.Client, url: str) -> dict: r = client.get(url) if r.status_code >= 500: # серверная ошибка — пробуем повторить raise FetchError(f"server_error={r.status_code}") r.raise_for_status() return r.json() def main() -> int: structlog.configure(processors=[structlog.processors.add_log_level, structlog.processors.JSONRenderer()]) with httpx.Client(timeout=TIMEOUT, headers={"User-Agent": "fetch-users/1.0"}) as client: t0 = time.perf_counter() data = fetch_json(client, API_URL) dt = time.perf_counter() - t0 log.info("fetched", bytes=len(json.dumps(data).encode("utf-8")), seconds=round(dt, 3)) print(json.dumps(data, ensure_ascii=False)) return 0 if __name__ == "__main__": try: sys.exit(main()) except httpx.RequestError as e: log.error("http_error", error=str(e)) sys.exit(2) except Exception as e: log.error("unexpected", error=str(e.__class__.__name__)) sys.exit(3)
Запуск:
uv run fetch_users.py # закрепляем версии рядом со скриптом uv lock --script fetch_users.py # экспорт для CI uv export --script fetch_users.py -o requirements.txt uvx pip-audit -r requirements.txt
uv задокументировал и lock для скриптов, и экспорт.
Инспекция дерева зависимостей и политика источников
Перед выкладкой в хук pre‑commit можно обозреть дерево:
uv tree --script fetch_users.py
Для частных индексов и зеркал используем конфигурационные файлы uv.toml или pyproject.toml для uv pip и не раздаем --allow-insecure-host. Конфиги поддерживаются на уровне проекта и пользователя; есть и системный уровень. Путь к кешу и параметры можно контролировать через команды и переменные окружения.
Итог
PEP 723 решает рутину: однофайловые скрипты могут быть самодостаточными, без вопросов о том, как установить зависимости. uv сделал это быстрым и операционно удобным: редактирование зависимостей, shebang, lock рядом со скриптом, экспорт для CI, контроль версии интерпретатора.
Как и в случае с однофайловыми скриптами по PEP 723 и uv, которые позволяют быстро проверить идею без лишних подготовительных шагов, у вас есть возможность так же познакомиться с курсом Python Developer. Professional — через бесплатные открытые уроки, доступные по ссылке.
Кроме того, вы можете пройти бесплатное вступительное тестирование, которое позволит оценить ваши знания и навыки.
А если хотите узнать больше о самом курсе и впечатлениях участников, загляните в секцию с отзывами.
ссылка на оригинал статьи https://habr.com/ru/articles/940674/
Добавить комментарий