jrtc 3.1.0__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.
- jrtc/__init__.py +40 -0
- jrtc/auth.py +60 -0
- jrtc/conf/__init__.py +4 -0
- jrtc/conf/_config.py +173 -0
- jrtc/conf/_janus.py +70 -0
- jrtc/conf/settings/__init__.py +45 -0
- jrtc/conf/settings/global_settings.py +81 -0
- jrtc/core/__init__.py +29 -0
- jrtc/core/exceptions.py +73 -0
- jrtc/core/logging/__init__.py +19 -0
- jrtc/core/logging/_json.py +96 -0
- jrtc/core/logging/formatting.py +58 -0
- jrtc/core/logging/utils.py +165 -0
- jrtc/core/utils.py +29 -0
- jrtc/lib/__init__.py +5 -0
- jrtc/lib/manager.py +149 -0
- jrtc/lib/plugins/__init__.py +0 -0
- jrtc/lib/plugins/base.py +446 -0
- jrtc/lib/registry.py +149 -0
- jrtc/lib/utils.py +20 -0
- jrtc/manager.py +297 -0
- jrtc/messaging/__init__.py +57 -0
- jrtc/messaging/constants.py +53 -0
- jrtc/messaging/dispatcher.py +241 -0
- jrtc/messaging/engines/__init__.py +9 -0
- jrtc/messaging/engines/kafka.py +636 -0
- jrtc/messaging/factory.py +117 -0
- jrtc/messaging/listeners.py +122 -0
- jrtc/messaging/metrics.py +115 -0
- jrtc/messaging/publisher.py +544 -0
- jrtc/models/__init__.py +4 -0
- jrtc/models/base.py +27 -0
- jrtc/models/common.py +71 -0
- jrtc/models/request.py +156 -0
- jrtc/models/response.py +197 -0
- jrtc/py.typed +1 -0
- jrtc/session/__init__.py +23 -0
- jrtc/session/base.py +387 -0
- jrtc/session/websocket.py +281 -0
- jrtc/transport/__init__.py +22 -0
- jrtc/transport/base.py +47 -0
- jrtc/transport/http.py +490 -0
- jrtc/transport/websocket.py +457 -0
- jrtc-3.1.0.dist-info/METADATA +535 -0
- jrtc-3.1.0.dist-info/RECORD +48 -0
- jrtc-3.1.0.dist-info/WHEEL +5 -0
- jrtc-3.1.0.dist-info/licenses/LICENSE +21 -0
- jrtc-3.1.0.dist-info/top_level.txt +1 -0
jrtc/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Janus Core: plugin-agnostic Python bindings for Janus Gateway."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from jrtc.auth import JanusCredentialProvider, JanusCredentials
|
|
6
|
+
from jrtc.lib import Plugin
|
|
7
|
+
from jrtc.manager import JanusSessionManager
|
|
8
|
+
from jrtc.messaging import (
|
|
9
|
+
JanusEventPublisher,
|
|
10
|
+
JanusResponseDispatcher,
|
|
11
|
+
LogVistaMetrics,
|
|
12
|
+
create_broker,
|
|
13
|
+
)
|
|
14
|
+
from jrtc.models import JanusRequest, JanusResponse
|
|
15
|
+
from jrtc.session import JanusSession, SessionState, WebsocketSession
|
|
16
|
+
from jrtc.transport import JanusTransport, WebsocketTransportClient
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
__version__ = version("jrtc")
|
|
20
|
+
except PackageNotFoundError: # source checkout
|
|
21
|
+
__version__ = "3.1.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
"JanusCredentialProvider",
|
|
26
|
+
"JanusCredentials",
|
|
27
|
+
"JanusEventPublisher",
|
|
28
|
+
"JanusRequest",
|
|
29
|
+
"JanusResponse",
|
|
30
|
+
"JanusResponseDispatcher",
|
|
31
|
+
"JanusSession",
|
|
32
|
+
"JanusSessionManager",
|
|
33
|
+
"JanusTransport",
|
|
34
|
+
"LogVistaMetrics",
|
|
35
|
+
"Plugin",
|
|
36
|
+
"SessionState",
|
|
37
|
+
"WebsocketSession",
|
|
38
|
+
"WebsocketTransportClient",
|
|
39
|
+
"create_broker",
|
|
40
|
+
)
|
jrtc/auth.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Credential providers for authenticated Janus API sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from jrtc.models.request import BaseJanusRequest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
13
|
+
class JanusCredentials:
|
|
14
|
+
"""Outer-envelope credentials required by an authenticated Janus server.
|
|
15
|
+
|
|
16
|
+
Values are deliberately redacted from ``repr`` and are applied immediately
|
|
17
|
+
before transport serialization. Both mechanisms may be configured by a
|
|
18
|
+
server, although most deployments use one or the other.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
token: str | None = None
|
|
22
|
+
api_secret: str | None = None
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
if self.token is not None and not self.token:
|
|
26
|
+
raise ValueError("token cannot be empty")
|
|
27
|
+
if self.api_secret is not None and not self.api_secret:
|
|
28
|
+
raise ValueError("api_secret cannot be empty")
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return (
|
|
32
|
+
"JanusCredentials("
|
|
33
|
+
f"token={'<redacted>' if self.token else None}, "
|
|
34
|
+
f"api_secret={'<redacted>' if self.api_secret else None})"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def apply(self, request: BaseJanusRequest) -> None:
|
|
38
|
+
if request.token is None:
|
|
39
|
+
request.token = self.token
|
|
40
|
+
if request.apisecret is None:
|
|
41
|
+
request.apisecret = self.api_secret
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@runtime_checkable
|
|
45
|
+
class JanusCredentialProvider(Protocol):
|
|
46
|
+
"""Provider used when credentials need rotation between requests."""
|
|
47
|
+
|
|
48
|
+
def __call__(self) -> JanusCredentials: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
type CredentialSource = JanusCredentials | Callable[[], JanusCredentials] | None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_credentials(source: CredentialSource) -> JanusCredentials | None:
|
|
55
|
+
if source is None or isinstance(source, JanusCredentials):
|
|
56
|
+
return source
|
|
57
|
+
value = source()
|
|
58
|
+
if not isinstance(value, JanusCredentials):
|
|
59
|
+
raise TypeError("credential provider must return JanusCredentials")
|
|
60
|
+
return value
|
jrtc/conf/__init__.py
ADDED
jrtc/conf/_config.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Small, deterministic settings proxy.
|
|
2
|
+
|
|
3
|
+
Applications may point ``JANUS_SETTINGS_MODULE`` at their own typed module.
|
|
4
|
+
Environment parsing belongs in that module, avoiding the old heuristic that
|
|
5
|
+
could turn numeric secrets into integers or comma-containing URLs into lists.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import os
|
|
12
|
+
import threading
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
from typing import Any
|
|
16
|
+
from urllib.parse import SplitResult, urlsplit, urlunsplit
|
|
17
|
+
|
|
18
|
+
_DEFAULT_MODULE = "jrtc.conf.settings"
|
|
19
|
+
_SENSITIVE_PARTS = ("SECRET", "TOKEN", "PASSWORD", "PASS", "CREDENTIAL", "API_KEY", "DSN")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sensitive(name: str) -> bool:
|
|
23
|
+
normalized = name.upper().replace("-", "_")
|
|
24
|
+
return any(part in normalized for part in _SENSITIVE_PARTS)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _redact_url(value: str) -> str:
|
|
28
|
+
try:
|
|
29
|
+
parsed = urlsplit(value)
|
|
30
|
+
if not parsed.scheme or parsed.hostname is None or parsed.username is None:
|
|
31
|
+
return value
|
|
32
|
+
host = parsed.hostname
|
|
33
|
+
if ":" in host and not host.startswith("["):
|
|
34
|
+
host = f"[{host}]"
|
|
35
|
+
if parsed.port is not None:
|
|
36
|
+
host = f"{host}:{parsed.port}"
|
|
37
|
+
return urlunsplit(
|
|
38
|
+
SplitResult(
|
|
39
|
+
parsed.scheme, f"<redacted>@{host}", parsed.path, parsed.query, parsed.fragment
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
except ValueError:
|
|
43
|
+
return "<redacted-url>" if "://" in value and "@" in value else value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _redact_value(value: Any, *, name: str = "") -> Any:
|
|
47
|
+
if _sensitive(name) and value not in (None, ""):
|
|
48
|
+
return "<redacted>"
|
|
49
|
+
if isinstance(value, Mapping):
|
|
50
|
+
return {str(key): _redact_value(item, name=str(key)) for key, item in value.items()}
|
|
51
|
+
if isinstance(value, (list, tuple)):
|
|
52
|
+
return [_redact_value(item) for item in value]
|
|
53
|
+
return _redact_url(value) if isinstance(value, str) else value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SettingsLoadError(RuntimeError):
|
|
57
|
+
"""Raised when a configured settings module cannot be loaded."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Settings:
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._lock = threading.RLock()
|
|
63
|
+
self._module_name = os.getenv("JANUS_SETTINGS_MODULE", _DEFAULT_MODULE)
|
|
64
|
+
self._module: ModuleType | None = None
|
|
65
|
+
self._defaults: dict[str, Any] = {}
|
|
66
|
+
self._overrides: dict[str, Any] = {}
|
|
67
|
+
self._frozen = False
|
|
68
|
+
|
|
69
|
+
def configure(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
module: str | None = None,
|
|
73
|
+
defaults: dict[str, Any] | None = None,
|
|
74
|
+
overrides: dict[str, Any] | None = None,
|
|
75
|
+
freeze: bool = False,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Select a settings module and explicit programmatic values.
|
|
78
|
+
|
|
79
|
+
Precedence is ``overrides > module > defaults``. The operation resets
|
|
80
|
+
prior overrides so test/application lifecycles do not retain stale
|
|
81
|
+
configuration.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
with self._lock:
|
|
85
|
+
if module is not None:
|
|
86
|
+
self._module_name = module
|
|
87
|
+
self._module = None
|
|
88
|
+
self._defaults = dict(defaults or {})
|
|
89
|
+
self._overrides = dict(overrides or {})
|
|
90
|
+
self._frozen = freeze
|
|
91
|
+
|
|
92
|
+
def reload(self) -> None:
|
|
93
|
+
with self._lock:
|
|
94
|
+
self._module = None
|
|
95
|
+
|
|
96
|
+
def _load(self) -> ModuleType:
|
|
97
|
+
with self._lock:
|
|
98
|
+
if self._module is not None:
|
|
99
|
+
return self._module
|
|
100
|
+
try:
|
|
101
|
+
module = importlib.import_module(self._module_name)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
raise SettingsLoadError(
|
|
104
|
+
f"Could not import Janus settings module {self._module_name!r}"
|
|
105
|
+
) from exc
|
|
106
|
+
self._module = module
|
|
107
|
+
return module
|
|
108
|
+
|
|
109
|
+
def __getattr__(self, name: str) -> Any:
|
|
110
|
+
if not name.isupper():
|
|
111
|
+
raise AttributeError(name)
|
|
112
|
+
with self._lock:
|
|
113
|
+
if name in self._overrides:
|
|
114
|
+
return self._overrides[name]
|
|
115
|
+
module = self._load()
|
|
116
|
+
if hasattr(module, name):
|
|
117
|
+
return getattr(module, name)
|
|
118
|
+
if name in self._defaults:
|
|
119
|
+
return self._defaults[name]
|
|
120
|
+
raise AttributeError(f"Janus setting {name!r} is not defined")
|
|
121
|
+
|
|
122
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
123
|
+
if name.startswith("_"):
|
|
124
|
+
object.__setattr__(self, name, value)
|
|
125
|
+
return
|
|
126
|
+
if not name.isupper():
|
|
127
|
+
raise AttributeError("settings names must be uppercase")
|
|
128
|
+
with self._lock:
|
|
129
|
+
if self._frozen:
|
|
130
|
+
raise RuntimeError("settings are frozen")
|
|
131
|
+
self._overrides[name] = value
|
|
132
|
+
|
|
133
|
+
def as_dict(self, *, redact: bool = True) -> dict[str, Any]:
|
|
134
|
+
module = self._load()
|
|
135
|
+
values = {name: value for name, value in vars(module).items() if name.isupper()}
|
|
136
|
+
values = {**self._defaults, **values, **self._overrides}
|
|
137
|
+
if redact:
|
|
138
|
+
values = {name: _redact_value(value, name=name) for name, value in values.items()}
|
|
139
|
+
return values
|
|
140
|
+
|
|
141
|
+
def inspect_settings(self) -> dict[str, dict[str, Any]]:
|
|
142
|
+
module = self._load()
|
|
143
|
+
keys = set(self._defaults) | set(self._overrides)
|
|
144
|
+
keys.update(name for name in vars(module) if name.isupper())
|
|
145
|
+
result: dict[str, dict[str, Any]] = {}
|
|
146
|
+
for name in sorted(keys):
|
|
147
|
+
if name in self._overrides:
|
|
148
|
+
source = "override"
|
|
149
|
+
elif hasattr(module, name):
|
|
150
|
+
source = "module"
|
|
151
|
+
else:
|
|
152
|
+
source = "default"
|
|
153
|
+
value = _redact_value(getattr(self, name), name=name)
|
|
154
|
+
result[name] = {"value": value, "source": source}
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
settings = Settings()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def configure(
|
|
162
|
+
*,
|
|
163
|
+
module: str | None = None,
|
|
164
|
+
defaults: dict[str, Any] | None = None,
|
|
165
|
+
overrides: dict[str, Any] | None = None,
|
|
166
|
+
freeze: bool = False,
|
|
167
|
+
) -> None:
|
|
168
|
+
settings.configure(
|
|
169
|
+
module=module,
|
|
170
|
+
defaults=defaults,
|
|
171
|
+
overrides=overrides,
|
|
172
|
+
freeze=freeze,
|
|
173
|
+
)
|
jrtc/conf/_janus.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Explicit application-level access to an installed session manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Janus:
|
|
9
|
+
"""Compatibility accessor populated explicitly by a host application.
|
|
10
|
+
|
|
11
|
+
The manager lifecycle remains explicit; importing this module starts no
|
|
12
|
+
thread, event loop, network connection, or background task.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
_manager: Any | None = None
|
|
16
|
+
_manager_owner: object | None = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def set_manager(cls, manager: Any | None) -> None:
|
|
20
|
+
"""Install a manager for compatibility with single-app hosts.
|
|
21
|
+
|
|
22
|
+
Multi-lifecycle hosts should use :meth:`install_manager`, which prevents
|
|
23
|
+
one owner from clearing another owner's manager.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if manager is not None and cls._manager is not None and cls._manager is not manager:
|
|
27
|
+
raise RuntimeError("a Janus session manager is already installed in this process")
|
|
28
|
+
cls._manager = manager
|
|
29
|
+
cls._manager_owner = None
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def install_manager(cls, manager: Any) -> object:
|
|
33
|
+
"""Install one process-global compatibility manager and return its lease."""
|
|
34
|
+
|
|
35
|
+
if cls._manager is not None and cls._manager is not manager:
|
|
36
|
+
raise RuntimeError("a Janus session manager is already installed in this process")
|
|
37
|
+
owner = object()
|
|
38
|
+
cls._manager = manager
|
|
39
|
+
cls._manager_owner = owner
|
|
40
|
+
return owner
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def remove_manager(cls, owner: object) -> None:
|
|
44
|
+
"""Remove a manager only when ``owner`` holds the current lease."""
|
|
45
|
+
|
|
46
|
+
if cls._manager_owner is owner:
|
|
47
|
+
cls._manager = None
|
|
48
|
+
cls._manager_owner = None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def get_manager(cls) -> Any | None:
|
|
52
|
+
return cls._manager
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_session(cls, key: str | int | None = None) -> Any | None:
|
|
56
|
+
manager = cls._manager
|
|
57
|
+
return None if manager is None else manager.get_session(key)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
async def setup(cls) -> None:
|
|
61
|
+
if cls._manager is None:
|
|
62
|
+
raise RuntimeError("configure a JanusSessionManager before setup()")
|
|
63
|
+
await cls._manager.start()
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
async def teardown(cls) -> None:
|
|
67
|
+
manager, cls._manager = cls._manager, None
|
|
68
|
+
cls._manager_owner = None
|
|
69
|
+
if manager is not None:
|
|
70
|
+
await manager.stop()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Default environment-backed Janus Core settings."""
|
|
2
|
+
|
|
3
|
+
from jrtc.conf.settings.global_settings import (
|
|
4
|
+
DEBUG,
|
|
5
|
+
jrtc_SECRET,
|
|
6
|
+
JANUS_BROKER_ADMISSION_TIMEOUT,
|
|
7
|
+
JANUS_BROKER_DRAIN_TIMEOUT,
|
|
8
|
+
JANUS_BROKER_ENGINE,
|
|
9
|
+
JANUS_BROKER_ENGINE_OPTIONS,
|
|
10
|
+
JANUS_BROKER_OPTIONS,
|
|
11
|
+
JANUS_BROKER_PUBLISH_TIMEOUT,
|
|
12
|
+
JANUS_BROKER_PUBLISH_WORKERS,
|
|
13
|
+
JANUS_BROKER_QUEUE_CAPACITY,
|
|
14
|
+
JANUS_BROKER_ROUTE,
|
|
15
|
+
JANUS_DETACH_CONCURRENCY,
|
|
16
|
+
JANUS_KEEPALIVE_FAILURES,
|
|
17
|
+
JANUS_KEEPALIVE_INTERVAL,
|
|
18
|
+
JANUS_REQUEST_TIMEOUT,
|
|
19
|
+
JANUS_SESSION_POOL_SIZE,
|
|
20
|
+
JANUS_SESSION_URL,
|
|
21
|
+
JANUS_SHUTDOWN_TIMEOUT,
|
|
22
|
+
JANUS_TOKEN,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"DEBUG",
|
|
27
|
+
"jrtc_SECRET",
|
|
28
|
+
"JANUS_BROKER_ADMISSION_TIMEOUT",
|
|
29
|
+
"JANUS_BROKER_DRAIN_TIMEOUT",
|
|
30
|
+
"JANUS_BROKER_ENGINE",
|
|
31
|
+
"JANUS_BROKER_ENGINE_OPTIONS",
|
|
32
|
+
"JANUS_BROKER_OPTIONS",
|
|
33
|
+
"JANUS_BROKER_PUBLISH_TIMEOUT",
|
|
34
|
+
"JANUS_BROKER_PUBLISH_WORKERS",
|
|
35
|
+
"JANUS_BROKER_QUEUE_CAPACITY",
|
|
36
|
+
"JANUS_BROKER_ROUTE",
|
|
37
|
+
"JANUS_DETACH_CONCURRENCY",
|
|
38
|
+
"JANUS_KEEPALIVE_FAILURES",
|
|
39
|
+
"JANUS_KEEPALIVE_INTERVAL",
|
|
40
|
+
"JANUS_REQUEST_TIMEOUT",
|
|
41
|
+
"JANUS_SESSION_POOL_SIZE",
|
|
42
|
+
"JANUS_SESSION_URL",
|
|
43
|
+
"JANUS_SHUTDOWN_TIMEOUT",
|
|
44
|
+
"JANUS_TOKEN",
|
|
45
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Secure environment-backed defaults for the Janus client core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _boolean(name: str, default: bool = False) -> bool:
|
|
12
|
+
value = os.getenv(name)
|
|
13
|
+
if value is None:
|
|
14
|
+
return default
|
|
15
|
+
normalized = value.strip().lower()
|
|
16
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
17
|
+
return True
|
|
18
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
19
|
+
return False
|
|
20
|
+
raise ValueError(f"{name} must be a boolean value")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _integer(name: str, default: int, *, minimum: int = 0) -> int:
|
|
24
|
+
value = int(os.getenv(name, str(default)))
|
|
25
|
+
if value < minimum:
|
|
26
|
+
raise ValueError(f"{name} must be at least {minimum}")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _number(name: str, default: float, *, minimum: float = 0.0) -> float:
|
|
31
|
+
value = float(os.getenv(name, str(default)))
|
|
32
|
+
if not math.isfinite(value) or value < minimum:
|
|
33
|
+
raise ValueError(f"{name} must be finite and at least {minimum}")
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _optional(name: str) -> str | None:
|
|
38
|
+
value = os.getenv(name)
|
|
39
|
+
return value if value else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _json_object(name: str) -> dict[str, Any]:
|
|
43
|
+
raw = os.getenv(name, "{}")
|
|
44
|
+
try:
|
|
45
|
+
value = json.loads(raw)
|
|
46
|
+
except json.JSONDecodeError as exc:
|
|
47
|
+
raise ValueError(f"{name} must contain valid JSON") from exc
|
|
48
|
+
if not isinstance(value, dict):
|
|
49
|
+
raise ValueError(f"{name} must contain a JSON object")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
DEBUG = _boolean("JANUS_DEBUG", False)
|
|
54
|
+
|
|
55
|
+
# Client runtime
|
|
56
|
+
JANUS_SESSION_URL = os.getenv("JANUS_SESSION_URL", "ws://localhost:8188/janus")
|
|
57
|
+
JANUS_REQUEST_TIMEOUT = _number("JANUS_REQUEST_TIMEOUT", 15.0, minimum=0.001)
|
|
58
|
+
JANUS_SESSION_POOL_SIZE = _integer("JANUS_SESSION_POOL_SIZE", 1, minimum=1)
|
|
59
|
+
JANUS_KEEPALIVE_INTERVAL = _number("JANUS_KEEPALIVE_INTERVAL", 25.0, minimum=0.001)
|
|
60
|
+
JANUS_KEEPALIVE_FAILURES = _integer("JANUS_KEEPALIVE_FAILURES", 3, minimum=1)
|
|
61
|
+
JANUS_SHUTDOWN_TIMEOUT = _number("JANUS_SHUTDOWN_TIMEOUT", 10.0, minimum=0.001)
|
|
62
|
+
JANUS_DETACH_CONCURRENCY = _integer("JANUS_DETACH_CONCURRENCY", 16, minimum=1)
|
|
63
|
+
JANUS_TOKEN = _optional("JANUS_TOKEN")
|
|
64
|
+
jrtc_SECRET = _optional("jrtc_SECRET")
|
|
65
|
+
|
|
66
|
+
# Transport-originated WebRTC events. All logical ``janus.*`` event types are
|
|
67
|
+
# mapped to one portable physical destination so Redis Streams, RabbitMQ and
|
|
68
|
+
# Kafka subscribers share the same contract.
|
|
69
|
+
JANUS_BROKER_ENGINE = os.getenv("JANUS_BROKER_ENGINE", "memory").strip().lower()
|
|
70
|
+
if JANUS_BROKER_ENGINE not in {"memory", "local", "redis", "rabbitmq", "kafka"}:
|
|
71
|
+
raise ValueError("JANUS_BROKER_ENGINE is invalid")
|
|
72
|
+
JANUS_BROKER_ROUTE = os.getenv("JANUS_BROKER_ROUTE", "janus.events").strip()
|
|
73
|
+
if not JANUS_BROKER_ROUTE or any(character.isspace() for character in JANUS_BROKER_ROUTE):
|
|
74
|
+
raise ValueError("JANUS_BROKER_ROUTE must be a non-empty route without whitespace")
|
|
75
|
+
JANUS_BROKER_ENGINE_OPTIONS = _json_object("JANUS_BROKER_ENGINE_OPTIONS")
|
|
76
|
+
JANUS_BROKER_OPTIONS = _json_object("JANUS_BROKER_OPTIONS")
|
|
77
|
+
JANUS_BROKER_PUBLISH_WORKERS = _integer("JANUS_BROKER_PUBLISH_WORKERS", 4, minimum=1)
|
|
78
|
+
JANUS_BROKER_QUEUE_CAPACITY = _integer("JANUS_BROKER_QUEUE_CAPACITY", 4096, minimum=1)
|
|
79
|
+
JANUS_BROKER_ADMISSION_TIMEOUT = _number("JANUS_BROKER_ADMISSION_TIMEOUT", 0.05, minimum=0.001)
|
|
80
|
+
JANUS_BROKER_PUBLISH_TIMEOUT = _number("JANUS_BROKER_PUBLISH_TIMEOUT", 5.0, minimum=0.001)
|
|
81
|
+
JANUS_BROKER_DRAIN_TIMEOUT = _number("JANUS_BROKER_DRAIN_TIMEOUT", 10.0, minimum=0.001)
|
jrtc/core/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Plugin-agnostic Janus Core exceptions."""
|
|
2
|
+
|
|
3
|
+
from jrtc.core.exceptions import (
|
|
4
|
+
JanusConfigurationError,
|
|
5
|
+
JanusConnectionClosed,
|
|
6
|
+
JanusErrorResponse,
|
|
7
|
+
JanusException,
|
|
8
|
+
JanusProtocolError,
|
|
9
|
+
JanusRequestTimeout,
|
|
10
|
+
JanusTransportError,
|
|
11
|
+
PluginAlreadyRegistered,
|
|
12
|
+
PluginLoadError,
|
|
13
|
+
PluginManagerError,
|
|
14
|
+
PluginNotRegistered,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"JanusConfigurationError",
|
|
19
|
+
"JanusConnectionClosed",
|
|
20
|
+
"JanusErrorResponse",
|
|
21
|
+
"JanusException",
|
|
22
|
+
"JanusProtocolError",
|
|
23
|
+
"JanusRequestTimeout",
|
|
24
|
+
"JanusTransportError",
|
|
25
|
+
"PluginAlreadyRegistered",
|
|
26
|
+
"PluginLoadError",
|
|
27
|
+
"PluginManagerError",
|
|
28
|
+
"PluginNotRegistered",
|
|
29
|
+
]
|
jrtc/core/exceptions.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Exception hierarchy for the Janus client runtime.
|
|
2
|
+
|
|
3
|
+
The exceptions in this module are intentionally plugin agnostic. Named plugin
|
|
4
|
+
packages should translate their own ``error_code`` payloads into package-local
|
|
5
|
+
exceptions while preserving :class:`JanusErrorResponse` as the cause.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JanusException(Exception):
|
|
14
|
+
"""Base class for errors raised by the toolkit."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JanusConfigurationError(JanusException):
|
|
18
|
+
"""Raised when required runtime configuration is invalid or missing."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JanusProtocolError(JanusException):
|
|
22
|
+
"""Raised when a peer sends a malformed Janus protocol message."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JanusTransportError(JanusException):
|
|
26
|
+
"""Raised when a transport cannot send, receive, or maintain a connection."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JanusConnectionClosed(JanusTransportError):
|
|
30
|
+
"""Raised when an operation is interrupted because its transport closed."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JanusRequestTimeout(JanusTransportError, TimeoutError):
|
|
34
|
+
"""Raised when a Janus transaction does not complete before its deadline."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, transaction: str, timeout: float) -> None:
|
|
37
|
+
self.transaction = transaction
|
|
38
|
+
self.timeout = timeout
|
|
39
|
+
super().__init__(f"Janus transaction {transaction!r} timed out after {timeout:g}s")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class JanusErrorResponse(JanusException):
|
|
43
|
+
"""A structured ``janus: error`` response returned by the gateway."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
code: int,
|
|
48
|
+
reason: str,
|
|
49
|
+
*,
|
|
50
|
+
transaction: str | None = None,
|
|
51
|
+
response: Any | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self.code = code
|
|
54
|
+
self.reason = reason
|
|
55
|
+
self.transaction = transaction
|
|
56
|
+
self.response = response
|
|
57
|
+
super().__init__(f"Janus error {code}: {reason}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PluginManagerError(JanusException):
|
|
61
|
+
"""Base class for plugin registration and handle ownership errors."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PluginAlreadyRegistered(PluginManagerError):
|
|
65
|
+
"""Raised when a handle or plugin identifier is registered twice."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PluginNotRegistered(PluginManagerError, KeyError):
|
|
69
|
+
"""Raised when a plugin identifier or handle cannot be resolved."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PluginLoadError(PluginManagerError):
|
|
73
|
+
"""Raised when an installed plugin entry point cannot be imported safely."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Opt-in logging utilities; importing Janus Core never configures logging."""
|
|
2
|
+
|
|
3
|
+
from jrtc.core.logging._json import JsonFormatter
|
|
4
|
+
from jrtc.core.logging.formatting import ColoredFormatter
|
|
5
|
+
from jrtc.core.logging.utils import (
|
|
6
|
+
get_colored_stream_handler,
|
|
7
|
+
get_json_file_handler,
|
|
8
|
+
get_plain_file_handler,
|
|
9
|
+
install_colored_logging,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ColoredFormatter",
|
|
14
|
+
"JsonFormatter",
|
|
15
|
+
"get_colored_stream_handler",
|
|
16
|
+
"get_json_file_handler",
|
|
17
|
+
"get_plain_file_handler",
|
|
18
|
+
"install_colored_logging",
|
|
19
|
+
]
|