alternator-client 1.0.0__tar.gz

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.
Files changed (30) hide show
  1. alternator_client-1.0.0/PKG-INFO +439 -0
  2. alternator_client-1.0.0/README.md +398 -0
  3. alternator_client-1.0.0/alternator/__init__.py +159 -0
  4. alternator_client-1.0.0/alternator/_constants.py +8 -0
  5. alternator_client-1.0.0/alternator/_http.py +196 -0
  6. alternator_client-1.0.0/alternator/_version.py +3 -0
  7. alternator_client-1.0.0/alternator/async_client.py +480 -0
  8. alternator_client-1.0.0/alternator/client.py +468 -0
  9. alternator_client-1.0.0/alternator/config.py +451 -0
  10. alternator_client-1.0.0/alternator/core/__init__.py +27 -0
  11. alternator_client-1.0.0/alternator/core/compression.py +56 -0
  12. alternator_client-1.0.0/alternator/core/go_rand.py +331 -0
  13. alternator_client-1.0.0/alternator/core/handlers.py +127 -0
  14. alternator_client-1.0.0/alternator/core/hashing.py +174 -0
  15. alternator_client-1.0.0/alternator/core/headers.py +98 -0
  16. alternator_client-1.0.0/alternator/core/key_affinity.py +263 -0
  17. alternator_client-1.0.0/alternator/core/live_nodes.py +455 -0
  18. alternator_client-1.0.0/alternator/core/query_plan.py +51 -0
  19. alternator_client-1.0.0/alternator/core/request.py +60 -0
  20. alternator_client-1.0.0/alternator/core/routing_scope.py +103 -0
  21. alternator_client-1.0.0/alternator/core/tls.py +55 -0
  22. alternator_client-1.0.0/alternator/exceptions.py +85 -0
  23. alternator_client-1.0.0/alternator/py.typed +0 -0
  24. alternator_client-1.0.0/alternator_client.egg-info/PKG-INFO +439 -0
  25. alternator_client-1.0.0/alternator_client.egg-info/SOURCES.txt +28 -0
  26. alternator_client-1.0.0/alternator_client.egg-info/dependency_links.txt +1 -0
  27. alternator_client-1.0.0/alternator_client.egg-info/requires.txt +18 -0
  28. alternator_client-1.0.0/alternator_client.egg-info/top_level.txt +1 -0
  29. alternator_client-1.0.0/pyproject.toml +131 -0
  30. alternator_client-1.0.0/setup.cfg +4 -0
