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/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()