У меня есть функция. Крошечная - десяток строк, которые приводят суммы из платёжек к нормальному виду: "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. Из этих разговоров вырастут и следующие статьи серии, и приоритеты в роадмапе. Я рядом и отвечу.

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


  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. titan_pc
        12.07.2026 12:25

        Вот да - "каждый проект тянет его со всем деревом зависимостей и исполняет у себя".
        А почему это проблема?

        Если мой пакет лёгкий и чистый питон. А у Вас статья так и начинается - надоело простые полезные функции везде таскать. То дерево зависимостей - 0 зависимостей. Ну или там 2-3.
        Архитектурно - я трачу такты ЦПУ, не напрягая сеть для передачи трафика между нодами, ради 2+2. То есть если у меня реально что-то лёгкое - то гонять ради этого сеть - как это там говорили раньше - зашквар, антипаттерн. Но по факту - анти профит.
        Ваше решение увеличивает нагрузку на сеть на ровном месте - маленький питон пакет, делающий операцию допустим за 100 микро-секунд - требует в Вашем случае сетевой доступ (который в лучшем случае +10 милли-секунд), снижающий пропускную способность системы, ради того, чтобы кода меньше сделать на 5 строчек.
        Ну это то чем Вы реально будете платить за уменьшение кода в данном случае.
        Вы ещё говорили что делать в таком случае микро-сервис - это как с пушки по воробьям.
        Но именно это и сделали, просто без докеров и контейнеров. То есть без обвеса в виде конфигов, ямлов и докерфайлов. А архитектурная суть не изменилась - сетевой доступ к простому пакету.

        Если вы написали крутую оболочку, которая например дережирует дровами всех СУБД на свете - ну это своя библиотека (SQLAclhemy) - Ваше решение точно не подходит.
        А если Вам нужен сетевой доступ к SQLAclhemy - ну это = микро-сервис.

        Если ваша задача - держать и пере-использовать вычисления разных pytorch - ну есть Ray. Ну вроде тот самый случай, когда зависимостей действительно много, а верхне-уровневого это выглядит как "Вот картинка - дай текст" def get_markdown(picture).
        И получается вы конкурируете только здесь с готовым решением.
        Но с ним и докер конкурирует. Иногда проще всё упаковать в один контейнер за кафкой,
        чем Ray притащить.

        Вобщем главный вывод - код всегда часть системы. Важно что он делает, и сколько кушает ресурсов и каких ресурсов он кушает.
        Как код устроен внутри - тоже важно безусловно. Но точно не важнее того, что код делает и сколько ресурсов кушает.

        Вы сделали нано-сервис по факту. Это когда микросервис на столько маленький, что вместо готового блюда, например - венигрет. Он является - кубиком свеклы.
        Но это не меняет сути - наличие сети между вызовами функций, по http/https (API) они там, gRPC, бинарю RPC, голому TCP/UDP. А отсюда есть золотое правило - наличие сети должно быть оправдано тем, что затраты на транспорт пакета должны быть существенно ниже чем время обработки операции. Ну там запаковать пакет, распаковать, послать, принять, распаковать, направить в нужный стек, вызвать функцию - вот эти все операционные расходы должны казаться ничтожными по сравнению с самой функцией.
        Если функция 2+2 - то у Вас плохой венигрет.
        Если функция КартинкуВМаркдаун - у Вас хороший венигрет.





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

          Правило Ваше я оспаривать не могу: clean_amount из статьи нодой делать не надо. Пример в статье учебный, взят ради наглядности механики.

          Реальная функциональная единица для упаковки - не 2+2, а пайплайн или специализированная функция: препроцессинг, скоринг, разбор документов, обвязка над моделью, etc. Таких в команде накапливаются десятки, у каждой свои зависимости и своя история версий. Общее пространство означает единую кодовую базу: функция живёт в одном месте одной версией и зовётся по имени из любого проекта, вместо того чтобы расползаться пятью копиями, каждая со своим багом.

          Оверхед амортизируется батчем: clean_amount(строка) - плохой винегрет; clean_amounts(10 000 строк) - уже нормальный. Сборка окружения разовая и кэшируется, а не платится на каждом вызове. А когда нода обязана выполниться там, где лежат данные, GPU или доступы - вы заплатите её и микросервисом, и Ray. Вопрос лишь в том, писать ли обвязку руками.

          И небольшая поправка: локальный вызов идёт на loopback, а не в сеть, так что «+10 мс» там нет. Однако на изолированных рантаймах мы на каждый запуск поднимаем процесс или контейнер (опционально), и это дороже сокета, тут согласен - Вы правы.


  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() без токенов в сеть не ходит вообще.