IMS control¶
The namespaces that make SIPhon an IMS core: iFC evaluation (isc), 5G SBI /
N5 policy authorization and Nbsf discovery (sbi), SIP presence (presence),
lawful intercept (li), and the Session Recording Server hooks (srs).
isc namespace¶
Initial Filter Criteria evaluation (3GPP TS 29.228 / IMS Service Control).
Mock ISC namespace — Initial Filter Criteria evaluation for testing.
Store per-user iFC profiles and evaluate them against requests.
Example::
from siphon_sdk import mock_module
mock_module.install()
from siphon import isc
# Store a profile (in mock, stores raw XML string)
count = isc.store_profile("sip:alice@example.com", ifc_xml)
# Evaluate — returns pre-configured matches
matches = isc.evaluate("sip:alice@example.com", "INVITE",
"sip:bob@example.com", [], "originating")
store_profile
¶
Parse and store an iFC XML profile for an AoR.
In the mock, the XML is stored as-is (no actual parsing).
Use set_eval_results() to configure what evaluate() returns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aor
|
str
|
Address of Record. |
required |
ifc_xml
|
str
|
Raw iFC XML string. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of iFCs "parsed" (always 1 in mock unless configured otherwise). |
remove_profile
¶
Remove a stored profile.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aor
|
str
|
Address of Record. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
has_profile
¶
Check if a profile is stored for an AoR.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aor
|
str
|
Address of Record. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
evaluate
¶
evaluate(
aor: str,
method: str,
ruri: str,
headers: "list[tuple[str, str]]",
session_case: str = "originating",
) -> list[dict]
Evaluate iFCs for a request.
Returns pre-configured results (via set_eval_results) or an empty list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aor
|
str
|
Address of Record. |
required |
method
|
str
|
SIP method (e.g. |
required |
ruri
|
str
|
Request-URI string. |
required |
headers
|
'list[tuple[str, str]]'
|
List of (name, value) tuples. |
required |
session_case
|
str
|
Session case string. |
'originating'
|
Returns:
| Type | Description |
|---|---|
list[dict]
|
List of dicts with keys: |
list[dict]
|
|
set_eval_results
¶
Configure what evaluate() returns for a given AoR.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aor
|
str
|
Address of Record. |
required |
results
|
list[dict]
|
List of dicts, each with keys |
required |
Example::
isc.set_eval_results("sip:alice@example.com", [
{"server_name": "sip:as1@example.com", "default_handling": 0,
"service_info": None, "priority": 0},
])
sbi namespace¶
5G Service-Based Interface — N5/Npcf policy authorization plus Nbsf_Management PCF discovery.
PCF callbacks¶
The PCF calls back over HTTP on the address in sbi.notif_listen. Advertise
http://<notif_listen>/sbi/events as notif_uri; the PCF appends the
TS 29.514 suffix for each callback, so one listener serves both.
| Route | Body (verbatim dict) | Hook |
|---|---|---|
POST /sbi/events/notify |
EventsNotification |
@sbi.on_event |
POST /sbi/events/terminate |
TerminationInfo |
@sbi.on_terminate |
Both answer 204 once the handlers ran (a handler that raises is logged and
still acknowledged), 400 for a body that is not JSON, and 503 when siphon's
Python executor could not take the job, so the PCF knows the callback was not
handled. Any other path is 404.
A termination can arrive for any app session created with notif_uri. Events
only arrive for what the session subscribed to, with
create_session(events=[...], notif_uri=...) or
update_session(events=[...]).
The bare POST /sbi/events, which reached @sbi.on_event for a PCF that posted
to the advertised URI without a suffix, was deprecated in 1.9.0 and removed in
1.10.0. It now answers 404 like any other unknown path. A PCF that appends the
TS 29.514 suffix — which is what the spec defines — is unaffected.
Mock SBI namespace for testing scripts that use from siphon import sbi.
Provides mock N5/Npcf policy authorization methods plus Nbsf_Management
discovery (discover_pcf_binding).
Example::
import asyncio
from siphon_sdk import mock_module
mock_module.install()
from siphon import sbi
# Awaitable, as in siphon, so a synchronous test drives it through
# asyncio.run; an async test just awaits it.
result = asyncio.run(sbi.create_session(sip_call_id="call-1", ue_ipv4="10.0.0.1"))
assert result["authorized"] is True
create_session
¶
create_session(
af_app_id: str = "IMS Services",
sip_call_id: Optional[str] = None,
supi: Optional[str] = None,
ue_ipv4: Optional[str] = None,
ue_ipv6: Optional[str] = None,
dnn: Optional[str] = None,
notif_uri: Optional[str] = None,
media_components: Optional[list] = None,
pcf_uri: Optional[str] = None,
events: Optional[list[str]] = None,
) -> Optional[dict]
Create an N5 app session for QoS policy authorization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
af_app_id
|
str
|
AF-Application identifier (default |
'IMS Services'
|
sip_call_id
|
Optional[str]
|
SIP Call-ID for correlation. |
None
|
supi
|
Optional[str]
|
Subscription Permanent Identifier. |
None
|
ue_ipv4
|
Optional[str]
|
UE IPv4 address. |
None
|
ue_ipv6
|
Optional[str]
|
UE IPv6 address. |
None
|
dnn
|
Optional[str]
|
Data Network Name. |
None
|
notif_uri
|
Optional[str]
|
base URI of siphon's PCF callback listener,
|
None
|
media_components
|
Optional[list]
|
list of media-component dicts (same shape as
|
None
|
pcf_uri
|
Optional[str]
|
per-call N5 target — address this session at the given PCF
base URL (e.g. a BSF-discovered |
None
|
events
|
Optional[list[str]]
|
PCF events to subscribe to, by TS 29.514 |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
Optional[dict]
|
(the absolute resource URI — persist it and hand it back to |
Optional[dict]
|
|
Optional[dict]
|
teardown), or |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
TypeError
|
|
Example::
result = await sbi.create_session(
sip_call_id=request.call_id,
ue_ipv4=request.source_ip,
notif_uri="http://192.0.2.10:8080/sbi/events",
events=["FAILED_RESOURCES_ALLOCATION"],
)
delete_session
async
¶
Delete an N5 app session.
True means the session no longer exists on the PCF: siphon deleted
it (the PCF answered 2xx), or it was already gone (the PCF answered
404). The second is the usual answer to the delete an
@sbi.on_terminate handler makes with termination["resUri"]. In
both cases siphon stops tracking the session, so
siphon_sbi_npcf_app_sessions_active comes back down.
False means the delete failed (a transport error or any other
non-2xx answer); siphon keeps tracking the session.
In this mock an unknown session is treated as the PCF's 404 and
returns True; use set_delete_failure to exercise False.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
str
|
The app session id from |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
|
Example::
@sbi.on_terminate
async def handle_termination(termination):
if not await sbi.delete_session(termination["resUri"]):
log.warn("app session delete failed; PCF still holds it")
update_session
¶
update_session(
session_id: str,
media_components: Optional[list] = None,
events: Optional[list[str]] = None,
notif_uri: Optional[str] = None,
) -> Optional[dict]
Update an N5 app session (media renegotiation, event subscription).
The modify is a JSON merge patch, so only what you pass is sent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
str
|
The app session id to update, or the absolute
|
required |
media_components
|
Optional[list]
|
list of media-component dicts (same shape as
|
None
|
events
|
Optional[list[str]]
|
replace the subscribed PCF events with these (same names
as |
None
|
notif_uri
|
Optional[str]
|
a new callback base for the subscription
( |
None
|
Returns:
| Type | Description |
|---|---|
Optional[dict]
|
Dict with |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
TypeError
|
|
discover_pcf_binding
¶
discover_pcf_binding(
ue_ipv4: Optional[str] = None,
ue_ipv6: Optional[str] = None,
) -> Awaitable[Optional[dict]]
Nbsf_Management discovery — look up the PCF binding for a UE IP.
Returns a binding dict (5G; configure via set_binding), None
when the BSF has no binding (404 / 4G), or raises sbi.BsfError when
configured unhealthy via set_bsf_error.
Exactly one of ue_ipv4 / ue_ipv6 must be supplied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ue_ipv4
|
Optional[str]
|
UE IPv4 address (the IPsec SA peer). |
None
|
ue_ipv6
|
Optional[str]
|
UE IPv6 address/prefix. |
None
|
Returns:
| Type | Description |
|---|---|
Awaitable[Optional[dict]]
|
The binding dict (incl. a ready-to-use |
on_event
staticmethod
¶
Register a handler for incoming PCF event notifications (N5).
The handler receives the PCF's EventsNotification document
(TS 29.514 §5.6.2.6) verbatim as a dict — every field is preserved,
so the keys are the exact 3GPP wire names. Use evSubsUri to
correlate the event with the app-session you created, and evNotifs
for the per-event list. Each entry's flows carries medCompN +
fNums (not flow descriptions).
siphon serves the callback on sbi.notif_listen at
POST /sbi/events/notify: advertise
http://<notif_listen>/sbi/events as notif_uri and the PCF
appends /notify. The PCF only sends events you subscribed to with
create_session(events=[...]) or update_session(events=[...]).
Sync and async handlers both work. A handler that raises is logged and
the PCF still gets 204: a retry would hit the same bug. When
siphon's Python executor is saturated and the handler cannot run, the
PCF gets 503 so it knows the notification was not taken.
POST /sbi/events (no suffix) also reaches this hook, for a PCF that
posts to the advertised URI as-is. It is deprecated and goes away in the
next minor release.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
Any
|
|
required |
Returns:
| Type | Description |
|---|---|
Any
|
|
Example::
@sbi.on_event
def handle_pcf_event(event):
session_events_uri = event.get("evSubsUri")
for notif in event.get("evNotifs", []):
if notif["event"] == "FAILED_RESOURCES_ALLOCATION":
log.warn(f"PCF could not allocate resources: {session_events_uri}")
on_terminate
staticmethod
¶
Register a handler for PCF-initiated app-session termination (N5).
The PCF sends this when the PDU session behind an app session is
released (the N5 counterpart of a Diameter Rx ASR). The handler
receives the PCF's TerminationInfo document (TS 29.514) verbatim as
a dict:
termCause: why, e.g."PDU_SESSION_TERMINATION"or"ALL_SDF_DEACTIVATION".resUri: the app-session resource URI, the same valuecreate_sessionreturned asapp_session_uri. Use it to find the call the session belonged to.
TS 29.514 has the AF acknowledge the termination and then delete the
app session, so a handler normally releases its per-call state and
calls sbi.delete_session(termination["resUri"]). That also drops
siphon's own tracking of the session.
This is a separate hook from on_event on purpose: an event leaves
the session in place, a termination means it is ending. siphon serves
it on sbi.notif_listen at POST /sbi/events/terminate; the PCF
appends /terminate to the notif_uri given to
create_session, so the same http://<notif_listen>/sbi/events
base serves both callbacks. Answers follow on_event: 204 once the
handlers ran (a raising handler is logged), 503 when the executor
could not run them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
Any
|
|
required |
Returns:
| Type | Description |
|---|---|
Any
|
|
Example::
@sbi.on_terminate
async def handle_termination(termination):
log.warn(f"PCF ended app session: {termination['termCause']}")
await sbi.delete_session(termination["resUri"])
set_authorized
¶
Configure whether create_session returns authorized (test helper).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
authorized
|
bool
|
Whether sessions should be authorized. |
required |
set_binding
¶
Configure what discover_pcf_binding returns (test helper).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
binding
|
Optional[dict]
|
a binding dict (5G case) or |
required |
set_bsf_error
¶
Configure discover_pcf_binding to raise BsfError (test helper).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raise_error
|
bool
|
when True, |
required |
set_delete_failure
¶
Make delete_session fail (test helper).
Stands in for a PCF that answers the delete with an error other than
404, or cannot be reached: delete_session returns False and the
session stays tracked.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fail
|
bool
|
when True, |
required |
BsfError¶
Raised by sbi.discover_pcf_binding(...) when the BSF is unhealthy.
Bases: RuntimeError
Raised by sbi.discover_pcf_binding() when the BSF is unhealthy
(5xx / timeout / transport / malformed body).
A 404 (no binding for the UE IP) is not a BsfError — it returns
None (the 4G UE case). Mirrors the Rust sbi.BsfError exception.
presence namespace¶
SIP presence document publish/lookup and subscription tracking (RFC 3856 / 6665).
Mock presence namespace — SIP presence publish/subscribe for testing.
Manages presence documents and subscriptions in-memory.
Example::
from siphon_sdk import mock_module
mock_module.install()
from siphon import presence
etag = presence.publish("sip:alice@example.com", "<presence/>", expires=3600)
doc = presence.lookup("sip:alice@example.com")
assert doc == "<presence/>"
sub_id = presence.subscribe("sip:bob@example.com", "sip:alice@example.com")
watchers = presence.subscribers("sip:alice@example.com")
assert len(watchers) == 1
Test helper::
from siphon_sdk.mock_module import get_presence
p = get_presence()
p.clear()
publish
¶
Publish a presence document for a presentity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity
|
str
|
Presentity URI (e.g. |
required |
pidf_xml
|
str
|
PIDF XML body string. |
required |
expires
|
int
|
Document expiry in seconds (default: 3600). |
3600
|
Returns:
| Type | Description |
|---|---|
str
|
An etag string assigned to the published document. |
Example::
etag = presence.publish("sip:alice@example.com",
"<presence><tuple><status><basic>open</basic></status></tuple></presence>")
lookup
¶
Look up the current presence document for a URI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity
|
str
|
Presentity URI to look up. |
required |
Returns:
| Type | Description |
|---|---|
Optional[str]
|
PIDF XML string, or |
subscribe
¶
Subscribe to presence for a resource.
Creates a new subscription and returns its ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscriber
|
str
|
Watcher URI (e.g. |
required |
resource
|
str
|
Presentity URI to watch. |
required |
event
|
str
|
Event package name (default: |
'presence'
|
expires
|
int
|
Subscription duration in seconds (default: 3600). |
3600
|
Returns:
| Type | Description |
|---|---|
str
|
Subscription ID string. |
subscribe_dialog
¶
subscribe_dialog(
subscriber: str,
resource: str,
event: str = "reg",
expires: int = 3600,
call_id: str = "",
from_tag: str = "",
to_tag: str = "",
route_set: Optional[list] = None,
local_uri: Optional[str] = None,
remote_uri: Optional[str] = None,
) -> str
Create a subscription with dialog state for in-dialog NOTIFY.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscriber
|
str
|
Watcher Contact URI from the SUBSCRIBE — the
dialog's remote target. Used for the NOTIFY's Request-URI
and to resolve where to send it; it is not what goes in
|
required |
resource
|
str
|
Presentity URI being watched. |
required |
event
|
str
|
Event package name. |
'reg'
|
expires
|
int
|
Subscription duration in seconds. |
3600
|
call_id
|
str
|
Call-ID from the SUBSCRIBE dialog. |
''
|
from_tag
|
str
|
From-tag from the SUBSCRIBE. |
''
|
to_tag
|
str
|
To-tag from the SUBSCRIBE. |
''
|
route_set
|
Optional[list]
|
Route headers from Record-Route. |
None
|
local_uri
|
Optional[str]
|
The SUBSCRIBE's To URI — the dialog's local URI,
which RFC 3261 §12.2.1.1 requires in the |
None
|
remote_uri
|
Optional[str]
|
The SUBSCRIBE's From URI — the dialog's remote
URI, required in the |
None
|
Both URI arguments default to resource, which is correct for any
package where the subscriber watches an AoR directly (the reg event
package). Supply them for a watcher subscribed to somebody else's
resource, where the remote URI is the watcher's own AoR.
Returns:
| Type | Description |
|---|---|
str
|
Subscription ID string. |
unsubscribe
¶
Unsubscribe by subscription ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscription_id
|
str
|
The subscription ID returned by :meth: |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
refresh
¶
Refresh a subscription's expiry (RFC 6665 §4.4.1 re-SUBSCRIBE).
Resets the subscription timer to expires seconds, keeping the dialog.
Pair with :meth:find_by_dialog to resolve the id from an in-dialog
SUBSCRIBE before refreshing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscription_id
|
str
|
The subscription ID (from |
required |
expires
|
int
|
New subscription duration in seconds. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
find_by_dialog
¶
Resolve a subscription id from its dialog (Call-ID, From-tag).
An in-dialog SUBSCRIBE (a refresh, or an un-SUBSCRIBE with Expires: 0)
carries the dialog's Call-ID and the subscriber's From-tag but not the
original subscription id. This maps that pair back so a notifier
(e.g. an S-CSCF handling reg-event) can :meth:refresh or
:meth:unsubscribe the right dialog. Only subscriptions created with
:meth:subscribe_dialog (which store dialog state) are findable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
call_id
|
str
|
Call-ID of the in-dialog SUBSCRIBE. |
required |
from_tag
|
str
|
From-tag of the in-dialog SUBSCRIBE (subscriber's tag). |
required |
Returns:
| Type | Description |
|---|---|
Optional[str]
|
The subscription ID string, or |
subscribers
¶
List subscribers (watchers) for a resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Presentity URI to query. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
List of dicts with keys: |
notify
¶
notify(
subscription_id: str,
body: Optional[str] = None,
content_type: Optional[str] = None,
subscription_state: str = "active",
) -> None
Send an in-dialog NOTIFY for a subscription.
In the mock, this records the notification for test assertions.
When subscription_state indicates a terminated subscription
(RFC 6665 §4.1.3 — bare "terminated" or
"terminated;reason=...") the subscription is also removed from
the store, mirroring the production auto-GC behavior.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscription_id
|
str
|
The subscription ID from |
required |
body
|
Optional[str]
|
Optional body string (reginfo XML, PIDF XML, etc.). |
None
|
content_type
|
Optional[str]
|
Content-Type of the body. |
None
|
subscription_state
|
str
|
Subscription-State header value (default |
'active'
|
terminate
¶
terminate(
subscription_id: str,
reason: Optional[str] = None,
body: Optional[str] = None,
content_type: Optional[str] = None,
) -> bool
Send a terminating NOTIFY and remove the subscription (RFC 6665 §4.4.1).
Sends an in-dialog NOTIFY with
Subscription-State: terminated;reason=<reason>, then removes
the subscription's dialog state from the store. Idempotent: a
second call with the same subscription_id returns False.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscription_id
|
str
|
The subscription ID from |
required |
reason
|
Optional[str]
|
Termination reason per RFC 6665 §4.2.2 — one of
|
None
|
body
|
Optional[str]
|
Optional final body. |
None
|
content_type
|
Optional[str]
|
Content-Type of the body. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
recorded; |
Example::
sub_id = presence.subscribe_dialog(...)
...
await presence.terminate(sub_id, reason="timeout")
parse_reginfo
¶
Parse an RFC 3680 application/reginfo+xml body for tests.
Mirrors the Rust presence.parse_reginfo shape — returns a
dict {"version": int, "state": "full"|"partial",
"registrations": [...]} so tests asserting against script logic
can use the same dict layout the production binary returns.
li namespace¶
Lawful intercept (ETSI X1/X2/X3) and SIPREC recording triggers.
Mock li namespace — lawful intercept operations for testing.
.. note:: Interception is not triggered from a script. siphon matches every SIP message against the warrants the ADMF provisioned over ETSI X1, in the dispatcher, on every leg — a warrant applies whether or not a script calls anything here. This namespace is for visibility (is this call warranted?) and for operator-driven SIPREC recording, which is not a warrant.
``intercept()`` and ``stop_intercept()`` are kept so existing scripts
keep working, but they only **report** whether a warrant matched. If
they still triggered, a script that called them would produce duplicate
IRI records for one event.
Pre-configure targets for testing::
from siphon_sdk.mock_module import get_li
li = get_li()
li.add_target("sip:alice@example.com")
Then in your script::
from siphon import li
if li.is_target(request):
log.info("this call is subject to a warrant")
Test assertions::
li = get_li()
assert li.is_target(request)
assert li.events == [("record", "call-1@example.com")]
is_enabled
property
¶
Whether the LI subsystem is enabled.
In the mock, returns True if _enabled is set and targets
are configured.
task_count
property
¶
How many intercept tasks the ADMF has provisioned over X1.
Read-only: warrants are provisioned by the ADMF, never by a script.
destination_count
property
¶
How many delivery destinations the ADMF has provisioned over X1.
events
property
¶
List of (operation, target_or_call_id) tuples recorded.
Operations: "intercept", "record", "stop_intercept",
"stop_recording".
is_target
¶
Check if a request matches an active intercept target.
Matches From URI, To URI, or RURI against configured targets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Any
|
The SIP request object. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
intercept
¶
Report whether this request is being intercepted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Any
|
The SIP request object. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
.. note:: Retained for compatibility. This does not trigger interception — the dispatcher has already emitted the IRI record for any matching message before a script handler runs. Calling it is harmless and changes nothing.
record
¶
Start SIPREC recording for a request or call.
Accepts either a Request (proxy mode) or Call (B2BUA mode).
In B2BUA mode, the dispatcher will start SIPREC recording on answer
using the SRS URI from lawful_intercept.siprec.srs_uri config.
SIPREC is a recording feature, not lawful interception: it produces no X2 record and is not tied to a provisioned warrant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
A |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Example::
@b2bua.on_invite
def on_invite(call):
li.record(call) # B2BUA mode
call.dial("sip:bob@example.com")
@proxy.on_request("INVITE")
def on_invite(request):
li.record(request) # proxy mode
request.relay()
stop_intercept
¶
Report whether this request is being intercepted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Any
|
The SIP request object. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
.. note:: Retained for compatibility. Session teardown records are emitted by the dispatcher when the dialog ends; this does not emit one.
set_provisioned_counts
¶
Test helper: set what task_count / destination_count report.
Not part of the siphon API — the real counts come from what the ADMF provisioned.
stop_recording
¶
Stop SIPREC recording for a request or call.
Accepts either a Request or Call object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
A |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
add_target
¶
Add a target URI for intercept matching (test helper).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
SIP URI to match against (e.g. |
required |
srs namespace¶
Session Recording Server acceptance hooks (RFC 7866 SIPREC).
Mock srs namespace — Session Recording Server hooks for testing.
Pre-configure accept/reject behavior::
from siphon_sdk.mock_module import get_srs
srs = get_srs()
srs.accept_all = False # reject all recordings
Register handlers as in production::
from siphon import srs
@srs.on_invite
async def on_recording(metadata):
return True
@srs.on_session_end
async def on_recording_end(session):
pass
Inspect events after test::
srs = get_srs()
assert len(srs.sessions) == 1
accept_all
property
writable
¶
Whether mock auto-accepts all recordings (default True).
sessions
property
¶
List of completed recording sessions (for test assertions).
invite_events
property
¶
List of on_invite calls received (for test assertions).
on_invite
¶
Register handler for incoming SIPREC INVITE (recording request).
The handler receives (metadata,) where metadata is a
:class:~siphon_sdk.srs.RecordingMetadata object.
Return True to accept the recording, False to reject (403).
Example::
@srs.on_invite
async def on_recording(metadata):
log.info(f"Recording: {metadata.session_id}")
return True
on_session_end
¶
Register handler for recording session completion.
The handler receives (session,) where session is a
:class:~siphon_sdk.srs.SrsSession object.
Example::
@srs.on_session_end
async def on_recording_end(session):
log.info(f"Recording {session.session_id} done")
record_invite
¶
Test helper: simulate an inbound SIPREC INVITE event.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
str
|
Recording session identifier. |
required |
participants
|
list[str] | None
|
List of participant AoRs. |
None
|
record_session_end
¶
record_session_end(
session_id: str,
recording_call_id: str = "",
duration_secs: int = 0,
recording_dir: str | None = None,
) -> None
Test helper: simulate a completed recording session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
str
|
Recording session identifier. |
required |
recording_call_id
|
str
|
Call-ID of the SIPREC dialog. |
''
|
duration_secs
|
int
|
Recording duration in seconds. |
0
|
recording_dir
|
str | None
|
Path where recordings were written. |
None
|
SrsSession¶
A completed recording session.
Completed recording session info.
Passed to @srs.on_session_end handlers after the recording
finishes (BYE from SRC or timeout).
Example::
@srs.on_session_end
async def on_recording_end(session):
log.info(f"Recording {session.session_id} complete")
log.info(f"Duration: {session.duration}s")
if session.recording_dir:
log.info(f"Files in: {session.recording_dir}")
RecordingMetadata¶
Parsed RFC 7866 recording metadata from a SIPREC INVITE.
Parsed RFC 7866 recording metadata from a SIPREC INVITE.
Passed to @srs.on_invite handlers so the script can inspect
participants, streams, and the session ID before accepting/rejecting.
Example::
@srs.on_invite
async def on_recording(metadata):
if any(p.aor == "sip:vip@example.com" for p in metadata.participants):
return True # always record VIP calls
return False # reject others
SrsParticipant¶
A participant in the recorded call.
Attributes:
| Name | Type | Description |
|---|---|---|
participant_id |
str
|
Unique identifier for this participant. |
aor |
str
|
Address of Record (SIP URI, e.g. |
name |
str | None
|
Optional display name. |