@@ -0,0 +1,439 @@
1
+ Metadata-Version: 2.4
2
+ Name: alternator-client
3
+ Version: 1.0.0
4
+ Summary: Client-side load balancing for ScyllaDB Alternator
5
+ Author-email: ScyllaDB <info@scylladb.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/scylladb/alternator-client-python
8
+ Project-URL: Documentation, https://github.com/scylladb/alternator-client-python#readme
9
+ Project-URL: Repository, https://github.com/scylladb/alternator-client-python
10
+ Project-URL: Issues, https://github.com/scylladb/alternator-client-python/issues
11
+ Keywords: scylladb,alternator,dynamodb,load-balancing,boto3
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: boto3>=1.28.0
26
+ Requires-Dist: botocore>=1.31.0
27
+ Provides-Extra: async
28
+ Requires-Dist: aioboto3>=12.0.0; extra == "async"
29
+ Requires-Dist: aiobotocore>=2.5.0; extra == "async"
30
+ Requires-Dist: aiohttp>=3.8.0; extra == "async"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0; extra == "dev"
33
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
34
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
35
+ Requires-Dist: pytest-timeout>=2.0; extra == "dev"
36
+ Requires-Dist: hypothesis>=6.0; extra == "dev"
37
+ Requires-Dist: mypy>=1.0; extra == "dev"
38
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
39
+ Requires-Dist: types-aiobotocore[dynamodb]>=2.5.0; extra == "dev"
40
+ Requires-Dist: boto3-stubs[dynamodb]>=1.28.0; extra == "dev"
41
+
42
+ # Alternator Load Balancing Client for Python
43
+
44
+ A Python library that provides client-side load balancing for [ScyllaDB Alternator](https://docs.scylladb.com/stable/alternator/), wrapping boto3/aioboto3 to transparently distribute requests across cluster nodes.
45
+
46
+ ## Features
47
+
48
+ - **Automatic Load Balancing**: Distributes requests across all available Alternator nodes using round-robin selection
49
+ - **Node Discovery**: Automatically discovers cluster topology via the `/localnodes` endpoint
50
+ - **Topology Awareness**: Route requests to specific datacenters or racks
51
+ - **Key Affinity Routing**: Optimizes LWT (Lightweight Transaction) operations by routing requests for the same partition key to the same node
52
+ - **Request Compression**: Optional gzip compression to reduce bandwidth
53
+ - **Header Optimization**: Filters unnecessary headers to reduce request overhead
54
+ - **TLS Support**: Full TLS/SSL support with custom CA certificates
55
+ - **Async Support**: Full async/await support via aioboto3
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ # Basic installation (sync client only)
61
+ pip install alternator-client
62
+
63
+ # With async support
64
+ pip install alternator-client[async]
65
+ ```
66
+
67
+ > **Note:** The PyPI package name is `alternator-client`, but the Python import remains `alternator`.
68
+
69
+ ## Quick Start
70
+
71
+ ### Synchronous Client
72
+
73
+ ```python
74
+ from alternator import AlternatorConfig, AlternatorClient
75
+
76
+ # Configure the client
77
+ config = AlternatorConfig(
78
+ seed_hosts=["192.168.1.1", "192.168.1.2"],
79
+ port=8000,
80
+ )
81
+
82
+ # Use as a context manager (recommended)
83
+ with AlternatorClient(config) as client:
84
+ # Use like a normal boto3 DynamoDB client
85
+ response = client.list_tables()
86
+ print(response["TableNames"])
87
+
88
+ # Put an item
89
+ client.put_item(
90
+ TableName="my_table",
91
+ Item={
92
+ "pk": {"S": "user123"},
93
+ "data": {"S": "Hello, World!"},
94
+ }
95
+ )
96
+ ```
97
+
98
+ ### Asynchronous Client
99
+
100
+ ```python
101
+ import asyncio
102
+ from alternator import AlternatorConfig
103
+ from alternator.async_client import AsyncAlternatorClient
104
+
105
+ async def main():
106
+ config = AlternatorConfig(
107
+ seed_hosts=["192.168.1.1"],
108
+ port=8000,
109
+ )
110
+
111
+ async with AsyncAlternatorClient(config) as client:
112
+ # Use like a normal aioboto3 DynamoDB client
113
+ response = await client.list_tables()
114
+ print(response["TableNames"])
115
+
116
+ asyncio.run(main())
117
+ ```
118
+
119
+ ## Configuration
120
+
121
+ ### Basic Configuration
122
+
123
+ ```python
124
+ from alternator import AlternatorConfig
125
+
126
+ config = AlternatorConfig(
127
+ seed_hosts=["node1.example.com", "node2.example.com"],
128
+ port=8000,
129
+ scheme="http", # or "https" for TLS
130
+ )
131
+ ```
132
+
133
+ ### Using the Builder Pattern
134
+
135
+ ```python
136
+ from alternator import (
137
+ AlternatorConfigBuilder,
138
+ CompressionAlgorithm,
139
+ KeyRouteAffinityMode,
140
+ TlsConfig,
141
+ )
142
+
143
+ config = (
144
+ AlternatorConfigBuilder()
145
+ .with_seeds("node1.example.com", "node2.example.com")
146
+ .with_port(8000)
147
+ .with_https(TlsConfig.system_default())
148
+ .with_datacenter("us-east-1")
149
+ .with_compression(CompressionAlgorithm.GZIP, min_size=1024)
150
+ .with_key_affinity(KeyRouteAffinityMode.RMW)
151
+ .with_refresh_intervals(active_ms=1000, idle_ms=60000)
152
+ .build()
153
+ )
154
+ ```
155
+
156
+ ### Configuration Options
157
+
158
+ | Option | Type | Default | Description |
159
+ |--------|------|---------|-------------|
160
+ | `seed_hosts` | `Sequence[str]` | (required) | Initial nodes for cluster discovery |
161
+ | `port` | `int` | (required) | Alternator port |
162
+ | `scheme` | `str` | `"http"` | Protocol scheme (`"http"` or `"https"`) |
163
+ | `routing_scope` | `RoutingScope` | `ClusterScope()` | Topology-aware routing |
164
+ | `compression` | `CompressionAlgorithm` | `NONE` | Request compression |
165
+ | `min_compression_size_bytes` | `int` | `1024` | Minimum body size to compress |
166
+ | `optimize_headers` | `bool` | `False` | Enable header filtering |
167
+ | `headers_whitelist` | `frozenset[str]` | `None` | Additional headers to keep |
168
+ | `authentication_enabled` | `bool` | `True` | Include auth headers |
169
+ | `tls` | `TlsConfig` | system default | TLS configuration |
170
+ | `key_affinity` | `KeyRouteAffinityConfig` | `NONE` | Key-based routing |
171
+ | `max_pool_connections` | `int` | `200` | Max connections per host |
172
+ | `active_refresh_interval_ms` | `int` | `1000` | Node refresh interval when active |
173
+ | `idle_refresh_interval_ms` | `int` | `60000` | Node refresh interval when idle |
174
+
175
+ ## Routing Scopes
176
+
177
+ Control which nodes receive your requests based on topology:
178
+
179
+ ```python
180
+ from alternator import ClusterScope, DatacenterScope, RackScope
181
+
182
+ # Route to any node in the cluster (default)
183
+ config = AlternatorConfig(
184
+ seed_hosts=["node1"],
185
+ port=8000,
186
+ routing_scope=ClusterScope(),
187
+ )
188
+
189
+ # Route to nodes in a specific datacenter
190
+ config = AlternatorConfig(
191
+ seed_hosts=["node1"],
192
+ port=8000,
193
+ routing_scope=DatacenterScope(datacenter="us-east-1"),
194
+ )
195
+
196
+ # Route to nodes in a specific rack (with fallback)
197
+ config = AlternatorConfig(
198
+ seed_hosts=["node1"],
199
+ port=8000,
200
+ routing_scope=RackScope(datacenter="us-east-1", rack="rack1"),
201
+ )
202
+ ```
203
+
204
+ Scopes automatically fall back to broader scopes if no nodes are available:
205
+ - `RackScope` → `DatacenterScope` → `ClusterScope`
206
+
207
+ ## Key Affinity (LWT Optimization)
208
+
209
+ For Lightweight Transactions (conditional writes), routing requests for the same partition key to the same node can improve performance:
210
+
211
+ ```python
212
+ from alternator import (
213
+ AlternatorConfigBuilder,
214
+ KeyRouteAffinityMode,
215
+ )
216
+
217
+ config = (
218
+ AlternatorConfigBuilder()
219
+ .with_seeds("node1")
220
+ .with_port(8000)
221
+ .with_key_affinity(
222
+ mode=KeyRouteAffinityMode.RMW, # Only for read-modify-write ops
223
+ table_pk_map={"my_table": "pk"}, # Optional: preload PK names
224
+ )
225
+ .build()
226
+ )
227
+ ```
228
+
229
+ ### Affinity Modes
230
+
231
+ | Mode | Description |
232
+ |------|-------------|
233
+ | `NONE` | Disabled (default round-robin) |
234
+ | `RMW` | Only for operations with `ConditionExpression` or `ReturnValues` |
235
+ | `ANY_WRITE` | For all write operations (`PutItem`, `UpdateItem`, `DeleteItem`) |
236
+
237
+ ## TLS Configuration
238
+
239
+ ```python
240
+ from alternator import TlsConfig, TlsSessionCacheConfig
241
+ from pathlib import Path
242
+
243
+ # Use system CA certificates (default)
244
+ tls = TlsConfig.system_default()
245
+
246
+ # Use custom CA certificate
247
+ tls = TlsConfig.with_custom_ca(Path("/path/to/ca.pem"))
248
+
249
+ # Trust all certificates (INSECURE - dev only)
250
+ tls = TlsConfig.trust_all()
251
+
252
+ # Full configuration
253
+ tls = TlsConfig(
254
+ custom_ca_cert_paths=[Path("/path/to/ca.pem")],
255
+ trust_system_ca_certs=True,
256
+ verify_hostname=True,
257
+ session_cache=TlsSessionCacheConfig(
258
+ enabled=True,
259
+ cache_size=1024,
260
+ timeout_seconds=86400,
261
+ ),
262
+ )
263
+ ```
264
+
265
+ ## Request Compression
266
+
267
+ Enable gzip compression for large request bodies:
268
+
269
+ > **Note:** Gzip request compression requires **ScyllaDB 2026.1.0 or later**. Earlier versions do not support the `Content-Encoding: gzip` header. Response compression (`Accept-Encoding: gzip`) is not yet supported by Alternator.
270
+
271
+ ```python
272
+ from alternator import AlternatorConfigBuilder, CompressionAlgorithm
273
+
274
+ config = (
275
+ AlternatorConfigBuilder()
276
+ .with_seeds("node1")
277
+ .with_port(8000)
278
+ .with_compression(
279
+ CompressionAlgorithm.GZIP,
280
+ min_size=1024, # Only compress bodies >= 1KB
281
+ )
282
+ .build()
283
+ )
284
+ ```
285
+
286
+ ## Error Handling
287
+
288
+ ```python
289
+ from alternator import (
290
+ AlternatorClient,
291
+ AlternatorConfig,
292
+ AlternatorError,
293
+ NoNodesAvailableError,
294
+ ConfigurationError,
295
+ )
296
+
297
+ try:
298
+ config = AlternatorConfig(seed_hosts=[], port=8000)
299
+ except ConfigurationError as e:
300
+ print(f"Invalid configuration: {e}")
301
+
302
+ try:
303
+ with AlternatorClient(config) as client:
304
+ client.list_tables()
305
+ except NoNodesAvailableError as e:
306
+ print(f"No nodes available: {e}")
307
+ except AlternatorError as e:
308
+ print(f"Alternator error: {e}")
309
+ ```
310
+
311
+ ## Logging
312
+
313
+ The library uses Python's standard logging module with the logger name `alternator`:
314
+
315
+ ```python
316
+ import logging
317
+
318
+ # Enable debug logging
319
+ logging.basicConfig(level=logging.DEBUG)
320
+ logging.getLogger("alternator").setLevel(logging.DEBUG)
321
+ ```
322
+
323
+ Log levels:
324
+ - `INFO`: Node discovery events
325
+ - `WARNING`: Fallback events, connection issues
326
+ - `DEBUG`: Detailed routing decisions, node lists
327
+ - `ERROR`: Failed operations
328
+
329
+ ## DynamoDB Resource Interface
330
+
331
+ For table-oriented operations, use `AlternatorResource` which wraps boto3's DynamoDB resource:
332
+
333
+ ```python
334
+ from alternator import AlternatorConfig, AlternatorResource
335
+
336
+ config = AlternatorConfig(seed_hosts=["192.168.1.1"], port=8000)
337
+
338
+ with AlternatorResource(config) as resource:
339
+ table = resource.Table("my_table")
340
+ table.put_item(Item={"pk": "user123", "data": "hello"})
341
+ response = table.get_item(Key={"pk": "user123"})
342
+ ```
343
+
344
+ You can also use the factory function:
345
+
346
+ ```python
347
+ from alternator import create_resource, close_resource, AlternatorConfig
348
+
349
+ config = AlternatorConfig(seed_hosts=["node1"], port=8000)
350
+ resource = create_resource(config)
351
+
352
+ try:
353
+ table = resource.Table("my_table")
354
+ table.scan()
355
+ finally:
356
+ close_resource(resource)
357
+ ```
358
+
359
+ ## Manual Resource Management
360
+
361
+ If you prefer not to use context managers:
362
+
363
+ ```python
364
+ from alternator import create_client, close_client, AlternatorConfig
365
+
366
+ config = AlternatorConfig(seed_hosts=["node1"], port=8000)
367
+ client = create_client(config)
368
+
369
+ try:
370
+ client.list_tables()
371
+ finally:
372
+ close_client(client) # Stop background refresh thread
373
+ ```
374
+
375
+ Async equivalent:
376
+
377
+ ```python
378
+ from alternator import AlternatorConfig
379
+ from alternator.async_client import create_async_client, close_async_client
380
+
381
+ config = AlternatorConfig(seed_hosts=["node1"], port=8000)
382
+ client = await create_async_client(config)
383
+
384
+ try:
385
+ await client.list_tables()
386
+ finally:
387
+ await close_async_client(client)
388
+ ```
389
+
390
+ ## Production Recommendations
391
+
392
+ - **Connection pool sizing**: The default `max_pool_connections=200` works for most workloads. Increase if you see connection pool exhaustion warnings under high concurrency.
393
+ - **Refresh intervals**: Default active refresh (1s) is appropriate for dynamic clusters. For stable clusters, increase `active_refresh_interval_ms` to reduce discovery overhead.
394
+ - **Timeouts**: Default `discovery_timeout_seconds=5.0` and `read_timeout_seconds=30.0` are conservative. Tune based on your network latency and query complexity.
395
+ - **Monitoring**: Enable `INFO`-level logging for the `alternator` logger to track node discovery events. Use `DEBUG` for detailed routing decisions during troubleshooting.
396
+ - **Seed hosts**: Configure at least 2-3 seed hosts for redundancy in case one seed is temporarily unavailable during startup.
397
+
398
+ ## Thread Safety
399
+
400
+ Sync clients created by `create_client` / `AlternatorClient` are thread-safe: the underlying node selection, round-robin counter, and node list updates are all protected by locks. You can safely share a single client across multiple threads.
401
+
402
+ Async clients created by `create_async_client` / `AsyncAlternatorClient` are safe to use from multiple concurrent coroutines within the same event loop. Do not share an async client across different event loops.
403
+
404
+ ## Known Limitations
405
+
406
+ - **Request Compression**: Gzip compression requires ScyllaDB 2026.1.0+. Response compression is not yet supported by Alternator.
407
+ - **TLS Session Cache Settings**: The `cache_size` and `timeout_seconds` parameters in `TlsSessionCacheConfig` are not currently used by Python's `ssl` module. Only the `enabled` flag controls session ticket behavior.
408
+ - **Async Key Affinity**: For async clients, partition key auto-discovery happens asynchronously. The first request for an unknown table will use round-robin routing while discovery runs in the background. Subsequent requests will use affinity. Preloading via `table_pk_map` avoids this initial miss.
409
+ - **Batch Operations**: Key affinity routing does not support `BatchWriteItem` operations with items targeting different partition keys.
410
+
411
+ ## Development
412
+
413
+ ```bash
414
+ # Clone the repository
415
+ git clone https://github.com/scylladb/alternator-client-python.git
416
+ cd alternator-client-python
417
+
418
+ # Install in development mode
419
+ make install
420
+
421
+ # Run tests
422
+ make test-unit
423
+
424
+ # Run linting
425
+ make lint
426
+
427
+ # Start local Scylla cluster for integration tests
428
+ make scylla-start
429
+ make test-integration
430
+ make scylla-stop
431
+ ```
432
+
433
+ ## License
434
+
435
+ Apache License 2.0
436
+
437
+ ## Contributing
438
+
439
+ Contributions are welcome! Please read the contributing guidelines before submitting a pull request.