У меня есть функция. Крошечная - десяток строк, которые приводят суммы из платёжек к нормальному виду: "1 234,50 ₽" превращают в 1234.5. Ничего гениального. Но я писал её, кажется, раз пять. В парсере банковских выписок, в импортёре заказов, в отчёте для бухгалтерии, в скрипте, который потом выкинул, и ещё где-то, о чём уже забыл.

И каждый раз - чуть по-другому. Где-то забыл про неразрывный пробел. Где-то евро вместо рубля. В одном месте нашёл баг с минусом у отрицательных сумм и починил - а в остальных четырёх копиях он так и остался, потому что кто вообще помнит, где ещё живёт этот код.

Если что-то из этого знакомо - думаю, дальше будет полезно. Это первая из нескольких статей про splime - инструмент, который вырос ровно из этой задачи: переиспользовать код между проектами, не копируя его. Начну с общей картины: что это, зачем и как выглядит в работе.

Что мы вообще делаем с такими функциями

Когда полезная функция нужна во втором проекте, вариантов обычно три, и каждый по-своему раздражает.

Скопировать. Быстро и честно ровно до того момента, пока копий не станет три. Дальше начинается то, с чего я начал: правку в одной копии остальные не видят, и функция потихоньку расползается на пять слегка разных версий с разными багами.

Вынести в общий пакет. Взрослое решение - и правильное, когда функций много. Но ради одной clean_amount вы теперь версионируете пакет, публикуете его, тянете целиком в каждый проект (со всеми его зависимостями) и обновляете везде, где он прижился. Многовато возни ради десятка строк.

Завернуть в микросервис. Тоже вариант - если это сервис. Но clean_amount не сервис. Писать HTTP-обвязку, деплой и следить за аптаймом ради нормализации строки - это из пушки по воробьям.

А по сути нужно одно: опубликовать функцию один раз и звать её по имени откуда угодно. Не таскать код, не тянуть зависимости, не поднимать сервис. Позвал по имени - получил результат. Вот из этого желания и вырос splime.

splime за шесть строк

Ставим пакет (нужен Python 3.13+) и поднимаем локальный демон - это фоновый процесс, который будет хранить ваши функции и запускать их в любом проекте и окружении:

pip install splime
spl-daemon serve        # поднимет локальный демон на http://127.0.0.1:8765

Теперь та самая функция. Публикуем её как объект-ноду - версионированную, переносимую единицу - и зовём по имени:

from spl import SPLClient

def clean_amount(raw: str) -> float:
    # "1 234,50 ₽"  ->  1234.5
    cleaned = raw.replace(" ", "").replace("\xa0", "").replace("₽", "")  # \xa0 - неразрывный пробел
    return float(cleaned.replace(",", "."))

client = SPLClient()                       # локально; на сервер ничего не уходит
client.publish(clean_amount, name="clean_amount")

result = client.call("clean_amount", kwargs={"raw": "1 234,50 ₽"})
print(result.mode)     # "local"
print(result.output)   # 1234.5
Первый запуск: поднимаем демон, публикуем clean_amount и зовём по имени - в ответ 1234.5.
Первый запуск: поднимаем демон, публикуем clean_amount и зовём по имени - в ответ 1234.5.

Всё. Это весь цикл: определили функцию, publish - и она стала объектом локальной библиотеки (нодой); call по имени - и вернулось значение. Причём из любого другого скрипта на этой машине достаточно client.call("clean_amount", ...) - саму функцию второй раз писать не нужно, она уже опубликована.

Вернулось, кстати, не только число. У результата есть result.output (само значение), result.mode (где выполнялось) и result.artifacts - файлы, которые нода сложила по ходу дела. Плюс у запуска остаётся история: что вызывали, с какими аргументами, чем завершилось даже в случае ошибки или падения.

Что здесь только что произошло

Три вещи, которые стоит удержать в голове, которые демонстрируют явно почти всю идею.

