siphon-sip 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. siphon_sip-1.0.0/.gitignore +18 -0
  2. siphon_sip-1.0.0/PKG-INFO +314 -0
  3. siphon_sip-1.0.0/README.md +288 -0
  4. siphon_sip-1.0.0/pyproject.toml +53 -0
  5. siphon_sip-1.0.0/siphon_sdk/__init__.py +34 -0
  6. siphon_sip-1.0.0/siphon_sdk/call.py +589 -0
  7. siphon_sip-1.0.0/siphon_sdk/mock_module.py +5636 -0
  8. siphon_sip-1.0.0/siphon_sdk/py.typed +0 -0
  9. siphon_sip-1.0.0/siphon_sdk/reply.py +341 -0
  10. siphon_sip-1.0.0/siphon_sdk/request.py +1012 -0
  11. siphon_sip-1.0.0/siphon_sdk/sdp.py +728 -0
  12. siphon_sip-1.0.0/siphon_sdk/srs.py +201 -0
  13. siphon_sip-1.0.0/siphon_sdk/testing.py +822 -0
  14. siphon_sip-1.0.0/siphon_sdk/types.py +274 -0
  15. siphon_sip-1.0.0/tests/test_b2bua_default.py +71 -0
  16. siphon_sip-1.0.0/tests/test_call_dial.py +82 -0
  17. siphon_sip-1.0.0/tests/test_ims_scripts.py +588 -0
  18. siphon_sip-1.0.0/tests/test_ims_ue_b2bua.py +88 -0
  19. siphon_sip-1.0.0/tests/test_inline_script.py +192 -0
  20. siphon_sip-1.0.0/tests/test_ipsec_pending_sa.py +85 -0
  21. siphon_sip-1.0.0/tests/test_li_mock.py +198 -0
  22. siphon_sip-1.0.0/tests/test_on_cancel.py +93 -0
  23. siphon_sip-1.0.0/tests/test_presence_mock.py +101 -0
  24. siphon_sip-1.0.0/tests/test_proxy_default.py +109 -0
  25. siphon_sip-1.0.0/tests/test_qos_mock.py +172 -0
  26. siphon_sip-1.0.0/tests/test_registrar_alias_chain.py +85 -0
  27. siphon_sip-1.0.0/tests/test_registration_aka.py +93 -0
  28. siphon_sip-1.0.0/tests/test_reply_reject.py +72 -0
  29. siphon_sip-1.0.0/tests/test_request_consumed_routes.py +103 -0
  30. siphon_sip-1.0.0/tests/test_request_reply_headers.py +118 -0
  31. siphon_sip-1.0.0/tests/test_rf_charging.py +187 -0
  32. siphon_sip-1.0.0/tests/test_rtpengine_media.py +189 -0
  33. siphon_sip-1.0.0/tests/test_save_proxy.py +109 -0
  34. siphon_sip-1.0.0/tests/test_sbi_bsf.py +93 -0
  35. siphon_sip-1.0.0/tests/test_sdp.py +404 -0
  36. siphon_sip-1.0.0/tests/test_stir_mock.py +152 -0
  37. siphon_sip-1.0.0/tests/test_timer_api.py +114 -0
