Skip to content
Open
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
25 changes: 13 additions & 12 deletions src/datajoint/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,26 +691,27 @@ def _load_secrets(self, secrets_dir: Path) -> None:
self.database.password = db_password
logger.debug(f"Loaded database.password from {secrets_dir}")

# Load per-store secrets (stores.<name>.access_key, stores.<name>.secret_key)
# Iterate through all files in secrets directory
# Load per-store secrets from any stores.<name>.<attr> file.
# The attr name is recorded as-is on stores.<name>; this lets
# plugin-registered adapters define their own secret fields
# (e.g. a Bearer ``token`` for HTTP-based protocols) without
# forcing AWS-style ``access_key`` / ``secret_key`` naming.
if secrets_dir.is_dir():
for secret_file in secrets_dir.iterdir():
if not secret_file.is_file() or secret_file.name.startswith("."):
continue

parts = secret_file.name.split(".")
# Check for stores.<name>.access_key or stores.<name>.secret_key pattern
if len(parts) == 3 and parts[0] == "stores":
store_name, attr = parts[1], parts[2]
if attr in ("access_key", "secret_key"):
value = secret_file.read_text().strip()
# Initialize store dict if needed
if store_name not in self.stores:
self.stores[store_name] = {}
# Only set if not already present
if attr not in self.stores[store_name]:
self.stores[store_name][attr] = value
logger.debug(f"Loaded stores.{store_name}.{attr} from {secrets_dir}")
value = secret_file.read_text().strip()
# Initialize store dict if needed
if store_name not in self.stores:
self.stores[store_name] = {}
# Only set if not already present (config / env vars win)
if attr not in self.stores[store_name]:
self.stores[store_name][attr] = value
logger.debug(f"Loaded stores.{store_name}.{attr} from {secrets_dir}")

@contextmanager
def override(self, **kwargs: Any) -> Iterator["Config"]:
Expand Down
13 changes: 3 additions & 10 deletions src/datajoint/storage_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,9 @@ def get_storage_adapter(protocol: str) -> StorageAdapter | None:

def _discover_adapters() -> None:
"""Load storage adapters from datajoint.storage entry points."""
try:
from importlib.metadata import entry_points
except ImportError:
logger.debug("importlib.metadata not available, skipping adapter discovery")
return

try:
eps = entry_points(group="datajoint.storage")
except TypeError:
eps = entry_points().get("datajoint.storage", [])
from importlib.metadata import entry_points

eps = entry_points(group="datajoint.storage")

for ep in eps:
if ep.name in _adapter_registry:
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,24 @@ def test_secrets_do_not_override_existing(self, tmp_path):
finally:
cfg.stores = original_stores

def test_load_store_arbitrary_attr(self, tmp_path):
"""Plugin-registered adapters can use arbitrary secret-field names."""
# e.g. an HTTP-based protocol that authenticates with a Bearer token
secrets_dir = tmp_path / SECRETS_DIRNAME
secrets_dir.mkdir()
(secrets_dir / "stores.bearer_store.token").write_text("dapibdfXXXX")
(secrets_dir / "stores.bearer_store.api_key").write_text("ak_yyy")

cfg = settings.Config()
original_stores = cfg.stores.copy()
try:
cfg._load_secrets(secrets_dir)

assert cfg.stores["bearer_store"]["token"] == "dapibdfXXXX"
assert cfg.stores["bearer_store"]["api_key"] == "ak_yyy"
finally:
cfg.stores = original_stores


class TestDisplaySettings:
"""Test display-related settings."""
Expand Down
Loading