Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DOCKER_CACHE_FILE := $(DOCKER_CACHE_DIR)/scylla-image.tar
CERT_CACHE_DIR := $(MAKEFILE_PATH)/.cert-cache
CERT_DIR := $(MAKEFILE_PATH)/tests/scylla

.PHONY: install verify build compile compile-test compile-demo test-unit test-integration test-integration-only test-demo test-demo-only test-all test lint lint-types lint-fix clean wait-for-alternator scylla-start scylla-stop scylla-kill scylla-rm docker-pull docker-cache-save docker-cache-load cert-cache-save cert-cache-load help
.PHONY: install verify build compile compile-test compile-demo test-unit test-integration test-integration-only test-demo test-demo-only test-all test lint lint-types typecheck lint-fix clean wait-for-alternator scylla-start scylla-stop scylla-kill scylla-rm docker-pull docker-cache-save docker-cache-load cert-cache-save cert-cache-load help

# Default target
help:
Expand All @@ -27,6 +27,7 @@ help:
@echo " test - Run all tests"
@echo " lint - Run linters (ruff + mypy)"
@echo " lint-types - Enforce type annotations with ruff"
@echo " typecheck - Run mypy type checks"
@echo " lint-fix - Auto-fix linting issues with ruff"
@echo " clean - Remove build artifacts"
@echo " scylla-start - Start 3-node Scylla cluster via Docker"
Expand Down Expand Up @@ -96,11 +97,10 @@ test:
--cov=alternator --cov-report=xml --cov-report=term-missing

# Run linters
lint: lint-types
lint: lint-types typecheck
uv run ruff check alternator/ tests/ examples/
uv run ruff format --check alternator/ tests/ examples/
$(MAKE) compile-demo
uv run mypy alternator/ --strict
@# Ensure every noqa/type: ignore has an explanation after it
@if grep -rn --include='*.py' -P '#\s*(noqa|type:\s*ignore)(?::\s*\S+)?\s*$$' alternator/ tests/ examples/; then \
echo "ERROR: Found noqa/type: ignore comments without explanations. Add ' -- reason' after each."; \
Expand All @@ -110,6 +110,9 @@ lint: lint-types
lint-types:
uv run ruff check --select ANN alternator/ tests/ examples/

typecheck:
uv run mypy alternator/ examples/ tests/unit/ --strict

