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/config.py ADDED
@@ -0,0 +1,298 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ Pydantic configuration models for SLIM examples.
5
+
6
+ This module provides a centralized configuration system using Pydantic,
7
+ supporting:
8
+ - Environment variable loading
9
+ - Config file loading (JSON, YAML, TOML)
10
+ - Type validation
11
+ - Sensible defaults
12
+ - Documentation via field descriptions
13
+ """
14
+
15
+ import os
16
+ from enum import Enum
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from pydantic import Field, field_validator
21
+ from pydantic_settings import BaseSettings, SettingsConfigDict
22
+
23
+
24
+ class AuthMode(str, Enum):
25
+ """Authentication mode for SLIM applications."""
26
+
27
+ SHARED_SECRET = "shared_secret"
28
+ JWT = "jwt"
29
+ SPIRE = "spire"
30
+
31
+
32
+ class BaseConfig(BaseSettings):
33
+ """
34
+ Base configuration shared across all SLIM examples.
35
+
36
+ Environment variables are prefixed with SLIM_ by default.
37
+ Example: SLIM_LOCAL=org/ns/app
38
+ """
39
+
40
+ model_config = SettingsConfigDict(
41
+ env_prefix="SLIM_",
42
+ env_file=".env",
43
+ env_file_encoding="utf-8",
44
+ case_sensitive=False,
45
+ extra="ignore",
46
+ )
47
+
48
+ # Core identity settings
49
+ local: str = Field(
50
+ ...,
51
+ description="Local ID in the format organization/namespace/application",
52
+ )
53
+
54
+ remote: str | None = Field(
55
+ None,
56
+ description="Remote ID in the format organization/namespace/application-or-stream",
57
+ )
58
+
59
+ # Service connection
60
+ slim: str = Field(
61
+ default="http://127.0.0.1:46357",
62
+ description="SLIM remote endpoint URL",
63
+ )
64
+
65
+ # Feature flags
66
+ enable_opentelemetry: bool = Field(
67
+ default=False,
68
+ description="Enable OpenTelemetry tracing",
69
+ )
70
+
71
+ enable_mls: bool = Field(
72
+ default=False,
73
+ description="Enable MLS (Message Layer Security) for sessions",
74
+ )
75
+
76
+ # Shared secret authentication (default, not for production)
77
+ shared_secret: str = Field(
78
+ default="abcde-12345-fedcb-67890-deadc",
79
+ description="Shared secret for authentication (development only)",
80
+ )
81
+
82
+ # JWT authentication
83
+ jwt: str | None = Field(
84
+ None,
85
+ description="Path to static JWT token file for authentication",
86
+ )
87
+
88
+ spire_trust_bundle: str | None = Field(
89
+ None,
90
+ description="Path to SPIRE trust bundle file (for JWT + JWKS mode)",
91
+ )
92
+
93
+ audience: list[str] | None = Field(
94
+ None,
95
+ description="Audience list for JWT verification",
96
+ )
97
+
98
+ # SPIRE dynamic identity
99
+ spire_socket_path: str | None = Field(
100
+ None,
101
+ description="SPIRE Workload API socket path",
102
+ )
103
+
104
+ spire_target_spiffe_id: str | None = Field(
105
+ None,
106
+ description="Target SPIFFE ID to request from SPIRE",
107
+ )
108
+
109
+ spire_jwt_audience: list[str] | None = Field(
110
+ default=None,
111
+ description="Audience(s) for SPIRE JWT SVID requests",
112
+ )
113
+
114
+ @field_validator("audience", mode="before")
115
+ @classmethod
116
+ def parse_audience(cls, v: Any) -> list[str] | None:
117
+ """Parse audience from comma-separated string or list."""
118
+ if v is None:
119
+ return None
120
+ if isinstance(v, str):
121
+ return [a.strip() for a in v.split(",") if a.strip()]
122
+ return v
123
+
124
+ @field_validator("jwt", "spire_trust_bundle", mode="after")
125
+ @classmethod
126
+ def validate_file_paths(cls, v: str | None) -> str | None:
127
+ """Validate that file paths exist if provided."""
128
+ if v is not None and not Path(v).exists():
129
+ raise ValueError(f"File not found: {v}")
130
+ return v
131
+
132
+ def get_auth_mode(self) -> AuthMode:
133
+ """Determine which authentication mode to use based on provided config."""
134
+ if (
135
+ self.spire_socket_path
136
+ or self.spire_target_spiffe_id
137
+ or self.spire_jwt_audience
138
+ ):
139
+ return AuthMode.SPIRE
140
+ elif self.jwt and self.spire_trust_bundle and self.audience:
141
+ return AuthMode.JWT
142
+ else:
143
+ return AuthMode.SHARED_SECRET
144
+
145
+ @classmethod
146
+ def from_args(cls, **kwargs: Any) -> "BaseConfig":
147
+ """
148
+ Create config from keyword arguments.
149
+
150
+ This allows programmatic creation while still validating fields.
151
+ """
152
+ # Filter out None values to let defaults take effect
153
+ filtered = {k: v for k, v in kwargs.items() if v is not None}
154
+ return cls(**filtered)
155
+
156
+ @classmethod
157
+ def from_file(cls, config_path: str) -> "BaseConfig":
158
+ """
159
+ Load configuration from a file (JSON, YAML, or TOML).
160
+
161
+ Args:
162
+ config_path: Path to configuration file
163
+
164
+ Returns:
165
+ Config instance with loaded values
166
+ """
167
+ path = Path(config_path)
168
+ if not path.exists():
169
+ raise FileNotFoundError(f"Config file not found: {config_path}")
170
+
171
+ content = path.read_text()
172
+ suffix = path.suffix.lower()
173
+
174
+ if suffix == ".json":
175
+ import json
176
+
177
+ data = json.loads(content)
178
+ elif suffix in (".yaml", ".yml"):
179
+ try:
180
+ import yaml # type: ignore[import-untyped]
181
+
182
+ data = yaml.safe_load(content)
183
+ except ImportError:
184
+ raise ImportError(
185
+ "PyYAML is required for YAML config files. Install with: pip install pyyaml"
186
+ )
187
+ elif suffix == ".toml":
188
+ try:
189
+ import tomli # type: ignore[import-untyped]
190
+
191
+ data = tomli.loads(content)
192
+ except ImportError:
193
+ raise ImportError(
194
+ "tomli is required for TOML config files. Install with: pip install tomli"
195
+ )
196
+ else:
197
+ raise ValueError(f"Unsupported config file format: {suffix}")
198
+
199
+ return cls(**data)
200
+
201
+
202
+ class GroupConfig(BaseConfig):
203
+ """Configuration specific to group messaging examples."""
204
+
205
+ invites: list[str] | None = Field(
206
+ default=None,
207
+ description="List of participant IDs to invite to the group session",
208
+ )
209
+
210
+ @field_validator("invites", mode="before")
211
+ @classmethod
212
+ def parse_invites(cls, v: Any) -> list[str] | None:
213
+ """Parse invites from comma-separated string or list."""
214
+ if v is None:
215
+ return None
216
+ if isinstance(v, str):
217
+ return [i.strip() for i in v.split(",") if i.strip()]
218
+ return v
219
+
220
+
221
+ class PointToPointConfig(BaseConfig):
222
+ """Configuration specific to point-to-point messaging examples."""
223
+
224
+ message: str | None = Field(
225
+ None,
226
+ description="Message to send (activates sender mode)",
227
+ )
228
+
229
+ iterations: int = Field(
230
+ default=10,
231
+ description="Number of request/reply cycles in sender mode",
232
+ ge=1,
233
+ )
234
+
235
+
236
+ class ServerConfig(BaseSettings):
237
+ """Configuration for SLIM server."""
238
+
239
+ model_config = SettingsConfigDict(
240
+ env_prefix="SLIM_",
241
+ env_file=".env",
242
+ env_file_encoding="utf-8",
243
+ case_sensitive=False,
244
+ extra="ignore",
245
+ )
246
+
247
+ slim: str = Field(
248
+ default="127.0.0.1:12345",
249
+ description="SLIM server address (host:port)",
250
+ )
251
+
252
+ enable_opentelemetry: bool = Field(
253
+ default=False,
254
+ description="Enable OpenTelemetry tracing",
255
+ )
256
+
257
+ @classmethod
258
+ def from_args(cls, **kwargs: Any) -> "ServerConfig":
259
+ """Create config from keyword arguments."""
260
+ filtered = {k: v for k, v in kwargs.items() if v is not None}
261
+ return cls(**filtered)
262
+
263
+
264
+ def load_config_with_cli_override(
265
+ config_class: type[BaseSettings], cli_args: dict[str, Any]
266
+ ) -> BaseSettings:
267
+ """
268
+ Load configuration with CLI argument override priority.
269
+
270
+ Priority order (highest to lowest):
271
+ 1. CLI arguments (non-None values)
272
+ 2. Environment variables
273
+ 3. Config file (if SLIM_CONFIG_FILE env var is set)
274
+ 4. Defaults
275
+
276
+ Args:
277
+ config_class: The Pydantic settings class to instantiate
278
+ cli_args: Dictionary of CLI arguments
279
+
280
+ Returns:
281
+ Configured settings instance
282
+ """
283
+ # Check if config file is specified
284
+ config_file = os.getenv("SLIM_CONFIG_FILE")
285
+
286
+ if config_file and hasattr(config_class, "from_file"):
287
+ # Load from file first
288
+ config = config_class.from_file(config_file)
289
+ # Override with CLI args
290
+ if cli_args:
291
+ filtered_cli = {k: v for k, v in cli_args.items() if v is not None}
292
+ config = config_class(**{**config.model_dump(), **filtered_cli})
293
+ else:
294
+ # Load from env vars and CLI args
295
+ filtered_cli = {k: v for k, v in cli_args.items() if v is not None}
296
+ config = config_class(**filtered_cli)
297
+
298
+ return config
@@ -0,0 +1,44 @@
1
+ # Copyright AGNTCY Contributors (https://github.com/agntcy)
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # Example YAML configuration for SLIM examples
5
+ # Usage: SLIM_CONFIG_FILE=example-config.yaml slim-bindings-group
6
+
7
+ # Core identity settings (required)
8
+ local: "org/namespace/my-app"
9
+ remote: "org/namespace/remote-app" # Optional for some examples
10
+
11
+ # Service connection
12
+ slim: "http://127.0.0.1:46357"
13
+
14
+ # Feature flags
15
+ enable_opentelemetry: false
16
+ enable_mls: false
17
+
18
+ # Authentication options (choose one)
19
+ # Option 1: Shared secret (default, development only)
20
+ shared_secret: "abcde-12345-fedcb-67890-deadc"
21
+
22
+ # Option 2: JWT with JWKS (uncomment to use)
23
+ # jwt: "/path/to/jwt-token.pem"
24
+ # spire_trust_bundle: "/path/to/spire-bundle.json"
25
+ # audience:
26
+ # - "audience1"
27
+ # - "audience2"
28
+
29
+ # Option 3: SPIRE dynamic identity (uncomment to use)
30
+ # spire_socket_path: "/run/spire/sockets/agent.sock"
31
+ # spire_target_spiffe_id: "spiffe://example.org/my-service"
32
+ # spire_jwt_audience:
33
+ # - "slim-service"
34
+ # - "other-service"
35
+
36
+ # Group-specific options (for group.py)
37
+ invites:
38
+ - "org/namespace/peer1"
39
+ - "org/namespace/peer2"
40
+ - "org/namespace/peer3"
41
+
42
+ # Point-to-point specific options (for point_to_point.py)
43
+ message: "Hello from SLIM!"
44
+ iterations: 10