@@ -335,17 +335,32 @@ class Config(BaseSettings):
335335 jobs : JobsSettings = Field (default_factory = JobsSettings )
336336
337337 # Unified stores configuration (replaces external and object_storage)
338+ # ``validation_alias`` redirects pydantic-settings' env source away from the
339+ # natural ``DJ_STORES`` so it doesn't auto-parse on Config() construction.
340+ # ``DJ_STORES`` is handled by ``_apply_stores_env`` after the config file
341+ # load so env-var precedence is honored. *New in 2.3.*
338342 stores : dict [str , Any ] = Field (
339343 default_factory = dict ,
344+ validation_alias = "_DJ_STORES_PYDANTIC_DISABLED" ,
340345 description = "Unified object storage configuration. "
341346 "Use stores.default to designate default store. "
342- "Configure named stores as stores.<name>.protocol, stores.<name>.location, etc." ,
347+ "Configure named stores as stores.<name>.protocol, stores.<name>.location, etc. "
348+ "Set via DJ_STORES (JSON object) or in datajoint.json. *New in 2.3* for "
349+ "DJ_STORES env-var support." ,
343350 )
344351
345352 # Top-level settings
346353 loglevel : Literal ["DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" ] = Field (default = "INFO" , validation_alias = "DJ_LOG_LEVEL" )
347354 safemode : bool = True
348355
356+ ignore_config_file : bool = Field (
357+ default = False ,
358+ validation_alias = "DJ_IGNORE_CONFIG_FILE" ,
359+ description = "If True, skip loading datajoint.json and the secrets directory. "
360+ "Intended for env-var-only deployments (e.g. the DataJoint platform). "
361+ "*New in 2.3.*" ,
362+ )
363+
349364 # Cache path for query results
350365 query_cache : Path | None = None
351366
@@ -691,26 +706,50 @@ def _load_secrets(self, secrets_dir: Path) -> None:
691706 self .database .password = db_password
692707 logger .debug (f"Loaded database.password from { secrets_dir } " )
693708
694- # Load per-store secrets (stores.<name>.access_key, stores.<name>.secret_key)
695- # Iterate through all files in secrets directory
709+ # Load per-store secrets from any stores.<name>.<attr> file.
710+ # The attr name is recorded as-is on stores.<name>; this lets
711+ # plugin-registered adapters define their own secret fields
712+ # (e.g. a Bearer ``token`` for HTTP-based protocols) without
713+ # forcing AWS-style ``access_key`` / ``secret_key`` naming.
696714 if secrets_dir .is_dir ():
697715 for secret_file in secrets_dir .iterdir ():
698716 if not secret_file .is_file () or secret_file .name .startswith ("." ):
699717 continue
700718
701719 parts = secret_file .name .split ("." )
702- # Check for stores.<name>.access_key or stores.<name>.secret_key pattern
703720 if len (parts ) == 3 and parts [0 ] == "stores" :
704721 store_name , attr = parts [1 ], parts [2 ]
705- if attr in ("access_key" , "secret_key" ):
706- value = secret_file .read_text ().strip ()
707- # Initialize store dict if needed
708- if store_name not in self .stores :
709- self .stores [store_name ] = {}
710- # Only set if not already present
711- if attr not in self .stores [store_name ]:
712- self .stores [store_name ][attr ] = value
713- logger .debug (f"Loaded stores.{ store_name } .{ attr } from { secrets_dir } " )
722+ value = secret_file .read_text ().strip ()
723+ # Initialize store dict if needed
724+ if store_name not in self .stores :
725+ self .stores [store_name ] = {}
726+ # Only set if not already present (config / env vars win)
727+ if attr not in self .stores [store_name ]:
728+ self .stores [store_name ][attr ] = value
729+ logger .debug (f"Loaded stores.{ store_name } .{ attr } from { secrets_dir } " )
730+
731+ def _apply_stores_env (self ) -> None :
732+ """Replace ``self.stores`` from the ``DJ_STORES`` env var if set.
733+
734+ ``DJ_STORES`` holds a JSON object in the same shape as the ``stores``
735+ block of ``datajoint.json``. This lets env-var-only deployments
736+ configure plugin-registered storage adapters with arbitrary attr
737+ names (e.g. a Bearer ``token`` field) without negotiating an env-var
738+ naming scheme per attr.
739+
740+ *New in 2.3.*
741+ """
742+ raw = os .environ .get ("DJ_STORES" )
743+ if not raw :
744+ return
745+ try :
746+ data = json .loads (raw )
747+ except json .JSONDecodeError as e :
748+ raise ValueError (f"DJ_STORES contains invalid JSON: { e } " ) from e
749+ if not isinstance (data , dict ):
750+ raise ValueError (f"DJ_STORES must be a JSON object, got { type (data ).__name__ } " )
751+ self .stores = data
752+ logger .debug ("Loaded stores from DJ_STORES env var" )
714753
715754 @contextmanager
716755 def override (self , ** kwargs : Any ) -> Iterator ["Config" ]:
@@ -785,9 +824,13 @@ def save_template(
785824
786825 Credentials should NOT be stored in datajoint.json. Instead, use either:
787826
788- - Environment variables (``DJ_USER``, ``DJ_PASS``, ``DJ_HOST``, etc.)
827+ - Environment variables (``DJ_USER``, ``DJ_PASS``, ``DJ_HOST``,
828+ ``DJ_STORES`` for JSON-encoded store configs, etc.)
789829 - The ``.secrets/`` directory (created alongside datajoint.json)
790830
831+ Set ``DJ_IGNORE_CONFIG_FILE=true`` to skip both ``datajoint.json`` and
832+ the secrets directory entirely (env-var-only configuration).
833+
791834 Parameters
792835 ----------
793836 path : str or Path, optional
@@ -962,25 +1005,29 @@ def _create_config() -> Config:
9621005 """Create and initialize the global config instance."""
9631006 cfg = Config ()
9641007
965- # Find config file (recursive parent search)
966- config_path = find_config_file ()
1008+ config_path : Path | None = None
1009+ if not cfg .ignore_config_file :
1010+ config_path = find_config_file ()
1011+ if config_path is not None :
1012+ try :
1013+ cfg .load (config_path )
1014+ except Exception as e :
1015+ warnings .warn (f"Failed to load config from { config_path } : { e } " )
1016+ else :
1017+ warnings .warn (
1018+ f"No { CONFIG_FILENAME } found. Using defaults and environment variables. "
1019+ f"Run `dj.config.save_template()` to create a template configuration." ,
1020+ stacklevel = 2 ,
1021+ )
9671022
968- if config_path is not None :
969- try :
970- cfg .load (config_path )
971- except Exception as e :
972- warnings .warn (f"Failed to load config from { config_path } : { e } " )
973- else :
974- warnings .warn (
975- f"No { CONFIG_FILENAME } found. Using defaults and environment variables. "
976- f"Run `dj.config.save_template()` to create a template configuration." ,
977- stacklevel = 2 ,
978- )
1023+ # DJ_STORES (if set) overrides the stores dict from the config file
1024+ cfg ._apply_stores_env ()
9791025
980- # Find and load secrets
981- secrets_dir = find_secrets_dir (config_path )
982- if secrets_dir is not None :
983- cfg ._load_secrets (secrets_dir )
1026+ # Secrets fill missing attrs in whatever ended up in self.stores
1027+ if not cfg .ignore_config_file :
1028+ secrets_dir = find_secrets_dir (config_path )
1029+ if secrets_dir is not None :
1030+ cfg ._load_secrets (secrets_dir )
9841031
9851032 # Set initial log level
9861033 logger .setLevel (cfg .loglevel )
0 commit comments