-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DM-14191: Merge butler configuration files with defaults #39
Changes from 1 commit
e929ce8
611cf94
f365908
120a0c9
fee316f
39dc7ef
5b170a0
d9b94b4
c88286f
48ac511
aa1472a
21dad01
2612595
2fc0e3d
3bcf273
fdc02e9
76b6020
86d9613
76e603e
6f65bbc
f022d5b
4e4620d
e0fc9cf
0507933
29cb101
bd82bd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,10 @@ | |
import os | ||
|
||
from .config import Config | ||
from .datastore import DatastoreConfig | ||
from .schema import SchemaConfig | ||
from .registry import RegistryConfig | ||
from .storageClass import StorageClassConfig | ||
from .utils import doImport | ||
|
||
__all__ = ("ButlerConfig",) | ||
|
@@ -36,9 +40,17 @@ class ButlerConfig(Config): | |
|
||
The configuration is read and merged with default configurations for | ||
the particular classes. The defaults are read from | ||
``$DAF_BUTLER_DIR/config`` using names specified by the appropriate classes | ||
for registry and datastore, and including the standard schema and storage | ||
class definitions. | ||
``$DAF_BUTLER_DIR/config`` and ``$DAF_BULTER_CONFIG_PATH``. The defaults | ||
are constructed by reading first the global defaults, and then adding | ||
in overrides from each entry in the colon-separated | ||
``$DAF_BUTLER_CONFIG_PATH`` in reverse order such that the entries ahead | ||
in the list take priority. The registry and datastore configurations | ||
are read using the names specified by the appropriate classes defined | ||
in the supplied butler configuration. | ||
|
||
The externally supplied butler configuration must include definitions | ||
for ``registry.cls`` and ``datastore.cls`` to enable the correct defaults | ||
to be located. | ||
|
||
Parameters | ||
---------- | ||
|
@@ -51,54 +63,64 @@ def __init__(self, other): | |
# Create an empty config for us to populate | ||
super().__init__() | ||
|
||
# Find the butler configs | ||
self.defaultsDir = None | ||
if "DAF_BUTLER_DIR" in os.environ: | ||
self.defaultsDir = os.path.join(os.environ["DAF_BUTLER_DIR"], "config") | ||
|
||
# Storage classes | ||
storageClasses = Config(os.path.join(self.defaultsDir, "storageClasses.yaml")) | ||
self.update(storageClasses) | ||
|
||
# Default schema | ||
schema = Config(os.path.join(self.defaultsDir, "registry", "default_schema.yaml")) | ||
self.update(schema) | ||
|
||
# Read the supplied config so that we can work out which other | ||
# defaults to use. | ||
butlerConfig = Config(other) | ||
|
||
# Check that fundamental keys are present | ||
self._validate(butlerConfig) | ||
# All the configs that can be associated with default files | ||
configComponents = (SchemaConfig, StorageClassConfig, DatastoreConfig, RegistryConfig) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps make this a class member? |
||
|
||
# This is a list of all the config files to search | ||
defaultsFiles = [(c, c.defaultConfigFile) for c in configComponents] | ||
|
||
# Look for class specific defaults | ||
for section in ("datastore", "registry"): | ||
k = f"{section}.cls" | ||
print("Checking {}: {}".format(k, butlerConfig[k])) | ||
# Components that derive their configurations from implementation | ||
# classes rather than configuration classes | ||
specializedConfigs = (DatastoreConfig, RegistryConfig) | ||
|
||
# Look for class specific default files to be added to the file list | ||
# These class definitions must come from the suppliedbutler config and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. supplied butler. |
||
# are not defaulted. | ||
for componentConfig in specializedConfigs: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not simply loop over all components and have those subclasses determine if they are special? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. E.g. by having a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you saying I should look through all the top level keys in the supplied butler config looking for |
||
k = "{}.cls".format(componentConfig.component) | ||
if k not in butlerConfig: | ||
raise ValueError("Required configuration {} not present in {}".format(k, other)) | ||
cls = doImport(butlerConfig[k]) | ||
defaultsPath = cls.defaults | ||
if defaultsPath is not None: | ||
if not os.path.isabs(defaultsPath): | ||
defaultsPath = os.path.join(self.defaultsDir, defaultsPath) | ||
c = Config(defaultsPath) | ||
# Merge into baseline | ||
self.update(c) | ||
defaultsFile = cls.defaultConfigFile | ||
if defaultsFile is not None: | ||
defaultsFiles.append((componentConfig, defaultsFile)) | ||
|
||
# We can pick up defaults from multiple search paths | ||
# We fill defaults by using the butler config path and then | ||
# the config path environment variable in reverse order. | ||
self.defaultsPaths = [] | ||
|
||
# Find the butler configs | ||
if "DAF_BUTLER_DIR" in os.environ: | ||
self.defaultsPaths.append(os.path.join(os.environ["DAF_BUTLER_DIR"], "config")) | ||
|
||
if "DAF_BUTLER_CONFIG_PATH" in os.environ: | ||
externalPaths = list(reversed(os.environ["DAF_BUTLER_CONFIG_PATH"].split(os.pathsep))) | ||
self.defaultsPaths.append(externalPaths) | ||
|
||
# Search each directory for each of the default files | ||
for pathDir in self.defaultsPaths: | ||
for configClass, configFile in defaultsFiles: | ||
# Assume external paths have same config files as global config | ||
# directory. Absolute paths are possible for external | ||
# code. | ||
# Should this be a log message? Are we using lsst.log? | ||
# print("Checking path {} for {} ({})".format(pathDir, configClass, configFile)) | ||
if not os.path.isabs(configFile): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this apply absolute paths over and over? Maybe that's not a problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. It does. Hadn't thought of that. I don't think it's a problem as such, but it will slow things down. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not simply invert the loop order? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. since the configs read from the paths are subsets of the config I can loop over configs and then paths. |
||
configFile = os.path.join(pathDir, configFile) | ||
if os.path.exists(configFile): | ||
# this checks a specific part of the tree | ||
# We may need to turn off validation since we do not | ||
# require that each defaults file found is fully | ||
# consistent. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do think we'll need to defer validation until the full config has been loaded and merged. |
||
config = configClass(configFile) | ||
# Attach it using the global namespace | ||
self.update({configClass.component: config}) | ||
|
||
# Now that we have all the defaults we can merge the externally | ||
# provided config into the defaults. | ||
self.update(butlerConfig) | ||
|
||
def _validate(self, config=None): | ||
"""Check a butler config contains mandatory keys. | ||
|
||
Parameters | ||
---------- | ||
config : `Config`, optional | ||
By default checks itself, but if ``config`` is given, this | ||
config will be checked instead. | ||
""" | ||
if config is None: | ||
config = self | ||
for k in ['datastore.cls', 'registry.cls']: | ||
if k not in config: | ||
raise ValueError(f"Missing ButlerConfig parameter: {k}") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -357,6 +357,10 @@ class ConfigSubset(Config): | |
"""Keys that are required to be specified in the configuration. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these relative to the top level or the component? (should be documented) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Relative to the Config root of this config (which does not include the component name). |
||
|
||
defaultConfigFile = None | ||
"""Name of the file containing defaults for this config class. | ||
""" | ||
|
||
def __init__(self, other=None): | ||
super().__init__(other) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What am I missing?
|
||
if self.component is not None and self.component in self.data: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,7 @@ | |
class DatastoreConfig(ConfigSubset): | ||
component = "datastore" | ||
requiredKeys = ("cls",) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this check now duplicated in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ButlerConfig doesn't inherit from ConfigSubset so no. I could move the validation code in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds good. |
||
defaultConfigFile = "datastore.yaml" | ||
|
||
|
||
class Datastore(metaclass=ABCMeta): | ||
|
@@ -55,7 +56,7 @@ class Datastore(metaclass=ABCMeta): | |
Load configuration | ||
""" | ||
|
||
defaults = None | ||
defaultConfigFile = None | ||
"""Path to configuration defaults. Relative to $DAF_BUTLER_DIR/config or | ||
absolute path. Can be None if no defaults specified. | ||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,20 +50,22 @@ class SqlRegistry(Registry): | |
---------- | ||
config : `SqlRegistryConfig` or `str` | ||
Load configuration | ||
schemaConfig : `SchemaConfig` or `str` | ||
Definition of the schema to use. | ||
""" | ||
|
||
defaults = None | ||
defaultConfigFile = None | ||
"""Path to configuration defaults. Relative to $DAF_BUTLER_DIR/config or | ||
absolute path. Can be None if no defaults specified. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we not actually have a default for |
||
|
||
def __init__(self, config): | ||
super().__init__(config) | ||
def __init__(self, registryConfig, schemaConfig): | ||
super().__init__(registryConfig) | ||
|
||
self.config = SqlRegistryConfig(config) | ||
self.config = SqlRegistryConfig(registryConfig) | ||
self.storageClasses = StorageClassFactory() | ||
self._schema = Schema(self.config['schema']) | ||
self._engine = create_engine(self.config['registry.db']) | ||
self._schema = Schema(schemaConfig) | ||
self._engine = create_engine(self.config['db']) | ||
self._schema.metadata.create_all(self._engine) | ||
self._datasetTypes = {} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this opposite behavior to shell
$PATH
? If so why?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not meant to be the opposite order. In
$PATH
things early in the path take precedence over things later in the path, so I thought I had to reverse the order when reading in the defaults so that earlier ones in the path override defaults appearing late in the path.