Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/python-azureml-client> 

3# 

4# License: 3-clause BSD, <https://github.com/smarie/python-azureml-client/blob/master/LICENSE> 

5import sys 

6from collections import OrderedDict 

7from distutils.util import strtobool 

8from warnings import warn 

9 

10from jinja2 import Environment, StrictUndefined 

11 

12try: # python 3+ 

13 from configparser import ConfigParser 

14except ImportError: 

15 from ConfigParser import ConfigParser 

16 

17try: # python 3.5+ 

18 from typing import Dict, List, Callable, Union, Iterable, Optional, Any 

19 from logging import Logger 

20except ImportError: 

21 pass 

22 

23from autoclass import autodict 

24from yamlable import YamlAble, yaml_info 

25 

26from .requests_utils import set_http_proxy 

27 

28 

29PY2 = sys.version_info < (3, 0) 

30 

31 

32YAML_NS = 'org.pypi.azmlclient' 

33"""The namespace used for yaml conversion""" 

34 

35 

36@autodict 

37class GlobalConfig: 

38 """ 

39 Represents a global component client configuration, that is, configuration that is transverse to all web services. 

40 """ 

41 def __init__(self, 

42 http_proxy=None, # type: str 

43 https_proxy=None, # type: str 

44 ssl_verify=True, # type: Union[str, bool] 

45 ): 

46 """ 

47 Global information used for all the calls. 

48 

49 :param http_proxy: an optional string representing the http proxy to use. For example "http://localhost:8888" 

50 :param https_proxy: an optional string representing the https proxy to use. For example "http://localhost:8888" 

51 :param ssl_verify: a boolean or string representing a boolean, indicating if we should validate the SSL 

52 certificate. This is True by default (highly recommended in production) 

53 """ 

54 self.http_proxy = http_proxy 

55 self.https_proxy = https_proxy 

56 self.ssl_verify = ssl_verify 

57 

58 def configure_session(self, session): 

59 """ 

60 Helper to get a `requests` (http client) session object, based on the local configuration. 

61 

62 If the client is configured for use with a proxy the session will be created accordingly. 

63 Note that if this client has no particular configuration for the http proxy this function will return None. 

64 

65 :param session: 

66 :return: 

67 """ 

68 use_http_for_https = self.http_proxy and not self.https_proxy 

69 set_http_proxy(session, http_url=self.http_proxy, https_url=self.https_proxy, 

70 use_http_proxy_for_https_requests=use_http_for_https) 

71 

72 if self.ssl_verify is not None: 72 ↛ exitline 72 didn't return from function 'configure_session', because the condition on line 72 was never false

73 try: 

74 # try to parse a boolean 

75 session.verify = bool(strtobool(self.ssl_verify)) 

76 except: 

77 # otherwise this is a path 

78 session.verify = self.ssl_verify 

79 

80 

81@autodict 

82class ServiceConfig: 

83 """ 

84 Represents the configuration to use to interact with an azureml service. 

85 

86 A service has a main endpoint defined by a base url and an api key. 

87 An optional blob storage configuration can be specified to interact with the service in batch mode (BES) 

88 

89 Finally, an optional alternate endpoint can be specified for "some inputs by reference" calls. In that case the 

90 endpoint should correspond to a service able to understand the input references and to retrieve them (this is not a 

91 standard AzureML mechanism). 

92 """ 

93 def __init__(self, 

94 base_url, # type: str 

95 api_key, # type: str 

96 by_ref_base_url=None, # type: str 

97 by_ref_api_key=None, # type: str 

98 blob_account=None, # type: str 

99 blob_api_key=None, # type: str 

100 blob_container=None, # type: str 

101 blob_path_prefix=None # type: str 

102 ): 

103 """ 

104 Constructor with 

105 

106 * an url and api key for normal (anonymized request-response + batch), 

107 * an optional url and api key for non-anonymized request-response with input by reference, 

108 * an optional account, api key and container name for batch 

109 

110 :param base_url: 

111 :param api_key: 

112 :param by_ref_base_url: an alternate URL to use in 'input by reference' mode. If not provided, the base URL 

113 will be used. 

114 :param by_ref_api_key: an alternate api key to use in 'input by reference' mode. If not provided, the base 

115 api key will be used. 

116 :param blob_account: an optional blob account that should be used in batch mode. A non-None value has 

117 to be provided 

118 :param blob_api_key: 

119 :param blob_container: 

120 :param blob_path_prefix: an optional prefix path for the blobs to be stored 

121 """ 

