config.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import json
  2. import pathlib
  3. import functools
  4. from .config_exceptions import key_not_implemented
  5. from .config_exceptions import bad_type_loaded
  6. from .config_exceptions import invalid_config_processor
  7. from .config_exceptions import bad_key_type
  8. from .config_exceptions import bad_value_type
  9. from .config_exceptions import config_file_not_exists
  10. from .config_exceptions import config_file_not_readable
  11. class config_getter:
  12. """
  13. That is config getter, it is builded by config processor. It is read
  14. only and is usefull to getting keys from config. When an key is not
  15. definied in configuration file, then default value is returned. Keys
  16. could be accessed by "get(key: str)" method, as attribute of instance
  17. of that class or as dict keys.
  18. """
  19. def __init__(self, content: dict) -> None:
  20. """
  21. That create new instance of getter. It would be used only by config
  22. processor.
  23. Parameters
  24. ----------
  25. content : dict
  26. Content of the config.
  27. """
  28. self.__content = dict(content)
  29. @functools.cache
  30. def get(self, key: str) -> str | int | float | bool:
  31. """
  32. That function return given key from config.
  33. Parameters
  34. ----------
  35. key : str
  36. Name of the key to return.
  37. Returns
  38. -------
  39. str | float | bool | int
  40. Value of the key.
  41. Raises
  42. ------
  43. key_not_implemented
  44. When tried to load key which not exists.
  45. """
  46. if not key in self.__content:
  47. raise key_not_implemented(key)
  48. return self.__content[key]
  49. def __getattr__(self, key: str) -> str | int | float | bool:
  50. """
  51. That is alias of "get(key: str)" function for getting keys as
  52. attributes of that class instance.
  53. """
  54. return self.get(key)
  55. def __getitem__(self, key: str) -> str | int | float | bool:
  56. """
  57. That is alias of "get(key: str)" function for getting keys as
  58. keys of dict.
  59. """
  60. return self.get(key)
  61. class config_processor:
  62. """
  63. That class is processor for configuration. It load defaults config
  64. options, then config loader addng keys from config file. That class
  65. validating options, it key must exists in the defaults config, and
  66. also type of default config value must be same as loaded value for
  67. that key.
  68. """
  69. def __init__(self) -> None:
  70. """
  71. It create new config processor. Content is set to default. Adding
  72. new keys replace defaults values by new ones from config loader.
  73. """
  74. self.__content = self.get_default_config()
  75. self.__validate_defaults()
  76. def __validate_defaults(self) -> None:
  77. """
  78. That function validate dictionary of defaults config. It could
  79. find typical issues with configuration.
  80. Raises
  81. ------
  82. bad_key_type
  83. When key in the configuration dict is not string.
  84. bad_value_type
  85. When value for key in the configuration is in invalid type. Valid
  86. types are str, int, float and bool.
  87. """
  88. for key, value in self.get_default_config().items():
  89. if type(key) is not str:
  90. raise bad_key_type(key)
  91. if type(value) is str or type(value) is int:
  92. continue
  93. if type(value) is float or type(value) is bool:
  94. continue
  95. raise bad_value_type(key, value)
  96. def add(self, key: str, value: str | int | float | bool) -> None:
  97. """
  98. It load new key to config. Key must exists in the default config. It
  99. replace value from default configuration by new value, loaded from
  100. configuration file.
  101. Parameters
  102. ----------
  103. key : str
  104. Name of the key from configuration.
  105. value : str | int | float | bool
  106. Value for that key.
  107. Raises
  108. ------
  109. key_not_implemented
  110. When key not exists in the default configuration.
  111. bad_type_loaded
  112. When type loaded from configuration is different from
  113. type in default config for that key.
  114. """
  115. if not key in self.get_default_config():
  116. raise key_not_implemented(key)
  117. default = self.get_default_config()[key]
  118. if type(default) is not type(value):
  119. raise bad_type_loaded(key, type(default), type(value))
  120. self.__content[key] = value
  121. def result(self) -> config_getter:
  122. """
  123. It return new config getter. Config getter is object which make
  124. configuration read only.
  125. Returns
  126. -------
  127. config_getter
  128. New config getter from configuration.
  129. """
  130. return config_getter(self.__content)
  131. @functools.cache
  132. def get_default_config(self) -> dict:
  133. """
  134. It return default configuration. It must be overwriten, it is
  135. abstract function.
  136. Returns
  137. -------
  138. dict <str, str | int | float | bool>
  139. Default configuration to load.
  140. """
  141. raise NotImplementedError()
  142. class config_loader:
  143. def __new__(
  144. cls,
  145. processor_class: type,
  146. location: pathlib.Path | None = None
  147. ) -> config_getter:
  148. """
  149. This load configuration from config file. It require configuration
  150. processor which could handle options from file. Processor must be
  151. given as class, which is subclass of config_processor. That subclass
  152. overwrite defaults config.
  153. Parameters
  154. ----------
  155. processor_class : type
  156. Subclass of config_processor with defaults config.
  157. location : pathlib.Path | None
  158. Location of config file, or none. When give none, then
  159. default location from that class is used. To overwrite default
  160. location create subclass of config_loader.
  161. Raises
  162. ------
  163. invalid_config_processor
  164. When config processor is not valid subclass.
  165. config_file_not_exists
  166. When config file does not exists.
  167. config_file_not_readable
  168. When config file contains syntax error, or file is
  169. not readable because of filesystem error.
  170. Returns
  171. -------
  172. config_getter
  173. Config getter created by config processor, and loaded from config
  174. file.
  175. """
  176. if type(processor_class) is not type:
  177. raise invalid_config_processor(processor_class)
  178. if not issubclass(processor_class, config_processor):
  179. raise invalid_config_processor(processor_class)
  180. processor = processor_class()
  181. default = cls.get_default_location()
  182. if location is None:
  183. location = default
  184. if not location.exists() or not location.is_file():
  185. raise config_file_not_exists(location)
  186. try:
  187. return cls.__load(processor, location)
  188. except Exception as error:
  189. raise config_file_not_readable(error)
  190. @classmethod
  191. def __load(
  192. cls,
  193. processor: config_processor,
  194. location: pathlib.Path
  195. ) -> config_getter:
  196. """
  197. This function load config from configuration file. It open file,
  198. and adding all keys to the config processor. Finally it create
  199. config_getter to make config readonly.
  200. Parameters
  201. ----------
  202. processor : config_processor
  203. Config processor to work with.
  204. location : pathlib.Path
  205. Location of the config file.
  206. Returns
  207. -------
  208. config_getter
  209. Result of config file.
  210. """
  211. with location.open() as handler:
  212. for key, value in json.loads(handler.read()).items():
  213. processor.add(key, value)
  214. return processor.result()
  215. @classmethod
  216. @functools.cache
  217. def get_default_location(cls) -> pathlib.Path:
  218. """
  219. It is default location of config file. It is abstract function and
  220. must being overwrite by subclass to make config loader useable.
  221. Returns
  222. -------
  223. pathlib.Path
  224. Path of the default config file.
  225. """
  226. raise NotImplementedError()