vaultriever 0.1.0__py3-none-any.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.
@@ -0,0 +1,33 @@
1
+ """vaultriever: retrieve secrets in an ARN-like way from different vaults.
2
+
3
+ Secrets are addressed with a Secret Resource Identifier (SRI)::
4
+
5
+ provider:region:secret_name:secret_key
6
+ """
7
+
8
+ from vaultriever.exceptions import (
9
+ DatabricksConfigurationError,
10
+ ProviderNotRegisteredError,
11
+ SecretRetrievalError,
12
+ SRIParseError,
13
+ VaultrieverError,
14
+ )
15
+ from vaultriever.providers import SecretProvider, SecretProviderRegistry
16
+ from vaultriever.resolver import resolve_secret
17
+ from vaultriever.settings_mixin import SecretSRIMixin
18
+ from vaultriever.sri import SecretProperties, is_sri, parse_sri
19
+
20
+ __all__ = [
21
+ 'DatabricksConfigurationError',
22
+ 'ProviderNotRegisteredError',
23
+ 'SecretProperties',
24
+ 'SecretProvider',
25
+ 'SecretProviderRegistry',
26
+ 'SecretRetrievalError',
27
+ 'SecretSRIMixin',
28
+ 'SRIParseError',
29
+ 'VaultrieverError',
30
+ 'is_sri',
31
+ 'parse_sri',
32
+ 'resolve_secret',
33
+ ]
@@ -0,0 +1,27 @@
1
+ """Package-specific exceptions.
2
+
3
+ Error messages must never contain secret values. Including the SRI's
4
+ secret_name/region is fine; the resolved value is not.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ class VaultrieverError(Exception):
11
+ """Base class for all vaultriever errors."""
12
+
13
+
14
+ class SRIParseError(VaultrieverError, ValueError):
15
+ """Raised when a string is not a valid Secret Resource Identifier."""
16
+
17
+
18
+ class ProviderNotRegisteredError(VaultrieverError, LookupError):
19
+ """Raised when an SRI references a provider that is not registered."""
20
+
21
+
22
+ class SecretRetrievalError(VaultrieverError):
23
+ """Raised when a provider fails to retrieve or decode a secret."""
24
+
25
+
26
+ class DatabricksConfigurationError(SecretRetrievalError):
27
+ """Raised when the Databricks provider cannot build a usable context."""
vaultriever/logging.py ADDED
@@ -0,0 +1,16 @@
1
+ """Logger helper.
2
+
3
+ All modules obtain their logger through :func:`get_logger` so the package
4
+ shares a single ``vaultriever`` namespace and users can configure it once.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+
12
+ def get_logger(name: str) -> logging.Logger:
13
+ """Return a logger under the ``vaultriever`` namespace."""
14
+ if name == 'vaultriever' or name.startswith('vaultriever.'):
15
+ return logging.getLogger(name)
16
+ return logging.getLogger(f'vaultriever.{name}')
@@ -0,0 +1,22 @@
1
+ """Secret providers and their registry.
2
+
3
+ Default providers are registered at import time. Provider SDK dependencies
4
+ (boto3, databricks-sdk) are imported lazily on first use, so registration is
5
+ safe even when the corresponding extra is not installed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from vaultriever.providers.aws import AWSSecretProvider
11
+ from vaultriever.providers.base import SecretProvider, SecretProviderRegistry
12
+ from vaultriever.providers.databricks import DatabricksSecretProvider
13
+
14
+ SecretProviderRegistry.register(AWSSecretProvider())
15
+ SecretProviderRegistry.register(DatabricksSecretProvider())
16
+
17
+ __all__ = [
18
+ 'AWSSecretProvider',
19
+ 'DatabricksSecretProvider',
20
+ 'SecretProvider',
21
+ 'SecretProviderRegistry',
22
+ ]
@@ -0,0 +1,79 @@
1
+ """AWS Secrets Manager provider.
2
+
3
+ SRI semantics: ``aws:<region>:<secret_name>:<json_key>``. The secret's
4
+ ``SecretString`` must be a JSON object; ``json_key`` selects one of its keys.
5
+ Credentials are resolved by the AWS SDK default chain (env vars, profile,
6
+ IAM role).
7
+
8
+ Requires the ``aws`` extra: ``pip install vaultriever[aws]``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from functools import lru_cache
15
+ from typing import Any
16
+
17
+ from vaultriever.exceptions import SecretRetrievalError
18
+ from vaultriever.logging import get_logger
19
+ from vaultriever.sri import SecretProperties
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ @lru_cache
25
+ def _get_aws_secret_json(secret_name: str, region_name: str) -> dict[str, Any]:
26
+ """Fetch and decode a JSON secret, caching by (secret_name, region)."""
27
+ try:
28
+ import boto3
29
+ except ImportError as exc: # pragma: no cover - depends on install extras
30
+ raise SecretRetrievalError(
31
+ "boto3 is required for the 'aws' provider. Install with: pip install vaultriever[aws]"
32
+ ) from exc
33
+
34
+ logger.debug('Fetching AWS secret %r in region %r', secret_name, region_name)
35
+ client = boto3.Session().client('secretsmanager', region_name=region_name)
36
+ try:
37
+ response = client.get_secret_value(SecretId=secret_name)
38
+ except Exception as exc:
39
+ raise SecretRetrievalError(
40
+ f'Failed to retrieve AWS secret {secret_name!r} in region {region_name!r}: '
41
+ f'{type(exc).__name__}'
42
+ ) from exc
43
+
44
+ secret_string = response.get('SecretString')
45
+ if secret_string is None:
46
+ raise SecretRetrievalError(
47
+ f'AWS secret {secret_name!r} has no SecretString (binary secrets are not supported)'
48
+ )
49
+ try:
50
+ data = json.loads(secret_string)
51
+ except json.JSONDecodeError as exc:
52
+ raise SecretRetrievalError(f'AWS secret {secret_name!r} is not valid JSON') from exc
53
+ if not isinstance(data, dict):
54
+ raise SecretRetrievalError(f'AWS secret {secret_name!r} is not a JSON object')
55
+ return data
56
+
57
+
58
+ class AWSSecretProvider:
59
+ """Retrieve keys from JSON secrets stored in AWS Secrets Manager."""
60
+
61
+ name = 'aws'
62
+
63
+ def get_secret_value(self, props: SecretProperties) -> Any:
64
+ if not props.region:
65
+ raise SecretRetrievalError(
66
+ "AWS SRIs require a non-empty region, e.g. 'aws:us-east-1:my-secret:MY_KEY'"
67
+ )
68
+ data = _get_aws_secret_json(props.secret_name, props.region)
69
+ try:
70
+ return data[props.secret_key]
71
+ except KeyError:
72
+ raise SecretRetrievalError(
73
+ f'Key {props.secret_key!r} not found in AWS secret {props.secret_name!r}'
74
+ ) from None
75
+
76
+ @staticmethod
77
+ def clear_cache() -> None:
78
+ """Clear the cached secret payloads (e.g. after rotation)."""
79
+ _get_aws_secret_json.cache_clear()
@@ -0,0 +1,19 @@
1
+ """Azure Key Vault provider (stub).
2
+
3
+ Intended SRI semantics: ``azure:<vault_or_region>:<secret_name>:<key_or_version>``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from vaultriever.sri import SecretProperties
11
+
12
+
13
+ class AzureSecretProvider:
14
+ """Placeholder provider; not implemented yet."""
15
+
16
+ name = 'azure'
17
+
18
+ def get_secret_value(self, props: SecretProperties) -> Any:
19
+ raise NotImplementedError('Azure provider not implemented yet.')
@@ -0,0 +1,60 @@
1
+ """Provider interface and registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, runtime_checkable
6
+
7
+ from vaultriever.exceptions import ProviderNotRegisteredError
8
+ from vaultriever.logging import get_logger
9
+ from vaultriever.sri import SecretProperties
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ @runtime_checkable
15
+ class SecretProvider(Protocol):
16
+ """Minimal interface every secret provider must implement."""
17
+
18
+ name: str # e.g. "aws", "databricks"
19
+
20
+ def get_secret_value(self, props: SecretProperties) -> Any: ...
21
+
22
+
23
+ class SecretProviderRegistry:
24
+ """Registry of available secret providers, keyed by provider name."""
25
+
26
+ _providers: dict[str, SecretProvider] = {}
27
+
28
+ @classmethod
29
+ def register(cls, provider: SecretProvider) -> None:
30
+ """Register a provider under its ``name``, replacing any existing one."""
31
+ logger.debug('Registering secret provider %r', provider.name)
32
+ cls._providers[provider.name] = provider
33
+
34
+ @classmethod
35
+ def get(cls, name: str) -> SecretProvider:
36
+ """Return the provider registered under ``name``.
37
+
38
+ Raises:
39
+ ProviderNotRegisteredError: If no provider is registered.
40
+ """
41
+ try:
42
+ return cls._providers[name]
43
+ except KeyError:
44
+ raise ProviderNotRegisteredError(
45
+ f'No secret provider registered for {name!r}. '
46
+ f'Available providers: {cls.available_providers()}'
47
+ ) from None
48
+
49
+ @classmethod
50
+ def unregister(cls, name: str) -> None:
51
+ """Remove a provider from the registry; no-op if not registered."""
52
+ cls._providers.pop(name, None)
53
+
54
+ @classmethod
55
+ def is_registered(cls, name: str) -> bool:
56
+ return name in cls._providers
57
+
58
+ @classmethod
59
+ def available_providers(cls) -> list[str]:
60
+ return sorted(cls._providers)
@@ -0,0 +1,95 @@
1
+ """Databricks secrets provider.
2
+
3
+ SRI semantics: ``databricks:<profile>:<scope>:<key>``.
4
+
5
+ - Empty region (``databricks::my-scope:MY_KEY``) -> default workspace: the
6
+ runtime ``dbutils`` when running on Databricks, otherwise the SDK's default
7
+ authentication.
8
+ - Non-empty region -> used as the Databricks CLI profile name, e.g.
9
+ ``databricks:staging:my-scope:MY_KEY`` reads via the ``staging`` profile.
10
+
11
+ Requires the ``databricks`` extra outside a Databricks runtime:
12
+ ``pip install vaultriever[databricks]``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+
20
+ from vaultriever.exceptions import DatabricksConfigurationError, SecretRetrievalError
21
+ from vaultriever.logging import get_logger
22
+ from vaultriever.sri import SecretProperties
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class DatabricksContext:
29
+ """Where to read Databricks secrets from. ``profile=None`` means default."""
30
+
31
+ profile: str | None
32
+
33
+
34
+ def resolve_databricks_context(region: str | None) -> DatabricksContext:
35
+ """Map the SRI region component to a Databricks context.
36
+
37
+ An empty/None region selects the default workspace; anything else is
38
+ interpreted as a Databricks CLI profile name.
39
+ """
40
+ if not region:
41
+ return DatabricksContext(profile=None)
42
+ return DatabricksContext(profile=region)
43
+
44
+
45
+ def _get_secret(context: DatabricksContext, scope: str, key: str) -> str:
46
+ """Fetch a secret for the given context. Split out so tests can patch it."""
47
+ if context.profile is None:
48
+ # On a Databricks runtime this is the native dbutils; elsewhere the
49
+ # SDK falls back to default authentication.
50
+ try:
51
+ from databricks.sdk.runtime import dbutils
52
+
53
+ return str(dbutils.secrets.get(scope=scope, key=key))
54
+ except ImportError:
55
+ try:
56
+ from databricks.sdk import WorkspaceClient
57
+ except ImportError as import_exc:
58
+ raise DatabricksConfigurationError(
59
+ "databricks-sdk is required for the 'databricks' provider. "
60
+ 'Install with: pip install vaultriever[databricks]'
61
+ ) from import_exc
62
+
63
+ client = WorkspaceClient()
64
+ return str(client.dbutils.secrets.get(scope=scope, key=key))
65
+
66
+ try:
67
+ from databricks.sdk import WorkspaceClient
68
+ except ImportError as exc:
69
+ raise DatabricksConfigurationError(
70
+ "databricks-sdk is required for the 'databricks' provider. "
71
+ 'Install with: pip install vaultriever[databricks]'
72
+ ) from exc
73
+ client = WorkspaceClient(profile=context.profile)
74
+ return str(client.dbutils.secrets.get(scope=scope, key=key))
75
+
76
+
77
+ class DatabricksSecretProvider:
78
+ """Retrieve secrets from Databricks secret scopes."""
79
+
80
+ name = 'databricks'
81
+
82
+ def get_secret_value(self, props: SecretProperties) -> Any:
83
+ context = resolve_databricks_context(props.region)
84
+ logger.debug(
85
+ 'Fetching Databricks secret scope=%r profile=%r', props.secret_name, context.profile
86
+ )
87
+ try:
88
+ return _get_secret(context, props.secret_name, props.secret_key)
89
+ except SecretRetrievalError:
90
+ raise
91
+ except Exception as exc:
92
+ raise SecretRetrievalError(
93
+ f'Failed to retrieve Databricks secret scope={props.secret_name!r} '
94
+ f'key={props.secret_key!r}: {type(exc).__name__}'
95
+ ) from exc
@@ -0,0 +1,19 @@
1
+ """GCP Secret Manager provider (stub).
2
+
3
+ Intended SRI semantics: ``gcp:<project_or_region>:<secret_name>:<key_or_version>``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from vaultriever.sri import SecretProperties
11
+
12
+
13
+ class GCPSecretProvider:
14
+ """Placeholder provider; not implemented yet."""
15
+
16
+ name = 'gcp'
17
+
18
+ def get_secret_value(self, props: SecretProperties) -> Any:
19
+ raise NotImplementedError('GCP provider not implemented yet.')
vaultriever/py.typed ADDED
File without changes
@@ -0,0 +1,30 @@
1
+ """Secret resolution: parse an SRI and dispatch to the right provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from vaultriever.logging import get_logger
6
+ from vaultriever.providers import SecretProviderRegistry
7
+ from vaultriever.sri import parse_sri
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ def resolve_secret(secret_sri: str) -> str:
13
+ """Resolve an SRI string to its secret value.
14
+
15
+ Raises:
16
+ SRIParseError: If the SRI is malformed.
17
+ ProviderNotRegisteredError: If the provider is unknown.
18
+ SecretRetrievalError: If the provider fails to retrieve the secret.
19
+ """
20
+ props = parse_sri(secret_sri)
21
+ provider = SecretProviderRegistry.get(props.provider)
22
+ logger.debug(
23
+ 'Resolving secret via provider=%r region=%r secret_name=%r',
24
+ props.provider,
25
+ props.region,
26
+ props.secret_name,
27
+ )
28
+ value = provider.get_secret_value(props)
29
+ # Always return a string so callers can wrap it in SecretStr.
30
+ return str(value)
@@ -0,0 +1,82 @@
1
+ """Pydantic-settings integration.
2
+
3
+ :class:`SecretSRIMixin` adds a wildcard ``mode='before'`` validator that
4
+ detects SRI strings in ``str``/``SecretStr`` fields and replaces them with the
5
+ resolved secret wrapped in ``SecretStr``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from typing import Any, ClassVar
13
+
14
+ from pydantic import SecretStr, ValidationInfo, field_validator
15
+ from pydantic_settings import BaseSettings, SettingsConfigDict
16
+
17
+ from vaultriever.logging import get_logger
18
+ from vaultriever.providers import SecretProviderRegistry
19
+ from vaultriever.resolver import resolve_secret
20
+ from vaultriever.sri import is_sri
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class SecretSRIMixin(BaseSettings):
26
+ """Base/mixin for ``BaseSettings`` models that resolves SRI values.
27
+
28
+ It extends ``BaseSettings`` (which keeps type checkers happy about
29
+ ``model_config``), so both forms work::
30
+
31
+ class MySettings(SecretSRIMixin):
32
+ api_key: str | SecretStr = 'aws:us-east-1:my-secret:API_KEY'
33
+
34
+ class MySettings(SecretSRIMixin, BaseSettings):
35
+ api_key: str | SecretStr = 'aws:us-east-1:my-secret:API_KEY'
36
+
37
+ Behavior:
38
+ - ``str``/``SecretStr`` values that look like an SRI *and* reference a
39
+ registered provider are resolved and returned as ``SecretStr``.
40
+ - All other values pass through unchanged.
41
+ - When ``enable_env_export`` is True (the default), validated values are
42
+ also written to ``os.environ`` under the field name. Note that this
43
+ writes resolved secrets in **plaintext** to the process environment;
44
+ set ``enable_env_export: ClassVar[bool] = False`` on your subclass to
45
+ opt out (the ``ClassVar`` annotation keeps pydantic from treating it as
46
+ a field).
47
+ """
48
+
49
+ # Ensures SRI strings used as field defaults are also resolved.
50
+ model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(validate_default=True)
51
+
52
+ enable_env_export: ClassVar[bool] = True
53
+
54
+ @field_validator('*', mode='before')
55
+ @classmethod
56
+ def resolve_sri(cls, value: Any, info: ValidationInfo) -> Any:
57
+ if isinstance(value, SecretStr):
58
+ raw = value.get_secret_value()
59
+ elif isinstance(value, str):
60
+ raw = value
61
+ else:
62
+ return value
63
+
64
+ # Only treat the value as an SRI if its provider is registered, so
65
+ # colon-heavy strings such as connection URLs pass through untouched.
66
+ if is_sri(raw) and SecretProviderRegistry.is_registered(raw.split(':', 1)[0]):
67
+ logger.debug('Resolving SRI for field %r', info.field_name)
68
+ secret = SecretStr(resolve_secret(raw))
69
+ cls._export_to_env(info.field_name, secret.get_secret_value())
70
+ return secret
71
+
72
+ cls._export_to_env(info.field_name, raw)
73
+ return value
74
+
75
+ @classmethod
76
+ def _export_to_env(cls, field_name: str | None, value: Any) -> None:
77
+ if not cls.enable_env_export or not field_name:
78
+ return
79
+ if isinstance(value, dict | list):
80
+ os.environ[field_name] = json.dumps(value)
81
+ else:
82
+ os.environ[field_name] = str(value)
vaultriever/sri.py ADDED
@@ -0,0 +1,92 @@
1
+ """Secret Resource Identifier (SRI) parsing and validation.
2
+
3
+ Canonical format::
4
+
5
+ provider:region:secret_name:secret_key
6
+
7
+ Examples:
8
+ - ``aws:us-east-1:my-secret:OPENAI_API_KEY``
9
+ - ``databricks::my-secret-scope:OPENAI_API_KEY`` (empty region -> default
10
+ profile/workspace)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from dataclasses import dataclass
17
+
18
+ from vaultriever.exceptions import SRIParseError
19
+
20
+ SRI_PARTS = 4
21
+ _PROVIDER_RE = re.compile(r'^[a-z][a-z0-9_-]*$')
22
+ # Regions/profiles are identifier-like. Keeping this strict prevents strings
23
+ # such as 'postgresql://user:pass@host:5432/db' from being detected as SRIs.
24
+ _REGION_RE = re.compile(r'^[A-Za-z0-9._-]*$')
25
+
26
+ _FORMAT_HINT = "Expected 'provider:region:secret_name:secret_key'"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class SecretProperties:
31
+ """Parsed components of an SRI string."""
32
+
33
+ provider: str
34
+ region: str | None
35
+ secret_name: str
36
+ secret_key: str
37
+
38
+
39
+ def is_sri(value: str) -> bool:
40
+ """Return True if ``value`` is structurally a valid SRI.
41
+
42
+ The check requires exactly 4 colon-separated parts, a lowercase provider
43
+ name, and non-empty secret_name/secret_key. Only the region may be empty.
44
+ This is a structural check only; it does not verify that the provider is
45
+ registered or that the secret exists.
46
+ """
47
+ if not isinstance(value, str):
48
+ return False
49
+ parts = value.split(':')
50
+ if len(parts) != SRI_PARTS:
51
+ return False
52
+ provider, region, secret_name, secret_key = parts
53
+ return (
54
+ bool(_PROVIDER_RE.match(provider))
55
+ and bool(_REGION_RE.match(region))
56
+ and bool(secret_name)
57
+ and bool(secret_key)
58
+ )
59
+
60
+
61
+ def parse_sri(value: str) -> SecretProperties:
62
+ """Parse an SRI string into :class:`SecretProperties`.
63
+
64
+ Raises:
65
+ SRIParseError: If the string is malformed. The message never includes
66
+ the full input to avoid leaking values that only look like SRIs.
67
+ """
68
+ parts = value.split(':')
69
+ if len(parts) != SRI_PARTS:
70
+ raise SRIParseError(f'Malformed SRI with {len(parts)} part(s). {_FORMAT_HINT}')
71
+
72
+ provider, region, secret_name, secret_key = parts
73
+ if not _PROVIDER_RE.match(provider):
74
+ raise SRIParseError(
75
+ f'Invalid SRI provider {provider!r}: must be a non-empty lowercase name. {_FORMAT_HINT}'
76
+ )
77
+ if not _REGION_RE.match(region):
78
+ raise SRIParseError(
79
+ f'Invalid SRI region {region!r}: only letters, digits, ".", "_" and "-" '
80
+ f'are allowed. {_FORMAT_HINT}'
81
+ )
82
+ if not secret_name:
83
+ raise SRIParseError(f'SRI secret_name must be non-empty. {_FORMAT_HINT}')
84
+ if not secret_key:
85
+ raise SRIParseError(f'SRI secret_key must be non-empty. {_FORMAT_HINT}')
86
+
87
+ return SecretProperties(
88
+ provider=provider,
89
+ region=region or None,
90
+ secret_name=secret_name,
91
+ secret_key=secret_key,
92
+ )
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaultriever
3
+ Version: 0.1.0
4
+ Summary: Retrieve secrets in an ARN-like way from different vaults seamlessly, compatible with pydantic-settings.
5
+ Project-URL: Homepage, https://github.com/DougTrajano/vaultriever
6
+ Project-URL: Repository, https://github.com/DougTrajano/vaultriever
7
+ Project-URL: Issues, https://github.com/DougTrajano/vaultriever/issues
8
+ Author: Douglas Trajano
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: aws,databricks,pydantic,pydantic-settings,secrets,vault
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: pydantic-settings>=2.0
25
+ Requires-Dist: pydantic>=2.0
26
+ Provides-Extra: aws
27
+ Requires-Dist: boto3>=1.26.0; extra == 'aws'
28
+ Provides-Extra: databricks
29
+ Requires-Dist: databricks-sdk>=0.20.0; extra == 'databricks'
30
+ Provides-Extra: docs
31
+ Requires-Dist: mkdocs-git-revision-date-localized-plugin>=1.2.0; extra == 'docs'
32
+ Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
33
+ Requires-Dist: mkdocs>=1.6.0; extra == 'docs'
34
+ Requires-Dist: mkdocstrings[python]>=0.25.0; extra == 'docs'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # vaultriever
38
+
39
+ Retrieve secrets in an ARN-like way from different vaults seamlessly, compatible with [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) — no custom field types, just `str` fields plus a reusable mixin.
40
+
41
+ ## Secret Resource Identifier (SRI)
42
+
43
+ Secrets are addressed with a 4-part string:
44
+
45
+ ```
46
+ provider:region:secret_name:secret_key
47
+ ```
48
+
49
+ | Provider | Example | Notes |
50
+ | --- | --- | --- |
51
+ | AWS Secrets Manager | `aws:us-east-1:my-secret:OPENAI_API_KEY` | Secret must be a JSON object; `secret_key` selects a key. |
52
+ | Databricks | `databricks::my-secret-scope:OPENAI_API_KEY` | Empty region → default workspace/profile; non-empty region → CLI profile name. |
53
+ | Azure Key Vault | `azure:<vault_or_region>:<secret_name>:<key_or_version>` | Planned. |
54
+ | GCP Secret Manager | `gcp:<project_or_region>:<secret_name>:<key_or_version>` | Planned. |
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install vaultriever[aws] # AWS Secrets Manager
60
+ pip install vaultriever[databricks] # Databricks (not needed on a Databricks runtime)
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ### With pydantic-settings
66
+
67
+ ```python
68
+ from pydantic import Field, SecretStr
69
+ from pydantic_settings import BaseSettings
70
+ from vaultriever import SecretSRIMixin
71
+
72
+
73
+ class OpenAISettings(SecretSRIMixin, BaseSettings):
74
+ openai_api_key: str | SecretStr = Field(
75
+ default='aws:us-east-1:my-secret:OPENAI_API_KEY',
76
+ description='OpenAI API key; SRI or literal.',
77
+ )
78
+
79
+
80
+ settings = OpenAISettings()
81
+ settings.openai_api_key # SecretStr('**********') — masked
82
+ settings.openai_api_key.get_secret_value() # the resolved secret
83
+ ```
84
+
85
+ Behavior:
86
+
87
+ - `str` / `SecretStr` values that look like an SRI (and reference a registered provider) are resolved and wrapped in `SecretStr`, so they stay masked in `repr()` and logs.
88
+ - Everything else passes through unchanged — literals, URLs, non-string values.
89
+ - Validated values are also exported to `os.environ` under the field name for downstream usage. **This writes resolved secrets in plaintext to the process environment**; opt out per model:
90
+
91
+ ```python
92
+ from typing import ClassVar
93
+
94
+
95
+ class MySettings(SecretSRIMixin, BaseSettings):
96
+ enable_env_export: ClassVar[bool] = False
97
+
98
+ api_key: str | SecretStr
99
+ ```
100
+
101
+ ### Standalone
102
+
103
+ ```python
104
+ from vaultriever import is_sri, resolve_secret
105
+
106
+ is_sri('aws:us-east-1:my-secret:API_KEY') # True
107
+ resolve_secret('aws:us-east-1:my-secret:API_KEY') # 'sk-...'
108
+ ```
109
+
110
+ ### Custom providers
111
+
112
+ ```python
113
+ from vaultriever import SecretProviderRegistry
114
+ from vaultriever.sri import SecretProperties
115
+
116
+
117
+ class MyVaultProvider:
118
+ name = 'myvault'
119
+
120
+ def get_secret_value(self, props: SecretProperties) -> str:
121
+ ...
122
+
123
+
124
+ SecretProviderRegistry.register(MyVaultProvider())
125
+ # Now 'myvault:region:name:key' SRIs resolve through it.
126
+ ```
127
+
128
+ ## Providers
129
+
130
+ ### AWS Secrets Manager
131
+
132
+ - Credentials come from the AWS SDK default chain (env vars, profile, IAM role).
133
+ - The region is required and taken from the SRI.
134
+ - Secret payloads are cached per `(secret_name, region)` for the process lifetime; call `AWSSecretProvider.clear_cache()` after a rotation.
135
+
136
+ ### Databricks
137
+
138
+ - On a Databricks runtime, secrets are read via the native `dbutils`.
139
+ - Elsewhere, the [databricks-sdk](https://github.com/databricks/databricks-sdk-py) is used with default authentication, or with the CLI profile named by the SRI's region component (`databricks:staging:my-scope:MY_KEY`).
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ uv sync --all-extras --dev
145
+ uv run pytest
146
+ uv run ruff check .
147
+ uv run mypy
148
+ ```
149
+
150
+ Releases are published to PyPI by pushing a `vX.Y.Z` tag.
151
+
152
+ ## License
153
+
154
+ [MIT](LICENSE)
@@ -0,0 +1,17 @@
1
+ vaultriever/__init__.py,sha256=qUG25TBeN9tcUJvEUMO25twEZ4qtkqpldzlErjFweGI,909
2
+ vaultriever/exceptions.py,sha256=bfbStUjIh4AsrfN_rlalr6KktUK81uuvVIq4ZuH0Y6U,808
3
+ vaultriever/logging.py,sha256=oEqR69yFqWKCShJ6vB8-7NioQGFn-QqrMHQpU5VDOBY,490
4
+ vaultriever/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ vaultriever/resolver.py,sha256=dhcJuN10Y67mqiP6_s2ZFcsRIE9U7v8rzqgbCsaCEoY,976
6
+ vaultriever/settings_mixin.py,sha256=c6amQmxC9uXHaEykn-wrNXz_xNrnXEG1-X0VIgxWyqY,3130
7
+ vaultriever/sri.py,sha256=yXnUpjiIqFkqORC3lcpduAK-XJBGFPw_2D8DDcFDS_I,2909
8
+ vaultriever/providers/__init__.py,sha256=kp5Gt35U39ILjT8H5hiL7KwCeNA9ixT8vvQBh-daH54,733
9
+ vaultriever/providers/aws.py,sha256=GrBF-qhXVmD3r0hTX5xkWh2N4f6Z4uNW9p1U4yh3JM8,2861
10
+ vaultriever/providers/azure.py,sha256=rMDhDuM4sO2LrSJe4lJ4y38QCZftr55-3SVnQz1QSAc,471
11
+ vaultriever/providers/base.py,sha256=dmG6dAaVEOtzjqZatVlMDBkebI1tLMofpoLhV55e-U4,1856
12
+ vaultriever/providers/databricks.py,sha256=0KHas-i-WhjPHEsZW7dPvzvLE9zMHxvu-WCaBwry9Fg,3481
13
+ vaultriever/providers/gcp.py,sha256=WuQynwRlgz94jKXNgb0do01JFiOi1zyV94IfJqVm_Yo,468
14
+ vaultriever-0.1.0.dist-info/METADATA,sha256=ZFx0f-qh2F5pXrBxH5EBPSpm3iypGpP1WZzg-_q0sSk,5201
15
+ vaultriever-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
16
+ vaultriever-0.1.0.dist-info/licenses/LICENSE,sha256=Qo4k0W8GvBayeeEC1Nytlxt4FnUjcfpuUEQfnGb2ZrE,1072
17
+ vaultriever-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Douglas Trajano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.