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.
Files changed (48) hide show
  1. jrtc/__init__.py +40 -0
  2. jrtc/auth.py +60 -0
  3. jrtc/conf/__init__.py +4 -0
  4. jrtc/conf/_config.py +173 -0
  5. jrtc/conf/_janus.py +70 -0
  6. jrtc/conf/settings/__init__.py +45 -0
  7. jrtc/conf/settings/global_settings.py +81 -0
  8. jrtc/core/__init__.py +29 -0
  9. jrtc/core/exceptions.py +73 -0
  10. jrtc/core/logging/__init__.py +19 -0
  11. jrtc/core/logging/_json.py +96 -0
  12. jrtc/core/logging/formatting.py +58 -0
  13. jrtc/core/logging/utils.py +165 -0
  14. jrtc/core/utils.py +29 -0
  15. jrtc/lib/__init__.py +5 -0
  16. jrtc/lib/manager.py +149 -0
  17. jrtc/lib/plugins/__init__.py +0 -0
  18. jrtc/lib/plugins/base.py +446 -0
  19. jrtc/lib/registry.py +149 -0
  20. jrtc/lib/utils.py +20 -0
  21. jrtc/manager.py +297 -0
  22. jrtc/messaging/__init__.py +57 -0
  23. jrtc/messaging/constants.py +53 -0
  24. jrtc/messaging/dispatcher.py +241 -0
  25. jrtc/messaging/engines/__init__.py +9 -0
  26. jrtc/messaging/engines/kafka.py +636 -0
  27. jrtc/messaging/factory.py +117 -0
  28. jrtc/messaging/listeners.py +122 -0
  29. jrtc/messaging/metrics.py +115 -0
  30. jrtc/messaging/publisher.py +544 -0
  31. jrtc/models/__init__.py +4 -0
  32. jrtc/models/base.py +27 -0
  33. jrtc/models/common.py +71 -0
  34. jrtc/models/request.py +156 -0
  35. jrtc/models/response.py +197 -0
  36. jrtc/py.typed +1 -0
  37. jrtc/session/__init__.py +23 -0
  38. jrtc/session/base.py +387 -0
  39. jrtc/session/websocket.py +281 -0
  40. jrtc/transport/__init__.py +22 -0
  41. jrtc/transport/base.py +47 -0
  42. jrtc/transport/http.py +490 -0
  43. jrtc/transport/websocket.py +457 -0
  44. jrtc-3.1.0.dist-info/METADATA +535 -0
  45. jrtc-3.1.0.dist-info/RECORD +48 -0
  46. jrtc-3.1.0.dist-info/WHEEL +5 -0
  47. jrtc-3.1.0.dist-info/licenses/LICENSE +21 -0
  48. 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
@@ -0,0 +1,4 @@
1
+ from jrtc.conf._config import configure, settings
2
+ from jrtc.conf._janus import Janus
3
+
4
+ __all__ = ["Janus", "configure", "settings"]
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
+ ]
@@ -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
+ ]