This repository has been archived on 2026-04-16. You can view files and clone it, but cannot push or open issues or pull requests.
substrate-sdk-python/substrate_sdk/app.py
Tyler King 89a054d656 initial: substrate-sdk-python v0.1.0
Python SDK for shellbound Django applications.
Provides ShellApp, ShardContext, ShellboundMiddleware.
Emits Chronicle events to stdout in dev mode.

Includes fix for IndexError in apps.py when
DJANGO_SETTINGS_MODULE has no dots (e.g.
instance_settings). Shard name now falls back
safely without eager default argument parsing.

Implements SHELLBOUND-APP-0001 §4 (dev mode).
Wired into entropyopposition as of 2026-03-18.
2026-03-18 13:53:58 -04:00

149 lines
4.7 KiB
Python

"""ShellApp — the main SDK entry point.
Usage:
from substrate_sdk import ShellApp
app = ShellApp(shard_name="my-app", version="1.0.0", capabilities={"network": True})
context = app.register()
"""
import json
import logging
import os
import socket
import uuid
from typing import Optional
from .chronicle import LAYER_APPLICATION, ShardEmitter, emit_event
from .context import ShardContext
logger = logging.getLogger(__name__)
SHELL_SOCKET = os.environ.get("SUBSTRATE_SHELL_SOCKET", "/run/bascule/shard.sock")
REGISTER_TIMEOUT = float(os.environ.get("SUBSTRATE_REGISTER_TIMEOUT", "5.0"))
class ShellApp:
"""A shellbound application instance.
Non-fatal everywhere: shell unavailable → dev mode. Emit fails → logged.
Never raises due to shell issues. Never blocks startup >5 seconds.
"""
def __init__(
self,
shard_name: str,
version: str = "0.0.0",
capabilities: Optional[dict] = None,
language: str = "python",
):
self.shard_name = shard_name
self.version = version
self.capabilities = capabilities or {}
self.language = language
self._context: Optional[ShardContext] = None
self._emitter: Optional[ShardEmitter] = None
self._socket: Optional[socket.socket] = None
def register(self) -> ShardContext:
"""Register with bascule shell. Falls back to dev mode if unavailable."""
context = ShardContext(
shard_name=self.shard_name,
shard_id=str(uuid.uuid4()),
language=self.language,
version=self.version,
)
if os.path.exists(SHELL_SOCKET):
try:
context = self._register_with_shell(context)
except Exception as e:
logger.warning("[substrate-sdk] Shell registration failed (dev mode): %s", e)
else:
logger.info("[substrate-sdk] %s: running without shell — development mode", self.shard_name)
self._context = context
self._emitter = ShardEmitter(context)
self._emitter.emit(
"APP_SHARD_STARTED",
LAYER_APPLICATION,
{
"shard_name": self.shard_name,
"shard_id": context.shard_id,
"shard_did": context.shard_did,
"language": self.language,
"version": self.version,
"capabilities": self.capabilities,
"ffc_did": context.ffc_did,
"governed": context.is_governed,
},
)
return context
def shutdown(self, reason: str = "normal") -> None:
"""Deregister from shell. Emits APP_SHARD_STOPPED."""
if self._emitter and self._context:
self._emitter.emit(
"APP_SHARD_STOPPED",
LAYER_APPLICATION,
{"shard_name": self.shard_name, "shard_id": self._context.shard_id, "reason": reason},
)
if self._socket:
try:
msg = json.dumps({
"type": "SHARD_DEREGISTER",
"shard_id": self._context.shard_id if self._context else "",
"reason": reason,
})
self._socket.sendall((msg + "\n").encode())
except Exception:
pass
finally:
self._socket.close()
@property
def emitter(self) -> Optional[ShardEmitter]:
return self._emitter
@property
def context(self) -> Optional[ShardContext]:
return self._context
def _register_with_shell(self, context: ShardContext) -> ShardContext:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(REGISTER_TIMEOUT)
s.connect(SHELL_SOCKET)
msg = json.dumps({
"type": "SHARD_REGISTER",
"shard_name": self.shard_name,
"version": self.version,
"pid": os.getpid(),
"capabilities": self.capabilities,
"sdk_version": "0.1.0",
"language": self.language,
})
s.sendall((msg + "\n").encode())
buf = b""
while b"\n" not in buf:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
response = json.loads(buf.split(b"\n")[0])
if response.get("type") == "SHARD_REGISTERED":
context.shard_did = response.get("shard_did", "")
context.svid = response.get("svid", "")
context.accord_hash = response.get("accord_hash", "")
context.ffc_did = response.get("ffc_did", "")
context.is_governed = True
self._socket = s
logger.info("[substrate-sdk] %s registered. DID: %s", self.shard_name, context.shard_did)
return context