Coverage for hookee/conf.py: 88.89%

54 statements  

« prev     ^ index     » next       coverage.py v7.3.4, created at 2023-12-27 17:55 +0000

1import os 

2 

3import click 

4import confuse 

5 

6from hookee.exception import HookeeConfigError 

7 

8__author__ = "Alex Laird" 

9__copyright__ = "Copyright 2023, Alex Laird" 

10__version__ = "2.0.7" 

11 

12template = { 

13 "port": int, 

14 "subdomain": confuse.String(default=None), 

15 "region": confuse.Choice(["us", "eu", "ap", "au", "sa", "jp", "in"], default=None), 

16 "hostname": confuse.String(default=None), 

17 "auth": confuse.String(default=None), 

18 "host_header": confuse.String(default=None), 

19 "response": confuse.String(default=None), 

20 "content_type": confuse.String(default=None), 

21 "request_script": confuse.Filename(default=None), 

22 "response_script": confuse.Filename(default=None), 

23 "auth_token": confuse.String(default=os.environ.get("NGROK_AUTHTOKEN")), 

24 "plugins_dir": confuse.Filename(), 

25 "plugins": list, 

26 "console_width": confuse.Integer(default=80), 

27 "header_color": confuse.Integer(default="green"), 

28 "default_color": confuse.Integer(default="white"), 

29 "request_color": confuse.Integer(default="white"), 

30} 

31 

32 

33class Config: 

34 """ 

35 An object with accessor methods containing ``hookee``'s configuration. Default configuration can be 

36 overridden by creating a custom config at ``~/.config/hookee/config.yaml`` (when setting config 

37 values from the command line, this is where updated values are stored) which in turn can be overridden by 

38 passing args to the CLI. 

39 

40 If instantiating for a custom integration, args that would otherwise have been passed to and validated by the CLI 

41 (see ``hookee --help``) can instead be passed as ``kwargs`` here to ensure the same validation is done. 

42 For example: 

43 

44 .. code-block:: python 

45 

46 from hookee.conf import Config 

47 

48 config = Config(subdomain="my_domain", 

49 region="eu") 

50 

51 A callback function can also be passed instead of ``response`` and ``content-type`` (or needing to use 

52 plugins) when integrating with ``hookee``'s APIs: 

53 

54 .. code-block:: python 

55 

56 from hookee.conf import Config 

57 

58 def response_callback(request, response): 

59 response.data = "<Response>Ok</Response>" 

60 response.headers["Content-Type"] = "application/xml" 

61 return response 

62 

63 config = Config(response_callback=response_callback) 

64 

65 :var config_obj: The templated config object. 

66 :vartype config_obj: confuse.core.Configuration 

67 :var config_dir: The directory of the config being used. 

68 :vartype config_dir: str 

69 :var config_path: The full path to the config file being used. 

70 :vartype config_path: str 

71 :var config_data: The parsed and validated config data. Use :func:`get`, :func:`set`, and other accessors 

72 to interact with the data. 

73 :vartype config_data: confuse.templates.AttrDict 

74 :var click_logging: ``True`` if ``click`` should be used for log output, which enables colors and formatting when 

75 logging to a console, ``False`` if a logger should be used. If not passed, ``True`` if a :class:`click.Context` 

76 is found to be active. Not persisted to the config file. 

77 :vartype click_logging: bool 

78 :var response_callback: The response callback function, if defined. Not persisted to the config file. 

79 :vartype response_callback: types.FunctionType, optional 

80 """ 

81 

82 def __init__(self, click_logging=None, **kwargs): 

83 try: 

84 if click_logging is None: 

85 click_logging = click.get_current_context(silent=True) is not None 

86 

87 self.response_callback = kwargs.pop("response_callback", None) 

88 

89 config = confuse.Configuration("hookee", __name__) 

90 config.set_args(kwargs) 

91 

92 self.config_obj = config 

93 self.config_dir = self.config_obj.config_dir() 

94 self.config_path = os.path.join(self.config_dir, confuse.CONFIG_FILENAME) 

95 

96 self.config_data = config.get(template) 

97 

98 self.click_logging = click_logging 

99 

100 if self.config_data.get("response") and self.response_callback: 

101 raise HookeeConfigError("Can't define both \"response\" and \"response_callback\".") 

102 elif self.response_callback and not callable(self.response_callback): 

103 raise HookeeConfigError("\"response_callback\" must be a function.") 

104 

105 plugins_dir = os.path.expanduser(self.config_data["plugins_dir"]) 

106 if not os.path.exists(plugins_dir): 

107 os.makedirs(plugins_dir) 

108 except confuse.NotFoundError as e: 

109 raise HookeeConfigError("The config file is invalid: {}.".format(str(e))) 

110 except (confuse.ConfigReadError, ValueError): 

111 raise HookeeConfigError("The config file is not valid YAML.") 

112 

113 def get(self, key): 

114 """ 

115 Get the config value for the given key of persisted data. 

116 

117 :param key: The key. 

118 :type key: str 

119 :return: The config value. 

120 :rtype: object 

121 """ 

122 return self.config_data[key] 

123 

124 def set(self, key, value): 

125 """ 

126 Update the config key to the given value, persisting to ``config.yaml``. 

127 

128 :param key: The key. 

129 :type key: str 

130 :param value: The value to set. 

131 :type key: object 

132 """ 

133 if value != self.config_data[key]: 

134 self._update_config_objects(key, value) 

135 

136 def append(self, key, value): 

137 """ 

138 Update the config key by appending to the list the given value, persisting to ``config.yaml``. 

139 

140 :param key: The key. 

141 :type key: str 

142 :param value: The value to append. 

143 :type value: object 

144 """ 

145 list_item = list(self.config_data[key]) 

146 

147 if value not in list_item: 

148 list_item.append(value) 

149 self._update_config_objects(key, list_item) 

150 

151 def remove(self, key, value): 

152 """ 

153 Update the config key by removing from the list the given value from the list for the given key, persisting to 

154 ``config.yaml``. 

155 

156 :param key: The key. 

157 :type key: str 

158 :param value: The value to remove. 

159 :type value: object 

160 """ 

161 list_item = list(self.config_data[key]) 

162 

163 if value in list_item: 

164 list_item.remove(value) 

165 self._update_config_objects(key, list_item) 

166 

167 def _update_config_objects(self, key, value): 

168 self.config_data[key] = value 

169 self.config_obj[key] = value 

170 

171 self._write_config_objects_to_file() 

172 

173 def _write_config_objects_to_file(self): 

174 with open(self.config_path, "w") as f: 

175 f.write(self.config_obj.dump())