# Auto-fix linting issues
lint-fix:
uv run ruff check --fix alternator/ tests/ examples/
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,9 @@ 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
Expand Down
2 changes: 1 addition & 1 deletion alternator/core/compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def compress_request(
return compress_request


def _set_request_body(request: AWSPreparedRequest | AWSRequest, body: bytes) -> None:
def _set_request_body(request: object, body: bytes) -> None:
"""Set request body for both prepared and pre-signing request objects."""
mutable_request = cast(Any, request)
if isinstance(request, AWSPreparedRequest):
Expand Down
2 changes: 1 addition & 1 deletion alternator/core/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def extract_operation_name(request: AWSPreparedRequest | AWSRequest) -> str:
Returns:
Operation name (e.g. "PutItem"), or empty string if not found
"""
target = request.headers.get("X-Amz-Target", "")
target: str | bytes = request.headers.get("X-Amz-Target", "")
if isinstance(target, bytes):
target = target.decode("utf-8")
return target.split(".")[-1] if "." in target else ""
Expand Down
7 changes: 4 additions & 3 deletions examples/async_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import logging
import os
import sys
from typing import Any, cast

from botocore.exceptions import ClientError

Expand Down Expand Up @@ -39,8 +40,8 @@ async def main() -> None:
async with AsyncAlternatorClient(config) as client:
# List existing tables
print("Listing tables...")
response = await client.list_tables()
tables = response.get("TableNames", [])
list_response = await client.list_tables()
tables = list_response.get("TableNames", [])
print(f"Found {len(tables)} tables: {tables}")

# Create a test table if it doesn't exist
Expand Down Expand Up @@ -95,7 +96,7 @@ async def main() -> None:
responses = await asyncio.gather(*read_tasks)

for i, response in enumerate(responses):
item = response.get("Item", {})
item = cast(dict[str, Any], response.get("Item", {}))
print(f" async_user_{i}: {item.get('data', {}).get('S', 'N/A')}")

print("Async demo complete!")
Expand Down
27 changes: 22 additions & 5 deletions examples/compare_aws_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,43 @@
from __future__ import annotations

import argparse
from typing import Protocol
from collections.abc import Mapping
from typing import Protocol, cast

import boto3

import alternator
from alternator import Auth


class DynamoDBServiceModelLike(Protocol):
"""Minimal service model protocol used by this example."""

service_name: str


class DynamoDBClientMetaLike(Protocol):
"""Minimal DynamoDB client metadata protocol used by this example."""

service_model: DynamoDBServiceModelLike
endpoint_url: str


class DynamoDBLikeClient(Protocol):
"""Minimal DynamoDB client protocol used by this example."""

@property
def meta(self) -> object: ...
def meta(self) -> DynamoDBClientMetaLike:
pass

def list_tables(self) -> dict[str, object]: ...
def list_tables(self) -> Mapping[str, object]:
pass


def print_client_details(name: str, client: DynamoDBLikeClient) -> None:
def print_client_details(name: str, client: object) -> None:
"""Print comparable client metadata."""
meta = client.meta
typed_client = cast(DynamoDBLikeClient, client)
meta = typed_client.meta
print(f"{name}:")
print(f" service: {meta.service_model.service_name}")
print(f" endpoint: {meta.endpoint_url}")
Expand Down
9 changes: 5 additions & 4 deletions examples/sync_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import logging
import os
import sys
from typing import Any, cast

from botocore.exceptions import ClientError

Expand All @@ -37,8 +38,8 @@ def main() -> None:
with AlternatorClient(config) as client:
# List existing tables
print("Listing tables...")
response = client.list_tables()
tables = response.get("TableNames", [])
list_response = client.list_tables()
tables = list_response.get("TableNames", [])
print(f"Found {len(tables)} tables: {tables}")

# Create a test table if it doesn't exist
Expand Down Expand Up @@ -75,11 +76,11 @@ def main() -> None:
# Read items back
print("Reading items...")
for i in range(5):
response = client.get_item(
get_response = client.get_item(
TableName=table_name,
Key={"pk": {"S": f"user_{i}"}},
)
item = response.get("Item", {})
item = cast(dict[str, Any], get_response.get("Item", {}))
print(f" user_{i}: {item.get('data', {}).get('S', 'N/A')}")

print("Demo complete!")
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,20 @@ python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
warn_unreachable = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
disallow_any_unimported = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
follow_imports = "silent"

[[tool.mypy.overrides]]
module = ["aioboto3"]
ignore_missing_imports = true

[tool.ruff]
Expand Down
19 changes: 14 additions & 5 deletions tests/unit/test_async_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for AsyncLiveNodesManager and AsyncPartitionKeyCache."""

import asyncio
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock

import pytest
Expand Down Expand Up @@ -39,11 +40,11 @@ async def mock_fetch(url: str) -> list[str]:

# Start
await manager.start()
assert manager._refresh_task is not None
assert cast(object, manager._refresh_task) is not None

# Stop
await manager.stop()
assert manager._refresh_task is None
assert cast(object, manager._refresh_task) is None

@pytest.mark.asyncio
async def test_next_node_uri_format(self, config: Config) -> None:
Expand Down Expand Up @@ -152,15 +153,23 @@ async def mock_fetch(url: str) -> list[str]:
@pytest.mark.asyncio
async def test_url_construction(self, config: Config) -> None:
"""Test build_localnodes_url constructs correct URL."""
manager = AsyncLiveNodesManager(config, lambda _: [])

async def empty_fetch(url: str) -> list[str]:
return []

manager = AsyncLiveNodesManager(config, empty_fetch)

url = manager._core.build_localnodes_url(ClusterScope(), "192.168.1.1")
assert url == "http://192.168.1.1:8000/localnodes"

@pytest.mark.asyncio
async def test_url_construction_with_dc_scope(self, config: Config) -> None:
"""Test URL construction with datacenter scope."""
manager = AsyncLiveNodesManager(config, lambda _: [])

async def empty_fetch(url: str) -> list[str]:
return []

manager = AsyncLiveNodesManager(config, empty_fetch)

url = manager._core.build_localnodes_url(
DatacenterScope(datacenter="dc1"), "192.168.1.1"
Expand Down Expand Up @@ -380,7 +389,7 @@ async def test_concurrent_requests_single_fetch(self) -> None:
fetch_count = 0
fetch_event = asyncio.Event()

async def slow_describe_table(TableName: str) -> dict:
async def slow_describe_table(TableName: str) -> dict[str, Any]:
nonlocal fetch_count
fetch_count += 1
# Wait a bit to simulate network delay
Expand Down
Loading
Loading