Samuel Dirksz

System · Internal

The PyRunner integration

Internal tool

My automation host accepts one flat Python file per script. No folders, no arguments, no shared code. This is how twenty scripts with a common library run there anyway.

Facts

Status
In daily use, not a product
Stack
Python · MCP · httpx
The host
PyRunner, self-hosted. Third party, not mine.
Scripts running
Around twenty
Keys
In the host's secret store, never in a conversation

The constraint

The host is deliberately simple, and mostly that is a benefit. One script is one file. Packages install per script. Secrets are injected as environment variables. Scheduling is cron, with no way to pass parameters in.

That works fine for one script. By script number eight I had the same brand-name matching function pasted into five places, in five slightly different versions. That is how you get a bug that only affects one customer.

The source bundler

The answer was a small tool that flattens a whole project into one file. It reads the main script and the shared modules, and registers each module as an already-loaded entry before the first import runs. The import statements in the code do not have to change.

The result is a single file that behaves exactly as the project did locally, with the same command-line flags. I develop in an ordinary multi-file project with tests, and only bundle it on the way up.

The shim between cron and the command line

Scheduled runs cannot pass arguments, but they can set environment variables. So the shim translates: a variable holding a brand name becomes the flag the script already understands, and the same for limits, dry runs and date ranges.

That means the same script runs identically when I test it on my machine and when it fires at six in the morning. No separate production path, which is the path that never gets tested.

Keys that never enter a conversation

The host answers a webhook by saying the job is queued, not with the result. So the scripts write to a datastore and the caller reads from there. The datastore is effectively an async result bus.

Access to it goes through a small MCP server where the API key lives in the server's own configuration. I can ask for a result without the key ever becoming part of the conversation. That came after one of my skills had a key written directly into it, and the key went stale without anything saying so.

Each skill now knows one address. Everything else it fetches from there at startup.

What this is not

The host itself is not my work. It is a self-hosted third-party product. What I built is the integration around it, and it only exists because the host has the constraints it has.

This is personal infrastructure. No test coverage to speak of, no monitoring on the bundler itself, and the documentation is notes to myself. If someone else took it over tomorrow they would spend a day working it out.

A real queueing system would solve this more cleanly. I chose to build around the constraint rather than switch platforms, because switching costs more than the shim did.

Back to the index