Нода - это ваша функция, упакованная в версионированную переносимую форму. Не pickle, не «слепок процесса», а разобранное описание функции, которое можно хранить, переносить и воспроизводить. При этом обо всех зависимостях и окружении уже позаботились фреймворк и локальный сервис splime.

Демон - локальный сервис. Он хранит опубликованные объекты, собирает под них окружение и запускает исполнение, в том числе в Docker-контейнере. Живёт у вас на локальной машине.

Local-first. Обратите внимание: SPLClient() без всяких токенов работает полностью локально и никуда не ходит по сети. Никакого обязательного облака, аккаунта и «войдите, чтобы продолжить». Сервер подключается позже и только если он вам нужен.

Схема: SPLClient публикует и зовёт ноды; локальный демон хранит их в библиотеке, собирает окружение и исполняет.
Схема: SPLClient публикует и зовёт ноды; локальный демон хранит их в библиотеке, собирает окружение и исполняет.

«А если данные не у меня на машине?»

Вот тут - самое интересное, и ровно то, ради чего всё затевалось. Допустим, у вас опубликована нода потяжелее - score_batch, которой нужны данные и GPU с другой машины. Тот же самый вызов становится удалённым, стоит указать, где выполнять:

result = client.call(
    "score_batch",
    kwargs={"date": "2026-06-08"},
    target_machine="gpu-box",     # выполнить там, где живут данные/GPU/доступы
)
print(result.mode)     # "server"

