sipx 0.0.4__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.
- sipx-0.0.4/PKG-INFO +319 -0
- sipx-0.0.4/README.md +311 -0
- sipx-0.0.4/pyproject.toml +55 -0
- sipx-0.0.4/setup.cfg +4 -0
- sipx-0.0.4/sipx/__init__.py +299 -0
- sipx-0.0.4/sipx/_depends.py +219 -0
- sipx-0.0.4/sipx/_events.py +337 -0
- sipx-0.0.4/sipx/_routing.py +126 -0
- sipx-0.0.4/sipx/_types.py +265 -0
- sipx-0.0.4/sipx/_uri.py +219 -0
- sipx-0.0.4/sipx/_utils.py +187 -0
- sipx-0.0.4/sipx/_version.py +3 -0
- sipx-0.0.4/sipx/client/__init__.py +4 -0
- sipx-0.0.4/sipx/client/_async.py +640 -0
- sipx-0.0.4/sipx/client/_base.py +184 -0
- sipx-0.0.4/sipx/client/_sync.py +1172 -0
- sipx-0.0.4/sipx/contrib/__init__.py +57 -0
- sipx-0.0.4/sipx/contrib/_fastapi.py +169 -0
- sipx-0.0.4/sipx/contrib/_isup.py +740 -0
- sipx-0.0.4/sipx/contrib/_sipi.py +258 -0
- sipx-0.0.4/sipx/contrib/_sipi_br.py +383 -0
- sipx-0.0.4/sipx/contrib/_stt_whisper.py +28 -0
- sipx-0.0.4/sipx/contrib/_tts_google.py +38 -0
- sipx-0.0.4/sipx/contrib/ivr/__init__.py +5 -0
- sipx-0.0.4/sipx/contrib/ivr/_async.py +93 -0
- sipx-0.0.4/sipx/contrib/ivr/_models.py +55 -0
- sipx-0.0.4/sipx/contrib/ivr/_sync.py +90 -0
- sipx-0.0.4/sipx/dns/__init__.py +5 -0
- sipx-0.0.4/sipx/dns/_async.py +122 -0
- sipx-0.0.4/sipx/dns/_models.py +19 -0
- sipx-0.0.4/sipx/dns/_sync.py +124 -0
- sipx-0.0.4/sipx/fsm/__init__.py +13 -0
- sipx-0.0.4/sipx/fsm/_manager.py +452 -0
- sipx-0.0.4/sipx/fsm/_models.py +386 -0
- sipx-0.0.4/sipx/fsm/_timer.py +142 -0
- sipx-0.0.4/sipx/main.py +239 -0
- sipx-0.0.4/sipx/media/__init__.py +48 -0
- sipx-0.0.4/sipx/media/_async.py +426 -0
- sipx-0.0.4/sipx/media/_audio.py +103 -0
- sipx-0.0.4/sipx/media/_codecs.py +258 -0
- sipx-0.0.4/sipx/media/_dtmf.py +242 -0
- sipx-0.0.4/sipx/media/_generators.py +225 -0
- sipx-0.0.4/sipx/media/_opus.py +111 -0
- sipx-0.0.4/sipx/media/_pyaudio.py +192 -0
- sipx-0.0.4/sipx/media/_rtp.py +365 -0
- sipx-0.0.4/sipx/media/_session.py +233 -0
- sipx-0.0.4/sipx/media/_stt.py +39 -0
- sipx-0.0.4/sipx/media/_tts.py +72 -0
- sipx-0.0.4/sipx/media/audio/__init__.py +27 -0
- sipx-0.0.4/sipx/media/codecs/__init__.py +4 -0
- sipx-0.0.4/sipx/media/dtmf/__init__.py +17 -0
- sipx-0.0.4/sipx/media/rtp/__init__.py +4 -0
- sipx-0.0.4/sipx/media/session/__init__.py +4 -0
- sipx-0.0.4/sipx/models/__init__.py +60 -0
- sipx-0.0.4/sipx/models/_auth.py +811 -0
- sipx-0.0.4/sipx/models/_body.py +1420 -0
- sipx-0.0.4/sipx/models/_header.py +535 -0
- sipx-0.0.4/sipx/models/_message.py +965 -0
- sipx-0.0.4/sipx/server/__init__.py +6 -0
- sipx-0.0.4/sipx/server/_async.py +116 -0
- sipx-0.0.4/sipx/server/_base.py +156 -0
- sipx-0.0.4/sipx/server/_sync.py +234 -0
- sipx-0.0.4/sipx/session/__init__.py +13 -0
- sipx-0.0.4/sipx/session/_subscription.py +315 -0
- sipx-0.0.4/sipx/session/_timer.py +375 -0
- sipx-0.0.4/sipx/transports/__init__.py +66 -0
- sipx-0.0.4/sipx/transports/_base.py +235 -0
- sipx-0.0.4/sipx/transports/_tcp.py +572 -0
- sipx-0.0.4/sipx/transports/_tls.py +683 -0
- sipx-0.0.4/sipx/transports/_udp.py +461 -0
- sipx-0.0.4/sipx/transports/_utils.py +28 -0
- sipx-0.0.4/sipx/transports/_ws.py +286 -0
- sipx-0.0.4/sipx.egg-info/PKG-INFO +319 -0
- sipx-0.0.4/sipx.egg-info/SOURCES.txt +156 -0
- sipx-0.0.4/sipx.egg-info/dependency_links.txt +1 -0
- sipx-0.0.4/sipx.egg-info/entry_points.txt +2 -0
- sipx-0.0.4/sipx.egg-info/requires.txt +1 -0
- sipx-0.0.4/sipx.egg-info/top_level.txt +1 -0
- sipx-0.0.4/tests/test_client.py +406 -0
- sipx-0.0.4/tests/test_contrib.py +85 -0
- sipx-0.0.4/tests/test_depends.py +149 -0
- sipx-0.0.4/tests/test_dns.py +255 -0
- sipx-0.0.4/tests/test_events.py +369 -0
- sipx-0.0.4/tests/test_fsm.py +484 -0
- sipx-0.0.4/tests/test_routing.py +119 -0
- sipx-0.0.4/tests/test_server.py +205 -0
- sipx-0.0.4/tests/test_session_timer.py +245 -0
- sipx-0.0.4/tests/test_sipi_br.py +257 -0
- sipx-0.0.4/tests/test_subscription.py +189 -0
- sipx-0.0.4/tests/test_uri.py +226 -0
sipx-0.0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sipx
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: rich>=14.1.0
|
|
8
|
+
|
|
9
|
+
# sipx
|
|
10
|
+
|
|
11
|
+
[](https://www.python.org/downloads/)
|
|
12
|
+
[](https://opensource.org/licenses/MIT)
|
|
13
|
+
|
|
14
|
+
A modern, high-performance SIP library for Python inspired by [httpx](https://www.python-httpx.org/).
|
|
15
|
+
|
|
16
|
+
Build voice automation scripts, IVR systems, AI-powered call handlers, and SIP testing tools with a clean, Pythonic API.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **httpx-inspired API** — `client.invite()`, `client.register()`, sync and async
|
|
21
|
+
- **FastAPI-style server** — `@server.invite`, `Annotated` DI extractors
|
|
22
|
+
- **Response builders** — `request.ok()`, `request.trying()`, `request.error(code)`
|
|
23
|
+
- **Dialog tracking** — `client.ack()` / `client.bye()` without passing response
|
|
24
|
+
- **Declarative events** — `@on('INVITE', status=200)` decorators
|
|
25
|
+
- **Full SIP support** — 14 methods, auto 100 Trying, auto DNS SRV resolution
|
|
26
|
+
- **Digest authentication** — MD5, SHA-256, auto-retry on 401/407
|
|
27
|
+
- **RTP + DTMF** — RFC 4733 + SIP INFO + Inband, sync and async
|
|
28
|
+
- **SDP with ICE/SRTP** — RFC 4566, offer/answer, ICE candidates, DTLS
|
|
29
|
+
- **Media pipeline** — RTP, tone generators, TTS/STT adapters, IVR builder
|
|
30
|
+
- **SIP-I** — Real ISUP binary encoding (ITU-T Q.763), Brazilian SIP-I (ANATEL)
|
|
31
|
+
- **Multiple transports** — UDP, TCP, TLS, WebSocket (sync + async)
|
|
32
|
+
- **State management** — Transaction FSMs with Timer A/E retransmission
|
|
33
|
+
- **Extensible** — ABC-based transports, body parsers, custom DI extractors
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import sipx
|
|
39
|
+
|
|
40
|
+
# One-liners
|
|
41
|
+
sipx.options("sip:pbx.example.com")
|
|
42
|
+
sipx.register("sip:alice@pbx.com", auth=("alice", "secret"))
|
|
43
|
+
sipx.send("sip:bob@pbx.com", "Hello!", auth=("alice", "secret"))
|
|
44
|
+
|
|
45
|
+
# Full call with dialog tracking
|
|
46
|
+
from sipx import Client
|
|
47
|
+
|
|
48
|
+
with Client(local_port=5061) as client:
|
|
49
|
+
client.auth = ("alice", "secret")
|
|
50
|
+
|
|
51
|
+
client.register("sip:alice@pbx.com")
|
|
52
|
+
|
|
53
|
+
sdp = client.create_sdp(port=8000)
|
|
54
|
+
r = client.invite("sip:bob@pbx.com", body=sdp.to_string())
|
|
55
|
+
|
|
56
|
+
if r.status_code == 200:
|
|
57
|
+
client.ack() # uses tracked dialog
|
|
58
|
+
# call is active...
|
|
59
|
+
client.bye() # uses tracked dialog
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# With uv (recommended)
|
|
66
|
+
uv add sipx
|
|
67
|
+
|
|
68
|
+
# With pip
|
|
69
|
+
pip install sipx
|
|
70
|
+
|
|
71
|
+
# From source
|
|
72
|
+
git clone https://github.com/yourusername/sipx.git
|
|
73
|
+
cd sipx
|
|
74
|
+
uv sync
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Requirements
|
|
78
|
+
|
|
79
|
+
Python 3.12+
|
|
80
|
+
|
|
81
|
+
## Usage Examples
|
|
82
|
+
|
|
83
|
+
### SIP Server (FastAPI-style decorators + DI)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from typing import Annotated
|
|
87
|
+
from sipx import SIPServer, Request, FromHeader, Header
|
|
88
|
+
|
|
89
|
+
server = SIPServer(local_host="127.0.0.1", local_port=5060)
|
|
90
|
+
|
|
91
|
+
@server.register
|
|
92
|
+
def on_register(request: Request, caller: Annotated[str, FromHeader]):
|
|
93
|
+
print(f"REGISTER from {caller}")
|
|
94
|
+
return request.ok()
|
|
95
|
+
|
|
96
|
+
@server.message
|
|
97
|
+
def on_message(request: Request, caller: Annotated[str, FromHeader]):
|
|
98
|
+
body = request.content.decode() if request.content else ""
|
|
99
|
+
print(f"MESSAGE from {caller}: {body}")
|
|
100
|
+
return request.ok()
|
|
101
|
+
|
|
102
|
+
server.start()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Async Client
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import asyncio
|
|
109
|
+
from sipx import AsyncClient
|
|
110
|
+
|
|
111
|
+
async def main():
|
|
112
|
+
async with AsyncClient(local_port=5061) as client:
|
|
113
|
+
client.auth = ("alice", "secret")
|
|
114
|
+
|
|
115
|
+
r = await client.register("sip:alice@pbx.com")
|
|
116
|
+
print(f"REGISTER: {r.status_code}")
|
|
117
|
+
|
|
118
|
+
sdp = client.create_sdp(port=8000)
|
|
119
|
+
r = await client.invite("sip:bob@pbx.com", body=sdp.to_string())
|
|
120
|
+
if r and r.status_code == 200:
|
|
121
|
+
await client.ack()
|
|
122
|
+
await asyncio.sleep(5)
|
|
123
|
+
await client.bye()
|
|
124
|
+
|
|
125
|
+
asyncio.run(main())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### FastAPI + SIP Integration
|
|
129
|
+
|
|
130
|
+
Run a REST API that controls SIP operations:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Terminal 1: SIP server
|
|
134
|
+
uv run python examples/server.py
|
|
135
|
+
|
|
136
|
+
# Terminal 2: FastAPI
|
|
137
|
+
uv run python examples/fastapi_sip.py
|
|
138
|
+
|
|
139
|
+
# Terminal 3: test via curl
|
|
140
|
+
curl -X POST http://localhost:8000/message \
|
|
141
|
+
-H "Content-Type: application/json" \
|
|
142
|
+
-d '{"uri":"sip:test@127.0.0.1:15090","content":"Hello from FastAPI!"}' | jq
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"status_code": 200,
|
|
148
|
+
"reason": "OK"
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Full request/response trace with retransmission timers, DI resolution, and FSM state transitions:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
# FastAPI (client side)
|
|
156
|
+
INFO UDP bound to 0.0.0.0:40703
|
|
157
|
+
DEBUG Transaction 592b5fee created: MESSAGE (type=NON_INVITE)
|
|
158
|
+
DEBUG Timer E started (0.5s) # retransmission armed
|
|
159
|
+
DEBUG Timer F started (32.0s) # timeout armed
|
|
160
|
+
DEBUG UDP send 335 bytes -> 127.0.0.1:15090
|
|
161
|
+
DEBUG UDP recv 245 bytes <- 127.0.0.1:15090
|
|
162
|
+
DEBUG <<< 200 OK
|
|
163
|
+
DEBUG Transaction 592b5fee: TRYING -> COMPLETED
|
|
164
|
+
DEBUG Timer E cancelled # retransmission stopped
|
|
165
|
+
DEBUG Timer F cancelled # timeout stopped
|
|
166
|
+
|
|
167
|
+
# SIP Server (server side)
|
|
168
|
+
DEBUG UDP recv 335 bytes <- 127.0.0.1:40703
|
|
169
|
+
DEBUG Resolving handler on_message
|
|
170
|
+
DEBUG Extracting caller via FromHeader()
|
|
171
|
+
MESSAGE from <sip:user@0.0.0.0>: Hello from FastAPI!
|
|
172
|
+
DEBUG >>> SENDING 200 OK to 127.0.0.1:40703
|
|
173
|
+
DEBUG UDP send 245 bytes -> 127.0.0.1:40703
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Event Handlers
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from sipx import Events, on
|
|
180
|
+
|
|
181
|
+
class CallEvents(Events):
|
|
182
|
+
@on('INVITE', status=200)
|
|
183
|
+
def on_call_ok(self, request, response, context):
|
|
184
|
+
print("Call accepted!")
|
|
185
|
+
|
|
186
|
+
@on('INVITE', status=(180, 183))
|
|
187
|
+
def on_ringing(self, request, response, context):
|
|
188
|
+
print(f"Ringing... ({response.status_code})")
|
|
189
|
+
|
|
190
|
+
@on(status=(401, 407))
|
|
191
|
+
def on_auth(self, request, response, context):
|
|
192
|
+
print("Auth challenge received")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Authentication
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
# Tuple auth (auto-retry on 401/407)
|
|
199
|
+
client.auth = ("alice", "secret")
|
|
200
|
+
|
|
201
|
+
# Or explicit Auth object
|
|
202
|
+
client.auth = Auth.Digest("alice", "secret")
|
|
203
|
+
|
|
204
|
+
# Per-request override
|
|
205
|
+
r = client.register("sip:alice@pbx.com")
|
|
206
|
+
if r.status_code == 401:
|
|
207
|
+
r = client.retry_with_auth(r, auth=Auth.Digest("alice", "other"))
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Supported Methods
|
|
211
|
+
|
|
212
|
+
| Method | Description | Example |
|
|
213
|
+
| --------- | ---------------------- | ------------------------------------------------------------- |
|
|
214
|
+
| INVITE | Establish a call | `client.invite('sip:bob@ex.com', body=sdp)` |
|
|
215
|
+
| ACK | Acknowledge INVITE | `client.ack()` (auto dialog) |
|
|
216
|
+
| BYE | Terminate a call | `client.bye()` (auto dialog) |
|
|
217
|
+
| CANCEL | Cancel pending INVITE | `client.cancel(response=r)` |
|
|
218
|
+
| REGISTER | Register location | `client.register('sip:alice@ex.com')` |
|
|
219
|
+
| OPTIONS | Query capabilities | `client.options('sip:ex.com')` |
|
|
220
|
+
| MESSAGE | Instant message | `client.message('sip:bob@ex.com', content='Hi')` |
|
|
221
|
+
| SUBSCRIBE | Subscribe to events | `client.subscribe('sip:bob@ex.com')` |
|
|
222
|
+
| NOTIFY | Event notification | `client.notify('sip:bob@ex.com')` |
|
|
223
|
+
| REFER | Call transfer | `client.refer('sip:bob@ex.com', refer_to='sip:carol@ex.com')` |
|
|
224
|
+
| INFO | Mid-dialog info (DTMF) | `client.info('sip:bob@ex.com', content=dtmf)` |
|
|
225
|
+
| UPDATE | Update session | `client.update('sip:bob@ex.com', sdp_content=sdp)` |
|
|
226
|
+
| PRACK | Provisional ACK | `client.prack(response=r)` |
|
|
227
|
+
| PUBLISH | Publish state | `client.publish('sip:bob@ex.com')` |
|
|
228
|
+
|
|
229
|
+
## RFC Compliance
|
|
230
|
+
|
|
231
|
+
| RFC | Title | Status |
|
|
232
|
+
| ---- | --------------------------------- | ------------------------------------------ |
|
|
233
|
+
| 3261 | SIP: Session Initiation Protocol | Complete (auto 100 Trying, retransmission) |
|
|
234
|
+
| 2617 | HTTP Digest Authentication | Complete (MD5, SHA-256) |
|
|
235
|
+
| 7616 | HTTP Digest (SHA-256) | Complete |
|
|
236
|
+
| 4566 | SDP: Session Description Protocol | Complete (ICE, SRTP, DTLS) |
|
|
237
|
+
| 3264 | Offer/Answer Model with SDP | Complete |
|
|
238
|
+
| 3550 | RTP: Real-time Transport Protocol | Complete (sync + async) |
|
|
239
|
+
| 4733 | DTMF via RTP (telephone-event) | Complete |
|
|
240
|
+
| 3263 | DNS SRV Resolution | Complete (auto in Client) |
|
|
241
|
+
| 4028 | Session Timers | Complete |
|
|
242
|
+
|
|
243
|
+
## Documentation
|
|
244
|
+
|
|
245
|
+
| Document | Description |
|
|
246
|
+
| ---------------------------------------- | ------------------------------------------------------- |
|
|
247
|
+
| [docs/SDD.md](docs/SDD.md) | Software Design Document (full spec, diagrams, roadmap) |
|
|
248
|
+
| [examples/README.md](examples/README.md) | Examples catalog (22 examples) |
|
|
249
|
+
| [docker/asterisk/](docker/asterisk/) | Asterisk test environment |
|
|
250
|
+
|
|
251
|
+
## Testing with Asterisk
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
cd docker/asterisk
|
|
255
|
+
docker-compose up -d
|
|
256
|
+
uv run python examples/asterisk.py
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Examples
|
|
260
|
+
|
|
261
|
+
See [examples/README.md](examples/README.md) for the full list. Highlights:
|
|
262
|
+
|
|
263
|
+
| Example | What it shows |
|
|
264
|
+
| ----------------------------------------------------- | ---------------------------------------- |
|
|
265
|
+
| [quickstart.py](examples/quickstart.py) | One-liner register, options, call, send |
|
|
266
|
+
| [call.py](examples/call.py) | REGISTER -> INVITE -> ACK -> BYE |
|
|
267
|
+
| [server.py](examples/server.py) | SIPServer with decorators + DI |
|
|
268
|
+
| [ivr.py](examples/ivr.py) | Async IVR with RTP + DTMF |
|
|
269
|
+
| [response_builders.py](examples/response_builders.py) | request.ok/trying/error, dialog tracking |
|
|
270
|
+
| [asterisk.py](examples/asterisk.py) | Comprehensive integration test |
|
|
271
|
+
|
|
272
|
+
## Roadmap
|
|
273
|
+
|
|
274
|
+
See [docs/SDD.md](docs/SDD.md) for the full roadmap. Key upcoming work:
|
|
275
|
+
|
|
276
|
+
- **SRTP** — packet encryption (RFC 3711)
|
|
277
|
+
- **RTCP** — control protocol (RFC 3550 Section 6)
|
|
278
|
+
- **Jitter buffer** — for smooth RTP playback
|
|
279
|
+
- **Coverage >80%** — currently 607 tests, ~60%
|
|
280
|
+
- **PyPI publishing** — package ready, CI pipeline configured
|
|
281
|
+
|
|
282
|
+
## Development
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
git clone https://github.com/yourusername/sipx.git
|
|
286
|
+
cd sipx
|
|
287
|
+
uv sync
|
|
288
|
+
uv run ruff check sipx
|
|
289
|
+
uv run pytest
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Inspirations
|
|
293
|
+
|
|
294
|
+
API design:
|
|
295
|
+
|
|
296
|
+
- [httpx](https://www.python-httpx.org/) — Pythonic API, sync/async, extensible
|
|
297
|
+
|
|
298
|
+
Reference implementations:
|
|
299
|
+
|
|
300
|
+
- [sipd](https://github.com/initbar/sipd) — SIP daemon implementation
|
|
301
|
+
- [sipmessage](https://github.com/spacinov/sipmessage) — SIP message parsing
|
|
302
|
+
- [sip-parser](https://github.com/alxgb/sip-parser) — SIP parser
|
|
303
|
+
- [PySipIvr](https://github.com/ersansrck/PySipIvr) — SIP IVR implementation
|
|
304
|
+
- [sip-resources](https://github.com/miconda/sip-resources) — Curated SIP resources and documentation
|
|
305
|
+
|
|
306
|
+
Other inspirations:
|
|
307
|
+
|
|
308
|
+
- [pyVoIP](https://github.com/tayler6000/pyVoIP) — VoIP functionality
|
|
309
|
+
- [aiosip](https://github.com/Eyepea/aiosip) — Async SIP
|
|
310
|
+
- [b2bua](https://github.com/sippy/b2bua) — Back-to-back user agent
|
|
311
|
+
- [pysipp](https://github.com/SIPp/pysipp) — SIP testing
|
|
312
|
+
- [PySIPio](https://pypi.org/project/PySIPio/) — SIP I/O
|
|
313
|
+
- [katariSIP](https://github.com/klocation/katarisip) — SIP library
|
|
314
|
+
- [SIP-Auth-helper](https://github.com/pbertera/SIP-Auth-helper) — SIP auth tools
|
|
315
|
+
- [callsip.py](https://github.com/rundekugel/callsip.py) — Simple SIP caller
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
MIT License
|
sipx-0.0.4/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# sipx
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A modern, high-performance SIP library for Python inspired by [httpx](https://www.python-httpx.org/).
|
|
7
|
+
|
|
8
|
+
Build voice automation scripts, IVR systems, AI-powered call handlers, and SIP testing tools with a clean, Pythonic API.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **httpx-inspired API** — `client.invite()`, `client.register()`, sync and async
|
|
13
|
+
- **FastAPI-style server** — `@server.invite`, `Annotated` DI extractors
|
|
14
|
+
- **Response builders** — `request.ok()`, `request.trying()`, `request.error(code)`
|
|
15
|
+
- **Dialog tracking** — `client.ack()` / `client.bye()` without passing response
|
|
16
|
+
- **Declarative events** — `@on('INVITE', status=200)` decorators
|
|
17
|
+
- **Full SIP support** — 14 methods, auto 100 Trying, auto DNS SRV resolution
|
|
18
|
+
- **Digest authentication** — MD5, SHA-256, auto-retry on 401/407
|
|
19
|
+
- **RTP + DTMF** — RFC 4733 + SIP INFO + Inband, sync and async
|
|
20
|
+
- **SDP with ICE/SRTP** — RFC 4566, offer/answer, ICE candidates, DTLS
|
|
21
|
+
- **Media pipeline** — RTP, tone generators, TTS/STT adapters, IVR builder
|
|
22
|
+
- **SIP-I** — Real ISUP binary encoding (ITU-T Q.763), Brazilian SIP-I (ANATEL)
|
|
23
|
+
- **Multiple transports** — UDP, TCP, TLS, WebSocket (sync + async)
|
|
24
|
+
- **State management** — Transaction FSMs with Timer A/E retransmission
|
|
25
|
+
- **Extensible** — ABC-based transports, body parsers, custom DI extractors
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import sipx
|
|
31
|
+
|
|
32
|
+
# One-liners
|
|
33
|
+
sipx.options("sip:pbx.example.com")
|
|
34
|
+
sipx.register("sip:alice@pbx.com", auth=("alice", "secret"))
|
|
35
|
+
sipx.send("sip:bob@pbx.com", "Hello!", auth=("alice", "secret"))
|
|
36
|
+
|
|
37
|
+
# Full call with dialog tracking
|
|
38
|
+
from sipx import Client
|
|
39
|
+
|
|
40
|
+
with Client(local_port=5061) as client:
|
|
41
|
+
client.auth = ("alice", "secret")
|
|
42
|
+
|
|
43
|
+
client.register("sip:alice@pbx.com")
|
|
44
|
+
|
|
45
|
+
sdp = client.create_sdp(port=8000)
|
|
46
|
+
r = client.invite("sip:bob@pbx.com", body=sdp.to_string())
|
|
47
|
+
|
|
48
|
+
if r.status_code == 200:
|
|
49
|
+
client.ack() # uses tracked dialog
|
|
50
|
+
# call is active...
|
|
51
|
+
client.bye() # uses tracked dialog
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# With uv (recommended)
|
|
58
|
+
uv add sipx
|
|
59
|
+
|
|
60
|
+
# With pip
|
|
61
|
+
pip install sipx
|
|
62
|
+
|
|
63
|
+
# From source
|
|
64
|
+
git clone https://github.com/yourusername/sipx.git
|
|
65
|
+
cd sipx
|
|
66
|
+
uv sync
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Requirements
|
|
70
|
+
|
|
71
|
+
Python 3.12+
|
|
72
|
+
|
|
73
|
+
## Usage Examples
|
|
74
|
+
|
|
75
|
+
### SIP Server (FastAPI-style decorators + DI)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from typing import Annotated
|
|
79
|
+
from sipx import SIPServer, Request, FromHeader, Header
|
|
80
|
+
|
|
81
|
+
server = SIPServer(local_host="127.0.0.1", local_port=5060)
|
|
82
|
+
|
|
83
|
+
@server.register
|
|
84
|
+
def on_register(request: Request, caller: Annotated[str, FromHeader]):
|
|
85
|
+
print(f"REGISTER from {caller}")
|
|
86
|
+
return request.ok()
|
|
87
|
+
|
|
88
|
+
@server.message
|
|
89
|
+
def on_message(request: Request, caller: Annotated[str, FromHeader]):
|
|
90
|
+
body = request.content.decode() if request.content else ""
|
|
91
|
+
print(f"MESSAGE from {caller}: {body}")
|
|
92
|
+
return request.ok()
|
|
93
|
+
|
|
94
|
+
server.start()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Async Client
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
from sipx import AsyncClient
|
|
102
|
+
|
|
103
|
+
async def main():
|
|
104
|
+
async with AsyncClient(local_port=5061) as client:
|
|
105
|
+
client.auth = ("alice", "secret")
|
|
106
|
+
|
|
107
|
+
r = await client.register("sip:alice@pbx.com")
|
|
108
|
+
print(f"REGISTER: {r.status_code}")
|
|
109
|
+
|
|
110
|
+
sdp = client.create_sdp(port=8000)
|
|
111
|
+
r = await client.invite("sip:bob@pbx.com", body=sdp.to_string())
|
|
112
|
+
if r and r.status_code == 200:
|
|
113
|
+
await client.ack()
|
|
114
|
+
await asyncio.sleep(5)
|
|
115
|
+
await client.bye()
|
|
116
|
+
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### FastAPI + SIP Integration
|
|
121
|
+
|
|
122
|
+
Run a REST API that controls SIP operations:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Terminal 1: SIP server
|
|
126
|
+
uv run python examples/server.py
|
|
127
|
+
|
|
128
|
+
# Terminal 2: FastAPI
|
|
129
|
+
uv run python examples/fastapi_sip.py
|
|
130
|
+
|
|
131
|
+
# Terminal 3: test via curl
|
|
132
|
+
curl -X POST http://localhost:8000/message \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d '{"uri":"sip:test@127.0.0.1:15090","content":"Hello from FastAPI!"}' | jq
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"status_code": 200,
|
|
140
|
+
"reason": "OK"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Full request/response trace with retransmission timers, DI resolution, and FSM state transitions:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
# FastAPI (client side)
|
|
148
|
+
INFO UDP bound to 0.0.0.0:40703
|
|
149
|
+
DEBUG Transaction 592b5fee created: MESSAGE (type=NON_INVITE)
|
|
150
|
+
DEBUG Timer E started (0.5s) # retransmission armed
|
|
151
|
+
DEBUG Timer F started (32.0s) # timeout armed
|
|
152
|
+
DEBUG UDP send 335 bytes -> 127.0.0.1:15090
|
|
153
|
+
DEBUG UDP recv 245 bytes <- 127.0.0.1:15090
|
|
154
|
+
DEBUG <<< 200 OK
|
|
155
|
+
DEBUG Transaction 592b5fee: TRYING -> COMPLETED
|
|
156
|
+
DEBUG Timer E cancelled # retransmission stopped
|
|
157
|
+
DEBUG Timer F cancelled # timeout stopped
|
|
158
|
+
|
|
159
|
+
# SIP Server (server side)
|
|
160
|
+
DEBUG UDP recv 335 bytes <- 127.0.0.1:40703
|
|
161
|
+
DEBUG Resolving handler on_message
|
|
162
|
+
DEBUG Extracting caller via FromHeader()
|
|
163
|
+
MESSAGE from <sip:user@0.0.0.0>: Hello from FastAPI!
|
|
164
|
+
DEBUG >>> SENDING 200 OK to 127.0.0.1:40703
|
|
165
|
+
DEBUG UDP send 245 bytes -> 127.0.0.1:40703
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Event Handlers
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from sipx import Events, on
|
|
172
|
+
|
|
173
|
+
class CallEvents(Events):
|
|
174
|
+
@on('INVITE', status=200)
|
|
175
|
+
def on_call_ok(self, request, response, context):
|
|
176
|
+
print("Call accepted!")
|
|
177
|
+
|
|
178
|
+
@on('INVITE', status=(180, 183))
|
|
179
|
+
def on_ringing(self, request, response, context):
|
|
180
|
+
print(f"Ringing... ({response.status_code})")
|
|
181
|
+
|
|
182
|
+
@on(status=(401, 407))
|
|
183
|
+
def on_auth(self, request, response, context):
|
|
184
|
+
print("Auth challenge received")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Authentication
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# Tuple auth (auto-retry on 401/407)
|
|
191
|
+
client.auth = ("alice", "secret")
|
|
192
|
+
|
|
193
|
+
# Or explicit Auth object
|
|
194
|
+
client.auth = Auth.Digest("alice", "secret")
|
|
195
|
+
|
|
196
|
+
# Per-request override
|
|
197
|
+
r = client.register("sip:alice@pbx.com")
|
|
198
|
+
if r.status_code == 401:
|
|
199
|
+
r = client.retry_with_auth(r, auth=Auth.Digest("alice", "other"))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Supported Methods
|
|
203
|
+
|
|
204
|
+
| Method | Description | Example |
|
|
205
|
+
| --------- | ---------------------- | ------------------------------------------------------------- |
|
|
206
|
+
| INVITE | Establish a call | `client.invite('sip:bob@ex.com', body=sdp)` |
|
|
207
|
+
| ACK | Acknowledge INVITE | `client.ack()` (auto dialog) |
|
|
208
|
+
| BYE | Terminate a call | `client.bye()` (auto dialog) |
|
|
209
|
+
| CANCEL | Cancel pending INVITE | `client.cancel(response=r)` |
|
|
210
|
+
| REGISTER | Register location | `client.register('sip:alice@ex.com')` |
|
|
211
|
+
| OPTIONS | Query capabilities | `client.options('sip:ex.com')` |
|
|
212
|
+
| MESSAGE | Instant message | `client.message('sip:bob@ex.com', content='Hi')` |
|
|
213
|
+
| SUBSCRIBE | Subscribe to events | `client.subscribe('sip:bob@ex.com')` |
|
|
214
|
+
| NOTIFY | Event notification | `client.notify('sip:bob@ex.com')` |
|
|
215
|
+
| REFER | Call transfer | `client.refer('sip:bob@ex.com', refer_to='sip:carol@ex.com')` |
|
|
216
|
+
| INFO | Mid-dialog info (DTMF) | `client.info('sip:bob@ex.com', content=dtmf)` |
|
|
217
|
+
| UPDATE | Update session | `client.update('sip:bob@ex.com', sdp_content=sdp)` |
|
|
218
|
+
| PRACK | Provisional ACK | `client.prack(response=r)` |
|
|
219
|
+
| PUBLISH | Publish state | `client.publish('sip:bob@ex.com')` |
|
|
220
|
+
|
|
221
|
+
## RFC Compliance
|
|
222
|
+
|
|
223
|
+
| RFC | Title | Status |
|
|
224
|
+
| ---- | --------------------------------- | ------------------------------------------ |
|
|
225
|
+
| 3261 | SIP: Session Initiation Protocol | Complete (auto 100 Trying, retransmission) |
|
|
226
|
+
| 2617 | HTTP Digest Authentication | Complete (MD5, SHA-256) |
|
|
227
|
+
| 7616 | HTTP Digest (SHA-256) | Complete |
|
|
228
|
+
| 4566 | SDP: Session Description Protocol | Complete (ICE, SRTP, DTLS) |
|
|
229
|
+
| 3264 | Offer/Answer Model with SDP | Complete |
|
|
230
|
+
| 3550 | RTP: Real-time Transport Protocol | Complete (sync + async) |
|
|
231
|
+
| 4733 | DTMF via RTP (telephone-event) | Complete |
|
|
232
|
+
| 3263 | DNS SRV Resolution | Complete (auto in Client) |
|
|
233
|
+
| 4028 | Session Timers | Complete |
|
|
234
|
+
|
|
235
|
+
## Documentation
|
|
236
|
+
|
|
237
|
+
| Document | Description |
|
|
238
|
+
| ---------------------------------------- | ------------------------------------------------------- |
|
|
239
|
+
| [docs/SDD.md](docs/SDD.md) | Software Design Document (full spec, diagrams, roadmap) |
|
|
240
|
+
| [examples/README.md](examples/README.md) | Examples catalog (22 examples) |
|
|
241
|
+
| [docker/asterisk/](docker/asterisk/) | Asterisk test environment |
|
|
242
|
+
|
|
243
|
+
## Testing with Asterisk
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
cd docker/asterisk
|
|
247
|
+
docker-compose up -d
|
|
248
|
+
uv run python examples/asterisk.py
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Examples
|
|
252
|
+
|
|
253
|
+
See [examples/README.md](examples/README.md) for the full list. Highlights:
|
|
254
|
+
|
|
255
|
+
| Example | What it shows |
|
|
256
|
+
| ----------------------------------------------------- | ---------------------------------------- |
|
|
257
|
+
| [quickstart.py](examples/quickstart.py) | One-liner register, options, call, send |
|
|
258
|
+
| [call.py](examples/call.py) | REGISTER -> INVITE -> ACK -> BYE |
|
|
259
|
+
| [server.py](examples/server.py) | SIPServer with decorators + DI |
|
|
260
|
+
| [ivr.py](examples/ivr.py) | Async IVR with RTP + DTMF |
|
|
261
|
+
| [response_builders.py](examples/response_builders.py) | request.ok/trying/error, dialog tracking |
|
|
262
|
+
| [asterisk.py](examples/asterisk.py) | Comprehensive integration test |
|
|
263
|
+
|
|
264
|
+
## Roadmap
|
|
265
|
+
|
|
266
|
+
See [docs/SDD.md](docs/SDD.md) for the full roadmap. Key upcoming work:
|
|
267
|
+
|
|
268
|
+
- **SRTP** — packet encryption (RFC 3711)
|
|
269
|
+
- **RTCP** — control protocol (RFC 3550 Section 6)
|
|
270
|
+
- **Jitter buffer** — for smooth RTP playback
|
|
271
|
+
- **Coverage >80%** — currently 607 tests, ~60%
|
|
272
|
+
- **PyPI publishing** — package ready, CI pipeline configured
|
|
273
|
+
|
|
274
|
+
## Development
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
git clone https://github.com/yourusername/sipx.git
|
|
278
|
+
cd sipx
|
|
279
|
+
uv sync
|
|
280
|
+
uv run ruff check sipx
|
|
281
|
+
uv run pytest
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Inspirations
|
|
285
|
+
|
|
286
|
+
API design:
|
|
287
|
+
|
|
288
|
+
- [httpx](https://www.python-httpx.org/) — Pythonic API, sync/async, extensible
|
|
289
|
+
|
|
290
|
+
Reference implementations:
|
|
291
|
+
|
|
292
|
+
- [sipd](https://github.com/initbar/sipd) — SIP daemon implementation
|
|
293
|
+
- [sipmessage](https://github.com/spacinov/sipmessage) — SIP message parsing
|
|
294
|
+
- [sip-parser](https://github.com/alxgb/sip-parser) — SIP parser
|
|
295
|
+
- [PySipIvr](https://github.com/ersansrck/PySipIvr) — SIP IVR implementation
|
|
296
|
+
- [sip-resources](https://github.com/miconda/sip-resources) — Curated SIP resources and documentation
|
|
297
|
+
|
|
298
|
+
Other inspirations:
|
|
299
|
+
|
|
300
|
+
- [pyVoIP](https://github.com/tayler6000/pyVoIP) — VoIP functionality
|
|
301
|
+
- [aiosip](https://github.com/Eyepea/aiosip) — Async SIP
|
|
302
|
+
- [b2bua](https://github.com/sippy/b2bua) — Back-to-back user agent
|
|
303
|
+
- [pysipp](https://github.com/SIPp/pysipp) — SIP testing
|
|
304
|
+
- [PySIPio](https://pypi.org/project/PySIPio/) — SIP I/O
|
|
305
|
+
- [katariSIP](https://github.com/klocation/katarisip) — SIP library
|
|
306
|
+
- [SIP-Auth-helper](https://github.com/pbertera/SIP-Auth-helper) — SIP auth tools
|
|
307
|
+
- [callsip.py](https://github.com/rundekugel/callsip.py) — Simple SIP caller
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT License
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sipx"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Add your description here"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
dependencies = ["rich>=14.1.0"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
sipx = "sipx.main:main"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
cli = ["typer>=0.17.4"]
|
|
18
|
+
dev = [
|
|
19
|
+
"fastapi[standard]>=0.135.2",
|
|
20
|
+
"pre-commit>=4.3.0",
|
|
21
|
+
"ruff>=0.12.12",
|
|
22
|
+
"taskipy>=1.14.1",
|
|
23
|
+
"ty>=0.0.1a20",
|
|
24
|
+
]
|
|
25
|
+
test = ["pytest>=8.4.2", "pytest-asyncio>=1.1.0", "pytest-cov>=6.3.0"]
|
|
26
|
+
|
|
27
|
+
[tool.taskipy.tasks]
|
|
28
|
+
# Quality
|
|
29
|
+
lint = "ruff check sipx/ tests/ examples/"
|
|
30
|
+
format = "ruff format sipx/ tests/ examples/"
|
|
31
|
+
check = "ruff format --check sipx/ tests/ examples/ && ruff check sipx/ tests/ examples/"
|
|
32
|
+
typecheck = "ty check"
|
|
33
|
+
|
|
34
|
+
# Test
|
|
35
|
+
test = "pytest tests/ -v"
|
|
36
|
+
test-asterisk = "ASTERISK_INTEGRATION=1 pytest tests/integration/test_asterisk.py -v"
|
|
37
|
+
cov = "pytest tests/ --cov=sipx --cov-report=term-missing"
|
|
38
|
+
cov-html = "pytest tests/ --cov=sipx --cov-report=html && open htmlcov/index.html"
|
|
39
|
+
|
|
40
|
+
# CI (run in order)
|
|
41
|
+
ci = "task check && task test"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools]
|
|
44
|
+
package-dir = { "" = "." }
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.dynamic]
|
|
47
|
+
version = { attr = "sipx._version.__version__" }
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["."]
|
|
51
|
+
include = ["sipx*"]
|
|
52
|
+
exclude = ["docker*", "docs*", "examples*", "tests*"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
sipx = ["py.typed"]
|
sipx-0.0.4/setup.cfg
ADDED