Splitting a script across multiple files¶
Once a script grows past a few handlers you'll want to move shared helpers into
their own .py files. siphon puts the script's own directory on the Python
sys.path, so a plain import of a sibling module just works — no
sys.path.insert boilerplate.
Sibling helper next to the script¶
# helpers.py
def normalize_number(raw: str) -> str:
digits = "".join(c for c in raw if c.isdigit())
if digits.startswith("00"):
return "+" + digits[2:]
return digits
# main.py
from siphon import proxy, log
import helpers
@proxy.on_request("INVITE")
def on_invite(request):
request.ruri.user = helpers.normalize_number(request.ruri.user or "")
request.relay()
Point the config at the main script as usual — nothing else is needed:
Shared library across scripts¶
For helpers shared by several scripts (or several NFs) that don't live next to
any one script, list the directories in include_paths. They're added to
sys.path after the script's own directory.
script:
path: "/etc/siphon/pcscf.py"
include_paths:
- "/etc/siphon/lib" # e.g. /etc/siphon/lib/ims_common.py
Hot-reload¶
Helper modules hot-reload exactly like the main script. Editing and saving
helpers.py (or anything under an include_paths directory that the script
imports) triggers a reload, and siphon re-imports the helper from its new
source — you don't have to touch main.py to pick up a helper change.
A reload resets module-level state. The script is re-executed in a fresh
namespace and every helper it imported is dropped from sys.modules, so any
dict, list, counter or connection held at module level starts empty again. That
is the cost of a reload, and it is why the rest of this section is careful about
what counts as one. Anything that has to survive a reload belongs in the
cache namespace, not in a module global — see No
cross-request state in helpers below.
Only your own code reloads you. siphon reloads for the script's own file and
for helper modules the running script has actually imported — not for every
.py in a watched directory. Two siphon processes whose scripts live in one
directory are therefore independent: deploying the routing script does not
re-execute the charging bridge next to it and empty its session table. A helper
nothing has imported yet is not a trigger either, and does not need to be: the
first import after it is written reads the new file.
A burst of writes is one reload. Events arriving within 250 ms are coalesced, so a deploy that writes three helpers recompiles the script once rather than three times.
SIGHUP reloads too, in both reload: auto and reload: sighup. Under
sighup the inotify watcher is off and the signal is the only trigger, which is
how a deployment decides when module state is wiped:
POST /admin/script/reload does the same thing over the admin API, in either
mode.
Rules and limits¶
- Absolute imports only. The main script runs as a plain module, not a
package, so
from . import helpersdoes not work — useimport helpers. - No cross-request state in helpers. The same rule as the main script: don't
keep per-call state in module-level dicts/lists (it isn't shared across the
worker threads or replicas and is wiped on reload). Use the
cachenamespace for shared state. Pure functions and constants are fine. - A helper named after a stdlib module shadows it — the same foot-gun as a
normal
python script.py. Give helpers distinct names (sip_helpers.py, notemail.py). - Watching is non-recursive. A helper directly in the script's directory or
in an
include_pathsentry is watched; one a level deeper is not, so a helper laid out as a package (lib/mypkg/__init__.py) will not hot-reload. Keep helper modules flat in a watched directory, or reload withSIGHUPafter deploying a package.