janus-api 0.1.1__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.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.3
2
+ Name: janus-api
3
+ Version: 0.1.1
4
+ Summary: Add your description here
5
+ Author: Lakan
6
+ Author-email: Lakan <leydotpy.dev@gmail.com>
7
+ Requires-Dist: asgiref>=3.10.0
8
+ Requires-Dist: clerk-backend-api>=3.3.1
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: pyee>=13.0.0
11
+ Requires-Dist: python-decouple>=3.8
12
+ Requires-Dist: reactivex>=4.0.4
13
+ Requires-Dist: redis>=6.4.0
14
+ Requires-Dist: websockets>=15.0.1
15
+ Requires-Python: >=3.13
16
+ Description-Content-Type: text/markdown
17
+
File without changes
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "janus-api"
3
+ version = "0.1.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Lakan", email = "leydotpy.dev@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "asgiref>=3.10.0",
12
+ "clerk-backend-api>=3.3.1",
13
+ "httpx>=0.28.1",
14
+ "pyee>=13.0.0",
15
+ "python-decouple>=3.8",
16
+ "reactivex>=4.0.4",
17
+ "redis>=6.4.0",
18
+ "websockets>=15.0.1",
19
+ ]
20
+
21
+ [project.scripts]
22
+ janus = "janus:main"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.8.16,<0.9.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [pypi]
29
+ username = "__token__"
30
+ password = "pypi-AgEIcHlwaS5vcmcCJGI1ZGQ3M2NlLTQ5ZmItNDk2Yi04YjYwLTcwMWFmYWFmNzRjNAACKlszLCIyNzRiNzkxZC1jYWQ4LTQyMGUtOWQ4Zi0wMDhhZDMzNjBjNGEiXQAABiAWuok7XhyTdZQosrCK9hNpuof9GVKMDXpEGi1D-M1B2g"
@@ -0,0 +1,27 @@
1
+ import threading
2
+
3
+ from asgiref.sync import async_to_sync
4
+
5
+ from janus.utils import run_coro_task
6
+ from janus.plugins import Plugin
7
+ from janus.session import WebsocketSession
8
+
9
+
10
+ def get_session(sid=None):
11
+ if sid is None:
12
+ return WebsocketSession()
13
+ return WebsocketSession(session_id=sid)
14
+
15
+
16
+ def setup():
17
+ def _create():
18
+ run_coro_task(get_session().create)
19
+
20
+ thread = threading.Thread(target=_create, daemon=True)
21
+ thread.start()
22
+
23
+ def teardown() -> None:
24
+ async_to_sync(get_session().destroy)()
25
+
26
+
27
+ __all__ = ["Plugin", "get_session", "teardown"]
@@ -0,0 +1,14 @@
1
+ # ----------------------
2
+ # Exceptions
3
+ # ----------------------
4
+
5
+ class PluginManagerError(Exception):
6
+ """Base exception for the plugin manager."""
7
+
8
+
9
+ class PluginAlreadyRegistered(PluginManagerError):
10
+ """Raised when attempting to register a plugin under a plugin_id that's already used."""
11
+
12
+
13
+ class PluginNotRegistered(PluginManagerError, KeyError):
14
+ """Raised when attempting to access or remove a plugin that isn't registered."""
@@ -0,0 +1,423 @@
1
+ # plugin_manager_with_base.py
2
+ """
3
+ Plugin manager implementation that uses an abstract PluginBase.
4
+
5
+ Features
6
+ - PluginBase: an ABC that defines lifecycle hooks (setup/start/stop) and a `plugin_id` property.
7
+ - PluginManager[P]: a MutableMapping registry bounded to PluginBase (P bound to PluginBase).
8
+ - Optional runtime validation (validate_plugin_type) to enforce plugins inherit from PluginBase.
9
+ - Default lifecycle handling: on_register calls plugin.setup() and plugin.start(); on_unregister calls plugin.stop().
10
+ - Thread-safety (thread_safe=True) using RLock.
11
+ - Sync and async lazy registration helpers.
12
+
13
+ Usage: see the example at the bottom of the file.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from collections import OrderedDict
18
+ from collections.abc import MutableMapping
19
+ from typing import (
20
+ TypeVar,
21
+ Generic,
22
+ Iterator,
23
+ Optional,
24
+ Callable,
25
+ Awaitable,
26
+ Any,
27
+ )
28
+ import threading
29
+ import asyncio
30
+ import logging
31
+
32
+ from reactivex import Subject as RxSubject
33
+
34
+ from janus.plugins.base import PluginBase
35
+ from janus.exceptions import PluginAlreadyRegistered, PluginNotRegistered
36
+
37
+
38
+ # ----------------------
39
+ # PluginManager
40
+ # ----------------------
41
+
42
+ P = TypeVar("P", bound=PluginBase)
43
+
44
+
45
+ class PluginManager(Generic[P], MutableMapping):
46
+ """A typed registry for plugins bounded to PluginBase.
47
+
48
+ Parameters
49
+ - thread_safe: if True, operations are protected by an RLock.
50
+ - logger: optional logger to use. If None, the module logger is used.
51
+ - validate_plugin_type: if True, enforce isinstance(plugin, PluginBase) at runtime.
52
+ - on_register: optional callback (plugin_id, plugin) called after registration.
53
+ By default it calls plugin.setup() then plugin.start().
54
+ - on_unregister: optional callback (plugin_id, plugin) called when a plugin is removed.
55
+ By default it calls plugin.stop().
56
+ """
57
+
58
+ def __init__(self, *, thread_safe: bool = False, logger: Optional[logging.Logger] = None,
59
+ validate_plugin_type: bool = False, on_register: Optional[Callable[[str | int, P], None]] = None,
60
+ on_unregister: Optional[Callable[[str | int, P], None]] = None) -> None:
61
+ self._registry: "OrderedDict[str|int, P]" = OrderedDict()
62
+ self._lock: Optional[threading.RLock] = threading.RLock() if thread_safe else None
63
+ self._async_lock: Optional[asyncio.Lock] = None
64
+ self.logger = logger or logging.getLogger(__name__)
65
+ self.validate_plugin_type = validate_plugin_type
66
+
67
+ # default lifecycle handlers
68
+ if on_register is None:
69
+ def _default_on_register(plugin_id: str | int, plugin: P) -> None:
70
+ try:
71
+ plugin.setup()
72
+ except (AttributeError, ValueError):
73
+ self.logger.exception("plugin.setup() raised for %s", plugin_id)
74
+ try:
75
+ plugin.start()
76
+ except (AttributeError, ValueError):
77
+ self.logger.exception("plugin.start() raised for %s", plugin_id)
78
+
79
+ self.on_register = _default_on_register
80
+ else:
81
+ self.on_register = on_register
82
+
83
+ if on_unregister is None:
84
+ def _default_on_unregister(plugin_id: str|int, plugin: P) -> None:
85
+ try:
86
+ plugin.stop()
87
+ except Exception:
88
+ self.logger.exception("plugin.stop() raised for %s", plugin_id)
89
+
90
+ self.on_unregister = _default_on_unregister
91
+ else:
92
+ self.on_unregister = on_unregister
93
+
94
+ # ----------------------
95
+ # internal helpers
96
+ # ----------------------
97
+
98
+ def _acquire(self) -> None:
99
+ if self._lock:
100
+ self._lock.acquire()
101
+
102
+ def _release(self) -> None:
103
+ if self._lock:
104
+ self._lock.release()
105
+
106
+ def _ensure_async_lock(self) -> asyncio.Lock:
107
+ # created lazily to avoid creating asyncio primitives in sync-only apps
108
+ if self._async_lock is None:
109
+ self._async_lock = asyncio.Lock()
110
+ return self._async_lock
111
+
112
+ def _ensure_plugin_type(self, plugin: Any) -> None:
113
+ if self.validate_plugin_type and not isinstance(plugin, PluginBase):
114
+ raise TypeError(f"plugin must inherit from PluginBase; got {type(plugin)!r}")
115
+
116
+ # ----------------------
117
+ # MutableMapping protocol
118
+ # ----------------------
119
+
120
+ def __getitem__(self, key: str) -> P:
121
+ try:
122
+ return self._registry[key]
123
+ except KeyError as exc:
124
+ raise PluginNotRegistered(f"Plugin '{key}' is not registered.") from exc
125
+
126
+ def __setitem__(self, key: str, value: P) -> None:
127
+ self._ensure_plugin_type(value)
128
+ if key in self._registry:
129
+ raise PluginAlreadyRegistered(f"Plugin '{key}' is already registered.")
130
+
131
+ # create per-plugin reactive subject (best-effort)
132
+ try:
133
+ if RxSubject is not None:
134
+ subj = RxSubject()
135
+ setattr(value, "_plugin_rx_base", subj)
136
+ except Exception:
137
+ setattr(value, "_plugin_rx_base", None)
138
+
139
+ # ensure plugin has emitter (PluginBase already sets one)
140
+ self._registry[key] = value
141
+ try:
142
+ self.on_register(key, value)
143
+ except Exception:
144
+ self.logger.exception("on_register callback raised for %s", key)
145
+
146
+ def __delitem__(self, key: str) -> None:
147
+ try:
148
+ plugin = self._registry.pop(key)
149
+ except KeyError as exc:
150
+ raise PluginNotRegistered(f"Plugin '{key}' is not registered.") from exc
151
+
152
+ # complete plugin rx subject (best-effort)
153
+ try:
154
+ subj = getattr(plugin, "_plugin_rx_base", None)
155
+ if subj is not None:
156
+ subj.on_completed()
157
+ except Exception:
158
+ pass
159
+
160
+ try:
161
+ self.on_unregister(key, plugin)
162
+ except Exception:
163
+ self.logger.exception("on_unregister callback raised for %s", key)
164
+
165
+ def __iter__(self) -> Iterator[str|int]:
166
+ return iter(self._registry)
167
+
168
+ def __len__(self) -> int:
169
+ return len(self._registry)
170
+
171
+ def __contains__(self, name: object) -> bool: # type: ignore[override]
172
+ return name in self._registry
173
+
174
+ def __repr__(self) -> str:
175
+ return f"{self.__class__.__name__}(plugins={list(self._registry.keys())})"
176
+
177
+ # ----------------------
178
+ # public API
179
+ # ----------------------
180
+
181
+ def register(self, plugin_id: str | int, plugin: P, *, force: bool = False) -> None:
182
+ self._ensure_plugin_type(plugin)
183
+ self._acquire()
184
+ try:
185
+ if not force and str(plugin_id) in self._registry:
186
+ raise PluginAlreadyRegistered(f"Plugin '{plugin_id}' already exists.")
187
+
188
+ # create per-plugin rx subject if missing (consistent with __setitem__)
189
+ try:
190
+ if getattr(plugin, "_plugin_rx_base", None) is None:
191
+ setattr(plugin, "_plugin_rx_base", RxSubject())
192
+ except Exception:
193
+ pass
194
+
195
+ previous = self._registry.get(plugin_id)
196
+ self._registry[plugin_id] = plugin
197
+ try:
198
+ self.on_register(plugin_id, plugin)
199
+ except Exception:
200
+ self.logger.exception("on_register callback failed for %s", plugin_id)
201
+ finally:
202
+ self._release()
203
+
204
+ def unregister(self, plugin_id: str | int) -> P:
205
+ self._acquire()
206
+ try:
207
+ try:
208
+ plugin = self._registry.pop(plugin_id)
209
+ except KeyError as exc:
210
+ raise PluginNotRegistered(f"Plugin '{plugin_id}' is not registered.") from exc
211
+ # complete plugin rx subject
212
+ try:
213
+ subj = getattr(plugin, "_plugin_rx_base", None)
214
+ if subj is not None:
215
+ subj.on_completed()
216
+ except Exception:
217
+ pass
218
+ try:
219
+ self.on_unregister(plugin_id, plugin)
220
+ except Exception:
221
+ self.logger.exception("on_unregister callback failed for %s", plugin_id)
222
+ return plugin
223
+ finally:
224
+ self._release()
225
+
226
+ def clear(self) -> None:
227
+ self._acquire()
228
+ try:
229
+ if self.on_unregister:
230
+ for n, p in list(self._registry.items()):
231
+ try:
232
+ # complete rx subject before calling on_unregister
233
+ try:
234
+ subj = getattr(p, "_plugin_rx_base", None)
235
+ if subj is not None:
236
+ subj.on_completed()
237
+ except Exception:
238
+ pass
239
+ self.on_unregister(n, p)
240
+ except Exception:
241
+ self.logger.exception("on_unregister callback failed for %s", n)
242
+ self._registry.clear()
243
+ finally:
244
+ self._release()
245
+
246
+ def get(self, plugin_id: str, default: Optional[P] = None) -> Optional[P]:
247
+ """Like dict.get — return plugin if present else default."""
248
+ return self._registry.get(plugin_id, default)
249
+
250
+ def register_if_missing(self, plugin_id: str, factory: Callable[[], P]) -> P:
251
+ self._acquire()
252
+ try:
253
+ if plugin_id in self._registry:
254
+ return self._registry[plugin_id]
255
+ plugin = factory()
256
+ self._ensure_plugin_type(plugin)
257
+ # create plugin rx subject
258
+ try:
259
+ if RxSubject is not None and getattr(plugin, "_plugin_rx_base", None) is None:
260
+ setattr(plugin, "_plugin_rx_base", RxSubject())
261
+ except Exception:
262
+ setattr(plugin, "_plugin_rx_base", None)
263
+ self._registry[plugin_id] = plugin
264
+ try:
265
+ self.on_register(plugin_id, plugin)
266
+ except Exception:
267
+ self.logger.exception("on_register callback failed for %s", plugin_id)
268
+ return plugin
269
+ finally:
270
+ self._release()
271
+
272
+ async def async_register_if_missing(self, handle_id: str, async_factory: Callable[[], Awaitable[P]]) -> P:
273
+ lock = self._ensure_async_lock()
274
+ async with lock:
275
+ if handle_id in self._registry:
276
+ return self._registry[handle_id]
277
+ plugin = await async_factory()
278
+ self._ensure_plugin_type(plugin)
279
+ # create plugin rx subject
280
+ try:
281
+ if RxSubject is not None and getattr(plugin, "_plugin_rx_base", None) is None:
282
+ setattr(plugin, "_plugin_rx_base", RxSubject())
283
+ except Exception:
284
+ setattr(plugin, "_plugin_rx_base", None)
285
+ self._acquire()
286
+ try:
287
+ self._registry[handle_id] = plugin
288
+ finally:
289
+ self._release()
290
+ try:
291
+ self.on_register(handle_id, plugin)
292
+ except Exception:
293
+ self.logger.exception("on_register callback failed for %s", handle_id)
294
+ return plugin
295
+
296
+ def register_or_replace(self, handle_id: str, plugin: P) -> Optional[P]:
297
+ self._ensure_plugin_type(plugin)
298
+ self._acquire()
299
+ try:
300
+ previous = self._registry.get(handle_id)
301
+ # create rx subject for new plugin
302
+ try:
303
+ if RxSubject is not None and getattr(plugin, "_plugin_rx_base", None) is None:
304
+ setattr(plugin, "_plugin_rx_base", RxSubject())
305
+ except Exception:
306
+ setattr(plugin, "_plugin_rx_base", None)
307
+ self._registry[handle_id] = plugin
308
+ try:
309
+ self.on_register(handle_id, plugin)
310
+ except Exception:
311
+ self.logger.exception("on_register callback failed for %s", handle_id)
312
+ return previous
313
+ finally:
314
+ self._release()
315
+
316
+ def as_dict(self) -> dict:
317
+ """Shallow copy as a plain dict preserving insertion order."""
318
+ return dict(self._registry)
319
+
320
+ def dispatch(self, handle_id: str|int, evt) -> None:
321
+ """
322
+ Deliver evt to single plugin (by handle_id). Best-effort:
323
+ - call plugin.on_event (sync or coroutine)
324
+ - push evt to plugin._plugin_rx_base.on_next(...)
325
+ - emit on plugin.emitter.emit("event", evt)
326
+ - if plugin is a bare callable, call it
327
+ """
328
+ plugin = self._registry.get(handle_id)
329
+ if plugin is None:
330
+ raise PluginNotRegistered(f"Plugin '{handle_id}' is not registered.")
331
+
332
+ # 1) on_event
333
+ try:
334
+ handler = getattr(plugin, "on_event", None)
335
+ if callable(handler):
336
+ r = handler(evt)
337
+ if asyncio.iscoroutine(r):
338
+ asyncio.create_task(r)
339
+ except Exception:
340
+ self.logger.exception("plugin on_event raised for %s", handle_id)
341
+
342
+ # 2) reactive subject push
343
+ try:
344
+ subj = getattr(plugin, "_plugin_rx_base", None)
345
+ if subj is not None:
346
+ try:
347
+ subj.on_next(evt)
348
+ except Exception:
349
+ # swallow but log
350
+ self.logger.exception("failed to push to plugin rx subject for %s", handle_id)
351
+ except Exception:
352
+ self.logger.exception("plugin rx dispatch error for %s", handle_id)
353
+
354
+ # 3) emitter
355
+ try:
356
+ em = getattr(plugin, "emitter", None)
357
+ if em is not None:
358
+ try:
359
+ em.emit("event", evt)
360
+ except Exception:
361
+ self.logger.exception("failed to emit to plugin emitter for %s", handle_id)
362
+ except Exception:
363
+ self.logger.exception("plugin emitter dispatch failed for %s", handle_id)
364
+
365
+
366
+ # ----------------------
367
+ # Example usage
368
+ # ----------------------
369
+
370
+ if __name__ == "__main__":
371
+ logging.basicConfig(level=logging.DEBUG)
372
+
373
+ class CachePlugin(PluginBase):
374
+ def __init__(self, name: str, size: int = 128) -> None:
375
+ self._name = name
376
+ self.size = size
377
+ self.started = False
378
+
379
+ @property
380
+ def name(self) -> str:
381
+ return self._name
382
+
383
+ def setup(self) -> None:
384
+ print(f"{self.name}: setup (size={self.size})")
385
+
386
+ def start(self) -> None:
387
+ self.started = True
388
+ print(f"{self.name}: started")
389
+
390
+ def stop(self) -> None:
391
+ self.started = False
392
+ print(f"{self.name}: stopped")
393
+
394
+ pm = PluginManager[PluginBase](thread_safe=True, validate_plugin_type=True)
395
+
396
+ cache = CachePlugin("cache", size=256)
397
+ pm.register("cache", cache)
398
+
399
+ print(pm)
400
+
401
+ # lazy creation example
402
+ def make_logger_plugin() -> CachePlugin:
403
+ return CachePlugin("logger", size=32)
404
+
405
+ logger_plugin = pm.register_if_missing("logger", make_logger_plugin)
406
+ print("logger started?", logger_plugin.started)
407
+
408
+ # unregister
409
+ removed = pm.unregister("cache")
410
+ print("removed", removed)
411
+
412
+ # async lazy registration demo
413
+ async def async_demo():
414
+ async def async_factory() -> CachePlugin:
415
+ await asyncio.sleep(0.01)
416
+ return CachePlugin("remote", size=16)
417
+
418
+ remote = await pm.async_register_if_missing("remote", async_factory)
419
+ print("remote registered:", remote)
420
+
421
+ asyncio.run(async_demo())
422
+
423
+ # End of file
@@ -0,0 +1,4 @@
1
+ from .request import JanusRequest
2
+ from .response import JanusResponse
3
+
4
+ __all__ = ("JanusRequest", "JanusResponse")
@@ -0,0 +1,12 @@
1
+ from typing import Literal, Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Jsep(BaseModel):
6
+ type: Literal["offer", "answer"]
7
+ sdp: str
8
+ trickle: Optional[bool]
9
+
10
+
11
+ class PluginMessageBase(BaseModel):
12
+ request: str
@@ -0,0 +1,95 @@
1
+ from typing import Optional, Literal, Union, Dict, List
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from core.janus.models.base import Jsep
6
+ from core.janus.models.videoroom import VideoRoomRequestBody
7
+ from core.utils import generate_transaction_id
8
+
9
+
10
+ class BaseJanusRequest(BaseModel):
11
+ janus: str
12
+ transaction: Optional[str] = Field(default_factory=generate_transaction_id, alias="transaction")
13
+
14
+
15
+ class CreateSessionRequest(BaseJanusRequest):
16
+ janus: Literal["create"]
17
+
18
+
19
+ class KeepAliveRequest(BaseJanusRequest):
20
+ janus: Literal["keepalive"]
21
+ session_id: int|str
22
+
23
+
24
+ class DestroySessionRequest(BaseJanusRequest):
25
+ janus: Literal["destroy"]
26
+ session_id: int|str
27
+
28
+
29
+ class AttachPluginRequest(BaseJanusRequest):
30
+ janus: Literal["attach"]
31
+ session_id: str|int
32
+ plugin: str|int # e.g., "janus.plugin.videoroom"
33
+
34
+
35
+ class DetachPluginRequest(BaseJanusRequest):
36
+ janus: Literal["detach"]
37
+ session_id: str|int
38
+ handle_id: str|int
39
+
40
+
41
+ PluginRequestBody = Union[
42
+ VideoRoomRequestBody,
43
+ ]
44
+
45
+ class PluginMessageRequest(BaseJanusRequest):
46
+ janus: Literal["message"]
47
+ session_id: str|int
48
+ handle_id: str|int
49
+ body: PluginRequestBody
50
+ jsep: Optional[Jsep] = None
51
+
52
+
53
+ class TrickleCandidate(BaseModel):
54
+ sdpMid: Optional[str] = None
55
+ sdpMLineIndex: Optional[int] = None
56
+ candidate: Union[str, Dict[str, bool]] # could be "completed": true
57
+
58
+
59
+ class TrickleRequest(BaseJanusRequest):
60
+ janus: Literal["trickle"]
61
+ candidate: Optional[TrickleCandidate] = None
62
+ candidates: Optional[List[TrickleCandidate]] = None
63
+
64
+
65
+ class TrickleMessageRequest(TrickleRequest):
66
+ session_id: str
67
+ handle_id: str
68
+
69
+
70
+ class HangupRequest(BaseJanusRequest):
71
+ janus: Literal["hangup"]
72
+ session_id: str
73
+ handle_id: str
74
+
75
+
76
+ class PluginJespMessageRequest(PluginMessageRequest):
77
+ jsep: Jsep
78
+
79
+
80
+ class InfoRequest(BaseJanusRequest):
81
+ janus: Literal["info"]
82
+
83
+
84
+ JanusRequest = Union[
85
+ CreateSessionRequest,
86
+ KeepAliveRequest,
87
+ AttachPluginRequest,
88
+ DetachPluginRequest,
89
+ PluginMessageRequest,
90
+ TrickleMessageRequest,
91
+ HangupRequest,
92
+ PluginJespMessageRequest,
93
+ InfoRequest,
94
+ DestroySessionRequest,
95
+ ]