122 self.base_url = base_url 

123 self.api_key = api_key 

124 

125 self._by_ref_base_url = by_ref_base_url 

126 self._by_ref_api_key = by_ref_api_key 

127 

128 self.blob_account = blob_account 

129 self.blob_api_key = blob_api_key 

130 self.blob_container = blob_container 

131 self.blob_path_prefix = blob_path_prefix 

132 

133 @property 

134 def by_ref_base_url(self): 

135 if self._by_ref_base_url is None: 

136 return self.base_url 

137 else: 

138 return self._by_ref_base_url 

139 

140 @property 

141 def by_ref_api_key(self): 

142 if self._by_ref_api_key is None: 

143 return self.api_key 

144 else: 

145 return self._by_ref_api_key 

146 

147 

148@yaml_info(yaml_tag_ns=YAML_NS) 

149@autodict 

150class ClientConfig(YamlAble): 

151 """ 

152 An AzureML client configuration. It is made of two parts: 

153 

154 * A 'global' configuration (a `GlobalConfig`) 

155 * services configurations (one `ServiceConfig` for each. Each is registered under a name that will be used 

156 to bind the configuration with the appropriate method in `AzureMLClient`. See `@azureml_service` for details. 

157 """ 

158 def __init__(self, 

159 global_config=None, # type: GlobalConfig 

160 **services_configs # type: ServiceConfig 

161 ): 

162 """ 

163 

164 :param global_config: the global configuration, a GlobalConfig 

165 :param services_configs: a dictionary of {service_name: ServiceConfig} 

166 """ 

167 if global_config is None: 

168 global_config = GlobalConfig() 

169 self.global_config = global_config 

170 

171 self.services_configs = services_configs 

172 

173 def assert_valid_for_services(self, 

174 service_names # type: Iterable[str] 

175 ): 

176 """ 

177 Asserts that the configuration corresponds to the list of services provided 

178 :param service_names: 

179 :return: 

180 """ 

181 unknown_services = set(self.services_configs.keys()) - set(service_names) 

182 if len(unknown_services) > 0: 182 ↛ 183line 182 didn't jump to line 183, because the condition on line 182 was never true

183 raise ValueError("Configuration is not able to handle services: '" + str(unknown_services) 

184 + "'. The list of services supported by this client is '" + str(service_names) 

185 + "'") 

186 

187 # ---- yamlable interface ---- 

188 

189 def __to_yaml_dict__(self): 

190 # type: (...) -> Dict[str, Any] 

191 """ This optional method is called when you call yaml.dump(). See `yamlable` for details.""" 

192 # notes: 

193 # - we do not make `GlobalConfig` and `ServiceConfig` yamlable objects because their custom yaml names would 

194 # have to appear in the configuration, which seems tideous 

195 # - we use `dict()` not `var()` so that we benefit from their `@autodict` capability to hide private fields 

196 return {'global': dict(self.global_config), 

197 'services': {service_name: dict(service) for service_name, service in self.services_configs.items()}} 

198 

199 @classmethod 

200 def __from_yaml_dict__(cls, dct, yaml_tag): 

201 # type: (...) -> ClientConfig 

202 """ This optional method is called when you call yaml.load(). See `yamlable` for details.""" 

203 global_cfg = GlobalConfig(**dct['global']) 

204 services_cfg = {service_name: ServiceConfig(**service_cfg_dct) 

205 for service_name, service_cfg_dct in dct['services'].items()} 

206 return ClientConfig(global_cfg, **services_cfg) 

207 

208 @classmethod 

209 def load_yaml(cls, # type: Type[Y] 

210 file_path_or_stream, # type: Union[str, IOBase, StringIO] 

211 safe=True, # type: 

212 **var_values # type: Any 

213 ): # type: (...) -> Y 

214 """ applies the template before loading """ 

215 contents = read_file_and_apply_template(file_path_or_stream, **var_values) 

216 return YamlAble.loads_yaml(contents, safe=safe) 

