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 ADDED
@@ -0,0 +1,44 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """
5
+ Examples package for SLIM Python bindings.
6
+
7
+ This package provides example applications demonstrating SLIM functionality:
8
+ - Group messaging (group.py)
9
+ - Point-to-point messaging (point_to_point.py)
10
+ - SLIM server (slim.py)
11
+
12
+ All examples use Pydantic for configuration, supporting:
13
+ - Command-line arguments
14
+ - Environment variables (SLIM_* prefix)
15
+ - Configuration files (JSON, YAML, TOML via SLIM_CONFIG_FILE env var)
16
+
17
+ To install dependencies:
18
+ pip install 'slim-bindings[examples]'
19
+ """
20
+
21
+ import sys
22
+ from importlib.util import find_spec
23
+
24
+ # Check for required dependencies
25
+ _missing_deps = []
26
+
27
+ if find_spec("pydantic") is None:
28
+ _missing_deps.append("pydantic")
29
+
30
+ if find_spec("pydantic_settings") is None:
31
+ _missing_deps.append("pydantic-settings")
32
+
33
+ if find_spec("prompt_toolkit") is None:
34
+ _missing_deps.append("prompt-toolkit")
35
+
36
+ if _missing_deps:
37
+ print(
38
+ f"Missing required dependencies: {', '.join(_missing_deps)}\n"
39
+ "Install them with: pip install 'slim-bindings[examples]'",
40
+ file=sys.stderr,
41
+ )
42
+ sys.exit(1)
43
+
44
+ __all__ = ["config", "common", "group", "point_to_point", "slim"]
examples/common.py ADDED
@@ -0,0 +1,411 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ Shared helper utilities for the slim_bindings CLI examples.
5
+
6
+ This module centralizes:
7
+ * Pretty-print / color formatting helpers
8
+ * Identity (auth) helper constructors (shared secret / JWT / JWKS / SPIRE)
9
+ * Convenience coroutine for constructing a local Slim app using global service
10
+ * Configuration parsing utilities
11
+
12
+ The heavy inline commenting is intentional: it is meant to teach newcomers
13
+ exactly what each step does, line by line.
14
+ """
15
+
16
+ import argparse
17
+ import base64 # Used to decode base64-encoded JWKS content (when provided).
18
+ import datetime # Used for timedelta in JWT configs
19
+ import json # Used for parsing JWKS JSON and dynamic option values.
20
+ from typing import Any
21
+
22
+ import slim_bindings # The Python bindings package we are demonstrating.
23
+
24
+ from .config import AuthMode, BaseConfig
25
+
26
+
27
+ class color:
28
+ """ANSI escape sequences for terminal styling."""
29
+
30
+ PURPLE = "\033[95m"
31
+ CYAN = "\033[96m"
32
+ DARKCYAN = "\033[36m"
33
+ BLUE = "\033[94m"
34
+ GREEN = "\033[92m"
35
+ YELLOW = "\033[93m"
36
+ RED = "\033[91m"
37
+ BOLD = "\033[1m"
38
+ UNDERLINE = "\033[4m"
39
+ END = "\033[0m"
40
+
41
+
42
+ def format_message(message1: str, message2: str = "") -> str:
43
+ """
44
+ Format a message for display with bold/cyan prefix column and optional suffix.
45
+
46
+ Args:
47
+ message1: Primary label (left column, capitalized & padded).
48
+ message2: Optional trailing description/value.
49
+
50
+ Returns:
51
+ A colorized string ready to print.
52
+ """
53
+ return f"{color.BOLD}{color.CYAN}{message1.capitalize():<45}{color.END}{message2}"
54
+
55
+
56
+ def format_message_print(message1: str, message2: str = "") -> None:
57
+ """Print a formatted message using format_message()."""
58
+ print(format_message(message1, message2))
59
+
60
+
61
+ def split_id(id: str) -> slim_bindings.Name:
62
+ """
63
+ Split an ID of form organization/namespace/application (or channel).
64
+
65
+ Args:
66
+ id: String in the canonical 'org/namespace/app-or-stream' format.
67
+
68
+ Raises:
69
+ ValueError: If the id cannot be split into exactly three segments.
70
+
71
+ Returns:
72
+ Name: Constructed identity object.
73
+ """
74
+ try:
75
+ organization, namespace, app = id.split("/")
76
+ except ValueError as e:
77
+ print("Error: IDs must be in the format organization/namespace/app-or-stream.")
78
+ raise e
79
+ return slim_bindings.Name(organization, namespace, app)
80
+
81
+
82
+ def jwt_identity(
83
+ jwt_path: str,
84
+ spire_bundle_path: str,
85
+ local_name: str,
86
+ iss: str | None = None,
87
+ sub: str | None = None,
88
+ aud: list[str] | None = None,
89
+ ):
90
+ """
91
+ Construct a JWT provider and verifier from file inputs.
92
+
93
+ Process:
94
+ 1. Read a JSON structure containing (base64-encoded) JWKS data (a SPIRE
95
+ bundle with a JWKS for each trust domain).
96
+ 2. Decode & merge all JWKS entries.
97
+ 3. Create a JWT identity provider with static file JWT.
98
+ 4. Wrap merged JWKS JSON as JwtKeyConfig with RS256 & JWKS format.
99
+ 5. Build a JWT verifier using the JWKS-derived public key.
100
+ """
101
+ print(f"Using SPIRE bundle file: {spire_bundle_path}")
102
+
103
+ with open(spire_bundle_path) as sb:
104
+ spire_bundle_string = sb.read()
105
+
106
+ spire_bundle = json.loads(spire_bundle_string)
107
+
108
+ all_keys = []
109
+ for trust_domain, v in spire_bundle.items():
110
+ print(f"Processing trust domain: {trust_domain}")
111
+ try:
112
+ decoded_jwks = base64.b64decode(v)
113
+ jwks_json = json.loads(decoded_jwks)
114
+ if "keys" in jwks_json:
115
+ all_keys.extend(jwks_json["keys"])
116
+ print(f" Added {len(jwks_json['keys'])} keys from {trust_domain}")
117
+ else:
118
+ print(f" Warning: No 'keys' found in JWKS for {trust_domain}")
119
+ except (json.JSONDecodeError, UnicodeDecodeError, ValueError) as e:
120
+ raise RuntimeError(
121
+ f"Failed to process trust domain {trust_domain}: {e}"
122
+ ) from e
123
+
124
+ spire_jwks = json.dumps({"keys": all_keys})
125
+ print(
126
+ f"Combined JWKS contains {len(all_keys)} total keys from {len(spire_bundle)} trust domains"
127
+ )
128
+
129
+ # Read the static JWT file for signing
130
+ with open(jwt_path) as jwt_file:
131
+ jwt_content = jwt_file.read()
132
+
133
+ # Create encoding key config for JWT signing
134
+ encoding_key_config = slim_bindings.JwtKeyConfig(
135
+ algorithm=slim_bindings.JwtAlgorithm.RS256,
136
+ format=slim_bindings.JwtKeyFormat.PEM,
137
+ key=slim_bindings.JwtKeyData.DATA(value=jwt_content),
138
+ )
139
+
140
+ # Create provider config for JWT authentication
141
+ provider_config = slim_bindings.IdentityProviderConfig.JWT(
142
+ config=slim_bindings.ClientJwtAuth(
143
+ key=slim_bindings.JwtKeyType.ENCODING(key=encoding_key_config),
144
+ audience=aud or ["default-audience"],
145
+ issuer=iss or "default-issuer",
146
+ subject=sub or local_name,
147
+ duration=datetime.timedelta(seconds=3600),
148
+ )
149
+ )
150
+
151
+ # Create decoding key config for JWKS verification
152
+ decoding_key_config = slim_bindings.JwtKeyConfig(
153
+ algorithm=slim_bindings.JwtAlgorithm.RS256,
154
+ format=slim_bindings.JwtKeyFormat.JWKS,
155
+ key=slim_bindings.JwtKeyData.DATA(value=spire_jwks),
156
+ )
157
+
158
+ # Create verifier config
159
+ verifier_config = slim_bindings.IdentityVerifierConfig.JWT(
160
+ config=slim_bindings.JwtAuth(
161
+ key=slim_bindings.JwtKeyType.DECODING(key=decoding_key_config),
162
+ audience=aud or ["default-audience"],
163
+ issuer=iss or "default-issuer",
164
+ subject=sub,
165
+ duration=datetime.timedelta(seconds=3600),
166
+ )
167
+ )
168
+ return provider_config, verifier_config
169
+
170
+
171
+ def spire_identity(
172
+ socket_path: str | None,
173
+ target_spiffe_id: str | None,
174
+ jwt_audiences: list[str] | None,
175
+ ):
176
+ """
177
+ Construct a SPIRE-based dynamic identity provider and verifier.
178
+
179
+ Args:
180
+ socket_path: SPIRE Workload API socket path (optional).
181
+ target_spiffe_id: Specific SPIFFE ID to request (optional).
182
+ jwt_audiences: Audience list for JWT SVID requests (optional).
183
+ """
184
+ spire_config = slim_bindings.SpireConfig(
185
+ trust_domains=[],
186
+ socket_path=socket_path,
187
+ target_spiffe_id=target_spiffe_id,
188
+ jwt_audiences=list(jwt_audiences) if jwt_audiences else [],
189
+ )
190
+
191
+ provider_config = slim_bindings.IdentityProviderConfig.SPIRE(config=spire_config)
192
+ verifier_config = slim_bindings.IdentityVerifierConfig.SPIRE(config=spire_config)
193
+
194
+ return provider_config, verifier_config
195
+
196
+
197
+ def setup_service(enable_opentelemetry: bool = False) -> slim_bindings.Service:
198
+ # Initialize tracing and global state
199
+ tracing_config = slim_bindings.new_tracing_config()
200
+ runtime_config = slim_bindings.new_runtime_config()
201
+ service_config = slim_bindings.new_service_config()
202
+
203
+ tracing_config.log_level = "info"
204
+
205
+ if enable_opentelemetry:
206
+ # Note: OpenTelemetry configuration through config objects is complex
207
+ # For now, we'll just initialize with default tracing
208
+ # Users can set OTEL environment variables for full OTEL support
209
+ pass
210
+
211
+ slim_bindings.initialize_with_configs(
212
+ tracing_config=tracing_config,
213
+ runtime_config=runtime_config,
214
+ service_config=[service_config],
215
+ )
216
+
217
+ # Get the global service instance
218
+ service = slim_bindings.get_global_service()
219
+
220
+ return service
221
+
222
+
223
+ async def create_local_app(config: BaseConfig) -> tuple[slim_bindings.App, int]:
224
+ """
225
+ Build a Slim application instance using the global service.
226
+
227
+ Resolution precedence for auth:
228
+ 1. If SPIRE options provided -> SPIRE dynamic identity flow.
229
+ 2. Else if jwt + bundle + audience provided -> JWT/JWKS flow.
230
+ 3. Else -> shared secret (must be provided).
231
+
232
+ Args:
233
+ config: BaseConfig instance containing all configuration.
234
+
235
+ Returns:
236
+ tuple[App, int]: Slim application instance and connection ID.
237
+ """
238
+ # Initialize tracing and global state
239
+ service = setup_service()
240
+
241
+ # Convert local identifier to a strongly typed Name.
242
+ local_name = split_id(config.local)
243
+
244
+ client_config = slim_bindings.new_insecure_client_config(config.slim)
245
+ conn_id = await service.connect_async(client_config)
246
+
247
+ # Determine authentication mode
248
+ auth_mode = config.get_auth_mode()
249
+
250
+ if auth_mode == AuthMode.SPIRE:
251
+ print("Using SPIRE dynamic identity authentication.")
252
+ provider_config, verifier_config = spire_identity(
253
+ socket_path=config.spire_socket_path,
254
+ target_spiffe_id=config.spire_target_spiffe_id,
255
+ jwt_audiences=config.spire_jwt_audience,
256
+ )
257
+ local_app = service.create_app(local_name, provider_config, verifier_config)
258
+ elif auth_mode == AuthMode.JWT:
259
+ print("Using JWT + JWKS authentication.")
260
+ # These should always be set if auth_mode is JWT
261
+ if not config.jwt or not config.spire_trust_bundle:
262
+ raise ValueError(
263
+ "JWT and SPIRE trust bundle are required for JWT auth mode"
264
+ )
265
+ provider_config, verifier_config = jwt_identity(
266
+ config.jwt,
267
+ config.spire_trust_bundle,
268
+ str(local_name),
269
+ aud=config.audience,
270
+ )
271
+ local_app = service.create_app(local_name, provider_config, verifier_config)
272
+ else:
273
+ print("Using shared-secret authentication.")
274
+ local_app = service.create_app_with_secret(local_name, config.shared_secret)
275
+
276
+ # Provide feedback to user (instance numeric id).
277
+ format_message_print(f"{local_app.id()}", "Created app")
278
+
279
+ # Subscribe to the local name
280
+ await local_app.subscribe_async(local_name, conn_id)
281
+
282
+ return local_app, conn_id
283
+
284
+
285
+ def create_base_parser(description: str) -> argparse.ArgumentParser:
286
+ """
287
+ Create an ArgumentParser with common options for all examples.
288
+
289
+ Args:
290
+ description: Description for the command.
291
+
292
+ Returns:
293
+ Configured ArgumentParser instance.
294
+ """
295
+ parser = argparse.ArgumentParser(
296
+ description=description,
297
+ formatter_class=argparse.RawDescriptionHelpFormatter,
298
+ )
299
+
300
+ # Core identity settings
301
+ parser.add_argument(
302
+ "--local",
303
+ type=str,
304
+ required=True,
305
+ help="Local ID in the format organization/namespace/application",
306
+ )
307
+
308
+ parser.add_argument(
309
+ "--remote",
310
+ type=str,
311
+ help="Remote ID in the format organization/namespace/application-or-stream",
312
+ )
313
+
314
+ # Service connection
315
+ parser.add_argument(
316
+ "--slim",
317
+ type=str,
318
+ default="http://127.0.0.1:46357",
319
+ help="SLIM remote endpoint URL (default: http://127.0.0.1:46357)",
320
+ )
321
+
322
+ # Feature flags
323
+ parser.add_argument(
324
+ "--enable-opentelemetry",
325
+ action="store_true",
326
+ help="Enable OpenTelemetry tracing",
327
+ )
328
+
329
+ parser.add_argument(
330
+ "--enable-mls",
331
+ action="store_true",
332
+ help="Enable MLS (Message Layer Security) for sessions",
333
+ )
334
+
335
+ # Shared secret authentication
336
+ parser.add_argument(
337
+ "--shared-secret",
338
+ type=str,
339
+ default="abcde-12345-fedcb-67890-deadc",
340
+ help="Shared secret for authentication (development only)",
341
+ )
342
+
343
+ # JWT authentication
344
+ parser.add_argument(
345
+ "--jwt",
346
+ type=str,
347
+ help="Path to static JWT token file for authentication",
348
+ )
349
+
350
+ parser.add_argument(
351
+ "--spire-trust-bundle",
352
+ type=str,
353
+ help="Path to SPIRE trust bundle file (for JWT + JWKS mode)",
354
+ )
355
+
356
+ parser.add_argument(
357
+ "--audience",
358
+ type=str,
359
+ help="Audience for JWT verification (comma-separated)",
360
+ )
361
+
362
+ # SPIRE dynamic identity
363
+ parser.add_argument(
364
+ "--spire-socket-path",
365
+ type=str,
366
+ help="SPIRE Workload API socket path",
367
+ )
368
+
369
+ parser.add_argument(
370
+ "--spire-target-spiffe-id",
371
+ type=str,
372
+ help="Target SPIFFE ID to request from SPIRE",
373
+ )
374
+
375
+ parser.add_argument(
376
+ "--spire-jwt-audience",
377
+ type=str,
378
+ action="append",
379
+ dest="spire_jwt_audience",
380
+ help="Audience(s) for SPIRE JWT SVID requests (can be specified multiple times)",
381
+ )
382
+
383
+ # Config file
384
+ parser.add_argument(
385
+ "--config",
386
+ type=str,
387
+ help="Path to configuration file (JSON, YAML, or TOML)",
388
+ )
389
+
390
+ return parser
391
+
392
+
393
+ def parse_args_to_dict(args: argparse.Namespace) -> dict[str, Any]:
394
+ """
395
+ Convert argparse Namespace to dictionary, handling special parsing.
396
+
397
+ Args:
398
+ args: Parsed arguments from argparse.
399
+
400
+ Returns:
401
+ Dictionary of parsed arguments.
402
+ """
403
+ args_dict = vars(args)
404
+
405
+ # Parse audience from comma-separated string
406
+ if args_dict.get("audience") and isinstance(args_dict["audience"], str):
407
+ args_dict["audience"] = [
408
+ a.strip() for a in args_dict["audience"].split(",") if a.strip()
409
+ ]
410
+
411
+ return args_dict