slim-bindings 1.0.0__py3-none-manylinux_2_28_aarch64.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.
- examples/__init__.py +44 -0
- examples/common.py +411 -0
- examples/config.py +298 -0
- examples/example-config.yaml +44 -0
- examples/group.py +399 -0
- examples/point_to_point.py +215 -0
- examples/slim.py +146 -0
- slim_bindings/__init__.py +1 -0
- slim_bindings/libslim_bindings.so +0 -0
- slim_bindings/slim_bindings.py +9780 -0
- slim_bindings-1.0.0.dist-info/METADATA +504 -0
- slim_bindings-1.0.0.dist-info/RECORD +14 -0
- slim_bindings-1.0.0.dist-info/WHEEL +4 -0
- slim_bindings-1.0.0.dist-info/entry_points.txt +4 -0
examples/group.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Group example (heavily commented).
|
|
5
|
+
|
|
6
|
+
Purpose:
|
|
7
|
+
Demonstrates how to:
|
|
8
|
+
* Start a local Slim app using the global service
|
|
9
|
+
* Optionally create a group session (becoming its moderator)
|
|
10
|
+
* Invite other participants (by their IDs) into the group
|
|
11
|
+
* Receive and display messages
|
|
12
|
+
* Interactively publish messages
|
|
13
|
+
|
|
14
|
+
Key concepts:
|
|
15
|
+
- Group sessions are created with SessionConfig with SessionType.GROUP and
|
|
16
|
+
reference a 'topic' (channel) Name.
|
|
17
|
+
- Invites are explicit: the moderator invites each participant after
|
|
18
|
+
creating the session.
|
|
19
|
+
- Participants that did not create the session simply wait for
|
|
20
|
+
listen_for_session_async() to yield their Session.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
slim-bindings-group \
|
|
24
|
+
--local org/default/me \
|
|
25
|
+
--remote org/default/chat-topic \
|
|
26
|
+
--invites org/default/peer1 --invites org/default/peer2
|
|
27
|
+
|
|
28
|
+
Notes:
|
|
29
|
+
* If --invites is omitted, the client runs in passive participant mode.
|
|
30
|
+
* If both remote and invites are supplied, the client acts as session moderator.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
import datetime
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
from prompt_toolkit.shortcuts import PromptSession, print_formatted_text
|
|
38
|
+
from prompt_toolkit.styles import Style
|
|
39
|
+
|
|
40
|
+
import slim_bindings
|
|
41
|
+
|
|
42
|
+
from .common import (
|
|
43
|
+
create_base_parser,
|
|
44
|
+
create_local_app,
|
|
45
|
+
format_message_print,
|
|
46
|
+
parse_args_to_dict,
|
|
47
|
+
split_id,
|
|
48
|
+
)
|
|
49
|
+
from .config import GroupConfig, load_config_with_cli_override
|
|
50
|
+
|
|
51
|
+
# Prompt style
|
|
52
|
+
custom_style = Style.from_dict(
|
|
53
|
+
{
|
|
54
|
+
"system": "ansibrightblue",
|
|
55
|
+
"friend": "ansiyellow",
|
|
56
|
+
"user": "ansigreen",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def handle_invite(session, invite_id):
|
|
62
|
+
"""Handle inviting a participant to the group."""
|
|
63
|
+
parts = invite_id.split()
|
|
64
|
+
if len(parts) != 1:
|
|
65
|
+
print_formatted_text(
|
|
66
|
+
"Error: 'invite' command expects exactly one participant ID (e.g., 'invite org/ns/client-1')",
|
|
67
|
+
style=custom_style,
|
|
68
|
+
)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
print(f"Inviting participant: {invite_id}")
|
|
72
|
+
invite_name = split_id(invite_id)
|
|
73
|
+
try:
|
|
74
|
+
handle = await session.invite_async(invite_name)
|
|
75
|
+
await handle.wait_async()
|
|
76
|
+
except Exception as e:
|
|
77
|
+
error_str = str(e)
|
|
78
|
+
if "participant already in group" in error_str:
|
|
79
|
+
print_formatted_text(
|
|
80
|
+
f"Error: Participant {invite_id} is already in the group.",
|
|
81
|
+
style=custom_style,
|
|
82
|
+
)
|
|
83
|
+
elif "failed to add participant to session" in error_str:
|
|
84
|
+
print_formatted_text(
|
|
85
|
+
f"Error: Failed to add participant {invite_id} to session.",
|
|
86
|
+
style=custom_style,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def handle_remove(session: slim_bindings.Session, remove_id: str):
|
|
93
|
+
"""Handle removing a participant from the group."""
|
|
94
|
+
parts = remove_id.split()
|
|
95
|
+
if len(parts) != 1:
|
|
96
|
+
print_formatted_text(
|
|
97
|
+
"Error: 'remove' command expects exactly one participant ID (e.g., 'remove org/ns/client-1')",
|
|
98
|
+
style=custom_style,
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
print(f"Removing participant: {remove_id}")
|
|
103
|
+
remove_name = split_id(remove_id)
|
|
104
|
+
try:
|
|
105
|
+
handle = await session.remove_async(remove_name)
|
|
106
|
+
await handle.wait_async()
|
|
107
|
+
except Exception as e:
|
|
108
|
+
error_str = str(e)
|
|
109
|
+
if "participant not found in group" in error_str:
|
|
110
|
+
print_formatted_text(
|
|
111
|
+
f"Error: Participant {remove_id} is not in the group.",
|
|
112
|
+
style=custom_style,
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def receive_loop(
|
|
119
|
+
local_app: slim_bindings.App,
|
|
120
|
+
created_session: slim_bindings.Session | None,
|
|
121
|
+
session_ready: asyncio.Event,
|
|
122
|
+
shared_session_container: list,
|
|
123
|
+
):
|
|
124
|
+
"""
|
|
125
|
+
Receive messages for the bound session.
|
|
126
|
+
|
|
127
|
+
Behavior:
|
|
128
|
+
* If not moderator: wait for a new group session (listen_for_session_async()).
|
|
129
|
+
* If moderator: reuse the created_session reference.
|
|
130
|
+
* Loop forever until cancellation or an error occurs.
|
|
131
|
+
"""
|
|
132
|
+
if created_session is None:
|
|
133
|
+
print_formatted_text("Waiting for session...", style=custom_style)
|
|
134
|
+
session = await local_app.listen_for_session_async(None)
|
|
135
|
+
else:
|
|
136
|
+
session = created_session
|
|
137
|
+
|
|
138
|
+
# Make session available to other tasks
|
|
139
|
+
shared_session_container[0] = session
|
|
140
|
+
session_ready.set()
|
|
141
|
+
|
|
142
|
+
# Get source and destination names for display
|
|
143
|
+
source_name = session.source()
|
|
144
|
+
|
|
145
|
+
while True:
|
|
146
|
+
try:
|
|
147
|
+
# Await next inbound message from the group session.
|
|
148
|
+
# Returns tuple (MessageContext, bytes).
|
|
149
|
+
received_msg = await session.get_message_async(
|
|
150
|
+
timeout=datetime.timedelta(seconds=30)
|
|
151
|
+
)
|
|
152
|
+
ctx = received_msg.context
|
|
153
|
+
payload = received_msg.payload
|
|
154
|
+
|
|
155
|
+
# Display sender name and message
|
|
156
|
+
sender = ctx.source_name if hasattr(ctx, "source_name") else source_name
|
|
157
|
+
print_formatted_text(
|
|
158
|
+
f"{sender} > {payload.decode()}",
|
|
159
|
+
style=custom_style,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# if the message metadata contains PUBLISH_TO this message is a reply
|
|
163
|
+
# to a previous one. In this case we do not reply to avoid loops
|
|
164
|
+
if "PUBLISH_TO" not in ctx.metadata:
|
|
165
|
+
reply = f"message received by {source_name}"
|
|
166
|
+
await session.publish_to_async(ctx, reply.encode(), None, ctx.metadata)
|
|
167
|
+
except asyncio.CancelledError:
|
|
168
|
+
# Graceful shutdown path (ctrl-c or program exit).
|
|
169
|
+
break
|
|
170
|
+
except Exception as e:
|
|
171
|
+
# Break if session is closed, otherwise continue listening
|
|
172
|
+
if "session closed" in str(e).lower():
|
|
173
|
+
break
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def keyboard_loop(
|
|
178
|
+
created_session: slim_bindings.Session,
|
|
179
|
+
session_ready: asyncio.Event,
|
|
180
|
+
shared_session_container: list[slim_bindings.Session],
|
|
181
|
+
local_app: slim_bindings.App,
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
Interactive loop allowing participants to publish messages.
|
|
185
|
+
|
|
186
|
+
Typing 'exit' or 'quit' (case-insensitive) terminates the loop.
|
|
187
|
+
Typing 'remove NAME' removes a participant from the group
|
|
188
|
+
Typing 'invite NAME' invites a participant to the group
|
|
189
|
+
Each line is published to the group channel as UTF-8 bytes.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# 1. Initialize an async session
|
|
194
|
+
prompt_session = PromptSession(style=custom_style)
|
|
195
|
+
|
|
196
|
+
# Wait for the session to be established
|
|
197
|
+
await session_ready.wait()
|
|
198
|
+
|
|
199
|
+
session = shared_session_container[0]
|
|
200
|
+
source_name = session.source()
|
|
201
|
+
dest_name = session.destination()
|
|
202
|
+
|
|
203
|
+
if created_session:
|
|
204
|
+
print_formatted_text(
|
|
205
|
+
f"Welcome to the group {dest_name}!\n"
|
|
206
|
+
"Commands:\n"
|
|
207
|
+
" - Type a message to send it to the group\n"
|
|
208
|
+
" - 'remove NAME' to remove a participant\n"
|
|
209
|
+
" - 'invite NAME' to invite a participant\n"
|
|
210
|
+
" - 'exit' or 'quit' to leave the group",
|
|
211
|
+
style=custom_style,
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
print_formatted_text(
|
|
215
|
+
f"Welcome to the group {dest_name}!\n"
|
|
216
|
+
"Commands:\n"
|
|
217
|
+
" - Type a message to send it to the group\n"
|
|
218
|
+
" - 'exit' or 'quit' to leave the group",
|
|
219
|
+
style=custom_style,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
while True:
|
|
223
|
+
# Run blocking input() in a worker thread so we do not block the event loop.
|
|
224
|
+
user_input = await prompt_session.prompt_async(f"{source_name} > ")
|
|
225
|
+
|
|
226
|
+
if user_input.lower() in ("exit", "quit") and created_session:
|
|
227
|
+
# Delete the session
|
|
228
|
+
handle = await local_app.delete_session_async(
|
|
229
|
+
shared_session_container[0]
|
|
230
|
+
)
|
|
231
|
+
await handle.wait_async()
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
if user_input.lower().startswith("invite ") and created_session:
|
|
235
|
+
invite_id = user_input[7:].strip() # Skip "invite " (7 chars)
|
|
236
|
+
await handle_invite(shared_session_container[0], invite_id)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
if user_input.lower().startswith("remove ") and created_session:
|
|
240
|
+
remove_id = user_input[7:].strip() # Skip "remove " (7 chars)
|
|
241
|
+
await handle_remove(shared_session_container[0], remove_id)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Send message to the channel_name specified when creating the session.
|
|
245
|
+
# As the session is group, all participants will receive it.
|
|
246
|
+
await shared_session_container[0].publish_async(
|
|
247
|
+
user_input.encode(), None, None
|
|
248
|
+
)
|
|
249
|
+
except KeyboardInterrupt:
|
|
250
|
+
# Handle Ctrl+C gracefully
|
|
251
|
+
pass
|
|
252
|
+
except asyncio.CancelledError:
|
|
253
|
+
# Handle task cancellation gracefully
|
|
254
|
+
pass
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print_formatted_text(f"-> Error sending message: {e}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def run_client(config: GroupConfig):
|
|
260
|
+
"""
|
|
261
|
+
Orchestrate one group-capable client instance.
|
|
262
|
+
|
|
263
|
+
Modes:
|
|
264
|
+
* Moderator (creator): remote (channel) + invites provided.
|
|
265
|
+
* Listener only: no remote; waits for inbound group sessions.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
config: GroupConfig instance containing all configuration.
|
|
269
|
+
"""
|
|
270
|
+
# Create the local Slim instance using global service
|
|
271
|
+
local_app, conn_id = await create_local_app(config)
|
|
272
|
+
|
|
273
|
+
# Parse the remote channel/topic if provided; else None triggers passive mode.
|
|
274
|
+
chat_channel = split_id(config.remote) if config.remote else None
|
|
275
|
+
|
|
276
|
+
# Track background tasks (receiver loop + optional keyboard loop).
|
|
277
|
+
tasks: list[asyncio.Task] = []
|
|
278
|
+
|
|
279
|
+
# Session sharing between tasks
|
|
280
|
+
session_ready = asyncio.Event()
|
|
281
|
+
shared_session_container = [None] # Use list to make it mutable across functions
|
|
282
|
+
|
|
283
|
+
# Session object only exists immediately if we are moderator.
|
|
284
|
+
created_session = None
|
|
285
|
+
if chat_channel and config.invites:
|
|
286
|
+
# We are the moderator; create the group session now.
|
|
287
|
+
format_message_print(
|
|
288
|
+
f"Creating new group session (moderator)... {split_id(config.local)}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Create group session configuration
|
|
292
|
+
session_config = slim_bindings.SessionConfig(
|
|
293
|
+
session_type=slim_bindings.SessionType.GROUP,
|
|
294
|
+
enable_mls=config.enable_mls,
|
|
295
|
+
max_retries=5,
|
|
296
|
+
interval=datetime.timedelta(seconds=5),
|
|
297
|
+
metadata={},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Create session - returns a tuple (SessionContext, CompletionHandle)
|
|
301
|
+
session = local_app.create_session(session_config, chat_channel)
|
|
302
|
+
# Wait for session to be established
|
|
303
|
+
await session.completion.wait_async()
|
|
304
|
+
created_session = session.session
|
|
305
|
+
|
|
306
|
+
# Invite each provided participant.
|
|
307
|
+
for invite in config.invites:
|
|
308
|
+
invite_name = split_id(invite)
|
|
309
|
+
await local_app.set_route_async(invite_name, conn_id)
|
|
310
|
+
handle = await created_session.invite_async(invite_name)
|
|
311
|
+
await handle.wait_async()
|
|
312
|
+
print(f"{config.local} -> add {invite_name} to the group")
|
|
313
|
+
|
|
314
|
+
# Launch the receiver immediately.
|
|
315
|
+
tasks.append(
|
|
316
|
+
asyncio.create_task(
|
|
317
|
+
receive_loop(
|
|
318
|
+
local_app, created_session, session_ready, shared_session_container
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
tasks.append(
|
|
324
|
+
asyncio.create_task(
|
|
325
|
+
keyboard_loop(
|
|
326
|
+
created_session, session_ready, shared_session_container, local_app
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Wait for any task to finish, then cancel the others.
|
|
332
|
+
try:
|
|
333
|
+
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
334
|
+
|
|
335
|
+
for task in pending:
|
|
336
|
+
task.cancel()
|
|
337
|
+
|
|
338
|
+
# We can await the pending tasks to allow them to clean up.
|
|
339
|
+
if pending:
|
|
340
|
+
await asyncio.wait(pending)
|
|
341
|
+
|
|
342
|
+
# Raise exceptions from completed tasks, if any
|
|
343
|
+
for task in done:
|
|
344
|
+
exc = task.exception()
|
|
345
|
+
if exc:
|
|
346
|
+
raise exc
|
|
347
|
+
|
|
348
|
+
except KeyboardInterrupt:
|
|
349
|
+
# Cancel all tasks on KeyboardInterrupt
|
|
350
|
+
for task in tasks:
|
|
351
|
+
task.cancel()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main():
|
|
355
|
+
"""
|
|
356
|
+
CLI entry-point for the group example.
|
|
357
|
+
|
|
358
|
+
Parses command-line arguments and config file, then runs the client.
|
|
359
|
+
"""
|
|
360
|
+
# Create parser with common options
|
|
361
|
+
parser = create_base_parser(
|
|
362
|
+
description="SLIM Group Messaging Example\n\n"
|
|
363
|
+
"Create or join a group messaging session with multiple participants."
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Add group-specific options
|
|
367
|
+
parser.add_argument(
|
|
368
|
+
"--invites",
|
|
369
|
+
type=str,
|
|
370
|
+
action="append",
|
|
371
|
+
dest="invites",
|
|
372
|
+
help="Invite participant to the group session (can be specified multiple times)",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Parse arguments
|
|
376
|
+
args = parser.parse_args()
|
|
377
|
+
|
|
378
|
+
# Convert to dictionary
|
|
379
|
+
args_dict = parse_args_to_dict(args)
|
|
380
|
+
|
|
381
|
+
# Load configuration (CLI args override env vars and config file)
|
|
382
|
+
try:
|
|
383
|
+
config = load_config_with_cli_override(GroupConfig, args_dict)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
print(f"Configuration error: {e}", file=sys.stderr)
|
|
386
|
+
sys.exit(1)
|
|
387
|
+
|
|
388
|
+
# Run the client
|
|
389
|
+
try:
|
|
390
|
+
asyncio.run(run_client(config))
|
|
391
|
+
except KeyboardInterrupt:
|
|
392
|
+
print("\nClient interrupted by user.")
|
|
393
|
+
except Exception as e:
|
|
394
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
395
|
+
sys.exit(1)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
main()
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Point-to-point messaging example for Slim bindings.
|
|
5
|
+
|
|
6
|
+
This example can operate in two primary modes:
|
|
7
|
+
|
|
8
|
+
1. Active sender (message mode):
|
|
9
|
+
- Creates a session.
|
|
10
|
+
- Publishes a fixed or user-supplied message multiple times to a remote identity.
|
|
11
|
+
- Receives replies for each sent message (request/reply pattern).
|
|
12
|
+
|
|
13
|
+
2. Passive listener (no --message provided):
|
|
14
|
+
- Waits for inbound sessions initiated by a remote party.
|
|
15
|
+
- Echoes replies for each received payload, tagging them with the local instance ID.
|
|
16
|
+
|
|
17
|
+
Key concepts demonstrated:
|
|
18
|
+
- Global service usage with create_app_with_secret()
|
|
19
|
+
- PointToPoint session creation logic.
|
|
20
|
+
- Publish / receive loop with per-message reply.
|
|
21
|
+
- Simple flow control via iteration count and sleeps (demo-friendly).
|
|
22
|
+
|
|
23
|
+
Notes:
|
|
24
|
+
* PointToPoint sessions stick to one specific peer (sticky / affinity semantics).
|
|
25
|
+
|
|
26
|
+
The heavy inline comments are intentional to guide new users line-by-line.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import datetime
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
import slim_bindings
|
|
34
|
+
|
|
35
|
+
from .common import (
|
|
36
|
+
create_base_parser,
|
|
37
|
+
create_local_app,
|
|
38
|
+
format_message_print,
|
|
39
|
+
parse_args_to_dict,
|
|
40
|
+
split_id,
|
|
41
|
+
)
|
|
42
|
+
from .config import PointToPointConfig, load_config_with_cli_override
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def run_client(config: PointToPointConfig):
|
|
46
|
+
"""
|
|
47
|
+
Core coroutine that performs either active send or passive listen logic.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config: PointToPointConfig instance containing all configuration.
|
|
51
|
+
|
|
52
|
+
Behavior:
|
|
53
|
+
- Builds Slim app using global service.
|
|
54
|
+
- If message is supplied -> create session & publish + receive replies.
|
|
55
|
+
- If message not supplied -> wait indefinitely for inbound sessions and echo payloads.
|
|
56
|
+
"""
|
|
57
|
+
# Build the Slim application using global service
|
|
58
|
+
local_app, conn_id = await create_local_app(config)
|
|
59
|
+
|
|
60
|
+
# Numeric unique instance ID (useful for distinguishing multiple processes).
|
|
61
|
+
instance = str(local_app.id())
|
|
62
|
+
|
|
63
|
+
# If user intends to send messages, remote must be provided for routing.
|
|
64
|
+
if config.message and not config.remote:
|
|
65
|
+
raise ValueError("Remote ID must be provided when message is specified.")
|
|
66
|
+
|
|
67
|
+
# ACTIVE MODE (publishing + expecting replies)
|
|
68
|
+
if config.message and config.remote:
|
|
69
|
+
# Convert the remote ID string into a Name.
|
|
70
|
+
remote_name = split_id(config.remote)
|
|
71
|
+
|
|
72
|
+
# Create local route to enable forwarding towards remote name
|
|
73
|
+
await local_app.set_route_async(remote_name, conn_id)
|
|
74
|
+
|
|
75
|
+
# Create point-to-point session configuration
|
|
76
|
+
session_config = slim_bindings.SessionConfig(
|
|
77
|
+
session_type=slim_bindings.SessionType.POINT_TO_POINT,
|
|
78
|
+
enable_mls=config.enable_mls,
|
|
79
|
+
max_retries=5,
|
|
80
|
+
interval=datetime.timedelta(seconds=5),
|
|
81
|
+
metadata={},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Create session - returns a context with completion and session
|
|
85
|
+
session_context = await local_app.create_session_async(
|
|
86
|
+
session_config, remote_name
|
|
87
|
+
)
|
|
88
|
+
# Wait for session to be established
|
|
89
|
+
await session_context.completion.wait_async()
|
|
90
|
+
session = session_context.session
|
|
91
|
+
|
|
92
|
+
session_id = session_context.session.session_id()
|
|
93
|
+
|
|
94
|
+
session_closed = False
|
|
95
|
+
# Iterate send->receive cycles.
|
|
96
|
+
for i in range(config.iterations):
|
|
97
|
+
try:
|
|
98
|
+
# Publish message to the session
|
|
99
|
+
await session.publish_async(config.message.encode(), None, None)
|
|
100
|
+
format_message_print(
|
|
101
|
+
f"{instance}",
|
|
102
|
+
f"Sent message {config.message} - {i + 1}/{config.iterations}",
|
|
103
|
+
)
|
|
104
|
+
# Wait for reply from remote peer.
|
|
105
|
+
received_msg = await session.get_message_async(
|
|
106
|
+
timeout=datetime.timedelta(seconds=30)
|
|
107
|
+
)
|
|
108
|
+
reply = received_msg.payload
|
|
109
|
+
format_message_print(
|
|
110
|
+
f"{instance}",
|
|
111
|
+
f"received (from session {session_id}): {reply.decode()}",
|
|
112
|
+
)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
# Surface an error but continue attempts
|
|
115
|
+
format_message_print(f"{instance}", f"error: {e}")
|
|
116
|
+
# if the session is closed exit
|
|
117
|
+
if "session closed" in str(e).lower():
|
|
118
|
+
session_closed = True
|
|
119
|
+
break
|
|
120
|
+
# 1s pacing so output remains readable.
|
|
121
|
+
await asyncio.sleep(1)
|
|
122
|
+
|
|
123
|
+
if not session_closed:
|
|
124
|
+
# Delete session
|
|
125
|
+
handle = await local_app.delete_session_async(session)
|
|
126
|
+
await handle.wait_async()
|
|
127
|
+
|
|
128
|
+
# PASSIVE MODE (listen for inbound sessions)
|
|
129
|
+
else:
|
|
130
|
+
while True:
|
|
131
|
+
format_message_print(
|
|
132
|
+
f"{instance}", "waiting for new session to be established"
|
|
133
|
+
)
|
|
134
|
+
# Block until a remote peer initiates a session to us.
|
|
135
|
+
session = await local_app.listen_for_session_async(None)
|
|
136
|
+
session_id = session.session_id()
|
|
137
|
+
format_message_print(f"{instance}", f"new session {session_id}")
|
|
138
|
+
|
|
139
|
+
async def session_loop(sess: slim_bindings.Session):
|
|
140
|
+
"""
|
|
141
|
+
Inner loop for a single inbound session:
|
|
142
|
+
* Receive messages until the session is closed or an error occurs.
|
|
143
|
+
* Echo each message back using publish.
|
|
144
|
+
"""
|
|
145
|
+
while True:
|
|
146
|
+
try:
|
|
147
|
+
received_msg = await sess.get_message_async(
|
|
148
|
+
timeout=datetime.timedelta(seconds=30)
|
|
149
|
+
)
|
|
150
|
+
payload = received_msg.payload
|
|
151
|
+
except Exception:
|
|
152
|
+
# Session likely closed or transport broken.
|
|
153
|
+
break
|
|
154
|
+
text = payload.decode()
|
|
155
|
+
format_message_print(f"{instance}", f"received: {text}")
|
|
156
|
+
# Echo reply with appended instance identifier.
|
|
157
|
+
await sess.publish_async(
|
|
158
|
+
f"{text} from {instance}".encode(), None, None
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Launch a dedicated task to handle this session (allow multiple).
|
|
162
|
+
asyncio.create_task(session_loop(session))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main():
|
|
166
|
+
"""
|
|
167
|
+
CLI entry-point for point-to-point example.
|
|
168
|
+
|
|
169
|
+
Parses command-line arguments and config file, then runs the client.
|
|
170
|
+
"""
|
|
171
|
+
# Create parser with common options
|
|
172
|
+
parser = create_base_parser(
|
|
173
|
+
description="SLIM Point-to-Point Messaging Example\n\n"
|
|
174
|
+
"Send messages to a specific peer or listen for incoming connections."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Add point-to-point specific options
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"--message",
|
|
180
|
+
type=str,
|
|
181
|
+
help="Message to send (activates sender mode)",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--iterations",
|
|
186
|
+
type=int,
|
|
187
|
+
default=10,
|
|
188
|
+
help="Number of request/reply cycles in sender mode (default: 10)",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Parse arguments
|
|
192
|
+
args = parser.parse_args()
|
|
193
|
+
|
|
194
|
+
# Convert to dictionary
|
|
195
|
+
args_dict = parse_args_to_dict(args)
|
|
196
|
+
|
|
197
|
+
# Load configuration (CLI args override env vars and config file)
|
|
198
|
+
try:
|
|
199
|
+
config = load_config_with_cli_override(PointToPointConfig, args_dict)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(f"Configuration error: {e}", file=sys.stderr)
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
# Run the client
|
|
205
|
+
try:
|
|
206
|
+
asyncio.run(run_client(config))
|
|
207
|
+
except KeyboardInterrupt:
|
|
208
|
+
print("\nClient interrupted by user.")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
main()
|