sipx 0.0.4__py3-none-any.whl

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 (73) hide show
  1. sipx/__init__.py +299 -0
  2. sipx/_depends.py +219 -0
  3. sipx/_events.py +337 -0
  4. sipx/_routing.py +126 -0
  5. sipx/_types.py +265 -0
  6. sipx/_uri.py +219 -0
  7. sipx/_utils.py +187 -0
  8. sipx/_version.py +3 -0
  9. sipx/client/__init__.py +4 -0
  10. sipx/client/_async.py +640 -0
  11. sipx/client/_base.py +184 -0
  12. sipx/client/_sync.py +1172 -0
  13. sipx/contrib/__init__.py +57 -0
  14. sipx/contrib/_fastapi.py +169 -0
  15. sipx/contrib/_isup.py +740 -0
  16. sipx/contrib/_sipi.py +258 -0
  17. sipx/contrib/_sipi_br.py +383 -0
  18. sipx/contrib/_stt_whisper.py +28 -0
  19. sipx/contrib/_tts_google.py +38 -0
  20. sipx/contrib/ivr/__init__.py +5 -0
  21. sipx/contrib/ivr/_async.py +93 -0
  22. sipx/contrib/ivr/_models.py +55 -0
  23. sipx/contrib/ivr/_sync.py +90 -0
  24. sipx/dns/__init__.py +5 -0
  25. sipx/dns/_async.py +122 -0
  26. sipx/dns/_models.py +19 -0
  27. sipx/dns/_sync.py +124 -0
  28. sipx/fsm/__init__.py +13 -0
  29. sipx/fsm/_manager.py +452 -0
  30. sipx/fsm/_models.py +386 -0
  31. sipx/fsm/_timer.py +142 -0
  32. sipx/main.py +239 -0
  33. sipx/media/__init__.py +48 -0
  34. sipx/media/_async.py +426 -0
  35. sipx/media/_audio.py +103 -0
  36. sipx/media/_codecs.py +258 -0
  37. sipx/media/_dtmf.py +242 -0
  38. sipx/media/_generators.py +225 -0
  39. sipx/media/_opus.py +111 -0
  40. sipx/media/_pyaudio.py +192 -0
  41. sipx/media/_rtp.py +365 -0
  42. sipx/media/_session.py +233 -0
  43. sipx/media/_stt.py +39 -0
  44. sipx/media/_tts.py +72 -0
  45. sipx/media/audio/__init__.py +27 -0
  46. sipx/media/codecs/__init__.py +4 -0
  47. sipx/media/dtmf/__init__.py +17 -0
  48. sipx/media/rtp/__init__.py +4 -0
  49. sipx/media/session/__init__.py +4 -0
  50. sipx/models/__init__.py +60 -0
  51. sipx/models/_auth.py +811 -0
  52. sipx/models/_body.py +1420 -0
  53. sipx/models/_header.py +535 -0
  54. sipx/models/_message.py +965 -0
  55. sipx/server/__init__.py +6 -0
  56. sipx/server/_async.py +116 -0
  57. sipx/server/_base.py +156 -0
  58. sipx/server/_sync.py +234 -0
  59. sipx/session/__init__.py +13 -0
  60. sipx/session/_subscription.py +315 -0
  61. sipx/session/_timer.py +375 -0
  62. sipx/transports/__init__.py +66 -0
  63. sipx/transports/_base.py +235 -0
  64. sipx/transports/_tcp.py +572 -0
  65. sipx/transports/_tls.py +683 -0
  66. sipx/transports/_udp.py +461 -0
  67. sipx/transports/_utils.py +28 -0
  68. sipx/transports/_ws.py +286 -0
  69. sipx-0.0.4.dist-info/METADATA +319 -0
  70. sipx-0.0.4.dist-info/RECORD +73 -0
  71. sipx-0.0.4.dist-info/WHEEL +5 -0
  72. sipx-0.0.4.dist-info/entry_points.txt +2 -0
  73. sipx-0.0.4.dist-info/top_level.txt +1 -0
