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/__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
|