
У меня есть функция. Крошечная - десяток строк, которые приводят суммы из платёжек к нормальному виду: "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.Всё. Это весь цикл: определили функцию, publish - и она стала объектом локальной библиотеки (нодой); call по имени - и вернулось значение. Причём из любого другого скрипта на этой машине достаточно client.call("clean_amount", ...) - саму функцию второй раз писать не нужно, она уже опубликована.
Вернулось, кстати, не только число. У результата есть result.output (само значение), result.mode (где выполнялось) и result.artifacts - файлы, которые нода сложила по ходу дела. Плюс у запуска остаётся история: что вызывали, с какими аргументами, чем завершилось даже в случае ошибки или падения.
Что здесь только что произошло
Три вещи, которые стоит удержать в голове, которые демонстрируют явно почти всю идею.
Нода - это ваша функция, упакованная в версионированную переносимую форму. Не pickle, не «слепок процесса», а разобранное описание функции, которое можно хранить, переносить и воспроизводить. При этом обо всех зависимостях и окружении уже позаботились фреймворк и локальный сервис splime.
Демон - локальный сервис. Он хранит опубликованные объекты, собирает под них окружение и запускает исполнение, в том числе в Docker-контейнере. Живёт у вас на локальной машине.
Local-first. Обратите внимание: 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.)

Сразу про рамки: где 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.
Что дальше в серии
Это была общая картина. Дальше мы планируем копнуть вглубь, рассказа о тех или иных инженерных решениях, которые мы приняли в ходе реализации, а также причины такого выбора; благо локальный движок открыт, и по ходу можно сверяться с кодом:
Локальный уровень. Как устроен демон, где и в каком виде живут ноды и как две ноды передают данные друг другу: адаптеры, теги артефактов и почему ребро между нодами это файл, а не pickle через pipe.
Окружения. Как демон собирает venv через uv и как выбрать рантайм отдельно для каждой ноды: native, venv-subprocess или docker.
Запуски как данные. Пайплайн упал на третьей ноде - смотрим манифест запуска, меняем адаптер и продолжаем с места падения, не пересчитывая то, что уже посчитано.
Куда мы ведём проект. Command-ноды, декларации, ноды прямо из GitHub-репозитория и первый не-Python язык и почему мы принимаем именно такие решения (включая подходы, которые попробовали и осознанно выкинули).
splime молодой, и то, куда он пойдёт дальше, во многом зависит от таких обсуждений. Если что-то показалось спорным, неудобным или, наоборот, попало в точку - напишите в комментариях или заведите issue на GitHub. Из этих разговоров вырастут и следующие статьи серии, и приоритеты в роадмапе. Я рядом и отвечу.
Комментарии (9)

titan_pc
12.07.2026 12:25Решение с пакетом было верным. В чем разница между pip install splime. И pip install МойПакетПолезногоБарахла.
Давать по сети незащищенный доступ к куче рабочих питонов - звучит как надежный план создать поле хакерам для творчества
Подозреваю что сделано на старом добром RPC. Со всеми вытекающими.
Ну а ещё есть ray с похожей идеей - запустить что угодно в каком хочешь окружении.
Ну куча возни со своим пакетом и выпуском ему тегов - на практике делается 5 минут. Вы автоматизировали то, что заниемает 5 минут. Из которых 4ре на деплой.
А интересно какая куча возни ждет в части ИБ если поставить себе это чудо, у которого представляете без авторизации можно на удаленной ноде делать чего хочется.
Думаю вы просто хотели ray. И распределенные вычисления. Но Вам попался "сложный" airflow с его дагами. После прочтения статьи возникло да ощущение что ray ещё раз разработали. Мне он не особо нравится, но вещь мощная.