sipx/__init__.py ADDED
@@ -0,0 +1,299 @@
1
+ """
2
+ sipx — Modern SIP library for Python.
3
+
4
+ Quick Start:
5
+ >>> import sipx
6
+ >>> r = sipx.register("sip:alice@pbx.com", auth=("alice", "secret"))
7
+ >>> r = sipx.options("sip:pbx.com")
8
+
9
+ With Client:
10
+ >>> from sipx import Client, Events, on, SDPBody
11
+ >>>
12
+ >>> class MyEvents(Events):
13
+ ... @on('INVITE', status=200)
14
+ ... def on_call(self, request, response, context):
15
+ ... print("Call accepted!")
16
+ ...
17
+ >>> with Client() as client:
18
+ ... client.auth = ("alice", "secret")
19
+ ... client.events = MyEvents()
20
+ ... r = client.invite("sip:bob@pbx.com", body=SDPBody.audio("10.0.0.1", 8000).to_string())
21
+
22
+ Server with DI:
23
+ >>> from sipx import SIPServer, Request, Response, SDPBody, FromHeader, SDP
24
+ >>> from typing import Annotated
25
+ >>>
26
+ >>> server = SIPServer(port=5060)
27
+ >>> @server.invite
28
+ ... def on_invite(request: Request, caller: Annotated[str, FromHeader]) -> Response:
29
+ ... return Response(200)
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import Optional
35
+
36
+ # ============================================================================
37
+ # Client / Server
38
+ # ============================================================================
39
+
40
+ from .client import Client, AsyncClient
41
+ from .server import SIPServer
42
+
43
+ # ============================================================================
44
+ # Events
45
+ # ============================================================================
46
+
47
+ from ._events import Events, EventContext, event_handler, on
48
+
49
+ # ============================================================================
50
+ # Models
51
+ # ============================================================================
52
+
53
+ from .models import (
54
+ SIPMessage,
55
+ Request,
56
+ Response,
57
+ MessageParser,
58
+ Headers,
59
+ HeaderParser,
60
+ HeaderContainer,
61
+ MessageBody,
62
+ RawBody,
63
+ SDPBody,
64
+ BodyParser,
65
+ Auth,
66
+ SipAuthCredentials,
67
+ AuthMethod,
68
+ DigestAuth,
69
+ DigestChallenge,
70
+ DigestCredentials,
71
+ Challenge,
72
+ Credentials,
73
+ AuthParser,
74
+ )
75
+
76
+ # ============================================================================
77
+ # DI Extractors
78
+ # ============================================================================
79
+
80
+ from ._depends import (
81
+ Extractor,
82
+ FromHeader,
83
+ ToHeader,
84
+ CallID,
85
+ CSeqValue,
86
+ ViaValue,
87
+ SDP,
88
+ Source,
89
+ Header,
90
+ AutoRTP,
91
+ resolve_handler,
92
+ )
93
+
94
+ # ============================================================================
95
+ # URI
96
+ # ============================================================================
97
+
98
+ from ._uri import SipURI
99
+ from .session import AsyncSessionTimer, SessionTimer, SessionTimerConfig
100
+ from ._routing import RouteSet
101
+ from .dns import SipResolver, ResolvedTarget
102
+ from .session import AsyncSubscription, Subscription, SubscriptionState
103
+
104
+ # ============================================================================
105
+ # FSM
106
+ # ============================================================================
107
+
108
+ from .fsm import AsyncTimerManager, Dialog, StateManager, TimerManager, Transaction
109
+
110
+ # ============================================================================
111
+ # Transport
112
+ # ============================================================================
113
+
114
+ from .transports import (
115
+ BaseTransport,
116
+ AsyncBaseTransport,
117
+ TransportAddress,
118
+ TransportConfig,
119
+ TransportError,
120
+ )
121
+
122
+ # ============================================================================
123
+ # Types
124
+ # ============================================================================
125
+
126
+ from ._types import (
127
+ DialogState,
128
+ TransactionState,
129
+ TransactionType,
130
+ HeaderTypes,
131
+ ConnectionError,
132
+ ReadError,
133
+ WriteError,
134
+ TimeoutError,
135
+ )
136
+
137
+ # ============================================================================
138
+ # Version
139
+ # ============================================================================
140
+
141
+ from ._version import __version__
142
+
143
+
144
+ # ============================================================================
145
+ # One-liner functions (httpx-style)
146
+ # ============================================================================
147
+
148
+
149
+ def register(
150
+ aor: str,
151
+ *,
152
+ auth: Optional[tuple[str, str]] = None,
153
+ transport: str = "UDP",
154
+ expires: int = 3600,
155
+ ) -> Response:
156
+ """Register with a SIP server.
157
+
158
+ >>> r = sipx.register("sip:alice@pbx.com", auth=("alice", "secret"))
159
+ """
160
+ with Client(transport=transport, auto_auth=bool(auth)) as client:
161
+ if auth:
162
+ client.auth = auth
163
+ return client.register(aor, expires=expires)
164
+
165
+
166
+ def options(
167
+ uri: str,
168
+ *,
169
+ auth: Optional[tuple[str, str]] = None,
170
+ transport: str = "UDP",
171
+ ) -> Response:
172
+ """Query server capabilities.
173
+
174
+ >>> r = sipx.options("sip:pbx.com")
175
+ """
176
+ with Client(transport=transport, auto_auth=bool(auth)) as client:
177
+ if auth:
178
+ client.auth = auth
179
+ return client.options(uri)
180
+
181
+
182
+ def call(
183
+ uri: str,
184
+ *,
185
+ auth: Optional[tuple[str, str]] = None,
186
+ transport: str = "UDP",
187
+ body: Optional[str] = None,
188
+ ) -> Response:
189
+ """Make a SIP call (INVITE).
190
+
191
+ >>> r = sipx.call("sip:100@pbx.com", auth=("alice", "secret"))
192
+ """
193
+ with Client(transport=transport, auto_auth=bool(auth)) as client:
194
+ if auth:
195
+ client.auth = auth
196
+ return client.invite(to_uri=uri, body=body)
197
+
198
+
199
+ def send(
200
+ uri: str,
201
+ content: str = "",
202
+ *,
203
+ auth: Optional[tuple[str, str]] = None,
204
+ transport: str = "UDP",
205
+ ) -> Response:
206
+ """Send a SIP MESSAGE.
207
+
208
+ >>> r = sipx.send("sip:bob@pbx.com", "Hello!", auth=("alice", "secret"))
209
+ """
210
+ with Client(transport=transport, auto_auth=bool(auth)) as client:
211
+ if auth:
212
+ client.auth = auth
213
+ return client.message(to_uri=uri, content=content)
214
+
215
+
216
+ # ============================================================================
217
+ # Public API
218
+ # ============================================================================
219
+
220
+ __all__ = [
221
+ # ------ Main API ------
222
+ "Client",
223
+ "AsyncClient",
224
+ "SIPServer",
225
+ "SipURI",
226
+ "AsyncSessionTimer",
227
+ "SessionTimer",
228
+ "SessionTimerConfig",
229
+ "RouteSet",
230
+ "SipResolver",
231
+ "ResolvedTarget",
232
+ "AsyncSubscription",
233
+ "Subscription",
234
+ "SubscriptionState",
235
+ "Events",
236
+ "EventContext",
237
+ "event_handler",
238
+ "on",
239
+ "__version__",
240
+ # ------ One-liners ------
241
+ "register",
242
+ "options",
243
+ "call",
244
+ "send",
245
+ # ------ Auth ------
246
+ "Auth",
247
+ "SipAuthCredentials",
248
+ # ------ Models ------
249
+ "Request",
250
+ "Response",
251
+ "SDPBody",
252
+ "Headers",
253
+ # ------ DI Extractors ------
254
+ "Extractor",
255
+ "FromHeader",
256
+ "ToHeader",
257
+ "CallID",
258
+ "CSeqValue",
259
+ "ViaValue",
260
+ "SDP",
261
+ "Source",
262
+ "Header",
263
+ "AutoRTP",
264
+ # ------ Transport ------
265
+ "BaseTransport",
266
+ "TransportAddress",
267
+ "TransportConfig",
268
+ "TransportError",
269
+ # ------ Advanced ------
270
+ "SIPMessage",
271
+ "MessageParser",
272
+ "HeaderParser",
273
+ "HeaderContainer",
274
+ "MessageBody",
275
+ "RawBody",
276
+ "BodyParser",
277
+ "AuthMethod",
278
+ "DigestAuth",
279
+ "DigestChallenge",
280
+ "DigestCredentials",
281
+ "Challenge",
282
+ "Credentials",
283
+ "AuthParser",
284
+ "AsyncBaseTransport",
285
+ "StateManager",
286
+ "AsyncTimerManager",
287
+ "TimerManager",
288
+ "Transaction",
289
+ "Dialog",
290
+ "TransactionState",
291
+ "DialogState",
292
+ "TransactionType",
293
+ "HeaderTypes",
294
+ "ConnectionError",
295
+ "ReadError",
296
+ "WriteError",
297
+ "TimeoutError",
298
+ "resolve_handler",
299
+ ]
sipx/_depends.py ADDED
@@ -0,0 +1,219 @@
1
+ """
2
+ Dependency injection extractors for SIP server handlers.
3
+
4
+ Provides an Extractor ABC and built-in extractors that resolve handler
5
+ parameters from SIP requests via ``typing.Annotated`` metadata, similar
6
+ to FastAPI's ``Depends``.
7
+
8
+ Example::
9
+
10
+ from typing import Annotated
11
+ from sipx._depends import FromHeader, SDP, AutoRTP
12
+
13
+ @server.invite
14
+ def on_invite(
15
+ request: Request,
16
+ caller: Annotated[str, FromHeader()],
17
+ sdp: Annotated[SDPBody, SDP()],
18
+ rtp: Annotated[RTPSession, AutoRTP(port=19000)],
19
+ ) -> Response:
20
+ ...
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import typing
26
+ from abc import ABC, abstractmethod
27
+ from typing import Any
28
+
29
+ from ._utils import logger
30
+ from .models._message import Request
31
+ from ._types import TransportAddress
32
+
33
+ _log = logger.getChild("di")
34
+
35
+
36
+ # ============================================================================
37
+ # Base Extractor
38
+ # ============================================================================
39
+
40
+
41
+ class Extractor(ABC):
42
+ """Abstract base class for dependency injection extractors.
43
+
44
+ Subclass and implement ``extract`` to create custom extractors
45
+ usable with ``typing.Annotated``.
46
+ """
47
+
48
+ @abstractmethod
49
+ def extract(self, request: Request, source: TransportAddress) -> Any:
50
+ """Extract a value from a SIP request and transport source."""
51
+ ...
52
+
53
+ @classmethod
54
+ def resolve_handler(
55
+ cls, handler: Any, request: Request, source: TransportAddress
56
+ ) -> Any:
57
+ """Inspect handler type hints and inject dependencies.
58
+
59
+ Resolves ``Annotated[X, <Extractor>]`` metadata, ``Request`` and
60
+ ``TransportAddress`` type hints. Falls back to ``handler(request, source)``.
61
+ """
62
+ import inspect
63
+
64
+ _log.debug("Resolving handler %s", getattr(handler, "__name__", handler))
65
+ hints = typing.get_type_hints(handler, include_extras=True)
66
+ kwargs: dict[str, Any] = {}
67
+
68
+ for name, hint in hints.items():
69
+ if name == "return":
70
+ continue
71
+ if hint is Request or (
72
+ hasattr(hint, "__name__") and hint.__name__ == "Request"
73
+ ):
74
+ kwargs[name] = request
75
+ continue
76
+ if hint is TransportAddress:
77
+ kwargs[name] = source
78
+ continue
79
+ if hasattr(hint, "__metadata__"):
80
+ for meta in hint.__metadata__:
81
+ if isinstance(meta, cls):
82
+ _log.debug("Extracting %s via %s", name, type(meta).__name__)
83
+ kwargs[name] = meta.extract(request, source)
84
+ break
85
+ if isinstance(meta, type) and issubclass(meta, cls):
86
+ _log.debug("Extracting %s via %s()", name, meta.__name__)
87
+ kwargs[name] = meta().extract(request, source)
88
+ break
89
+
90
+ if not kwargs:
91
+ return handler(request, source)
92
+
93
+ sig = inspect.signature(handler)
94
+ positional = [p.name for p in sig.parameters.values() if p.name != "return"]
95
+ for i, name in enumerate(positional):
96
+ if name not in kwargs:
97
+ if i == 0:
98
+ kwargs[name] = request
99
+ elif i == 1:
100
+ kwargs[name] = source
101
+
102
+ return handler(**kwargs)
103
+
104
+
105
+ # ============================================================================
106
+ # Built-in Extractors
107
+ # ============================================================================
108
+
109
+
110
+ class FromHeader(Extractor):
111
+ """Extract the ``From`` header value."""
112
+
113
+ def extract(self, request: Request, source: TransportAddress) -> str | None:
114
+ return request.from_header
115
+
116
+
117
+ class ToHeader(Extractor):
118
+ """Extract the ``To`` header value."""
119
+
120
+ def extract(self, request: Request, source: TransportAddress) -> str | None:
121
+ return request.to_header
122
+
123
+
124
+ class CallID(Extractor):
125
+ """Extract the ``Call-ID`` header value."""
126
+
127
+ def extract(self, request: Request, source: TransportAddress) -> str | None:
128
+ return request.call_id
129
+
130
+
131
+ class CSeqValue(Extractor):
132
+ """Extract the ``CSeq`` header value."""
133
+
134
+ def extract(self, request: Request, source: TransportAddress) -> str | None:
135
+ return request.cseq
136
+
137
+
138
+ class ViaValue(Extractor):
139
+ """Extract the ``Via`` header value."""
140
+
141
+ def extract(self, request: Request, source: TransportAddress) -> str | None:
142
+ return request.via
143
+
144
+
145
+ class SDP(Extractor):
146
+ """Extract the lazily-parsed SDP body (``SDPBody`` or ``None``)."""
147
+
148
+ def extract(self, request: Request, source: TransportAddress) -> Any:
149
+ return request.body
150
+
151
+
152
+ class Source(Extractor):
153
+ """Extract the ``TransportAddress`` the request arrived from."""
154
+
155
+ def extract(self, request: Request, source: TransportAddress) -> TransportAddress:
156
+ return source
157
+
158
+
159
+ class Header(Extractor):
160
+ """Extract an arbitrary header by name.
161
+
162
+ Args:
163
+ name: The header name to look up.
164
+ """
165
+
166
+ def __init__(self, name: str) -> None:
167
+ self.name = name
168
+
169
+ def extract(self, request: Request, source: TransportAddress) -> str | None:
170
+ return request.headers.get(self.name)
171
+
172
+
173
+ class AutoRTP(Extractor):
174
+ """Create an ``RTPSession`` from the request SDP automatically.
175
+
176
+ Args:
177
+ port: Local RTP port to bind.
178
+ """
179
+
180
+ def __init__(self, port: int) -> None:
181
+ self.port = port
182
+
183
+ def extract(self, request: Request, source: TransportAddress) -> Any:
184
+ from .media._rtp import RTPSession
185
+ from .models._body import SDPBody
186
+
187
+ body = request.body
188
+ if isinstance(body, SDPBody):
189
+ return RTPSession.from_sdp(body, source.host, self.port)
190
+ return None
191
+
192
+
193
+ # ============================================================================
194
+ # Handler Resolver
195
+ # ============================================================================
196
+
197
+
198
+ def resolve_handler(handler: Any, request: Request, source: TransportAddress) -> Any:
199
+ """Shortcut for Extractor.resolve_handler()."""
200
+ return Extractor.resolve_handler(handler, request, source)
201
+
202
+
203
+ # ============================================================================
204
+ # Exports
205
+ # ============================================================================
206
+
207
+ __all__ = [
208
+ "Extractor",
209
+ "FromHeader",
210
+ "ToHeader",
211
+ "CallID",
212
+ "CSeqValue",
213
+ "ViaValue",
214
+ "SDP",
215
+ "Source",
216
+ "Header",
217
+ "AutoRTP",
218
+ "resolve_handler",
219
+ ]