kryten-robot 0.6.9__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.
- kryten/CONFIG.md +504 -0
- kryten/__init__.py +127 -0
- kryten/__main__.py +882 -0
- kryten/application_state.py +98 -0
- kryten/audit_logger.py +237 -0
- kryten/command_subscriber.py +341 -0
- kryten/config.example.json +35 -0
- kryten/config.py +510 -0
- kryten/connection_watchdog.py +209 -0
- kryten/correlation.py +241 -0
- kryten/cytube_connector.py +754 -0
- kryten/cytube_event_sender.py +1476 -0
- kryten/errors.py +161 -0
- kryten/event_publisher.py +416 -0
- kryten/health_monitor.py +482 -0
- kryten/lifecycle_events.py +274 -0
- kryten/logging_config.py +314 -0
- kryten/nats_client.py +468 -0
- kryten/raw_event.py +165 -0
- kryten/service_registry.py +371 -0
- kryten/shutdown_handler.py +383 -0
- kryten/socket_io.py +903 -0
- kryten/state_manager.py +711 -0
- kryten/state_query_handler.py +698 -0
- kryten/state_updater.py +314 -0
- kryten/stats_tracker.py +108 -0
- kryten/subject_builder.py +330 -0
- kryten_robot-0.6.9.dist-info/METADATA +469 -0
- kryten_robot-0.6.9.dist-info/RECORD +32 -0
- kryten_robot-0.6.9.dist-info/WHEEL +4 -0
- kryten_robot-0.6.9.dist-info/entry_points.txt +3 -0
- kryten_robot-0.6.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""NATS Subject Builder for CyTube Events.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for constructing and parsing hierarchical NATS
|
|
4
|
+
subject strings following the format: kryten.events.cytube.{channel}.{event_name}
|
|
5
|
+
|
|
6
|
+
Subject Format
|
|
7
|
+
--------------
|
|
8
|
+
kryten.events.cytube.{channel}.{event_name}
|
|
9
|
+
^ ^ ^ ^
|
|
10
|
+
| | | +-- Event type (e.g., chatmsg)
|
|
11
|
+
| | +------------ Channel name (e.g., 420grindhouse)
|
|
12
|
+
| +--------------------- Platform literal (always "cytube")
|
|
13
|
+
+----------------------------- Namespace (always "kryten")
|
|
14
|
+
|
|
15
|
+
All tokens are normalized: lowercase, dots removed, spaces to hyphens.
|
|
16
|
+
This ensures cy.tube, Cy.tube, cytu.be all normalize to "cytube".
|
|
17
|
+
Channels like "420Grindhouse" normalize to "420grindhouse".
|
|
18
|
+
|
|
19
|
+
Wildcard Subscriptions
|
|
20
|
+
----------------------
|
|
21
|
+
NATS supports wildcard subscriptions for flexible filtering:
|
|
22
|
+
|
|
23
|
+
- Single level wildcard (*):
|
|
24
|
+
kryten.events.cytube.*.chatmsg # All channels, chatmsg events
|
|
25
|
+
|
|
26
|
+
- Multi-level wildcard (>):
|
|
27
|
+
kryten.events.cytube.420grindhouse.> # All events from 420grindhouse channel
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> from .raw_event import RawEvent
|
|
32
|
+
>>> from .subject_builder import build_subject, build_event_subject
|
|
33
|
+
>>>
|
|
34
|
+
>>> # Build subject from components
|
|
35
|
+
>>> subject = build_subject("cytu.be", "420Grindhouse", "chatMsg")
|
|
36
|
+
>>> print(subject)
|
|
37
|
+
'kryten.events.cytube.420grindhouse.chatmsg'
|
|
38
|
+
>>>
|
|
39
|
+
>>> # Build subject from RawEvent
|
|
40
|
+
>>> event = RawEvent("chatMsg", {"user": "bob"}, "420Grindhouse", "cytu.be")
|
|
41
|
+
>>> subject = build_event_subject(event)
|
|
42
|
+
>>>
|
|
43
|
+
>>> # Parse subject back to components
|
|
44
|
+
>>> from .subject_builder import parse_subject
|
|
45
|
+
>>> components = parse_subject("kryten.events.cytube.420grindhouse.chatmsg")
|
|
46
|
+
>>> print(components['channel'])
|
|
47
|
+
'420grindhouse'
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from .raw_event import RawEvent
|
|
51
|
+
|
|
52
|
+
SUBJECT_PREFIX = "kryten.events"
|
|
53
|
+
"""NATS subject prefix for all CyTube events."""
|
|
54
|
+
|
|
55
|
+
COMMAND_PREFIX = "kryten.commands"
|
|
56
|
+
"""NATS subject prefix for all CyTube commands."""
|
|
57
|
+
|
|
58
|
+
MAX_TOKEN_LENGTH = 100
|
|
59
|
+
"""Maximum length for individual subject tokens to prevent exceeding NATS limits."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize_token(token: str) -> str:
|
|
63
|
+
"""Normalize token for consistent NATS subject matching.
|
|
64
|
+
|
|
65
|
+
Aggressively normalizes domains and channels to ensure consistent matching:
|
|
66
|
+
- Converts to lowercase
|
|
67
|
+
- Removes ALL dots (cy.tube -> cytube, cytu.be -> cytube)
|
|
68
|
+
- Replaces spaces with hyphens
|
|
69
|
+
- Removes special characters
|
|
70
|
+
|
|
71
|
+
This ensures that variations like "cy.tube", "Cy.tube", "cytu.be" all
|
|
72
|
+
normalize to the same subject token, making NATS routing reliable.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
token: Raw token string to normalize.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Normalized token suitable for NATS subject.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
>>> normalize_token("cy.tube")
|
|
82
|
+
'cytube'
|
|
83
|
+
>>> normalize_token("Cytu.be")
|
|
84
|
+
'cytube'
|
|
85
|
+
>>> normalize_token("420Grindhouse")
|
|
86
|
+
'420grindhouse'
|
|
87
|
+
>>> normalize_token("My Channel!")
|
|
88
|
+
'my-channel'
|
|
89
|
+
"""
|
|
90
|
+
if not token:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
# Convert to lowercase first
|
|
94
|
+
token = token.lower()
|
|
95
|
+
|
|
96
|
+
# Remove ALL dots (critical for domain normalization)
|
|
97
|
+
token = token.replace(".", "")
|
|
98
|
+
|
|
99
|
+
# Replace spaces with hyphens
|
|
100
|
+
token = token.replace(" ", "-")
|
|
101
|
+
|
|
102
|
+
# Remove NATS wildcard characters
|
|
103
|
+
token = token.replace("*", "").replace(">", "")
|
|
104
|
+
|
|
105
|
+
# Remove invalid characters for NATS subjects
|
|
106
|
+
# Keep: alphanumeric (ASCII + Unicode), hyphens, underscores only
|
|
107
|
+
# Remove: all other special chars
|
|
108
|
+
invalid_chars = "!@#$%^&*()+=[]{|}\\:;\"'<>,?/"
|
|
109
|
+
for char in invalid_chars:
|
|
110
|
+
token = token.replace(char, "")
|
|
111
|
+
|
|
112
|
+
# Truncate to prevent exceeding NATS subject length limit
|
|
113
|
+
if len(token) > MAX_TOKEN_LENGTH:
|
|
114
|
+
token = token[:MAX_TOKEN_LENGTH]
|
|
115
|
+
|
|
116
|
+
return token
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def sanitize_token(token: str) -> str:
|
|
120
|
+
"""Legacy alias for normalize_token. Use normalize_token instead.
|
|
121
|
+
|
|
122
|
+
Deprecated: This function exists for backward compatibility only.
|
|
123
|
+
Use normalize_token() for new code.
|
|
124
|
+
"""
|
|
125
|
+
return normalize_token(token)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_subject(domain: str, channel: str, event_name: str) -> str:
|
|
129
|
+
"""Build NATS subject from event components.
|
|
130
|
+
|
|
131
|
+
Constructs hierarchical subject following the format:
|
|
132
|
+
kryten.events.cytube.{channel}.{event_name}
|
|
133
|
+
|
|
134
|
+
All components are aggressively normalized (lowercase, dots removed).
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
domain: CyTube server domain (e.g., "cytu.be", "cy.tube").
|
|
138
|
+
channel: Channel name (e.g., "420Grindhouse").
|
|
139
|
+
event_name: Socket.IO event name (e.g., "chatMsg").
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Formatted NATS subject string.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If any component is empty after normalization.
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
>>> build_subject("cytu.be", "lounge", "chatMsg")
|
|
149
|
+
'kryten.events.cytube.lounge.chatmsg'
|
|
150
|
+
>>> build_subject("cy.tube", "420Grindhouse", "chatMsg")
|
|
151
|
+
'kryten.events.cytube.420grindhouse.chatmsg'
|
|
152
|
+
>>> build_subject("CYTU.BE", "Test Channel", "userJoin")
|
|
153
|
+
'kryten.events.cytube.test-channel.userjoin'
|
|
154
|
+
"""
|
|
155
|
+
# Normalize all components (domain dots removed, everything lowercase)
|
|
156
|
+
normalize_token(domain)
|
|
157
|
+
channel_clean = normalize_token(channel)
|
|
158
|
+
event_clean = normalize_token(event_name)
|
|
159
|
+
|
|
160
|
+
# Validate components are not empty
|
|
161
|
+
if not channel_clean:
|
|
162
|
+
raise ValueError("Channel cannot be empty after normalization")
|
|
163
|
+
if not event_clean:
|
|
164
|
+
raise ValueError("Event name cannot be empty after normalization")
|
|
165
|
+
|
|
166
|
+
# Build subject with "cytube" as literal platform name (domain normalized out)
|
|
167
|
+
subject = f"{SUBJECT_PREFIX}.cytube.{channel_clean}.{event_clean}"
|
|
168
|
+
|
|
169
|
+
# Final validation
|
|
170
|
+
if len(subject) > 255:
|
|
171
|
+
raise ValueError(f"Subject exceeds NATS limit of 255 characters: {len(subject)}")
|
|
172
|
+
|
|
173
|
+
return subject
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def build_event_subject(event: RawEvent) -> str:
|
|
177
|
+
"""Build NATS subject from RawEvent.
|
|
178
|
+
|
|
179
|
+
Convenience function that extracts domain, channel, and event_name from
|
|
180
|
+
a RawEvent instance and builds the subject string.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
event: RawEvent instance with domain, channel, and event_name fields.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Formatted NATS subject string.
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
>>> from kryten import RawEvent
|
|
190
|
+
>>> event = RawEvent(event_name="chatMsg", payload={}, channel="lounge", domain="cytu.be")
|
|
191
|
+
>>> build_event_subject(event)
|
|
192
|
+
'cytube.events.cytu.be.lounge.chatmsg'
|
|
193
|
+
"""
|
|
194
|
+
return build_subject(event.domain, event.channel, event.event_name)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def build_command_subject(domain: str, channel: str, action: str) -> str:
|
|
198
|
+
"""Build NATS subject for commands.
|
|
199
|
+
|
|
200
|
+
Constructs hierarchical subject following the format:
|
|
201
|
+
kryten.commands.cytube.{channel}.{action}
|
|
202
|
+
|
|
203
|
+
All components are aggressively normalized (lowercase, dots removed).
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
domain: Domain name (e.g., "cytu.be", "cy.tube").
|
|
207
|
+
channel: Channel name (e.g., "420Grindhouse").
|
|
208
|
+
action: Command action (e.g., "chat", "queue").
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Formatted NATS subject string.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: If any component is empty after normalization.
|
|
215
|
+
|
|
216
|
+
Examples:
|
|
217
|
+
>>> build_command_subject("cytu.be", "lounge", "chat")
|
|
218
|
+
'kryten.commands.cytube.lounge.chat'
|
|
219
|
+
>>> build_command_subject("cy.tube", "420Grindhouse", "chat")
|
|
220
|
+
'kryten.commands.cytube.420grindhouse.chat'
|
|
221
|
+
>>> build_command_subject("Cytu.be", "Test Channel", "queue")
|
|
222
|
+
'kryten.commands.cytube.test-channel.queue'
|
|
223
|
+
"""
|
|
224
|
+
# Normalize all components (domain dots removed, everything lowercase)
|
|
225
|
+
normalize_token(domain)
|
|
226
|
+
channel_clean = normalize_token(channel)
|
|
227
|
+
action_clean = normalize_token(action)
|
|
228
|
+
|
|
229
|
+
# Validate components are not empty
|
|
230
|
+
if not channel_clean:
|
|
231
|
+
raise ValueError("Channel cannot be empty after normalization")
|
|
232
|
+
if not action_clean:
|
|
233
|
+
raise ValueError("Action cannot be empty after normalization")
|
|
234
|
+
|
|
235
|
+
# Build subject with "cytube" as literal platform name (domain normalized out)
|
|
236
|
+
subject = f"{COMMAND_PREFIX}.cytube.{channel_clean}.{action_clean}"
|
|
237
|
+
|
|
238
|
+
# Final validation
|
|
239
|
+
if len(subject) > 255:
|
|
240
|
+
raise ValueError(f"Subject exceeds NATS limit of 255 characters: {len(subject)}")
|
|
241
|
+
|
|
242
|
+
return subject
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def parse_subject(subject: str) -> dict[str, str]:
|
|
246
|
+
"""Parse NATS subject into components.
|
|
247
|
+
|
|
248
|
+
Extracts prefix, domain, channel, and event_name from a hierarchical
|
|
249
|
+
subject string. Expected format: cytube.events.{domain}.{channel}.{event}
|
|
250
|
+
|
|
251
|
+
Domain may contain dots (e.g., cytu.be). Uses TLD detection heuristic.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
subject: NATS subject string to parse.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Dictionary with keys: prefix, domain, channel, event_name.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValueError: If subject format is invalid or missing required components.
|
|
261
|
+
|
|
262
|
+
Examples:
|
|
263
|
+
>>> components = parse_subject("cytube.events.cytu.be.lounge.chatMsg")
|
|
264
|
+
>>> components['domain']
|
|
265
|
+
'cytu.be'
|
|
266
|
+
>>> components['channel']
|
|
267
|
+
'lounge'
|
|
268
|
+
>>> components['event_name']
|
|
269
|
+
'chatMsg'
|
|
270
|
+
"""
|
|
271
|
+
if not subject:
|
|
272
|
+
raise ValueError("Subject cannot be empty")
|
|
273
|
+
|
|
274
|
+
# Check prefix first
|
|
275
|
+
if not subject.startswith(SUBJECT_PREFIX + "."):
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f"Invalid subject prefix: expected '{SUBJECT_PREFIX}.', " f"got '{subject[:20]}...'"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Remove prefix to get remaining components
|
|
281
|
+
remaining = subject[len(SUBJECT_PREFIX) + 1 :] # +1 for the dot
|
|
282
|
+
|
|
283
|
+
# Split remaining part
|
|
284
|
+
tokens = remaining.split(".")
|
|
285
|
+
|
|
286
|
+
if len(tokens) < 3:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"Invalid subject format: expected 'cytube.events.{{domain}}.{{channel}}.{{event}}', "
|
|
289
|
+
f"got '{subject}'"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Heuristic: Check if second token looks like a TLD
|
|
293
|
+
# Common TLDs for CyTube servers
|
|
294
|
+
tld_extensions = {"com", "be", "org", "net", "io", "tv", "gg", "me", "co"}
|
|
295
|
+
|
|
296
|
+
if len(tokens) >= 2 and tokens[1] in tld_extensions:
|
|
297
|
+
# Domain has TLD (e.g., cytu.be)
|
|
298
|
+
domain = f"{tokens[0]}.{tokens[1]}"
|
|
299
|
+
channel = tokens[2] if len(tokens) > 2 else ""
|
|
300
|
+
event_name = ".".join(tokens[3:]) if len(tokens) > 3 else ""
|
|
301
|
+
else:
|
|
302
|
+
# Domain is single token (e.g., localhost)
|
|
303
|
+
domain = tokens[0]
|
|
304
|
+
channel = tokens[1] if len(tokens) > 1 else ""
|
|
305
|
+
event_name = ".".join(tokens[2:]) if len(tokens) > 2 else ""
|
|
306
|
+
|
|
307
|
+
if not channel or not event_name:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
f"Invalid subject format: expected 'cytube.events.{{domain}}.{{channel}}.{{event}}', "
|
|
310
|
+
f"got '{subject}'"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
"prefix": SUBJECT_PREFIX,
|
|
315
|
+
"domain": domain,
|
|
316
|
+
"channel": channel,
|
|
317
|
+
"event_name": event_name,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
__all__ = [
|
|
322
|
+
"SUBJECT_PREFIX",
|
|
323
|
+
"COMMAND_PREFIX",
|
|
324
|
+
"MAX_TOKEN_LENGTH",
|
|
325
|
+
"sanitize_token",
|
|
326
|
+
"build_subject",
|
|
327
|
+
"build_event_subject",
|
|
328
|
+
"build_command_subject",
|
|
329
|
+
"parse_subject",
|
|
330
|
+
]
|