YastrebovKS Автор
12.07.2026 12:25Про ИБ вы правы наполовину: демон слушает 127.0.0.1, аутентификации у локального API нет. Как docker.sock- забиндите на 0.0.0.0 и получите ровно то, что описали.
Но удалёнка устроена иначе: входящего порта на рабочей машине нет вообще. Демон сам подключается к серверу по приватным токенам и сам забирает задания, кнопки «выполни у себя этот код» не существует. Доступ только к доверенным машинам, исполняется только то, что вы опубликовали сами (или из доверенных источников, если, например, работаете в команде).
Про пакет соглашусь: опубликовать и правда пять минут. Но "платите" вы не за публикацию, а за потребление: каждый проект тянет его со всем деревом зависимостей и исполняет у себя. splime ставится один раз, и зависимости ноды к вызывающему не приезжают.
Ray заменить мы не пытаемся: нужны распределённые вычисления - берите Ray. У него юнит - это таска в кластере ради скорости; у нас - версионированная нода для легкого переиспользования. Кластер не нужен вообще.
Модель угроз в статье описана слишком достаточно плохо, спасибо за замечания, в дальнейшем учту!

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

YastrebovKS Автор
12.07.2026 12:25Спасибо за вопрос! Про версии: publish изменившейся функции создаёт новую версию, старая не мутирует. Версионирование сейчас сделано достаточно банально и новый элемент с тем же именем просто инкрементирует предыдущий паблиш. Вызов call по имени объекта берёт latest, но версию можно прибить гвоздём: version=N.
А про несовпадение kwargs отвечу честно: проверки сигнатуры на этапе call сейчас нет - несовпадение ловит уже Python при исполнении, обычным TypeError, и это приезжает как упавший запуск (его стейт сохраняется, можно посмотреть run-show). То есть не тихо, но поздно. Валидировать до запуска- отличная идея, забираю в бэклог.

YastrebovKS Автор
12.07.2026 12:25Про side-effects: идемпотентность целиком на авторе ноды, демон её не гарантирует и не притворяется. Он даёт изоляцию, а не чистоту: отдельное окружение, у docker-рантайма сеть по умолчанию выключена (--network none), файлы ходят через объявленные артефакты. Важный нюанс: resume считает ноду функцией от входов. Ноды из множества пересчёта выполнятся заново вместе со всеми сайд-эффектами, а замороженные не выполнятся вообще. Если внутри ноды запись в БД, это надо держать в голове.
За local-first спасибо - это осознанная позиция: SPLClient() без токенов в сеть не ходит вообще.
Ninil
Не совсем понял цель. Зачем очередной велосипед, если уже давно есть Artifactory?
ПыСы это же не случай когда вайбкодеры без фундаментальных знаний даже “не знают того что они не знают” и поэтому изобретают велосипед? Ведь нет?)
YastrebovKS Автор
Отличный вопрос про Atrifactory! Но тут идея совсем другая - это же не менеджер пакетов, коим является и Artifactory, и Nexus, напрмер. В splime вы публикуете функкцию, а не пакет со всей его обвязкой. Вызывающему не нужно ставить зависимости опубликованного объекта: демон собирает изолированное окружение (uv / venv / docker) и исполняет ноду там. И тот же самый вызов можно выполнить не только у себя локально, а на любой машине, заранее связанной с текущим пользователем.
Ninil
Ниже сформулированили частично мой consern. Какие реальные продакшен-кейсы вы видите для данного решения?
YastrebovKS Автор
Фреймворк и демон спроектированы изначально под три типа задач:
1) функция с неудобными зависимостями: скоринг на torch, например, который надо вызывать из лёгкого сервиса или ноутбука, не втаскивая тяжелые зависимости torch’а к вызывающему.
2) выполнить задачу там, где находятся данные: вызываешь ноду по имени, а исполняется она на машине с доступом к БД или какими-то конкретными ресурсами, причём вызывающему выдаётся execute, а не креды.
3) самое распространенное приземлённое - длинные пайплайны, падающие на N-м шаге из K: resume продолжает с места падения, не пересчитывая дорогой апстрим.
Сейчас в тестовом режиме гоняем в ML-процессах внутри команды для разработки и деплоя моделей.