@@ -0,0 +1,18 @@
1
+ /target
2
+ fuzz/target/
3
+ sbom.spdx.json
4
+ sbom.cdx.json
5
+ __pycache__/
6
+ *.pyc
7
+ *.log
8
+ .venv/
9
+ sdk/.venv/
10
+ sdk/dist/
11
+ sdk/*.egg-info/
12
+ .claude/
13
+ CLAUDE.md
14
+
15
+ # Private deployment configs
16
+
17
+ # Private test fixtures
18
+ sipp/ipsec/
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.4
2
+ Name: siphon-sip
3
+ Version: 1.0.0
4
+ Summary: Mock library and type stubs for SIPhon SIP proxy/B2BUA scripts — enables unit testing and LLM-assisted script authoring
5
+ Project-URL: Homepage, https://github.com/siphon-project/siphon-sip
6
+ Project-URL: Documentation, https://github.com/siphon-project/siphon-sip/tree/main/sdk
7
+ Project-URL: Repository, https://github.com/siphon-project/siphon-sip
8
+ Author: SIPhon Authors
9
+ License-Expression: MIT
10
+ Keywords: b2bua,ims,mock,proxy,sip,testing,voip
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Telecommunications Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Communications :: Telephony
22
+ Classifier: Topic :: Software Development :: Testing :: Mocking
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+
27
+ <p align="center">
28
+ <img src="https://raw.githubusercontent.com/siphon-project/siphon-sip/main/assets/logo.svg" alt="SIPhon" width="100">
29
+ </p>
30
+
31
+ # siphon-sip
32
+
33
+ Mock library and type stubs for [SIPhon](https://github.com/siphon-project/siphon-sip) scripts — enables unit testing without the Rust binary and provides rich context for LLM-assisted script authoring.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install siphon-sip
39
+ ```
40
+
41
+ > The PyPI distribution is **`siphon-sip`** (matching the `siphon-sip` crate). The import package is still **`siphon_sdk`** — `from siphon_sdk import …`.
42
+
43
+ ## What is SIPhon?
44
+
45
+ SIPhon is a high-performance SIP proxy, B2BUA, and IMS platform written in Rust with Python scripting. Scripts use decorators to handle SIP events:
46
+
47
+ ```python
48
+ from siphon import proxy, registrar, auth, log
49
+
50
+ @proxy.on_request
51
+ def route(request):
52
+ if request.method == "REGISTER":
53
+ if not auth.require_digest(request, realm="example.com"):
54
+ return
55
+ registrar.save(request)
56
+ request.reply(200, "OK")
57
+ return
58
+
59
+ contacts = registrar.lookup(request.ruri)
60
+ if not contacts:
61
+ request.reply(404, "Not Found")
62
+ return
63
+
64
+ request.record_route()
65
+ request.fork([c.uri for c in contacts])
66
+ ```
67
+
68
+ This SDK lets you **test** these scripts with pytest — no Rust binary needed.
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ from siphon_sdk.testing import SipTestHarness
74
+ from siphon_sdk.types import Contact
75
+
76
+ harness = SipTestHarness(local_domains=["example.com"])
77
+ harness.load_script("scripts/proxy_default.py")
78
+
79
+ # Pre-populate the registrar
80
+ harness.registrar.add_contact(
81
+ "sip:alice@example.com",
82
+ Contact(uri="sip:alice@192.168.1.5:5060"),
83
+ )
84
+
85
+ # Test REGISTER challenge
86
+ result = harness.send_request("REGISTER", "sip:alice@example.com",
87
+ from_uri="sip:alice@example.com")
88
+ assert result.status_code == 401 # digest challenge
89
+
90
+ # Test INVITE routing
91
+ result = harness.send_request("INVITE", "sip:alice@example.com")
92
+ assert result.action == "fork"
93
+ assert "sip:alice@192.168.1.5:5060" in result.targets
94
+ ```
95
+
96
+ ## Testing B2BUA scripts
97
+
98
+ ```python
99
+ harness = SipTestHarness()
100
+ harness.load_script("scripts/b2bua_default.py")
101
+
102
+ harness.registrar.add_contact(
103
+ "sip:bob@example.com",
104
+ Contact(uri="sip:bob@10.0.0.2:5060"),
105
+ )
106
+
107
+ result = harness.send_invite(ruri="sip:bob@example.com")
108
+ assert result.action == "fork"
109
+ assert result.targets == ["sip:bob@10.0.0.2:5060"]
110
+
111
+ # Test BYE handling
112
+ result = harness.send_bye(initiator_side="a")
113
+ assert result.was_terminated
114
+ ```
115
+
116
+ ## Inline scripts
117
+
118
+ Test scripts without separate files:
119
+
120
+ ```python
121
+ harness = SipTestHarness()
122
+ harness.load_source("""
123
+ from siphon import proxy
124
+
125
+ @proxy.on_request
126
+ def route(request):
127
+ if request.source_ip_in(["10.0.0.0/8"]):
128
+ request.relay()
129
+ else:
130
+ request.reply(403, "Forbidden")
131
+ """)
132
+
133
+ result = harness.send_request("INVITE", "sip:bob@host", source_ip="10.1.2.3")
134
+ assert result.was_relayed
135
+
136
+ result = harness.send_request("INVITE", "sip:bob@host", source_ip="8.8.8.8")
137
+ assert result.status_code == 403
138
+ ```
139
+
140
+ ## Async handlers + RTPEngine
141
+
142
+ ```python
143
+ harness = SipTestHarness()
144
+ harness.load_source("""
145
+ from siphon import proxy, rtpengine
146
+
147
+ @proxy.on_request
148
+ async def route(request):
149
+ if request.method == "INVITE" and request.body:
150
+ await rtpengine.offer(request, profile="srtp_to_rtp")
151
+ request.relay()
152
+ """)
153
+
154
+ result = harness.send_request("INVITE", "sip:bob@host",
155
+ body=b"v=0\\r\\n...",
156
+ content_type="application/sdp")
157
+ assert result.was_relayed
158
+ assert harness.rtpengine.operations == [("offer", "srtp_to_rtp")]
159
+ ```
160
+
161
+ ## Controlling mock behavior
162
+
163
+ ```python
164
+ # Auth: allow or deny all
165
+ harness.auth._allow = True # all auth checks pass
166
+
167
+ # Rate limiting
168
+ harness.proxy._utils._rate_limit_allow = False # simulate overload
169
+
170
+ # Cache: pre-populate
171
+ harness.cache.set_data("cnam", {"key": "value"})
172
+
173
+ # Registrar: add contacts directly
174
+ harness.registrar.add_contact("sip:alice@host", Contact(uri="sip:alice@1.2.3.4"))
175
+
176
+ # Log: inspect captured messages
177
+ assert any("error" in msg for level, msg in harness.log.messages)
178
+
179
+ # Reset between tests
180
+ harness.reset()
181
+ ```
182
+
183
+ ## Result assertions
184
+
185
+ `RequestResult` provides convenient properties:
186
+
187
+ | Property | Description |
188
+ |----------|-------------|
189
+ | `.action` | Primary action: `"reply"`, `"relay"`, `"fork"`, `"silent_drop"` |
190
+ | `.status_code` | SIP status code (200, 401, 404, etc.) |
191
+ | `.reason` | Reason phrase |
192
+ | `.targets` | Fork targets list |
193
+ | `.strategy` | Fork strategy (`"parallel"` / `"sequential"`) |
194
+ | `.was_relayed` | `True` if `relay()` was called |
195
+ | `.was_forked` | `True` if `fork()` was called |
196
+ | `.was_dropped` | `True` if handler returned without action (silent drop) |
197
+ | `.record_routed` | `True` if `record_route()` was called |
198
+ | `.request` | The mock `Request` object for header inspection |
199
+
200
+ ## API reference
201
+
202
+ ### Namespaces
203
+
204
+ | Import | Description |
205
+ |--------|-------------|
206
+ | `proxy` | Stateful/stateless proxy decorators and utilities |
207
+ | `registrar` | Address-of-record contact store |
208
+ | `auth` | SIP digest authentication |
209
+ | `b2bua` | Back-to-back user agent call control |
210
+ | `log` | Structured logging |
211
+ | `cache` | Named cache (local LRU + Redis) |
212
+ | `rtpengine` | RTPEngine media proxy operations |
213
+ | `gateway` | Destination groups, load balancing, health probing |
214
+ | `cdr` | Call detail records |
215
+ | `diameter` | Diameter protocol (Cx, Ro, Rx, Rf, Sh) |
216
+ | `presence` | SUBSCRIBE/NOTIFY, PIDF presence |
217
+ | `li` | Lawful intercept (ETSI X1/X2/X3, SIPREC) |
218
+ | `registration` | Outbound REGISTER client (trunk registration) |
219
+
220
+ ### Request properties
221
+
222
+ | Property | Type | Description |
223
+ |----------|------|-------------|
224
+ | `method` | `str` | SIP method (`"INVITE"`, `"REGISTER"`, etc.) |
225
+ | `ruri` | `SipUri` | Request-URI |
226
+ | `from_uri` | `SipUri \| None` | From header URI |
227
+ | `to_uri` | `SipUri \| None` | To header URI |
228
+ | `from_tag` | `str \| None` | From-tag |
229
+ | `to_tag` | `str \| None` | To-tag (`None` for initial requests) |
230
+ | `call_id` | `str \| None` | Call-ID |
231
+ | `cseq` | `(int, str) \| None` | CSeq tuple |
232
+ | `in_dialog` | `bool` | Both tags present |
233
+ | `max_forwards` | `int` | Max-Forwards value |
234
+ | `body` | `bytes \| None` | Message body |
235
+ | `content_type` | `str \| None` | Content-Type |
236
+ | `transport` | `str` | `"udp"`, `"tcp"`, `"tls"`, `"ws"`, `"wss"` |
237
+ | `source_ip` | `str` | Sender IP |
238
+ | `auth_user` | `str \| None` | Authenticated username |
239
+ | `event` | `str \| None` | Event header |
240
+
241
+ ### Request methods
242
+
243
+ | Method | Description |
244
+ |--------|-------------|
245
+ | `reply(code, reason)` | Send SIP response |
246
+ | `relay(next_hop=None)` | Forward to destination |
247
+ | `fork(targets, strategy="parallel")` | Fork to multiple targets |
248
+ | `record_route()` | Insert Record-Route |
249
+ | `loose_route() -> bool` | RFC 3261 loose routing |
250
+ | `get_header(name) -> str \| None` | Get header value |
251
+ | `set_header(name, value)` | Set header |
252
+ | `remove_header(name)` | Remove header |
253
+ | `has_header(name) -> bool` | Check header exists |
254
+ | `has_body(content_type) -> bool` | Check body type |
255
+ | `set_ruri_user(value)` | Set R-URI user part |
256
+ | `set_ruri_host(value)` | Set R-URI host |
257
+ | `source_ip_in(cidrs) -> bool` | CIDR membership check |
258
+ | `generate_icid() -> str` | Generate charging ID |
259
+ | `add_path(uri)` | Prepend Path header |
260
+ | `prepend_route(uri)` | Prepend Route header |
261
+ | `fix_nated_register()` | NAT fixup for REGISTER |
262
+ | `fix_nated_contact()` | NAT fixup for Contact |
263
+
264
+ ### Registrar
265
+
266
+ | Method | Description |
267
+ |--------|-------------|
268
+ | `save(request, force=False)` | Save REGISTER bindings |
269
+ | `lookup(uri) -> list[Contact]` | Look up contacts (sorted by q-value) |
270
+ | `is_registered(uri) -> bool` | Check if URI has contacts |
271
+ | `service_route(uri) -> list[str]` | Get stored service routes (RFC 3608) |
272
+ | `set_service_routes(aor, routes)` | Store service routes for an AoR |
273
+ | `save_pending(request)` | IMS: save binding in pending state |
274
+ | `confirm_pending(uri)` | IMS: promote pending to active after SAR |
275
+ | `asserted_identity(uri) -> str \| None` | IMS: stored P-Asserted-Identity |
276
+ | `reginfo_xml(aor, state, version) -> str` | Generate reginfo XML (RFC 3680) |
277
+ | `on_change` | Decorator: fires on registration state changes |
278
+
279
+ ### Auth
280
+
281
+ | Method | Description |
282
+ |--------|-------------|
283
+ | `require_www_digest(request, realm) -> bool` | 401 challenge |
284
+ | `require_proxy_digest(request, realm) -> bool` | 407 challenge |
285
+ | `require_digest(request, realm) -> bool` | Alias for www_digest |
286
+ | `verify_digest(request, realm) -> bool` | Verify without challenge |
287
+ | `require_ims_digest(request, realm) -> bool` | IMS AKA via Diameter Cx MAR |
288
+ | `require_aka_digest(request, realm) -> bool` | Local Milenage AKA (no HSS) |
289
+
290
+ ### B2BUA call
291
+
292
+ Each B-leg gets a fresh Call-ID and From-tag by default, fully decoupling the two SIP dialogs. Use `keep_call_id()` to opt out of Call-ID regeneration.
293
+
294
+ | Property/Method | Description |
295
+ |----------------|-------------|
296
+ | `call.id` | UUID |
297
+ | `call.state` | `"calling"`, `"ringing"`, `"answered"`, `"terminated"` |
298
+ | `call.from_uri` | A-leg From URI |
299
+ | `call.ruri` | A-leg Request-URI |
300
+ | `call.reject(code, reason)` | Reject call |
301
+ | `call.dial(uri, timeout=30)` | Dial single target |
302
+ | `call.fork(targets, strategy, timeout)` | Fork to multiple |
303
+ | `call.terminate()` | End call (BYE both legs) |
304
+ | `call.keep_call_id()` | Copy A-leg Call-ID to B-leg (From-tag always unique) |
305
+ | `call.set_credentials(user, pass)` | B-leg digest auth credentials (auto 401/407 retry) |
306
+ | `call.media.anchor(engine)` | Anchor media through RTPEngine |
307
+ | `call.media.release()` | Release media anchor |
308
+ | `call.session_timer(expires, min_se, refresher)` | Per-call RFC 4028 session timer |
309
+ | `call.record(srs_uri)` | Start SIPREC recording |
310
+ | `call.stop_recording()` | Stop SIPREC recording |
311
+
312
+ ## License
313
+
314
+ MIT
@@ -0,0 +1,288 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/siphon-project/siphon-sip/main/assets/logo.svg" alt="SIPhon" width="100">
3
+ </p>
4
+
5
+ # siphon-sip
6
+
7
+ Mock library and type stubs for [SIPhon](https://github.com/siphon-project/siphon-sip) scripts — enables unit testing without the Rust binary and provides rich context for LLM-assisted script authoring.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install siphon-sip
13
+ ```
14
+
15
+ > The PyPI distribution is **`siphon-sip`** (matching the `siphon-sip` crate). The import package is still **`siphon_sdk`** — `from siphon_sdk import …`.
16
+
17
+ ## What is SIPhon?
18
+
19
+ SIPhon is a high-performance SIP proxy, B2BUA, and IMS platform written in Rust with Python scripting. Scripts use decorators to handle SIP events:
20
+
21
+ ```python
22
+ from siphon import proxy, registrar, auth, log
23
+
24
+ @proxy.on_request
25
+ def route(request):
26
+ if request.method == "REGISTER":
27
+ if not auth.require_digest(request, realm="example.com"):
28
+ return
29
+ registrar.save(request)
30
+ request.reply(200, "OK")
31
+ return
32
+
33
+ contacts = registrar.lookup(request.ruri)
34
+ if not contacts:
35
+ request.reply(404, "Not Found")
36
+ return
37
+
38
+ request.record_route()
39
+ request.fork([c.uri for c in contacts])
40
+ ```
41
+
42
+ This SDK lets you **test** these scripts with pytest — no Rust binary needed.
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ from siphon_sdk.testing import SipTestHarness
48
+ from siphon_sdk.types import Contact
49
+
50
+ harness = SipTestHarness(local_domains=["example.com"])
51
+ harness.load_script("scripts/proxy_default.py")
52
+
53
+ # Pre-populate the registrar
54
+ harness.registrar.add_contact(
55
+ "sip:alice@example.com",
56
+ Contact(uri="sip:alice@192.168.1.5:5060"),
57
+ )
58
+
59
+ # Test REGISTER challenge
60
+ result = harness.send_request("REGISTER", "sip:alice@example.com",
61
+ from_uri="sip:alice@example.com")
62
+ assert result.status_code == 401 # digest challenge
63
+
64
+ # Test INVITE routing
65
+ result = harness.send_request("INVITE", "sip:alice@example.com")
66
+ assert result.action == "fork"
67
+ assert "sip:alice@192.168.1.5:5060" in result.targets
68
+ ```
69
+
70
+ ## Testing B2BUA scripts
71
+
72
+ ```python
73
+ harness = SipTestHarness()
74
+ harness.load_script("scripts/b2bua_default.py")
75
+
76
+ harness.registrar.add_contact(
77
+ "sip:bob@example.com",
78
+ Contact(uri="sip:bob@10.0.0.2:5060"),
79
+ )
80
+
81
+ result = harness.send_invite(ruri="sip:bob@example.com")
82
+ assert result.action == "fork"
83
+ assert result.targets == ["sip:bob@10.0.0.2:5060"]
84
+
85
+ # Test BYE handling
86
+ result = harness.send_bye(initiator_side="a")
87
+ assert result.was_terminated
88
+ ```
89
+
90
+ ## Inline scripts
91
+
92
+ Test scripts without separate files:
93
+
94
+ ```python
95
+ harness = SipTestHarness()
96
+ harness.load_source("""
97
+ from siphon import proxy
98
+
99
+ @proxy.on_request
100
+ def route(request):
101
+ if request.source_ip_in(["10.0.0.0/8"]):
102
+ request.relay()
103
+ else:
104
+ request.reply(403, "Forbidden")
105
+ """)
106
+
107
+ result = harness.send_request("INVITE", "sip:bob@host", source_ip="10.1.2.3")
108
+ assert result.was_relayed
109
+
110
+ result = harness.send_request("INVITE", "sip:bob@host", source_ip="8.8.8.8")
111
+ assert result.status_code == 403
112
+ ```
113
+
114
+ ## Async handlers + RTPEngine
115
+
116
+ ```python
117
+ harness = SipTestHarness()
118
+ harness.load_source("""
119
+ from siphon import proxy, rtpengine
120
+
121
+ @proxy.on_request
122
+ async def route(request):
123
+ if request.method == "INVITE" and request.body:
124
+ await rtpengine.offer(request, profile="srtp_to_rtp")
125
+ request.relay()
126
+ """)
127
+
128
+ result = harness.send_request("INVITE", "sip:bob@host",
129
+ body=b"v=0\\r\\n...",
130
+ content_type="application/sdp")
131
+ assert result.was_relayed
132
+ assert harness.rtpengine.operations == [("offer", "srtp_to_rtp")]
133
+ ```
134
+
135
+ ## Controlling mock behavior
136
+
137
+ ```python
138
+ # Auth: allow or deny all
139
+ harness.auth._allow = True # all auth checks pass
140
+
141
+ # Rate limiting
142
+ harness.proxy._utils._rate_limit_allow = False # simulate overload
143
+
144
+ # Cache: pre-populate
145
+ harness.cache.set_data("cnam", {"key": "value"})
146
+
147
+ # Registrar: add contacts directly
148
+ harness.registrar.add_contact("sip:alice@host", Contact(uri="sip:alice@1.2.3.4"))
149
+
150
+ # Log: inspect captured messages
151
+ assert any("error" in msg for level, msg in harness.log.messages)
152
+
153
+ # Reset between tests
154
+ harness.reset()
155
+ ```
156
+
157
+ ## Result assertions
158
+
159
+ `RequestResult` provides convenient properties:
160
+
161
+ | Property | Description |
162
+ |----------|-------------|
163
+ | `.action` | Primary action: `"reply"`, `"relay"`, `"fork"`, `"silent_drop"` |
164
+ | `.status_code` | SIP status code (200, 401, 404, etc.) |
165
+ | `.reason` | Reason phrase |
166
+ | `.targets` | Fork targets list |
167
+ | `.strategy` | Fork strategy (`"parallel"` / `"sequential"`) |
168
+ | `.was_relayed` | `True` if `relay()` was called |
169
+ | `.was_forked` | `True` if `fork()` was called |
170
+ | `.was_dropped` | `True` if handler returned without action (silent drop) |
171
+ | `.record_routed` | `True` if `record_route()` was called |
172
+ | `.request` | The mock `Request` object for header inspection |
173
+
174
+ ## API reference
175
+
176
+ ### Namespaces
177
+
178
+ | Import | Description |
179
+ |--------|-------------|
180
+ | `proxy` | Stateful/stateless proxy decorators and utilities |
181
+ | `registrar` | Address-of-record contact store |
182
+ | `auth` | SIP digest authentication |
183
+ | `b2bua` | Back-to-back user agent call control |
184
+ | `log` | Structured logging |
185
+ | `cache` | Named cache (local LRU + Redis) |
186
+ | `rtpengine` | RTPEngine media proxy operations |
187
+ | `gateway` | Destination groups, load balancing, health probing |
188
+ | `cdr` | Call detail records |
189
+ | `diameter` | Diameter protocol (Cx, Ro, Rx, Rf, Sh) |
190
+ | `presence` | SUBSCRIBE/NOTIFY, PIDF presence |
191
+ | `li` | Lawful intercept (ETSI X1/X2/X3, SIPREC) |
192
+ | `registration` | Outbound REGISTER client (trunk registration) |
193
+
194
+ ### Request properties
195
+
196
+ | Property | Type | Description |
197
+ |----------|------|-------------|
198
+ | `method` | `str` | SIP method (`"INVITE"`, `"REGISTER"`, etc.) |
199
+ | `ruri` | `SipUri` | Request-URI |
200
+ | `from_uri` | `SipUri \| None` | From header URI |
201
+ | `to_uri` | `SipUri \| None` | To header URI |
202
+ | `from_tag` | `str \| None` | From-tag |
203
+ | `to_tag` | `str \| None` | To-tag (`None` for initial requests) |
204
+ | `call_id` | `str \| None` | Call-ID |
205
+ | `cseq` | `(int, str) \| None` | CSeq tuple |
206
+ | `in_dialog` | `bool` | Both tags present |
207
+ | `max_forwards` | `int` | Max-Forwards value |
208
+ | `body` | `bytes \| None` | Message body |
209
+ | `content_type` | `str \| None` | Content-Type |
210
+ | `transport` | `str` | `"udp"`, `"tcp"`, `"tls"`, `"ws"`, `"wss"` |
211
+ | `source_ip` | `str` | Sender IP |
212
+ | `auth_user` | `str \| None` | Authenticated username |
213
+ | `event` | `str \| None` | Event header |
214
+
215
+ ### Request methods
216
+
217
+ | Method | Description |
218
+ |--------|-------------|
219
+ | `reply(code, reason)` | Send SIP response |
220
+ | `relay(next_hop=None)` | Forward to destination |
221
+ | `fork(targets, strategy="parallel")` | Fork to multiple targets |
222
+ | `record_route()` | Insert Record-Route |
223
+ | `loose_route() -> bool` | RFC 3261 loose routing |
224
+ | `get_header(name) -> str \| None` | Get header value |
225
+ | `set_header(name, value)` | Set header |
226
+ | `remove_header(name)` | Remove header |
227
+ | `has_header(name) -> bool` | Check header exists |
228
+ | `has_body(content_type) -> bool` | Check body type |
229
+ | `set_ruri_user(value)` | Set R-URI user part |
230
+ | `set_ruri_host(value)` | Set R-URI host |
231
+ | `source_ip_in(cidrs) -> bool` | CIDR membership check |
232
+ | `generate_icid() -> str` | Generate charging ID |
233
+ | `add_path(uri)` | Prepend Path header |
234
+ | `prepend_route(uri)` | Prepend Route header |
235
+ | `fix_nated_register()` | NAT fixup for REGISTER |
236
+ | `fix_nated_contact()` | NAT fixup for Contact |
237
+
238
+ ### Registrar
239
+
240
+ | Method | Description |
241
+ |--------|-------------|
242
+ | `save(request, force=False)` | Save REGISTER bindings |
243
+ | `lookup(uri) -> list[Contact]` | Look up contacts (sorted by q-value) |
244
+ | `is_registered(uri) -> bool` | Check if URI has contacts |
245
+ | `service_route(uri) -> list[str]` | Get stored service routes (RFC 3608) |
246
+ | `set_service_routes(aor, routes)` | Store service routes for an AoR |
247
+ | `save_pending(request)` | IMS: save binding in pending state |
248
+ | `confirm_pending(uri)` | IMS: promote pending to active after SAR |
249
+ | `asserted_identity(uri) -> str \| None` | IMS: stored P-Asserted-Identity |
250
+ | `reginfo_xml(aor, state, version) -> str` | Generate reginfo XML (RFC 3680) |
251
+ | `on_change` | Decorator: fires on registration state changes |
252
+
253
+ ### Auth
254
+
255
+ | Method | Description |
256
+ |--------|-------------|
257
+ | `require_www_digest(request, realm) -> bool` | 401 challenge |
258
+ | `require_proxy_digest(request, realm) -> bool` | 407 challenge |
259
+ | `require_digest(request, realm) -> bool` | Alias for www_digest |
260
+ | `verify_digest(request, realm) -> bool` | Verify without challenge |
261
+ | `require_ims_digest(request, realm) -> bool` | IMS AKA via Diameter Cx MAR |
262
+ | `require_aka_digest(request, realm) -> bool` | Local Milenage AKA (no HSS) |
263
+
264
+ ### B2BUA call
265
+
266
+ Each B-leg gets a fresh Call-ID and From-tag by default, fully decoupling the two SIP dialogs. Use `keep_call_id()` to opt out of Call-ID regeneration.
267
+
268
+ | Property/Method | Description |
269
+ |----------------|-------------|
270
+ | `call.id` | UUID |
271
+ | `call.state` | `"calling"`, `"ringing"`, `"answered"`, `"terminated"` |
272
+ | `call.from_uri` | A-leg From URI |
273
+ | `call.ruri` | A-leg Request-URI |
274
+ | `call.reject(code, reason)` | Reject call |
275
+ | `call.dial(uri, timeout=30)` | Dial single target |
276
+ | `call.fork(targets, strategy, timeout)` | Fork to multiple |
277
+ | `call.terminate()` | End call (BYE both legs) |
278
+ | `call.keep_call_id()` | Copy A-leg Call-ID to B-leg (From-tag always unique) |
279
+ | `call.set_credentials(user, pass)` | B-leg digest auth credentials (auto 401/407 retry) |
280
+ | `call.media.anchor(engine)` | Anchor media through RTPEngine |
281
+ | `call.media.release()` | Release media anchor |
282
+ | `call.session_timer(expires, min_se, refresher)` | Per-call RFC 4028 session timer |
283
+ | `call.record(srs_uri)` | Start SIPREC recording |
284
+ | `call.stop_recording()` | Stop SIPREC recording |
285
+
286
+ ## License
287
+
288
+ MIT
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ # Distribution renamed siphon-sdk -> siphon-sip so the pip name matches the
7
+ # crate name (cargo add siphon-sip ↔ pip install siphon-sip). The import
8
+ # package stays `siphon_sdk` (see [tool.hatch.build.targets.wheel]) to avoid
9
+ # breaking existing `import siphon_sdk` test code.
10
+ name = "siphon-sip"
11
+ # Version comes from the git tag via hatch-vcs — there is intentionally no
12
+ # `version =` line to hand-bump. The release tag (e.g. v1.0.0) is the single
13
+ # source of truth, lockstep with the crate. This stops the per-change SDK
14
+ # version churn.
15
+ dynamic = ["version"]
16
+ description = "Mock library and type stubs for SIPhon SIP proxy/B2BUA scripts — enables unit testing and LLM-assisted script authoring"
17
+ readme = "README.md"
18
+ license = "MIT"
19
+ requires-python = ">=3.10"
20
+ authors = [{ name = "SIPhon Authors" }]
21
+ keywords = ["sip", "voip", "proxy", "b2bua", "ims", "testing", "mock"]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "Intended Audience :: Telecommunications Industry",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
33
+ "Topic :: Communications :: Telephony",
34
+ "Topic :: Software Development :: Testing :: Mocking",
35
+ "Typing :: Typed",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/siphon-project/siphon-sip"
40
+ Documentation = "https://github.com/siphon-project/siphon-sip/tree/main/sdk"
41
+ Repository = "https://github.com/siphon-project/siphon-sip"
42
+
43
+ # Version from the repo's git tags. `root = ".."` points hatch-vcs at the repo
44
+ # root (this pyproject lives in sdk/). On a tagged commit (vX.Y.Z) the version
45
+ # is X.Y.Z; between tags it's a dev version that is never published.
46
+ [tool.hatch.version]
47
+ source = "vcs"
48
+
49
+ [tool.hatch.version.raw-options]
50
+ root = ".."
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["siphon_sdk"]