Функция может ходить в базу, которой нет на вашем компьютере. Ей может быть нужен GPU или доступы, которые есть только на одной конкретной удаленной машине. Код при этом не переезжает к данным вручную и не дублируется - вы зовёте ноду по имени, а выполняется она там, где надо. Как это устроено внутри - тема следующих статей; пока достаточно того, что вызов один и тот же, меняется только «где». (Для удалённого запуска нужно просто подключить очередной локальный демон целевой машины к серверу splime – https://splime.io/app/ - про настройку расскажу дальше в серии; локально же всё работает сразу после pip install.)

Console на splime.io/app: подключённые машины-демоны, библиотеки и история запусков.
Console на splime.io/app: подключённые машины-демоны, библиотеки и история запусков.

Сразу про рамки: где splime, а где - другие инструменты

Чтобы не было завышенных ожиданий - и чтобы сэкономить нам время в комментариях.

Для расписаний и сложных DAG’ов есть прекрасные оркестраторы: Airflow, Prefect, Temporal. Сегодня splime не про это - он не гоняет пайплайны по расписанию, а даёт переиспользуемые ноды, которые такой оркестратор может вызывать. Одно неплохо ложится на другое.

Для массивной параллельности - размазать одну задачу на сотни ядер - есть Ray и родственные ему штуки. splime не соревнуется с ними за пропускную способность: его забота - версии, переносимость и вызов той или иной функции “просто по имени”.

Публичного маркетплейса кода splime сегодня тоже не строит: ноды приватные, вы публикуете их для себя и своей команды, с явным владением и доступами, а не выкладываете в общую витрину.

И это не просто “запусти любой код где угодно”: splime исполняет функции, которым вы осознанно доверяете, на машинах, которые вы контролируете, - граница доверия проходит там же, где всегда: на том, что вы сами решили опубликовать.

Если одной строкой: splime превращает проверенные Python-функции в версионированные переносимые ноды, которые можно переиспользовать между проектами и запускать локально или удалённо.

Стадия готовности splime

splime - молодой проект. Это alpha: публичный API ещё может меняться между релизами, и мы не притворяемся, что за плечами годы продакшена и толпы пользователей. Открыто главное - сам фреймворк и локальный демон, то есть весь локальный движок: он ставится из PyPI, а в код можно залезть на GitHub и посмотреть, как всё устроено, а не верить мне на слово. Сервер и веб-панель (Console на splime.io/app), через которые демоны собираются в команду и появляется удалёнка, - управляемая часть, её мы держим у себя. Если идея выглядит для вас интересно или, наоборот, кажется, что мы делаем ерунду, - сейчас как раз тот момент, когда обратная связь влияет на продукт.

Попробовать: pip install splime (Python 3.13+). Код, issue и звезда, если зашло: github.com/yastrebovks/splime. Документация и честный статус проекта: splime.io.

Что дальше в серии

Это была общая картина. Дальше мы планируем копнуть вглубь, рассказа о тех или иных инженерных решениях, которые мы приняли в ходе реализации, а также причины такого выбора; благо локальный движок открыт, и по ходу можно сверяться с кодом:

  1. Локальный уровень. Как устроен демон, где и в каком виде живут ноды и как две ноды передают данные друг другу: адаптеры, теги артефактов и почему ребро между нодами это файл, а не pickle через pipe.

  2. Окружения. Как демон собирает venv через uv и как выбрать рантайм отдельно для каждой ноды: native, venv-subprocess или docker.

  3. Запуски как данные. Пайплайн упал на третьей ноде - смотрим манифест запуска, меняем адаптер и продолжаем с места падения, не пересчитывая то, что уже посчитано.

  4. Куда мы ведём проект. Command-ноды, декларации, ноды прямо из GitHub-репозитория и первый не-Python язык и почему мы принимаем именно такие решения (включая подходы, которые попробовали и осознанно выкинули).

splime молодой, и то, куда он пойдёт дальше, во многом зависит от таких обсуждений. Если что-то показалось спорным, неудобным или, наоборот, попало в точку - напишите в комментариях или заведите issue на GitHub. Из этих разговоров вырастут и следующие статьи серии, и приоритеты в роадмапе. Я рядом и отвечу.

Комментарии (9)


  1. Ninil
    12.07.2026 12:25

    Не совсем понял цель. Зачем очередной велосипед, если уже давно есть Artifactory?

    ПыСы это же не случай когда вайбкодеры без фундаментальных знаний даже “не знают того что они не знают” и поэтому изобретают велосипед? Ведь нет?)


    1. YastrebovKS Автор
      12.07.2026 12:25

      Отличный вопрос про Atrifactory! Но тут идея совсем другая - это же не менеджер пакетов, коим является и Artifactory, и Nexus, напрмер. В splime вы публикуете функкцию, а не пакет со всей его обвязкой. Вызывающему не нужно ставить зависимости опубликованного объекта: демон собирает изолированное окружение (uv / venv / docker) и исполняет ноду там. И тот же самый вызов можно выполнить не только у себя локально, а на любой машине, заранее связанной с текущим пользователем.


      1. Ninil
        12.07.2026 12:25

        Ниже сформулированили частично мой consern. Какие реальные продакшен-кейсы вы видите для данного решения?


        1. YastrebovKS Автор
          12.07.2026 12:25

          Фреймворк и демон спроектированы изначально под три типа задач:
          1) функция с неудобными зависимостями: скоринг на torch, например, который надо вызывать из лёгкого сервиса или ноутбука, не втаскивая тяжелые зависимости torch’а к вызывающему.
          2) выполнить задачу там, где находятся данные: вызываешь ноду по имени, а исполняется она на машине с доступом к БД или какими-то конкретными ресурсами, причём вызывающему выдаётся execute, а не креды.
          3) самое распространенное приземлённое - длинные пайплайны, падающие на N-м шаге из K: resume продолжает с места падения, не пересчитывая дорогой апстрим.

          Сейчас в тестовом режиме гоняем в ML-процессах внутри команды для разработки и деплоя моделей.


  1. titan_pc
    12.07.2026 12:25

    Решение с пакетом было верным. В чем разница между pip install splime. И pip install МойПакетПолезногоБарахла.

    Давать по сети незащищенный доступ к куче рабочих питонов - звучит как надежный план создать поле хакерам для творчества

    Подозреваю что сделано на старом добром RPC. Со всеми вытекающими.

    Ну а ещё есть ray с похожей идеей - запустить что угодно в каком хочешь окружении.

    Ну куча возни со своим пакетом и выпуском ему тегов - на практике делается 5 минут. Вы автоматизировали то, что заниемает 5 минут. Из которых 4ре на деплой.

    А интересно какая куча возни ждет в части ИБ если поставить себе это чудо, у которого представляете без авторизации можно на удаленной ноде делать чего хочется.

    Думаю вы просто хотели ray. И распределенные вычисления. Но Вам попался "сложный" airflow с его дагами. После прочтения статьи возникло да ощущение что ray ещё раз разработали. Мне он не особо нравится, но вещь мощная.


    1. YastrebovKS Автор
      12.07.2026 12:25

      Про ИБ вы правы наполовину: демон слушает 127.0.0.1, аутентификации у локального API нет. Как docker.sock- забиндите на 0.0.0.0 и получите ровно то, что описали.

      Но удалёнка устроена иначе: входящего порта на рабочей машине нет вообще. Демон сам подключается к серверу по приватным токенам и сам забирает задания, кнопки «выполни у себя этот код» не существует. Доступ только к доверенным машинам, исполняется только то, что вы опубликовали сами (или из доверенных источников, если, например, работаете в команде).

      Про пакет соглашусь: опубликовать и правда пять минут. Но "платите" вы не за публикацию, а за потребление: каждый проект тянет его со всем деревом зависимостей и исполняет у себя. splime ставится один раз, и зависимости ноды к вызывающему не приезжают.

      Ray заменить мы не пытаемся: нужны распределённые вычисления - берите Ray. У него юнит - это таска в кластере ради скорости; у нас - версионированная нода для легкого переиспользования. Кластер не нужен вообще.

      Модель угроз в статье описана слишком достаточно плохо, спасибо за замечания, в дальнейшем учту!


  1. WordEngineer
    12.07.2026 12:25

    Идея с локальным демоном как альтернативой между copy-paste и публичным пакетом закрывает реальную дыру: обычно между этими крайностями ничего вменяемого нет, разве что монорепо с общим воркспейсом, но там своя возня с версиями внутри. Вопрос по механике: как splime резолвит конфликт версий, если clean_amount опубликована с одной сигнатурой, а в другом проекте ее вызывают с изменившимся набором kwargs? Падает молча или кидает ошибку валидации на call? И как быть с side-effects внутри ноды (например, если функция пишет в файл или делает сетевой запрос), демон гарантирует идемпотентность вызова, или это целиком забота автора ноды? Local-first подход, при котором сервер подключается только по желанию, по-моему, правильный выбор доверия. Многие альтернативы сразу тащат обязательный аккаунт и хотят логин на старте.


    1. YastrebovKS Автор
      12.07.2026 12:25

      Спасибо за вопрос! Про версии: publish изменившейся функции создаёт новую версию, старая не мутирует. Версионирование сейчас сделано достаточно банально и новый элемент с тем же именем просто инкрементирует предыдущий паблиш. Вызов call по имени объекта берёт latest, но версию можно прибить гвоздём: version=N.

      А про несовпадение kwargs отвечу честно: проверки сигнатуры на этапе call сейчас нет - несовпадение ловит уже Python при исполнении, обычным TypeError, и это приезжает как упавший запуск (его стейт сохраняется, можно посмотреть run-show). То есть не тихо, но поздно. Валидировать до запуска- отличная идея, забираю в бэклог.


    1. YastrebovKS Автор
      12.07.2026 12:25

      Про side-effects: идемпотентность целиком на авторе ноды, демон её не гарантирует и не притворяется. Он даёт изоляцию, а не чистоту: отдельное окружение, у docker-рантайма сеть по умолчанию выключена (--network none), файлы ходят через объявленные артефакты. Важный нюанс: resume считает ноду функцией от входов. Ноды из множества пересчёта выполнятся заново вместе со всеми сайд-эффектами, а замороженные не выполнятся вообще. Если внутри ноды запись в БД, это надо держать в голове.

      За local-first спасибо - это осознанная позиция: SPLClient() без токенов в сеть не ходит вообще.