217 

218 @classmethod 

219 def loads_yaml(cls, # type: Type[Y] 

220 yaml_str, # type: str 

221 safe=True, # type: bool 

222 **var_values # type: Any 

223 ): # type: (...) -> Y 

224 """ applies the template before loading """ 

225 contents = apply_template(yaml_str, **var_values) 

226 return YamlAble.loads_yaml(contents, safe=safe) 

227 

228 # ---- configparser interface ---- 

229 

230 @staticmethod 

231 def load_config(cfg_file_path, # type: str 

232 **var_values # type: Any 

233 ): 

234 # type: (...) -> ClientConfig 

235 """ 

236 Utility method to create a `ClientConfig` from a configuration file (.ini or .cfg, see `ConfigParser`). 

237 That configuration file should have a 'global' section, and one section per service named with the service name. 

238 

239 :param cfg_file_path: the path to the config file in `ConfigParser` supported format 

240 :param var_values: variables to replace in the configuration file. For example `api_key="abcd"` will inject 

241 `"abcd"` everywhere where `{{api_key}}` will be found in the file. 

242 :return: 

243 """ 

244 # read and apply template 

245 contents = read_file_and_apply_template(cfg_file_path, **var_values) 

246 

247 # load the config 

248 config = ConfigParser() 

249 config.read_string(contents, source=cfg_file_path) 

250 

251 global_cfg = GlobalConfig() 

252 services_cfgs = dict() 

253 

254 if PY2: 254 ↛ 255line 254 didn't jump to line 255, because the condition on line 254 was never true

255 _config = config 

256 config = OrderedDict() 

257 for section_name in _config.sections(): 

258 config[section_name] = OrderedDict(_config.items(section_name)) 

259 config['DEFAULT'] = _config.defaults() 

260 

261 for section_name, section_contents in config.items(): 

262 if section_name == 'global': 

263 global_cfg = GlobalConfig(**section_contents) 

264 elif section_name == 'DEFAULT': 

265 if len(section_contents) > 0: 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true

266 warn('Configuration contains a DEFAULT section, that will be ignored') 

267 else: 

268 services_cfgs[section_name] = ServiceConfig(**section_contents) 

269 

270 return ClientConfig(global_cfg, **services_cfgs) 

271 

272 

273def read_file_and_apply_template(file_path_or_stream, # type: Union[str, IOBase, StringIO] 

274 **var_values 

275 ): 

276 # type: (...) -> str 

277 """ 

278  

279 :param file_path_or_stream: 

280 :param var_values:  

281 :return:  

282 """ 

283 # first read the file or stream 

284 if isinstance(file_path_or_stream, str): 284 ↛ 288line 284 didn't jump to line 288, because the condition on line 284 was never false

285 with open(file_path_or_stream, mode='rt') as f: 

286 contents = f.read() 

287 else: 

288 with file_path_or_stream as f: 

289 contents = f.read() 

290 

291 return apply_template(contents, **var_values) 

292 

293 

294# the jinja2 environment that will be used 

295env = Environment(undefined=StrictUndefined) 

296 

297 

298class ConfigTemplateSyntaxError(Exception): 

299 def __init__(self, contents, idx, original_path): 

300 self.extract = contents[idx-30:idx+32] 

301 self.original_path = original_path 

302 

303 def __str__(self): 

304 if self.original_path is not None: 

305 tmpstr = "File: %s. " % self.original_path 

306 else: 

307 tmpstr = "" 

308 return "Syntax error in template: a double curly brace remains after template processing. %s" \ 

309 "Extract: %s" % (tmpstr, self.extract) 

310 

311 

312def apply_template(contents, # type: str 

313 original_path=None, # type: str 

314 **var_values 

315 ): 

316 # type: (...) -> str 

317 

318 # apply the template using Jinja2 

319 template = env.from_string(contents) 

320 contents = template.render(**var_values) 

321 

322 # this check can not be done by Jinja2, do it ourselves 

323 if '{{' in contents or '}}' in contents: 

324 try: 

325 idx = contents.index("{{") 

326 except ValueError: 

327 idx = contents.index("}}") 

328 raise ConfigTemplateSyntaxError(contents, idx, original_path) 

329 

330 return contents