credchain 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.
- credchain/__init__.py +38 -0
- credchain/chain.py +38 -0
- credchain/env.py +50 -0
- credchain/file.py +65 -0
- credchain/keyring.py +115 -0
- credchain/protocol.py +5 -0
- credchain/py.typed +0 -0
- credchain-0.1.0.dist-info/METADATA +160 -0
- credchain-0.1.0.dist-info/RECORD +11 -0
- credchain-0.1.0.dist-info/WHEEL +4 -0
- credchain-0.1.0.dist-info/licenses/LICENSE +21 -0
credchain/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""credchain: read credentials from a chain of sources, with automatic fallback.
|
|
2
|
+
|
|
3
|
+
Providers (import directly):
|
|
4
|
+
CredentialsProviderChain — tries providers in order until one succeeds
|
|
5
|
+
EnvironmentCredentialsProvider — reads from environment variables
|
|
6
|
+
FileCredentialsProvider — reads from files, e.g. mounted container secrets
|
|
7
|
+
KeyringCredentialsProvider — reads from the OS keychain (requires credchain[keyring])
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from credchain import CredentialsProviderChain, EnvironmentCredentialsProvider
|
|
12
|
+
from credchain.keyring import KeyringCredentialsProvider # optional dep
|
|
13
|
+
|
|
14
|
+
chain = CredentialsProviderChain([
|
|
15
|
+
EnvironmentCredentialsProvider("MY_USERNAME", "MY_PASSWORD"),
|
|
16
|
+
KeyringCredentialsProvider("my-service"),
|
|
17
|
+
])
|
|
18
|
+
username, password = chain.get_credentials()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
22
|
+
|
|
23
|
+
from .chain import CredentialsProviderChain
|
|
24
|
+
from .env import EnvironmentCredentialsProvider
|
|
25
|
+
from .file import FileCredentialsProvider
|
|
26
|
+
from .protocol import CredentialsProvider
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
__version__ = version("credchain")
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
__version__ = "0.0.0-dev"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CredentialsProvider",
|
|
35
|
+
"CredentialsProviderChain",
|
|
36
|
+
"EnvironmentCredentialsProvider",
|
|
37
|
+
"FileCredentialsProvider",
|
|
38
|
+
]
|
credchain/chain.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .protocol import CredentialsProvider
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CredentialsProviderChain:
|
|
9
|
+
"""Chain of credentials providers invoked in priority order until one succeeds.
|
|
10
|
+
|
|
11
|
+
Nothing derived from a credential is logged, not even a prefix: in token
|
|
12
|
+
mode a provider returns the token in both positions, so a "username" may
|
|
13
|
+
be a secret.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, providers: list[CredentialsProvider]):
|
|
17
|
+
self.providers = providers
|
|
18
|
+
|
|
19
|
+
def get_credentials(self) -> tuple[str | None, str | None]:
|
|
20
|
+
logger.debug("Retrieving credentials using credentials provider chain...")
|
|
21
|
+
for i, provider in enumerate(self.providers):
|
|
22
|
+
name = provider.__class__.__name__
|
|
23
|
+
try:
|
|
24
|
+
logger.debug(
|
|
25
|
+
f"Trying provider {i + 1}/{len(self.providers)}: {name}..."
|
|
26
|
+
)
|
|
27
|
+
username, password = provider.get_credentials()
|
|
28
|
+
if username and password:
|
|
29
|
+
logger.debug(f"Retrieved credentials from {name}")
|
|
30
|
+
return username, password
|
|
31
|
+
logger.debug(f"No credentials found from {name}")
|
|
32
|
+
except Exception as e:
|
|
33
|
+
# The exception type only: a message may quote the secret or a path to it.
|
|
34
|
+
logger.error(
|
|
35
|
+
f"Error retrieving credentials from {name}: {type(e).__name__}"
|
|
36
|
+
)
|
|
37
|
+
logger.debug("No credentials found from any provider in the chain")
|
|
38
|
+
return None, None
|
credchain/env.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EnvironmentCredentialsProvider:
|
|
8
|
+
"""Provider for credentials from environment variables.
|
|
9
|
+
|
|
10
|
+
Two-var mode (username + password)::
|
|
11
|
+
|
|
12
|
+
EnvironmentCredentialsProvider("MY_USERNAME", "MY_PASSWORD")
|
|
13
|
+
|
|
14
|
+
Token mode (single env var)::
|
|
15
|
+
|
|
16
|
+
EnvironmentCredentialsProvider.for_token("MY_TOKEN")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, username_env_var_name: str, password_env_var_name: str):
|
|
20
|
+
self.username_env_var_name: str | None = username_env_var_name
|
|
21
|
+
self.password_env_var_name = password_env_var_name
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def for_token(cls, token_env_var_name: str) -> "EnvironmentCredentialsProvider":
|
|
25
|
+
"""Create a token-mode provider that reads a single environment variable."""
|
|
26
|
+
instance = cls.__new__(cls)
|
|
27
|
+
instance.username_env_var_name = None
|
|
28
|
+
instance.password_env_var_name = token_env_var_name
|
|
29
|
+
return instance
|
|
30
|
+
|
|
31
|
+
def get_credentials(self) -> tuple[str | None, str | None]:
|
|
32
|
+
if self.username_env_var_name is None:
|
|
33
|
+
token = os.environ.get(self.password_env_var_name)
|
|
34
|
+
if token:
|
|
35
|
+
logger.debug(f"Read token from ${self.password_env_var_name}")
|
|
36
|
+
return token, token
|
|
37
|
+
logger.debug(f"No token in ${self.password_env_var_name}")
|
|
38
|
+
return None, None
|
|
39
|
+
|
|
40
|
+
username = os.environ.get(self.username_env_var_name)
|
|
41
|
+
password = os.environ.get(self.password_env_var_name)
|
|
42
|
+
if username and password:
|
|
43
|
+
logger.debug(
|
|
44
|
+
f"Read credentials from ${self.username_env_var_name} and ${self.password_env_var_name}"
|
|
45
|
+
)
|
|
46
|
+
return username, password
|
|
47
|
+
logger.debug(
|
|
48
|
+
f"No credentials in ${self.username_env_var_name} and ${self.password_env_var_name}"
|
|
49
|
+
)
|
|
50
|
+
return None, None
|
credchain/file.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FileCredentialsProvider:
|
|
8
|
+
"""Provider for credentials stored in files, such as mounted container secrets.
|
|
9
|
+
|
|
10
|
+
Two-file mode (username + password)::
|
|
11
|
+
|
|
12
|
+
FileCredentialsProvider("/run/secrets/username", "/run/secrets/password")
|
|
13
|
+
|
|
14
|
+
Token mode (single file)::
|
|
15
|
+
|
|
16
|
+
FileCredentialsProvider.for_token("/run/secrets/token")
|
|
17
|
+
|
|
18
|
+
Each file holds one value; one trailing line break is removed, because
|
|
19
|
+
secret files are often written with one. A missing file means no
|
|
20
|
+
credentials, so a provider chain falls through to its next provider;
|
|
21
|
+
any other read error propagates.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, username_file: str | Path, password_file: str | Path):
|
|
25
|
+
self.username_file: Path | None = Path(username_file)
|
|
26
|
+
self.password_file = Path(password_file)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def for_token(cls, token_file: str | Path) -> "FileCredentialsProvider":
|
|
30
|
+
"""Create a token-mode provider that reads a single file."""
|
|
31
|
+
instance = cls.__new__(cls)
|
|
32
|
+
instance.username_file = None
|
|
33
|
+
instance.password_file = Path(token_file)
|
|
34
|
+
return instance
|
|
35
|
+
|
|
36
|
+
def get_credentials(self) -> tuple[str | None, str | None]:
|
|
37
|
+
if self.username_file is None:
|
|
38
|
+
token = _read(self.password_file)
|
|
39
|
+
if token:
|
|
40
|
+
logger.debug(f"Read token from {self.password_file}")
|
|
41
|
+
return token, token
|
|
42
|
+
logger.debug(f"No token in {self.password_file}")
|
|
43
|
+
return None, None
|
|
44
|
+
|
|
45
|
+
username = _read(self.username_file)
|
|
46
|
+
password = _read(self.password_file)
|
|
47
|
+
if username and password:
|
|
48
|
+
logger.debug(
|
|
49
|
+
f"Read credentials from {self.username_file} and {self.password_file}"
|
|
50
|
+
)
|
|
51
|
+
return username, password
|
|
52
|
+
logger.debug(f"No credentials in {self.username_file} and {self.password_file}")
|
|
53
|
+
return None, None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read(path: Path) -> str | None:
|
|
57
|
+
try:
|
|
58
|
+
text = path.read_text(encoding="utf-8")
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
return None
|
|
61
|
+
if text.endswith("\r\n"):
|
|
62
|
+
return text[:-2]
|
|
63
|
+
if text.endswith("\n"):
|
|
64
|
+
return text[:-1]
|
|
65
|
+
return text
|
credchain/keyring.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import keyring as _keyring
|
|
6
|
+
except ImportError as e:
|
|
7
|
+
raise ImportError(
|
|
8
|
+
"keyring is required for KeyringCredentialsProvider. "
|
|
9
|
+
"Install it with: pip install credchain[keyring]"
|
|
10
|
+
) from e
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# A hung OS keychain call (e.g. a locked keychain waiting for an unlock prompt
|
|
15
|
+
# that never appears in a headless session) must not stall the caller forever.
|
|
16
|
+
DEFAULT_KEYRING_TIMEOUT = 5.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KeyringCredentialsProvider:
|
|
20
|
+
"""Provider for credentials stored in the OS keychain via keyring.
|
|
21
|
+
|
|
22
|
+
Two-key mode (username + password stored under separate account keys)::
|
|
23
|
+
|
|
24
|
+
KeyringCredentialsProvider("my-service")
|
|
25
|
+
# reads: keyring get my-service username
|
|
26
|
+
# keyring get my-service password
|
|
27
|
+
|
|
28
|
+
Token mode (single keyring entry)::
|
|
29
|
+
|
|
30
|
+
KeyringCredentialsProvider.for_token("my-service")
|
|
31
|
+
# reads: keyring get my-service token
|
|
32
|
+
|
|
33
|
+
Or with a custom key name::
|
|
34
|
+
|
|
35
|
+
KeyringCredentialsProvider.for_token("my-service", token_key="jwt")
|
|
36
|
+
# reads: keyring get my-service jwt
|
|
37
|
+
|
|
38
|
+
The keychain lookup is bounded by ``timeout`` seconds (default 5). When the
|
|
39
|
+
keychain does not answer in time, a warning is logged and no credentials are
|
|
40
|
+
returned, so a provider chain falls through to its next provider. Pass
|
|
41
|
+
``timeout=None`` to wait indefinitely.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
service: str,
|
|
47
|
+
username_key: str = "username",
|
|
48
|
+
password_key: str = "password",
|
|
49
|
+
timeout: float | None = DEFAULT_KEYRING_TIMEOUT,
|
|
50
|
+
):
|
|
51
|
+
self.service = service
|
|
52
|
+
self.username_key: str | None = username_key
|
|
53
|
+
self.password_key = password_key
|
|
54
|
+
self.timeout = timeout
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def for_token(
|
|
58
|
+
cls,
|
|
59
|
+
service: str,
|
|
60
|
+
token_key: str = "token",
|
|
61
|
+
timeout: float | None = DEFAULT_KEYRING_TIMEOUT,
|
|
62
|
+
) -> "KeyringCredentialsProvider":
|
|
63
|
+
"""Create a token-mode provider that reads a single keyring entry."""
|
|
64
|
+
instance = cls.__new__(cls)
|
|
65
|
+
instance.service = service
|
|
66
|
+
instance.username_key = None
|
|
67
|
+
instance.password_key = token_key
|
|
68
|
+
instance.timeout = timeout
|
|
69
|
+
return instance
|
|
70
|
+
|
|
71
|
+
def get_credentials(self) -> tuple[str | None, str | None]:
|
|
72
|
+
if self.timeout is None:
|
|
73
|
+
return self._read_credentials()
|
|
74
|
+
|
|
75
|
+
# The keyring API is synchronous and cannot be cancelled, so run it in a
|
|
76
|
+
# daemon thread and stop waiting after the timeout. A thread left hanging
|
|
77
|
+
# in the keychain does not keep the process alive on exit.
|
|
78
|
+
outcome: dict = {}
|
|
79
|
+
|
|
80
|
+
def read() -> None:
|
|
81
|
+
try:
|
|
82
|
+
outcome["result"] = self._read_credentials()
|
|
83
|
+
except BaseException as e:
|
|
84
|
+
outcome["error"] = e
|
|
85
|
+
|
|
86
|
+
thread = threading.Thread(
|
|
87
|
+
target=read, name=f"keyring-{self.service}", daemon=True
|
|
88
|
+
)
|
|
89
|
+
thread.start()
|
|
90
|
+
thread.join(self.timeout)
|
|
91
|
+
if thread.is_alive():
|
|
92
|
+
logger.warning(
|
|
93
|
+
f"Keyring service '{self.service}' did not respond within {self.timeout}s, skipping it"
|
|
94
|
+
)
|
|
95
|
+
return None, None
|
|
96
|
+
if "error" in outcome:
|
|
97
|
+
raise outcome["error"]
|
|
98
|
+
return outcome["result"]
|
|
99
|
+
|
|
100
|
+
def _read_credentials(self) -> tuple[str | None, str | None]:
|
|
101
|
+
if self.username_key is None:
|
|
102
|
+
token = _keyring.get_password(self.service, self.password_key)
|
|
103
|
+
if token:
|
|
104
|
+
logger.debug(f"Read token from keyring service '{self.service}'")
|
|
105
|
+
return token, token
|
|
106
|
+
logger.debug(f"No token in keyring service '{self.service}'")
|
|
107
|
+
return None, None
|
|
108
|
+
|
|
109
|
+
username = _keyring.get_password(self.service, self.username_key)
|
|
110
|
+
password = _keyring.get_password(self.service, self.password_key)
|
|
111
|
+
if username and password:
|
|
112
|
+
logger.debug(f"Read credentials from keyring service '{self.service}'")
|
|
113
|
+
return username, password
|
|
114
|
+
logger.debug(f"No credentials in keyring service '{self.service}'")
|
|
115
|
+
return None, None
|
credchain/protocol.py
ADDED
credchain/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: credchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Credential provider chain: read usernames, passwords and tokens from environment variables, the OS keychain or secret files, with automatic fallback.
|
|
5
|
+
Author-email: Stephan Eberle <stephaneberle9@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Provides-Extra: keyring
|
|
10
|
+
Requires-Dist: keyring>=25.0.0; extra == 'keyring'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# credchain <!-- omit in toc -->
|
|
14
|
+
|
|
15
|
+
Read the credentials an application needs — a username and password, or an
|
|
16
|
+
API token — from environment variables, the OS keychain or secret files, with
|
|
17
|
+
automatic fallback through a priority chain.
|
|
18
|
+
|
|
19
|
+
`credchain` has no framework dependency. FastMCP servers that also take
|
|
20
|
+
credentials from HTTP headers use
|
|
21
|
+
[fastmcp-creds](https://github.com/stephaneberle9/fastmcp-creds), which builds
|
|
22
|
+
on it.
|
|
23
|
+
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
- [Usage](#usage)
|
|
26
|
+
- [Credential provider chain](#credential-provider-chain)
|
|
27
|
+
- [Environment variables](#environment-variables)
|
|
28
|
+
- [OS keychain](#os-keychain)
|
|
29
|
+
- [Secret files](#secret-files)
|
|
30
|
+
- [Logging](#logging)
|
|
31
|
+
- [API Reference](#api-reference)
|
|
32
|
+
- [`CredentialsProvider`](#credentialsprovider)
|
|
33
|
+
- [`CredentialsProviderChain`](#credentialsproviderchain)
|
|
34
|
+
- [`EnvironmentCredentialsProvider`](#environmentcredentialsprovider)
|
|
35
|
+
- [`KeyringCredentialsProvider`](#keyringcredentialsprovider)
|
|
36
|
+
- [`FileCredentialsProvider`](#filecredentialsprovider)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
The package is not published to PyPI yet. Install it from its Git repository
|
|
43
|
+
or a local checkout:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add "credchain @ git+https://github.com/stephaneberle9/python-credchain.git"
|
|
47
|
+
# with the OS keychain provider:
|
|
48
|
+
uv add "credchain[keyring] @ git+https://github.com/stephaneberle9/python-credchain.git"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Python 3.10+.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
### Credential provider chain
|
|
56
|
+
|
|
57
|
+
Compose providers in priority order. The chain tries each in sequence and
|
|
58
|
+
returns the first result where both username and password (or token) are
|
|
59
|
+
non-empty.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from credchain import CredentialsProviderChain, EnvironmentCredentialsProvider
|
|
63
|
+
from credchain.keyring import KeyringCredentialsProvider # optional dep
|
|
64
|
+
|
|
65
|
+
chain = CredentialsProviderChain([
|
|
66
|
+
EnvironmentCredentialsProvider("MY_USERNAME", "MY_PASSWORD"),
|
|
67
|
+
KeyringCredentialsProvider("my-service"),
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
username, password = chain.get_credentials()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A provider that raises is logged (exception type only) and skipped.
|
|
74
|
+
|
|
75
|
+
### Environment variables
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from credchain import EnvironmentCredentialsProvider
|
|
79
|
+
|
|
80
|
+
provider = EnvironmentCredentialsProvider("MY_USERNAME", "MY_PASSWORD")
|
|
81
|
+
token_provider = EnvironmentCredentialsProvider.for_token("MY_API_TOKEN")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### OS keychain
|
|
85
|
+
|
|
86
|
+
Reads credentials stored in the OS keychain via
|
|
87
|
+
[`keyring`](https://pypi.org/project/keyring/). Requires `credchain[keyring]`.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from credchain.keyring import KeyringCredentialsProvider
|
|
91
|
+
|
|
92
|
+
provider = KeyringCredentialsProvider("my-service") # "username" + "password" entries
|
|
93
|
+
provider = KeyringCredentialsProvider.for_token("my-service") # one "token" entry
|
|
94
|
+
provider = KeyringCredentialsProvider("my-service", timeout=10)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The keychain lookup is bounded by a timeout (default 5 seconds), so a locked
|
|
98
|
+
or unresponsive keychain, e.g. one waiting for an unlock prompt in a headless
|
|
99
|
+
session, cannot hang the caller. On timeout, a warning is logged and
|
|
100
|
+
`(None, None)` is returned, so a chain falls through to its next provider.
|
|
101
|
+
|
|
102
|
+
### Secret files
|
|
103
|
+
|
|
104
|
+
Reads one value per file, such as a secret mounted into a container. One
|
|
105
|
+
trailing line break is removed; a missing file means no credentials.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from credchain import FileCredentialsProvider
|
|
109
|
+
|
|
110
|
+
provider = FileCredentialsProvider("/run/secrets/username", "/run/secrets/password")
|
|
111
|
+
token_provider = FileCredentialsProvider.for_token("/run/secrets/api-token")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Logging
|
|
115
|
+
|
|
116
|
+
Providers log at debug level where they looked and whether they found
|
|
117
|
+
something, never a credential or any part of one: in token mode the token is
|
|
118
|
+
returned in both positions, so even a "username" can be a secret. Errors are
|
|
119
|
+
logged with their exception type only, because messages may quote values.
|
|
120
|
+
|
|
121
|
+
## API Reference
|
|
122
|
+
|
|
123
|
+
### `CredentialsProvider`
|
|
124
|
+
|
|
125
|
+
A `Protocol`: any object with a
|
|
126
|
+
`get_credentials() -> tuple[str | None, str | None]` method satisfies it.
|
|
127
|
+
|
|
128
|
+
### `CredentialsProviderChain`
|
|
129
|
+
|
|
130
|
+
`CredentialsProviderChain(providers)` — tries each provider in order and
|
|
131
|
+
returns the first result where both values are non-empty, else
|
|
132
|
+
`(None, None)`.
|
|
133
|
+
|
|
134
|
+
### `EnvironmentCredentialsProvider`
|
|
135
|
+
|
|
136
|
+
`EnvironmentCredentialsProvider(username_env_var_name, password_env_var_name)`
|
|
137
|
+
reads two environment variables;
|
|
138
|
+
`EnvironmentCredentialsProvider.for_token(token_env_var_name)` reads one and
|
|
139
|
+
returns it as both elements.
|
|
140
|
+
|
|
141
|
+
### `KeyringCredentialsProvider`
|
|
142
|
+
|
|
143
|
+
`KeyringCredentialsProvider(service, username_key="username", password_key="password", timeout=5.0)`
|
|
144
|
+
reads two keychain entries of `service`;
|
|
145
|
+
`KeyringCredentialsProvider.for_token(service, token_key="token", timeout=5.0)`
|
|
146
|
+
reads one. `timeout=None` waits indefinitely. Requires `credchain[keyring]`.
|
|
147
|
+
|
|
148
|
+
### `FileCredentialsProvider`
|
|
149
|
+
|
|
150
|
+
`FileCredentialsProvider(username_file, password_file)` reads two files;
|
|
151
|
+
`FileCredentialsProvider.for_token(token_file)` reads one. Errors other than a
|
|
152
|
+
missing file propagate.
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
credchain/__init__.py,sha256=FB50Ct0oPjKBB87EPsn9boQ-Bz_MjMiWVz9_7DXtzEw,1330
|
|
2
|
+
credchain/chain.py,sha256=8G1MvqtqLBRyHBF-arLrzmHaWYQGrTwukUEER6rBgec,1535
|
|
3
|
+
credchain/env.py,sha256=-zrHsKsJiYAx9FkbGGVtoS4w1Jh0xsziRdDGqng9DUc,1855
|
|
4
|
+
credchain/file.py,sha256=Q0bigjHBlzrObXfSAokkqP0K-Tr4kJmbJf5kfNrq2Aw,2228
|
|
5
|
+
credchain/keyring.py,sha256=YZBJa2Q1UCo92QDo9cspT42htEVuu-Nz7998xC078H4,4134
|
|
6
|
+
credchain/protocol.py,sha256=QMZ4brqsqjzs5b-jh1BBGcvSbpZzSGBQ5wPXCK628b0,135
|
|
7
|
+
credchain/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
credchain-0.1.0.dist-info/METADATA,sha256=1UH2vueRnffQX2pfB1NobVKMRDa17tGBaSrebdV0HzQ,5492
|
|
9
|
+
credchain-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
credchain-0.1.0.dist-info/licenses/LICENSE,sha256=gj8yMQmXVQ-u8qsZ6UdnofKnIp2wv0tBByT9E-mkpYE,1071
|
|
11
|
+
credchain-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stephan Eberle
|
|
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.
|