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.
- janus_api-0.1.1/PKG-INFO +17 -0
- janus_api-0.1.1/README.md +0 -0
- janus_api-0.1.1/pyproject.toml +30 -0
- janus_api-0.1.1/src/janus_api/__init__.py +27 -0
- janus_api-0.1.1/src/janus_api/exceptions.py +14 -0
- janus_api-0.1.1/src/janus_api/manager.py +423 -0
- janus_api-0.1.1/src/janus_api/models/__init__.py +4 -0
- janus_api-0.1.1/src/janus_api/models/base.py +12 -0
- janus_api-0.1.1/src/janus_api/models/request.py +95 -0
- janus_api-0.1.1/src/janus_api/models/response.py +135 -0
- janus_api-0.1.1/src/janus_api/models/videoroom/__init__.py +4 -0
- janus_api-0.1.1/src/janus_api/models/videoroom/fields.py +94 -0
- janus_api-0.1.1/src/janus_api/models/videoroom/request.py +224 -0
- janus_api-0.1.1/src/janus_api/models/videoroom/response.py +278 -0
- janus_api-0.1.1/src/janus_api/plugins/__init__.py +12 -0
- janus_api-0.1.1/src/janus_api/plugins/base.py +285 -0
- janus_api-0.1.1/src/janus_api/plugins/textroom.py +14 -0
- janus_api-0.1.1/src/janus_api/plugins/videoroom.py +201 -0
- janus_api-0.1.1/src/janus_api/session/__init__.py +4 -0
- janus_api-0.1.1/src/janus_api/session/base.py +245 -0
- janus_api-0.1.1/src/janus_api/session/websocket.py +61 -0
- janus_api-0.1.1/src/janus_api/transport/__init__.py +0 -0
- janus_api-0.1.1/src/janus_api/transport/websocket.py +606 -0
- janus_api-0.1.1/src/janus_api/types/__init__.py +0 -0
- janus_api-0.1.1/src/janus_api/utils.py +22 -0
janus_api-0.1.1/PKG-INFO
ADDED
|
@@ -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,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
|
+
]
|