slim-bindings 0.7.1__cp313-cp313-win_amd64.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.
@@ -0,0 +1,52 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from asyncio import TimeoutError
5
+
6
+
7
+ class SLIMTimeoutError(TimeoutError):
8
+ """
9
+ Exception raised for SLIM timeout errors.
10
+
11
+ This exception is raised when an operation in an SLIM session times out.
12
+ It encapsulates detailed information about the timeout event, including the
13
+ ID of the message that caused the timeout and the session identifier. An
14
+ optional underlying exception can also be provided to offer additional context.
15
+
16
+ Attributes:
17
+ message_id (int): The identifier associated with the message triggering the timeout.
18
+ session_id (int): The identifier of the session where the timeout occurred.
19
+ message (str): A brief description of the timeout error.
20
+ original_exception (Exception, optional): The underlying exception that caused the timeout, if any.
21
+
22
+ The string representation of the exception (via __str__) returns a full message that
23
+ includes the custom message, session ID, and message ID, as well as details of the
24
+ original exception (if present). This provides a richer context when the exception is logged
25
+ or printed.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ message_id: int,
31
+ session_id: int,
32
+ message: str = "SLIM timeout error",
33
+ original_exception: Exception | None = None,
34
+ ):
35
+ self.message_id = message_id
36
+ self.session_id = session_id
37
+ self.message = message
38
+ self.original_exception = original_exception
39
+ full_message = f"{message} for session {session_id} and message {message_id}"
40
+ if original_exception:
41
+ full_message = f"{full_message}. Caused by: {original_exception!r}"
42
+ super().__init__(full_message)
43
+
44
+ def __str__(self):
45
+ return self.args[0]
46
+
47
+ def __repr__(self):
48
+ return (
49
+ f"{self.__class__.__name__}(session_id={self.session_id!r}, "
50
+ f"message_id={self.message_id!r}, "
51
+ f"message={self.message!r}, original_exception={self.original_exception!r})"
52
+ )
slim_bindings/py.typed ADDED
File without changes
@@ -0,0 +1,178 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import datetime
7
+
8
+ from ._slim_bindings import (
9
+ CompletionHandle,
10
+ MessageContext,
11
+ Name,
12
+ SessionConfiguration,
13
+ SessionContext,
14
+ SessionType,
15
+ )
16
+
17
+
18
+ class Session:
19
+ """High level Python wrapper around a `SessionContext`.
20
+
21
+ This object provides a Pythonic façade over the lower-level Rust session
22
+ context. It retains a reference to the owning `App` so the existing
23
+ service-level binding functions (publish, invite, remove, get_message,
24
+ delete_session) can be invoked without duplicating logic on the Rust side.
25
+
26
+ Threading / Concurrency:
27
+ The methods are all async (where network / I/O is involved) and are
28
+ safe to await concurrently.
29
+
30
+ Lifecycle:
31
+ A `Session` is typically obtained from `Slim.create_session(...)`
32
+ or `Slim.listen_for_session(...)`. Call `delete()`to release
33
+ server-side resources.
34
+
35
+ Attributes (properties):
36
+ id (int): Unique numeric session identifier.
37
+ metadata (dict[str, str]): Free-form key/value metadata attached
38
+ to the current session configuration.
39
+ session_type (SessionType): PointToPoint / Group classification.
40
+ session_config (SessionConfiguration): Current effective configuration.
41
+ src (Name): Source name (creator / initiator of the session).
42
+ dst (Name): Destination name (PointToPoint), Channel name (group)
43
+ """
44
+
45
+ def __init__(self, ctx: SessionContext):
46
+ self._ctx = ctx
47
+
48
+ @property
49
+ def id(self) -> int:
50
+ """Return the unique numeric identifier for this session."""
51
+ return self._ctx.id # exposed by SessionContext
52
+
53
+ @property
54
+ def metadata(self) -> dict[str, str]:
55
+ """Return a copy of the session metadata mapping (string keys/values)."""
56
+ return self._ctx.metadata
57
+
58
+ @property
59
+ def session_type(self) -> SessionType:
60
+ """Return the type of this session (PointToPoint / Group)."""
61
+ return self._ctx.session_type
62
+
63
+ @property
64
+ def session_config(self) -> SessionConfiguration:
65
+ """Return the current effective session configuration enum variant."""
66
+ return self._ctx.session_config
67
+
68
+ @property
69
+ def src(self) -> Name:
70
+ """Return the source name of this session."""
71
+ return self._ctx.src
72
+
73
+ @property
74
+ def dst(self) -> Name | None:
75
+ """Return the destination name"""
76
+ return self._ctx.dst
77
+
78
+ async def publish(
79
+ self,
80
+ msg: bytes,
81
+ payload_type: str | None = None,
82
+ metadata: dict | None = None,
83
+ ) -> CompletionHandle:
84
+ """
85
+ Publish a message on the current session.
86
+
87
+ Args:
88
+ msg (bytes): The message payload to publish.
89
+ payload_type (str, optional): The type of the payload, if applicable.
90
+ metadata (dict, optional): Additional metadata to include with the
91
+ message.
92
+
93
+ Returns:
94
+ None
95
+ """
96
+
97
+ return await self._ctx.publish(
98
+ 1,
99
+ msg,
100
+ message_ctx=None,
101
+ name=None,
102
+ payload_type=payload_type,
103
+ metadata=metadata,
104
+ )
105
+
106
+ async def publish_to(
107
+ self,
108
+ message_ctx: MessageContext,
109
+ msg: bytes,
110
+ payload_type: str | None = None,
111
+ metadata: dict | None = None,
112
+ ) -> CompletionHandle:
113
+ """
114
+ Publish a message directly back to the originator associated with the
115
+ supplied `message_ctx` (reply semantics).
116
+
117
+ Args:
118
+ message_ctx: The context previously received with a message from
119
+ `get_message()` / `recv()`. Provides addressing info.
120
+ msg: Raw bytes payload to send as the reply.
121
+ payload_type: Optional content-type / discriminator.
122
+ metadata: Optional message-scoped metadata.
123
+
124
+ Notes:
125
+ The explicit `dest` parameter is not required because the routing
126
+ information is derived from `message_ctx`.
127
+
128
+ Raises:
129
+ RuntimeError (wrapped) if sending fails or the session is closed.
130
+ """
131
+
132
+ return await self._ctx.publish_to(
133
+ message_ctx,
134
+ msg,
135
+ payload_type=payload_type,
136
+ metadata=metadata,
137
+ )
138
+
139
+ async def invite(self, name: Name) -> CompletionHandle:
140
+ """Invite (add) a participant to this session. Only works for Group.
141
+
142
+ Args:
143
+ name: Name of the participant to invite.
144
+
145
+ Raises:
146
+ RuntimeError (wrapped) if the invite fails.
147
+ """
148
+ return await self._ctx.invite(name)
149
+
150
+ async def remove(self, name: Name) -> CompletionHandle:
151
+ """Remove (eject) a participant from this session. Only works for Group.
152
+
153
+ Args:
154
+ name: Name of the participant to remove.
155
+
156
+ Raises:
157
+ RuntimeError (wrapped) if removal fails.
158
+ """
159
+ return await self._ctx.remove(name)
160
+
161
+ async def get_message(
162
+ self, timeout: datetime.timedelta | None = None
163
+ ) -> tuple[MessageContext, bytes]: # MessageContext, blob
164
+ """Wait for and return the next inbound message.
165
+
166
+ Returns:
167
+ (MessageContext, bytes): A tuple containing the message context
168
+ (routing / origin metadata) and the raw payload bytes.
169
+
170
+ Raises:
171
+ RuntimeError (wrapped) if the session is closed or receive fails.
172
+ """
173
+ return await self._ctx.get_message(timeout)
174
+
175
+
176
+ __all__ = [
177
+ "Session",
178
+ ]
slim_bindings/slim.py ADDED
@@ -0,0 +1,325 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from datetime import timedelta
5
+
6
+ from slim_bindings._slim_bindings import (
7
+ App,
8
+ CompletionHandle,
9
+ IdentityProvider,
10
+ IdentityVerifier,
11
+ Name,
12
+ SessionConfiguration,
13
+ SessionContext,
14
+ )
15
+
16
+ from .session import Session
17
+
18
+
19
+ class Slim:
20
+ """
21
+ High-level façade over the underlying Service (Rust core) providing a
22
+ Pythonic API for:
23
+ * Service initialization & authentication
24
+ * Client connections to remote Slim services (connect / disconnect)
25
+ * Server lifecycle management (run_server / stop_server)
26
+ * Subscription & routing management (subscribe / unsubscribe / set_route / remove_route)
27
+ * Session lifecycle (create_session / delete_session / listen_for_session)
28
+
29
+ Core Concepts:
30
+ - Name: Fully-qualified name of the app (org / namespace / app-or-channel). Used for
31
+ routing, subscriptions.
32
+ - Session: Logical communication context. Types supported include:
33
+ * PointToPoint : Point-to-point with a fixed, stable destination (sticky).
34
+ * Group: Many-to-many via a named channel/topic.
35
+ - Default Session Configuration: A fallback used when inbound sessions are created
36
+ towards this service (set via set_default_session_config).
37
+
38
+ Typical Lifecycle (Client):
39
+ 1. slim = Slim(local_name, identity_provider, identity_verifier)
40
+ 2. await slim.connect({"endpoint": "...", "tls": {"insecure": True}})
41
+ 3. await slim.set_route(remote_name)
42
+ 4. session = await slim.create_session(SessionConfiguration.PointToPoint(peer_name=remote_name, ...))
43
+ 5. await session.publish(b"payload")
44
+ 6. await slim.delete_session(session)
45
+ 7. await slim.disconnect("endpoint-string")
46
+
47
+ Typical Lifecycle (Server):
48
+ 1. slim = Slim(local_name, provider, verifier)
49
+ 2. await slim.run_server({"endpoint": "127.0.0.1:12345", "tls": {"insecure": True}})
50
+ 3. inbound = await slim.listen_for_session()
51
+ 4. msg_ctx, data = await inbound.get_message()
52
+ 5. await inbound.publish_to(msg_ctx, b"reply")
53
+ 6. await slim.stop_server("127.0.0.1:12345")
54
+
55
+ Threading / Concurrency:
56
+ - All network / I/O operations are async and awaitable.
57
+ - A single Slim instance can service multiple concurrent awaiters.
58
+
59
+ Error Handling:
60
+ - Methods propagate underlying exceptions (e.g., invalid routing, closed sessions).
61
+ - connect / run_server may raise if the endpoint is unreachable or already bound.
62
+
63
+ Performance Notes:
64
+ - Route changes are lightweight but may take a short time to propagate remotely.
65
+ - listen_for_session can be long-lived; provide a timeout if you need bounded wait.
66
+
67
+ Security Notes:
68
+ - Identity provider & verifier determine trust model (e.g. shared secret vs JWT).
69
+ - For production, prefer asymmetric keys / JWT over shared secrets.
70
+
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ name: Name,
76
+ provider: IdentityProvider,
77
+ verifier: IdentityVerifier,
78
+ local_service: bool = False,
79
+ ):
80
+ """
81
+ Primary constructor for initializing a Slim instance.
82
+
83
+ Creates a Slim instance with the provided identity components and initializes
84
+ the underlying service handle.
85
+
86
+ Args:
87
+ name (Name): Fully qualified local name (org/namespace/app).
88
+ provider (IdentityProvider): Identity provider for authentication.
89
+ verifier (IdentityVerifier): Identity verifier for validating peers.
90
+ local_service (bool): Whether this is a local service. Defaults to False.
91
+
92
+ Note: Service initialization happens here via PyApp construction.
93
+ """
94
+
95
+ # Initialize service
96
+ self._app = App(
97
+ name,
98
+ provider,
99
+ verifier,
100
+ local_service=local_service,
101
+ )
102
+
103
+ # Create connection ID map
104
+ self.conn_ids: dict[str, int] = {}
105
+
106
+ # For the moment we manage one connection only
107
+ self.conn_id: int | None = None
108
+
109
+ @property
110
+ def id(self) -> int:
111
+ """Unique numeric identifier of the underlying app instance.
112
+
113
+ Returns:
114
+ int: Service ID allocated by the native layer.
115
+ """
116
+ return self._app.id
117
+
118
+ @property
119
+ def id_str(self) -> str:
120
+ """String representation of the unique identifier of the underlying app instance.
121
+
122
+ Returns:
123
+ str: String representation of the Service ID allocated by the native layer.
124
+ """
125
+
126
+ components_string = self.local_name.components_strings()
127
+
128
+ return f"{components_string[0]}/{components_string[1]}/{components_string[2]}/{self._app.id}"
129
+
130
+ @property
131
+ def local_name(self) -> Name:
132
+ """Local fully-qualified Name (org/namespace/app) for this app.
133
+
134
+ Returns:
135
+ Name: Immutable name used for routing, subscriptions, etc.
136
+ """
137
+ return self._app.name
138
+
139
+ async def create_session(
140
+ self,
141
+ destination: Name,
142
+ session_config: SessionConfiguration,
143
+ ) -> tuple[Session, CompletionHandle]:
144
+ """Create a new session and return its high-level Session wrapper.
145
+
146
+ Args:
147
+ destination (Name): Target peer or channel name.
148
+ session_config (SessionConfiguration): Parameters controlling creation.
149
+
150
+ Returns:
151
+ Session: Wrapper exposing high-level async operations for the session.
152
+ """
153
+ ctx, completion_handle = await self._app.create_session(
154
+ destination, session_config
155
+ )
156
+ return Session(ctx), completion_handle
157
+
158
+ async def delete_session(self, session: Session) -> CompletionHandle:
159
+ """
160
+ Terminate and remove an existing session.
161
+
162
+ Args:
163
+ session (Session): Session wrapper previously returned by create_session.
164
+
165
+ Returns:
166
+ None
167
+
168
+ Notes:
169
+ Underlying errors from delete_session are propagated.
170
+ """
171
+
172
+ # Remove the session from SLIM
173
+ return await self._app.delete_session(session._ctx)
174
+
175
+ async def run_server(self, config: dict):
176
+ """
177
+ Start a GRPC server component with the supplied config.
178
+ This allocates network resources (e.g. binds listening sockets).
179
+
180
+ Args:
181
+ config (dict): Server configuration parameters (check SLIM configuration for examples).
182
+
183
+ Returns:
184
+ None
185
+ """
186
+
187
+ await self._app.run_server(config)
188
+
189
+ async def stop_server(self, endpoint: str):
190
+ """
191
+ Stop the server component listening at the specified endpoint.
192
+
193
+ Args:
194
+ endpoint (str): Endpoint identifier / address previously passed to run_server.
195
+
196
+ Returns:
197
+ None
198
+ """
199
+
200
+ await self._app.stop_server(endpoint)
201
+
202
+ async def connect(self, client_config: dict) -> int:
203
+ """
204
+ Establish an outbound connection to a remote SLIM service.
205
+ Awaits completion until the connection is fully established and subscribed.
206
+
207
+ Args:
208
+ client_config (dict): Dial parameters; must include 'endpoint'.
209
+
210
+ Returns:
211
+ int: Numeric connection identifier assigned by the service.
212
+ """
213
+
214
+ conn_id = await self._app.connect(client_config)
215
+
216
+ # Save the connection ID
217
+ self.conn_ids[client_config["endpoint"]] = conn_id
218
+
219
+ # For the moment we manage one connection only
220
+ self.conn_id = conn_id
221
+
222
+ # Subscribe to the local name
223
+ await self._app.subscribe(
224
+ self._app.name,
225
+ conn_id,
226
+ )
227
+
228
+ # return the connection ID
229
+ return conn_id
230
+
231
+ async def disconnect(self, endpoint: str):
232
+ """
233
+ Disconnect from a previously established remote connection.
234
+ Awaits completion; underlying resources are released before return.
235
+
236
+ Args:
237
+ endpoint (str): The endpoint string used when connect() was invoked.
238
+
239
+ Returns:
240
+ None
241
+
242
+ """
243
+ conn = self.conn_ids[endpoint]
244
+ await self._app.disconnect(conn)
245
+
246
+ async def set_route(
247
+ self,
248
+ name: Name,
249
+ ):
250
+ """
251
+ Add (or update) an explicit routing rule for outbound messages.
252
+
253
+ Args:
254
+ name (Name): Destination app/channel name to route traffic toward.
255
+
256
+ Returns:
257
+ None
258
+ """
259
+
260
+ if self.conn_id is None:
261
+ raise RuntimeError("No active connection. Please connect first.")
262
+
263
+ await self._app.set_route(
264
+ name,
265
+ self.conn_id,
266
+ )
267
+
268
+ async def remove_route(
269
+ self,
270
+ name: Name,
271
+ ):
272
+ """
273
+ Remove a previously established outbound routing rule.
274
+
275
+ Args:
276
+ name (Name): Destination app/channel whose route should be removed.
277
+
278
+ Returns:
279
+ None
280
+ """
281
+
282
+ if self.conn_id is None:
283
+ raise RuntimeError("No active connection. Please connect first.")
284
+
285
+ await self._app.remove_route(
286
+ name,
287
+ self.conn_id,
288
+ )
289
+
290
+ async def subscribe(self, name: Name):
291
+ """
292
+ Subscribe to inbound messages addressed to the specified name.
293
+
294
+ Args:
295
+ name (Name): App or channel name to subscribe for deliveries.
296
+
297
+ Returns:
298
+ None
299
+ """
300
+
301
+ await self._app.subscribe(name, self.conn_id)
302
+
303
+ async def unsubscribe(self, name: Name):
304
+ """
305
+ Cancel a previous subscription for the specified name.
306
+
307
+ Args:
308
+ name (Name): App or channel name whose subscription is removed.
309
+
310
+ Returns:
311
+ None
312
+ """
313
+
314
+ await self._app.unsubscribe(name, self.conn_id)
315
+
316
+ async def listen_for_session(self, timeout: timedelta | None = None) -> Session:
317
+ """
318
+ Await the next inbound session (optionally bounded by timeout).
319
+
320
+ Returns:
321
+ Session: Wrapper for the accepted session context.
322
+ """
323
+
324
+ ctx: SessionContext = await self._app.listen_for_session(timeout)
325
+ return Session(ctx)
@@ -0,0 +1,38 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from slim_bindings._slim_bindings import ( # type: ignore[attr-defined]
5
+ __version__,
6
+ build_info,
7
+ build_profile,
8
+ )
9
+
10
+
11
+ def get_version():
12
+ """
13
+ Get the version of the SLIM bindings.
14
+
15
+ Returns:
16
+ str: The version of the SLIM bindings.
17
+ """
18
+ return __version__
19
+
20
+
21
+ def get_build_profile():
22
+ """
23
+ Get the build profile of the SLIM bindings.
24
+
25
+ Returns:
26
+ str: The build profile of the SLIM bindings.
27
+ """
28
+ return build_profile
29
+
30
+
31
+ def get_build_info():
32
+ """
33
+ Get the build information of the SLIM bindings.
34
+
35
+ Returns:
36
+ str: The build information of the SLIM bindings.
37
+ """
38
+ return build_info