legacy.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. from collections import OrderedDict, namedtuple
  2. from configparser import RawConfigParser
  3. Section_format = namedtuple('Section_format', ('name', 'options'))
  4. Config_option = namedtuple('Config_option', ('name', 'value_type', 'required'))
  5. def option(name, value_type=str, required=True):
  6. '''
  7. Given a config file option name, an expected type for its value, and whether it's required,
  8. return a Config_option capturing that information.
  9. '''
  10. return Config_option(name, value_type, required)
  11. CONFIG_FORMAT = (
  12. Section_format(
  13. 'location',
  14. (
  15. option('source_directories'),
  16. option('one_file_system', value_type=bool, required=False),
  17. option('remote_path', required=False),
  18. option('repository'),
  19. ),
  20. ),
  21. Section_format(
  22. 'storage',
  23. (
  24. option('encryption_passphrase', required=False),
  25. option('compression', required=False),
  26. option('umask', required=False),
  27. ),
  28. ),
  29. Section_format(
  30. 'retention',
  31. (
  32. option('keep_within', required=False),
  33. option('keep_hourly', int, required=False),
  34. option('keep_daily', int, required=False),
  35. option('keep_weekly', int, required=False),
  36. option('keep_monthly', int, required=False),
  37. option('keep_yearly', int, required=False),
  38. option('prefix', required=False),
  39. ),
  40. ),
  41. Section_format(
  42. 'consistency', (option('checks', required=False), option('check_last', required=False))
  43. ),
  44. )
  45. def validate_configuration_format(parser, config_format):
  46. '''
  47. Given an open RawConfigParser and an expected config file format, validate that the parsed
  48. configuration file has the expected sections, that any required options are present in those
  49. sections, and that there aren't any unexpected options.
  50. A section is required if any of its contained options are required.
  51. Raise ValueError if anything is awry.
  52. '''
  53. section_names = set(parser.sections())
  54. required_section_names = tuple(
  55. section.name
  56. for section in config_format
  57. if any(option.required for option in section.options)
  58. )
  59. unknown_section_names = section_names - set(
  60. section_format.name for section_format in config_format
  61. )
  62. if unknown_section_names:
  63. raise ValueError(
  64. 'Unknown config sections found: {}'.format(', '.join(unknown_section_names))
  65. )
  66. missing_section_names = set(required_section_names) - section_names
  67. if missing_section_names:
  68. raise ValueError('Missing config sections: {}'.format(', '.join(missing_section_names)))
  69. for section_format in config_format:
  70. if section_format.name not in section_names:
  71. continue
  72. option_names = parser.options(section_format.name)
  73. expected_options = section_format.options
  74. unexpected_option_names = set(option_names) - set(
  75. option.name for option in expected_options
  76. )
  77. if unexpected_option_names:
  78. raise ValueError(
  79. 'Unexpected options found in config section {}: {}'.format(
  80. section_format.name, ', '.join(sorted(unexpected_option_names))
  81. )
  82. )
  83. missing_option_names = tuple(
  84. option.name
  85. for option in expected_options
  86. if option.required
  87. if option.name not in option_names
  88. )
  89. if missing_option_names:
  90. raise ValueError(
  91. 'Required options missing from config section {}: {}'.format(
  92. section_format.name, ', '.join(missing_option_names)
  93. )
  94. )
  95. def parse_section_options(parser, section_format):
  96. '''
  97. Given an open RawConfigParser and an expected section format, return the option values from that
  98. section as a dict mapping from option name to value. Omit those options that are not present in
  99. the parsed options.
  100. Raise ValueError if any option values cannot be coerced to the expected Python data type.
  101. '''
  102. type_getter = {str: parser.get, int: parser.getint, bool: parser.getboolean}
  103. return OrderedDict(
  104. (option.name, type_getter[option.value_type](section_format.name, option.name))
  105. for option in section_format.options
  106. if parser.has_option(section_format.name, option.name)
  107. )
  108. def parse_configuration(config_filename, config_format):
  109. '''
  110. Given a config filename and an expected config file format, return the parsed configuration
  111. as a namedtuple with one attribute for each parsed section.
  112. Raise IOError if the file cannot be read, or ValueError if the format is not as expected.
  113. '''
  114. parser = RawConfigParser()
  115. if not parser.read(config_filename):
  116. raise ValueError('Configuration file cannot be opened: {}'.format(config_filename))
  117. validate_configuration_format(parser, config_format)
  118. # Describes a parsed configuration, where each attribute is the name of a configuration file
  119. # section and each value is a dict of that section's parsed options.
  120. Parsed_config = namedtuple(
  121. 'Parsed_config', (section_format.name for section_format in config_format)
  122. )
  123. return Parsed_config(
  124. *(parse_section_options(parser, section_format) for section_format in config_format)
  125. )