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
4 changes: 3 additions & 1 deletion cloudrift/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from cloudrift.document import get_mongodb, get_mongodb_sync
from cloudrift.cache import get_cache, cache_broker_url
from cloudrift.secrets import get_secrets
from cloudrift.sql import get_sql
from cloudrift.pubsub import get_pubsub
from cloudrift.email import get_email

__version__ = "0.2.4"
__version__ = "0.2.5"
__all__ = [
"get_storage",
"get_queue",
Expand All @@ -15,6 +16,7 @@
"get_cache",
"cache_broker_url",
"get_secrets",
"get_sql",
"get_pubsub",
"get_email",
]
7 changes: 7 additions & 0 deletions cloudrift/cache/redis_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def from_access_key(
port: int = 6380,
db: int = 0,
ssl: bool = True,
decode_responses: bool = False,
) -> "AzureRedisCacheBackend":
"""Authenticate with an Azure Cache for Redis access key.

Expand All @@ -38,6 +39,7 @@ def from_access_key(
port: Redis SSL port (default 6380; non-TLS is 6379).
db: Database index (default 0).
ssl: Enable TLS (default ``True``; required for Azure Cache for Redis).
decode_responses: When ``True``, reads return ``str`` instead of ``bytes``.
"""
try:
client = aioredis.Redis(
Expand All @@ -46,6 +48,7 @@ def from_access_key(
password=access_key,
db=db,
ssl=ssl,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand All @@ -60,6 +63,7 @@ def from_managed_identity(
db: int = 0,
ssl: bool = True,
client_id: str | None = None,
decode_responses: bool = False,
) -> "AzureRedisCacheBackend":
"""Authenticate via Azure Managed Identity (Entra ID token auth).

Expand All @@ -85,6 +89,7 @@ def from_managed_identity(
db=db,
ssl=ssl,
credential_provider=provider,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand All @@ -103,6 +108,7 @@ def from_service_principal(
port: int = 6380,
db: int = 0,
ssl: bool = True,
decode_responses: bool = False,
) -> "AzureRedisCacheBackend":
"""Authenticate via Azure AD service principal (Entra ID token auth).

Expand Down Expand Up @@ -131,6 +137,7 @@ def from_service_principal(
db=db,
ssl=ssl,
credential_provider=provider,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand Down
8 changes: 8 additions & 0 deletions cloudrift/cache/redis_elasticache.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def from_auth_token(
db: int = 0,
ssl: bool = True,
ssl_ca_certs: str | None = None,
decode_responses: bool = False,
) -> "AWSElastiCacheBackend":
"""Connect using an ElastiCache AUTH token (shared secret).

Expand All @@ -43,6 +44,7 @@ def from_auth_token(
db: Database index (default 0).
ssl: Enable TLS in-transit encryption (default ``True``).
ssl_ca_certs: Optional path to the CA bundle (PEM) for server verification.
decode_responses: When ``True``, reads return ``str`` instead of ``bytes``.
"""
try:
client = aioredis.Redis(
Expand All @@ -52,6 +54,7 @@ def from_auth_token(
db=db,
ssl=ssl,
ssl_ca_certs=ssl_ca_certs,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand All @@ -71,6 +74,7 @@ def from_iam_auth(
aws_secret_access_key: str | None = None,
aws_session_token: str | None = None,
profile_name: str | None = None,
decode_responses: bool = False,
) -> "AWSElastiCacheBackend":
"""Connect using IAM-based authentication (ElastiCache Redis 7+ with IAM enabled).

Expand Down Expand Up @@ -108,6 +112,7 @@ def from_iam_auth(
ssl=ssl,
ssl_ca_certs=ssl_ca_certs,
credential_provider=provider,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand All @@ -123,6 +128,7 @@ def from_tls_cert(
ssl_certfile: str | None = None,
ssl_keyfile: str | None = None,
ssl_ca_certs: str | None = None,
decode_responses: bool = False,
) -> "AWSElastiCacheBackend":
"""Connect using mutual TLS (mTLS) with a client certificate and key.

Expand All @@ -134,6 +140,7 @@ def from_tls_cert(
ssl_certfile: Path to the client certificate PEM file.
ssl_keyfile: Path to the client private key PEM file.
ssl_ca_certs: Path to the CA certificate bundle (PEM) for server verification.
decode_responses: When ``True``, reads return ``str`` instead of ``bytes``.
"""
try:
client = aioredis.Redis(
Expand All @@ -145,6 +152,7 @@ def from_tls_cert(
ssl_certfile=ssl_certfile,
ssl_keyfile=ssl_keyfile,
ssl_ca_certs=ssl_ca_certs,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand Down
14 changes: 13 additions & 1 deletion cloudrift/cache/redis_standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,20 @@ def from_url(
cls,
url: str,
ssl_ca_certs: str | None = None,
decode_responses: bool = False,
) -> "StandaloneRedisBackend":
"""Connect using a Redis URL.

Args:
url: e.g. ``redis://user:pass@localhost:6379/0`` or
``rediss://user:pass@localhost:6380/0`` (TLS).
ssl_ca_certs: Optional path to the CA certificate bundle (PEM) for TLS.
decode_responses: When ``True``, read operations return ``str`` instead
of ``bytes`` (mirrors redis-py's ``decode_responses``). Default
``False`` keeps the cache-backend contract of returning ``bytes``.
"""
try:
kwargs: dict = {}
kwargs: dict = {"decode_responses": decode_responses}
if ssl_ca_certs:
kwargs["ssl_ca_certs"] = ssl_ca_certs
return cls(aioredis.from_url(url, **kwargs))
Expand All @@ -51,6 +55,7 @@ def from_credentials(
db: int = 0,
ssl: bool = False,
ssl_ca_certs: str | None = None,
decode_responses: bool = False,
) -> "StandaloneRedisBackend":
"""Connect using explicit host, port, and optional credentials.

Expand All @@ -62,6 +67,8 @@ def from_credentials(
db: Database index (default 0).
ssl: Enable TLS (default ``False``).
ssl_ca_certs: Optional path to the CA certificate bundle (PEM).
decode_responses: When ``True``, read operations return ``str`` instead
of ``bytes`` (mirrors redis-py's ``decode_responses``).
"""
try:
client = aioredis.Redis(
Expand All @@ -72,6 +79,7 @@ def from_credentials(
db=db,
ssl=ssl,
ssl_ca_certs=ssl_ca_certs,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand All @@ -88,6 +96,7 @@ def from_tls_cert(
ssl_certfile: str | None = None,
ssl_keyfile: str | None = None,
ssl_ca_certs: str | None = None,
decode_responses: bool = False,
) -> "StandaloneRedisBackend":
"""Connect using mutual TLS (mTLS) with client certificate and key files.

Expand All @@ -100,6 +109,8 @@ def from_tls_cert(
ssl_certfile: Path to the client certificate PEM file.
ssl_keyfile: Path to the client private key PEM file.
ssl_ca_certs: Path to the CA certificate bundle (PEM) for server verification.
decode_responses: When ``True``, read operations return ``str`` instead
of ``bytes`` (mirrors redis-py's ``decode_responses``).
"""
try:
client = aioredis.Redis(
Expand All @@ -112,6 +123,7 @@ def from_tls_cert(
ssl_certfile=ssl_certfile,
ssl_keyfile=ssl_keyfile,
ssl_ca_certs=ssl_ca_certs,
decode_responses=decode_responses,
)
return cls(client)
except Exception as e:
Expand Down
14 changes: 14 additions & 0 deletions cloudrift/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ class SecretPermissionError(SecretError):
"""Raised on secret access permission failures."""


# SQL database exceptions
class SQLError(CloudRiftError):
"""Base exception for SQL database operations."""


class SQLConnectionError(SQLError):
"""Raised when a SQL database connection cannot be established."""


class SQLAuthError(SQLError):
"""Raised when SQL authentication / credential acquisition fails (e.g. an
Azure AD access token or RDS IAM token could not be obtained)."""


# Pub/Sub exceptions
class PubSubError(CloudRiftError):
"""Base exception for pub/sub operations."""
Expand Down
31 changes: 26 additions & 5 deletions cloudrift/secrets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ def get_secrets(provider: str, **kwargs) -> SecretBackend:
"""Factory to instantiate a secret management backend.

Args:
provider: ``"aws_secrets_manager"`` or ``"azure_keyvault"``.
**kwargs: Provider-specific config. The factory routes to the appropriate
``from_*`` classmethod based on which credential keys are present.
provider: ``"aws_secrets_manager"``, ``"azure_keyvault"``, or a non-cloud
source — ``"env"`` (environment variables), ``"file"`` (a JSON file),
or ``"memory"``/``"local"`` (in-memory mapping, mainly dev/tests).
**kwargs: Provider-specific config. The cloud factories route to the
appropriate ``from_*`` classmethod based on which credential keys are
present; the local backends take their own simple kwargs.

Returns:
A SecretBackend instance.
Expand All @@ -19,7 +22,25 @@ def get_secrets(provider: str, **kwargs) -> SecretBackend:
get_secrets("azure_keyvault", vault_url="https://myvault.vault.azure.net")
get_secrets("azure_keyvault", vault_url="...", tenant_id="...",
client_id="...", client_secret="...")
get_secrets("env", prefix="SECRET_") # read SECRET_<name> env vars
get_secrets("file", path="/run/secrets.json") # JSON {name: value}
get_secrets("memory", mapping={"db": "..."}) # in-memory (dev/tests)
"""
if provider == "env":
from cloudrift.secrets.local import EnvSecretBackend

return EnvSecretBackend(**kwargs)

if provider == "file":
from cloudrift.secrets.local import FileSecretBackend

return FileSecretBackend(**kwargs)

if provider in ("memory", "local"):
from cloudrift.secrets.local import MappingSecretBackend

return MappingSecretBackend(**kwargs)

if provider == "aws_secrets_manager":
from cloudrift.secrets.aws_secrets_manager import AWSSecretsManagerBackend

Expand All @@ -37,8 +58,8 @@ def get_secrets(provider: str, **kwargs) -> SecretBackend:
return AzureKeyVaultBackend.from_managed_identity(**kwargs)

raise ValueError(
f"Unknown secrets provider: {provider!r}. "
"Choose 'aws_secrets_manager' or 'azure_keyvault'."
f"Unknown secrets provider: {provider!r}. Choose 'aws_secrets_manager', "
"'azure_keyvault', 'env', 'file', or 'memory'."
)


Expand Down
Loading
Loading