A Python library that provides client-side load balancing for ScyllaDB Alternator, wrapping boto3/aioboto3 to transparently distribute requests across cluster nodes.
- Automatic Load Balancing: Distributes requests across all available Alternator nodes using round-robin selection
- Node Discovery: Automatically discovers cluster topology via the
/localnodesendpoint - Topology Awareness: Route requests to specific datacenters or racks
- Key Affinity Routing: Optimizes LWT (Lightweight Transaction) operations by routing requests for the same partition key to the same node
- Request Compression: Optional gzip request compression to reduce bandwidth
- Response Compression: Optional gzip/deflate response decompression
- Header Optimization: Filters unnecessary headers to reduce request overhead
- TLS Support: Full TLS/SSL support with custom CA certificates
- Async Support: Full async/await support via aioboto3
See docs/CAPABILITY_MATRIX.md for the current capability matrix and planned follow-up work.
# Basic installation (sync client only)
pip install alternator-client
# With async support
pip install alternator-client[async]Note: The PyPI package name is
alternator-client, but the Python import remainsalternator.
Use alternator.client(...) for the common synchronous case where a context
manager can own the SDK client and background node refresh.
Use create_client / close_client when the SDK client must be created in one
place and closed elsewhere. Use create_resource / close_resource for the
boto3 table-oriented resource interface.
Use Helper or AsyncHelper when one object should own client/resource
lifecycle and expose topology diagnostics such as node refresh, node inspection,
routing validation, and partition-key cache inspection.
For the common case, use the top-level alternator.client context manager.
Seeds are host names or IP addresses only; use port for the single Alternator
port.
import alternator
with alternator.client(
seeds=["192.168.1.1", "192.168.1.2"],
port=8000,
) as client:
response = client.list_tables()
print(response["TableNames"])from alternator import Config, AlternatorClient
# Configure the client
config = Config(
seed_hosts=["192.168.1.1", "192.168.1.2"],
port=8000,
)
# Use as a context manager (recommended)
with AlternatorClient(config) as client:
# Use like a normal boto3 DynamoDB client
response = client.list_tables()
print(response["TableNames"])
# Put an item
client.put_item(
TableName="my_table",
Item={
"pk": {"S": "user123"},
"data": {"S": "Hello, World!"},
}
)import asyncio
from alternator import Config
from alternator.async_client import AsyncAlternatorClient
async def main():
config = Config(
seed_hosts=["192.168.1.1"],
port=8000,
)
async with AsyncAlternatorClient(config) as client:
# Use like a normal aioboto3 DynamoDB client
response = await client.list_tables()
print(response["TableNames"])
asyncio.run(main())Use Helper when you need explicit lifecycle control or diagnostics in addition
to standard boto3 clients and resources.
from alternator import Config, Helper
config = Config(seed_hosts=["192.168.1.1", "192.168.1.2"], port=8000)
with Helper(config) as helper:
client = helper.client()
resource = helper.resource()
helper.update_live_nodes()
print(helper.get_nodes())
print(helper.next_node())
client.list_tables()
resource.Table("my_table").get_item(Key={"pk": "user123"})Async code can use AsyncHelper:
from alternator import Config
from alternator.async_client import AsyncHelper
config = Config(seed_hosts=["192.168.1.1"], port=8000)
async with AsyncHelper(config) as helper:
client = await helper.client()
await helper.update_live_nodes()
print(helper.get_nodes())
await client.list_tables()get_active_nodes() currently returns the live-node list, and
get_quarantined_nodes() returns an empty list because node health and
quarantine behavior are intentionally deferred.
from alternator import Config
config = Config(
seed_hosts=["node1.example.com", "node2.example.com"],
port=8000,
scheme="http", # or "https" for TLS
)Compatibility:
AlternatorConfigandTlsConfigremain available for existing callers, but are deprecated. PreferConfigandTLSfor new code. See docs/COMPATIBILITY_AND_RELEASE.md for compatibility and versioning decisions.
from alternator import (
AlternatorConfigBuilder,
CompressionAlgorithm,
KeyRouteAffinityMode,
ResponseCompression,
TLS,
)
config = (
AlternatorConfigBuilder()
.with_seeds("node1.example.com", "node2.example.com")
.with_port(8000)
.with_https(TLS.system_default())
.with_datacenter("us-east-1")
.with_compression(CompressionAlgorithm.GZIP, min_size=1024)
.with_response_compression(ResponseCompression.GZIP)
.with_key_affinity(KeyRouteAffinityMode.RMW)
.with_refresh_intervals(active_ms=1000, idle_ms=60000)
.build()
)| Option | Type | Default | Description |
|---|---|---|---|
seed_hosts |
Sequence[str] |
(required) | Initial nodes for cluster discovery |
port |
int |
(required) | Alternator port |
scheme |
str |
"http" |
Protocol scheme ("http" or "https") |
routing_scope |
RoutingScope |
ClusterScope() |
Topology-aware routing |
compression |
CompressionAlgorithm |
NONE |
Request compression |
min_compression_size_bytes |
int |
1024 |
Minimum body size to compress |
gzip_level |
int |
9 |
gzip compression level, 0 through 9 |
response_compression |
Sequence[ResponseCompression] |
empty | Accepted response compression encodings |
optimize_headers |
bool |
False |
Enable header filtering |
headers_whitelist |
frozenset[str] |
None |
Additional headers to keep |
header_whitelist_callback |
callable | None |
Callback for dynamic header whitelist additions |
tls |
TLS |
system default | TLS trust, client certificates, and key logging |
key_affinity |
KeyRouteAffinityConfig |
NONE |
Key-based routing |
retries |
RetryConfig |
standard, 3 attempts | SDK retry behavior |
max_pool_connections |
int |
200 |
Max connections per host |
timeouts |
TimeoutConfig |
discovery 5s, connect 5s, read 30s | Discovery and SDK per-attempt timeouts |
aws_region |
str |
"us-east-1" |
Region placeholder required by the SDK |
user_agent |
str, callable, or None |
alternator-client-python/<version> |
Final User-Agent; None omits the wire header |
active_refresh_interval_ms |
int |
1000 |
Node refresh interval when active |
idle_refresh_interval_ms |
int |
60000 |
Node refresh interval when idle |
Authentication is disabled by default. Alternator authentication in this client supports static credentials only; AWS SDK environment, profile, and provider-chain credentials are not used for Alternator auth.
from alternator import Auth, AlternatorClient, Config
config = Config(seed_hosts=["node1"], port=8000)
# Default: unsigned requests
with AlternatorClient(config, auth=Auth.disabled()) as client:
client.list_tables()
# Signed requests with static Alternator credentials
with AlternatorClient(
config,
auth=Auth.static_credentials("alternator", "secret"),
) as client:
client.list_tables()Passing raw boto credential kwargs such as aws_access_key_id still works for
compatibility, but is deprecated. Prefer auth=Auth.static_credentials(...).
An Alternator client is a boto3 DynamoDB client configured with ScyllaDB Alternator node discovery and load balancing. A regular AWS SDK client uses the normal AWS DynamoDB regional endpoint and AWS SDK credential chain.
import boto3
import alternator
with alternator.client(
seeds=["node1.example.com", "node2.example.com"],
port=8000,
) as alternator_client:
aws_client = boto3.client("dynamodb", region_name="us-east-1")
print("Alternator endpoint:", alternator_client.meta.endpoint_url)
print("AWS endpoint:", aws_client.meta.endpoint_url)
print("Alternator tables:", alternator_client.list_tables()["TableNames"])
# Requires normal AWS credentials:
# print("AWS tables:", aws_client.list_tables()["TableNames"])See examples/compare_aws_sdk.py for a runnable version. The example keeps
Alternator seeds host-only and uses one port setting for all seeds.
Control which nodes receive your requests based on topology:
from alternator import Config, ClusterScope, DatacenterScope, RackScope
# Route to any node in the cluster (default)
config = Config(
seed_hosts=["node1"],
port=8000,
routing_scope=ClusterScope(),
)
# Route only to nodes in a specific datacenter
config = Config(
seed_hosts=["node1"],
port=8000,
routing_scope=DatacenterScope(datacenter="us-east-1"),
)
# Route only to nodes in a specific rack
config = Config(
seed_hosts=["node1"],
port=8000,
routing_scope=RackScope(datacenter="us-east-1", rack="rack1"),
)ClusterScope queries every configured seed host and combines the returned
/localnodes results. Because ScyllaDB's optionless /localnodes endpoint
returns nodes from the contacted seed's local datacenter, cluster-wide routing
spans multiple datacenters only when the configuration includes at least one
reachable seed from each datacenter.
Default constructors stay constrained to their requested scope:
DatacenterScope("dc1")tries only datacenterdc1RackScope("dc1", "rack1")tries only rackrack1in datacenterdc1
Use the named fallback argument when a broader fallback chain is desired:
from alternator import ClusterScope, DatacenterScope, RackScope
cluster_only = ClusterScope()
datacenter_only = DatacenterScope("dc1")
datacenter_then_cluster = DatacenterScope("dc1", fallback=ClusterScope())
rack_only = RackScope("dc1", "rack1")
rack_then_datacenter = RackScope(
"dc1",
"rack1",
fallback=DatacenterScope("dc1", fallback=None),
)
rack_then_datacenter_then_cluster = RackScope(
"dc1",
"rack1",
fallback=DatacenterScope("dc1", fallback=ClusterScope()),
)Helper.check_rack_and_datacenter_set_correctly() validates the configured
scope by querying /localnodes with the configured datacenter and rack filters
without replacing the helper's current live-node list.
AsyncHelper exposes the same validation methods as awaitable methods.
For Lightweight Transactions (conditional writes), routing requests for the same partition key to the same node can improve performance:
from alternator import (
AlternatorConfigBuilder,
KeyRouteAffinityMode,
)
config = (
AlternatorConfigBuilder()
.with_seeds("node1")
.with_port(8000)
.with_key_affinity(
mode=KeyRouteAffinityMode.RMW, # Only for read-modify-write ops
table_pk_map={"my_table": "pk"}, # Optional: preload PK names
)
.build()
)| Mode | Description |
|---|---|
NONE |
Disabled (default round-robin) |
RMW |
Only for write operations that require a read-before-write path |
ANY_WRITE |
For all write operations (PutItem, UpdateItem, DeleteItem, BatchWriteItem) |
RMW mode applies affinity to conditional PutItem/DeleteItem, ALL_OLD
returns, and UpdateItem requests that need prior item state, including
non-empty update or condition expressions, Expected, selected ReturnValues,
ADD, and value-bearing DELETE attribute updates. BatchWriteItem does not
use affinity in RMW mode.
ANY_WRITE mode applies affinity to single-item writes using the request
partition key. For BatchWriteItem, each valid put/delete votes for its
preferred node. The request tries the unique winning node first and keeps the
remaining nodes in the retry plan. Missing partition-key metadata, unsupported
key values, no active nodes, no votes, or tied votes fall back to normal routing.
from alternator import TLS, TlsSessionCacheConfig
from pathlib import Path
# Use system CA certificates (default)
tls = TLS.system_default()
# Use custom CA certificate
tls = TLS.with_custom_ca(Path("/path/to/ca.pem"))
# Trust all certificates (INSECURE - dev only)
tls = TLS.trust_all()
# Mutual TLS with separate certificate and key files
tls = TLS(
custom_ca_cert_paths=[Path("/path/to/ca.pem")],
client_cert_path=Path("/path/to/client.crt"),
client_key_path=Path("/path/to/client.key"),
)
# Mutual TLS with a combined certificate/key PEM file
tls = TLS(
custom_ca_cert_paths=[Path("/path/to/ca.pem")],
client_cert_path=Path("/path/to/client-combined.pem"),
)
# Debug TLS traffic with a key log file
tls = TLS(
custom_ca_cert_paths=[Path("/path/to/ca.pem")],
key_log_file_path=Path("/secure/tmp/alternator-tls.keys"),
)
# Full configuration
tls = TLS(
custom_ca_cert_paths=[Path("/path/to/ca.pem")],
trust_system_ca_certs=True,
verify_hostname=True,
session_cache=TlsSessionCacheConfig(
enabled=True,
cache_size=1024,
timeout_seconds=86400,
),
client_cert_path=Path("/path/to/client.crt"),
client_key_path=Path("/path/to/client.key"),
key_log_file_path=Path("/secure/tmp/alternator-tls.keys"),
)Client certificate settings are loaded into the SSL context used for
/localnodes discovery and passed to the SDK as client_cert for HTTPS
DynamoDB API calls. If you configure custom server CA certificates, continue to
pass the matching SDK verify argument or an equivalent SDK setting for API
calls.
TLS key logs contain traffic decryption material. Store them only in protected
temporary locations, delete them after debugging, and never commit them. Key log
support depends on Python/OpenSSL exposing SSLContext.keylog_filename; runtimes
without that attribute ignore key_log_file_path.
Enable gzip compression for large request bodies:
Note: Gzip request compression requires ScyllaDB 2026.1.0 or later. HTTP response compression requires an Alternator build that includes ScyllaDB core response-compression support from
scylladb/scylladb#27454.
from alternator import (
AlternatorConfigBuilder,
CompressionAlgorithm,
ResponseCompression,
)
config = (
AlternatorConfigBuilder()
.with_seeds("node1")
.with_port(8000)
.with_compression(
CompressionAlgorithm.GZIP,
min_size=1024, # Only compress bodies >= 1KB
gzip_level=6, # Python gzip level 0-9; default is 9
)
.with_response_compression(
ResponseCompression.GZIP,
ResponseCompression.DEFLATE,
)
.build()
)Compression uses Python's gzip.compress implementation. Levels 0 through
9 are accepted: lower levels spend less CPU and usually produce larger bodies;
higher levels spend more CPU and usually produce smaller bodies. The client only
sends compressed bodies when the compressed payload is smaller than the original
payload.
Response compression is disabled by default. When enabled, the client sends
Accept-Encoding with the configured encodings and decodes Content-Encoding: gzip or Content-Encoding: deflate responses before boto3/aioboto3 parses the
DynamoDB JSON body. Use .without_response_compression() to disable it again in
builder chains.
Header optimization remains opt-in. Required protocol, compression, and auth
headers are preserved automatically. Use whitelist for static additions and
whitelist_callback when the allowed headers depend on configuration or auth
state:
from alternator import AlternatorConfigBuilder, HeaderWhitelistContext
def extra_headers(context: HeaderWhitelistContext) -> set[str]:
if context.auth_enabled:
return {"X-Service-Trace"}
return {"X-Anonymous-Trace"}
config = (
AlternatorConfigBuilder()
.with_seeds("node1")
.with_port(8000)
.with_header_optimization(
whitelist={"X-Static-Header"},
whitelist_callback=extra_headers,
)
.build()
)The callback returns additional headers to keep. It cannot remove the required
headers exposed in context.required_headers.
from alternator import (
AlternatorClient,
Config,
AlternatorError,
NoNodesAvailableError,
ConfigurationError,
)
try:
config = Config(seed_hosts=[], port=8000)
except ConfigurationError as e:
print(f"Invalid configuration: {e}")
try:
with AlternatorClient(config) as client:
client.list_tables()
except NoNodesAvailableError as e:
print(f"No nodes available: {e}")
except AlternatorError as e:
print(f"Alternator error: {e}")The library uses Python's standard logging module with the logger name alternator:
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("alternator").setLevel(logging.DEBUG)Log levels:
INFO: Node discovery eventsWARNING: Fallback events, connection issuesDEBUG: Detailed routing decisions, node listsERROR: Failed operations
For table-oriented operations, use AlternatorResource which wraps boto3's DynamoDB resource:
from alternator import Config, AlternatorResource
config = Config(seed_hosts=["192.168.1.1"], port=8000)
with AlternatorResource(config) as resource:
table = resource.Table("my_table")
table.put_item(Item={"pk": "user123", "data": "hello"})
response = table.get_item(Key={"pk": "user123"})You can also use the factory function:
from alternator import create_resource, close_resource, Config
config = Config(seed_hosts=["node1"], port=8000)
resource = create_resource(config)
try:
table = resource.Table("my_table")
table.scan()
finally:
close_resource(resource)ScyllaDB Alternator supports vector similarity search, which is not part of the standard AWS DynamoDB API. All clients and resources created by this library have vector search support enabled automatically — no extra setup is needed.
Note: Vector search requires ScyllaDB with Alternator vector search support enabled. These operations are not available on AWS DynamoDB. The feature is fully supported from ScyllaDB 2026.3, and only partially supported in ScyllaDB 2026.2: Version 2026.2 did not yet support the optimized "Vector" type, configurable SimilarityFunction, returning scores (ReturnScores), pre-filtering (KeyConditionExpression) or projected attributes (ProjectionType=INCLUDE).
client.create_table(
TableName="embeddings",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
VectorIndexes=[{
"IndexName": "embedding_index",
"VectorAttribute": {"AttributeName": "embedding", "Dimensions": 128},
"SimilarityFunction": "COSINE", # or "DOT_PRODUCT" / "EUCLIDEAN"
}],
)You can also add a vector index to an existing table via UpdateTable:
client.update_table(
TableName="embeddings",
VectorIndexUpdates=[{
"Create": {
"IndexName": "embedding_index",
"VectorAttribute": {"AttributeName": "embedding", "Dimensions": 128},
"SimilarityFunction": "COSINE",
}
}],
)# Store an item with a vector attribute
client.put_item(
TableName="embeddings",
Item={
"id": {"S": "item1"},
"embedding": {"FLOAT32VECTOR": [0.1, 0.2, 0.3, 0.4]},
},
)
# Query by vector similarity (returns the k nearest neighbors)
result = client.query(
TableName="embeddings",
VectorSearch={
"QueryVector": {"FLOAT32VECTOR": [0.1, 0.2, 0.3, 0.4]},
"ReturnScores": "SIMILARITY", # optional: include similarity scores
},
Limit=10,
)
for item in result["Items"]:
print(item)
# If ReturnScores was set, similarity scores are in result["Scores"]The Vector class is a list subclass that signals to Alternator that the
value should be stored as an array of 32-bit floats using the FLOAT32VECTOR
wire type. Without Vector, a list of numbers is serialized as a DynamoDB L
(list) of high-precision N (decimal) values — correct for arbitrary numbers,
but wasteful for embedding vectors where 32-bit precision is sufficient.
Using Vector for stored attributes reduces storage significantly (4 bytes per
element instead of a variable-length decimal string). For query vectors it
makes less difference, but using Vector consistently keeps the code uniform.
from alternator.vector import Vector
table = resource.Table("embeddings")
# Store a vector (sent as FLOAT32VECTOR, stored as compact 32-bit floats)
table.put_item(Item={"id": "item1", "embedding": Vector([0.1, 0.2, 0.3, 0.4])})
# Query by vector similarity
result = table.query(
VectorSearch={
"QueryVector": Vector([0.1, 0.2, 0.3, 0.4]),
"ReturnScores": "SIMILARITY",
},
Limit=10,
)
for item in result["Items"]:
# embedding is automatically deserialized back as a Vector instance
print(item["embedding"])If you prefer not to use context managers:
from alternator import create_client, close_client, Config
config = Config(seed_hosts=["node1"], port=8000)
client = create_client(config)
try:
client.list_tables()
finally:
close_client(client) # Stop background refresh threadAsync equivalent:
from alternator import Config
from alternator.async_client import create_async_client, close_async_client
config = Config(seed_hosts=["node1"], port=8000)
client = await create_async_client(config)
try:
await client.list_tables()
finally:
await close_async_client(client)TimeoutConfig.discovery_seconds applies only to /localnodes discovery
requests. TimeoutConfig.connect_seconds and TimeoutConfig.read_seconds are
passed to botocore/aiobotocore as per-attempt SDK connect and read timeouts;
they are not whole-operation deadlines. Use application-level cancellation or
your own deadline wrapper for end-to-end call deadlines.
RetryConfig, max_pool_connections, aws_region, and SDK timeouts are passed
to the generated SDK config. aws_region is a placeholder required by the SDK;
Alternator request routing still uses discovered Alternator endpoints.
from alternator import Config, RetryConfig, RetryMode, TimeoutConfig
config = Config(
seed_hosts=["node1", "node2"],
port=8000,
retries=RetryConfig(max_attempts=4, mode=RetryMode.STANDARD),
max_pool_connections=300,
timeouts=TimeoutConfig(
discovery_seconds=3.0,
connect_seconds=2.0,
read_seconds=10.0,
),
)By default, Alternator sends alternator-client-python/<version> as the final
wire User-Agent header. Pass None to omit the header:
from alternator import Config, create_client
config = Config(
seed_hosts=["node1", "node2"],
port=8000,
user_agent=None,
)
client = create_client(config)Pass a string to user_agent when you need to set a final value:
config = Config(
seed_hosts=["node1", "node2"],
port=8000,
user_agent="orders-service/1.0",
)
client = create_client(config)Pass a callback when you need to wrap or add to the default
alternator-client-python/<version> identity:
config = Config(
seed_hosts=["node1", "node2"],
port=8000,
user_agent=lambda default: f"orders-service {default}",
)
client = create_client(config)The client still owns the SDK config object, endpoint routing, and the final
wire User-Agent header. Use typed Alternator config fields for SDK transport
settings: RetryConfig for retry behavior, TimeoutConfig for connect/read
timeouts, max_pool_connections for pool sizing, aws_region for the SDK
region placeholder, and TLS for client certificates. Python botocore does not
expose direct knobs for max idle connections, max idle connections per host, or
idle connection timeout; tune max_pool_connections, retries, and timeouts
instead.
- Connection pool sizing: The default
max_pool_connections=200works for most workloads. Increase if you see connection pool exhaustion warnings under high concurrency. - Refresh intervals: Default active refresh (1s) is appropriate for dynamic clusters. For stable clusters, increase
active_refresh_interval_msto reduce discovery overhead. - Timeouts: Default
TimeoutConfig.discovery_seconds=5.0,connect_seconds=5.0, andread_seconds=30.0are conservative. Tune based on your network latency and query complexity. - Monitoring: Enable
INFO-level logging for thealternatorlogger to track node discovery events. UseDEBUGfor detailed routing decisions during troubleshooting. - Seed hosts: Configure at least 2-3 seed hosts for redundancy in case one seed is temporarily unavailable during startup.
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.
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.
- Request Compression: Gzip request compression requires ScyllaDB 2026.1.0+.
- Response Compression: Response gzip/deflate decoding requires an Alternator build that includes
scylladb/scylladb#27454and must be enabled explicitly withwith_response_compression(...). - Gzip Compression Levels: Python's gzip module supports levels
0through9; this client does not expose alternative compression algorithms or custom compressor objects. - TLS Session Cache Settings: The
cache_sizeandtimeout_secondsparameters inTlsSessionCacheConfigare not currently used by Python'ssslmodule. Only theenabledflag controls session ticket behavior. - TLS Key Logs: Key log file support depends on Python/OpenSSL runtime support for
SSLContext.keylog_filenameand should only be used in protected debugging environments. - mTLS Integration Fixtures: The local Scylla fixture in this repository does not require client certificate authentication, so automated tests cover configuration propagation and SSL context setup rather than a full mutual-TLS handshake.
- 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_mapavoids this initial miss. - Batch Operations:
BatchWriteItemkey affinity inANY_WRITEmode uses preferred-node voting across eligible put/delete entries. Ties, missing partition-key metadata, unsupported key values, no active nodes, or no eligible votes fall back to normal routing. Batches are not split by affinity target. - Node Health: Node health, quarantine behavior, decommission handling, and dead-node handling are planning-only.
get_quarantined_nodes()returns an empty list until a future implementation is explicitly added.
examples/sync_demo.py: synchronous client lifecycle and basic operationsexamples/async_demo.py: async client lifecycle and concurrent operationsexamples/compare_aws_sdk.py: Alternator client setup compared with a regular AWS SDK DynamoDB clientexamples/capability_configuration.py: helper lifecycle, explicit routing fallback, static auth, timeouts/retries, mTLS, compression/header optimization, and key affinity configuration recipes
See docs/RELEASE_NOTES.md for capability release-note guidance covering additive APIs, deprecations, behavior notes, migration steps, and versioning expectations.
# Clone the repository
git clone https://github.com/scylladb/alternator-client-python.git
cd alternator-client-python
# Install in development mode
make install
# Run tests
make test-unit
# Run linting
make lint
# Run mypy type checks
make typecheck
# Start local Scylla cluster for integration tests
make scylla-start
make test-integration
make scylla-stopApache License 2.0
Contributions are welcome! Please read the contributing guidelines before submitting a pull request.