1# --------------------------------------------------------------------------
2#
3# Copyright (c) Microsoft Corporation. All rights reserved.
4#
5# The MIT License (MIT)
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the ""Software""), to deal
9# in the Software without restriction, including without limitation the rights
10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11# copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in
15# all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23# THE SOFTWARE.
24#
25# --------------------------------------------------------------------------
26"""Provide access to settings for globally used Azure configuration values.
27"""
28from __future__ import annotations
29from collections import namedtuple
30from enum import Enum
31from functools import cache
32import logging
33import os
34from typing import (
35 Type,
36 Optional,
37 Callable,
38 Union,
39 Dict,
40 Any,
41 TypeVar,
42 Tuple,
43 Generic,
44 Mapping,
45 List,
46 TYPE_CHECKING,
47)
48from ._azure_clouds import AzureClouds
49
50if TYPE_CHECKING:
51 from azure.core.tracing import AbstractSpan
52
53ValidInputType = TypeVar("ValidInputType")
54ValueType = TypeVar("ValueType")
55
56
57__all__ = ("settings", "Settings")
58
59
60# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
61class _Unset(Enum):
62 token = 0
63
64
65_unset = _Unset.token
66
67
68def convert_bool(value: Union[str, bool]) -> bool:
69 """Convert a string to True or False
70
71 If a boolean is passed in, it is returned as-is. Otherwise the function
72 maps the following strings, ignoring case:
73
74 * "yes", "1", "on" -> True
75 " "no", "0", "off" -> False
76
77 :param value: the value to convert
78 :type value: str or bool
79 :returns: A boolean value matching the intent of the input
80 :rtype: bool
81 :raises ValueError: If conversion to bool fails
82
83 """
84 if isinstance(value, bool):
85 return value
86 val = value.lower()
87 if val in ["yes", "1", "on", "true", "True"]:
88 return True
89 if val in ["no", "0", "off", "false", "False"]:
90 return False
91 raise ValueError("Cannot convert {} to boolean value".format(value))
92
93
94def convert_tracing_enabled(value: Optional[Union[str, bool]]) -> bool:
95 """Convert tracing value to bool with regard to tracing implementation.
96
97 :param value: the value to convert
98 :type value: str or bool or None
99 :returns: A boolean value matching the intent of the input
100 :rtype: bool
101 :raises ValueError: If conversion to bool fails
102 """
103 if value is None:
104 # If tracing_enabled was not explicitly set to a boolean, determine tracing enablement
105 # based on tracing_implementation being set.
106 if settings.tracing_implementation():
107 return True
108 return False
109 return convert_bool(value)
110
111
112_levels = {
113 "CRITICAL": logging.CRITICAL,
114 "ERROR": logging.ERROR,
115 "WARNING": logging.WARNING,
116 "INFO": logging.INFO,
117 "DEBUG": logging.DEBUG,
118}
119
120
121def convert_logging(value: Union[str, int]) -> int:
122 """Convert a string to a Python logging level
123
124 If a log level is passed in, it is returned as-is. Otherwise the function
125 understands the following strings, ignoring case:
126
127 * "critical"
128 * "error"
129 * "warning"
130 * "info"
131 * "debug"
132
133 :param value: the value to convert
134 :type value: str or int
135 :returns: A log level as an int. See the logging module for details.
136 :rtype: int
137 :raises ValueError: If conversion to log level fails
138
139 """
140 if isinstance(value, int):
141 # If it's an int, return it. We don't need to check if it's in _levels, as custom int levels are allowed.
142 # https://docs.python.org/3/library/logging.html#levels
143 return value
144 val = value.upper()
145 level = _levels.get(val)
146 if not level:
147 raise ValueError("Cannot convert {} to log level, valid values are: {}".format(value, ", ".join(_levels)))
148 return level
149
150
151def convert_azure_cloud(value: Union[str, AzureClouds]) -> AzureClouds:
152 """Convert a string to an Azure Cloud
153
154 :param value: the value to convert
155 :type value: string
156 :returns: An AzureClouds enum value
157 :rtype: AzureClouds
158 :raises ValueError: If conversion to AzureClouds fails
159
160 """
161 if isinstance(value, AzureClouds):
162 return value
163 if isinstance(value, str):
164 azure_clouds = {cloud.name: cloud for cloud in AzureClouds}
165 if value in azure_clouds:
166 return azure_clouds[value]
167 raise ValueError(
168 "Cannot convert {} to Azure Cloud, valid values are: {}".format(value, ", ".join(azure_clouds.keys()))
169 )
170 raise ValueError("Cannot convert {} to Azure Cloud".format(value))
171
172
173def _get_opencensus_span() -> Optional[Type[AbstractSpan]]:
174 """Returns the OpenCensusSpan if the opencensus tracing plugin is installed else returns None.
175
176 :rtype: type[AbstractSpan] or None
177 :returns: OpenCensusSpan type or None
178 """
179 try:
180 from azure.core.tracing.ext.opencensus_span import (
181 OpenCensusSpan,
182 )
183
184 return OpenCensusSpan
185 except ImportError:
186 return None
187
188
189def _get_opentelemetry_span() -> Optional[Type[AbstractSpan]]:
190 """Returns the OpenTelemetrySpan if the opentelemetry tracing plugin is installed else returns None.
191
192 :rtype: type[AbstractSpan] or None
193 :returns: OpenTelemetrySpan type or None
194 """
195 try:
196 from azure.core.tracing.ext.opentelemetry_span import (
197 OpenTelemetrySpan,
198 )
199
200 return OpenTelemetrySpan
201 except ImportError:
202 return None
203
204
205_tracing_implementation_dict: Dict[str, Callable[[], Optional[Type[AbstractSpan]]]] = {
206 "opencensus": _get_opencensus_span,
207 "opentelemetry": _get_opentelemetry_span,
208}
209
210
211@cache
212def convert_tracing_impl(value: Optional[Union[str, Type[AbstractSpan]]]) -> Optional[Type[AbstractSpan]]:
213 """Convert a string to AbstractSpan
214
215 If a AbstractSpan is passed in, it is returned as-is. Otherwise the function
216 understands the following strings, ignoring case:
217
218 * "opencensus"
219 * "opentelemetry"
220
221 :param value: the value to convert
222 :type value: string
223 :returns: AbstractSpan
224 :raises ValueError: If conversion to AbstractSpan fails
225
226 """
227 if value is None:
228 return None
229
230 if not isinstance(value, str):
231 return value
232
233 value = value.lower()
234 get_wrapper_class = _tracing_implementation_dict.get(value, lambda: _unset)
235 wrapper_class: Optional[Union[_Unset, Type[AbstractSpan]]] = get_wrapper_class()
236 if wrapper_class is _unset:
237 raise ValueError(
238 "Cannot convert {} to AbstractSpan, valid values are: {}".format(
239 value, ", ".join(_tracing_implementation_dict)
240 )
241 )
242 return wrapper_class
243
244
245class PrioritizedSetting(Generic[ValidInputType, ValueType]):
246 """Return a value for a global setting according to configuration precedence.
247
248 The following methods are searched in order for the setting:
249
250 4. immediate values
251 3. previously user-set value
252 2. environment variable
253 1. system setting
254 0. implicit default
255
256 If a value cannot be determined, a RuntimeError is raised.
257
258 The ``env_var`` argument specifies the name of an environment to check for
259 setting values, e.g. ``"AZURE_LOG_LEVEL"``.
260 If a ``convert`` function is provided, the result will be converted before being used.
261
262 The optional ``system_hook`` can be used to specify a function that will
263 attempt to look up a value for the setting from system-wide configurations.
264 If a ``convert`` function is provided, the hook result will be converted before being used.
265
266 The optional ``default`` argument specified an implicit default value for
267 the setting that is returned if no other methods provide a value. If a ``convert`` function is provided,
268 ``default`` will be converted before being used.
269
270 A ``convert`` argument may be provided to convert values before they are
271 returned. For instance to concert log levels in environment variables
272 to ``logging`` module values. If a ``convert`` function is provided, it must support
273 str as valid input type.
274
275 :param str name: the name of the setting
276 :param str env_var: the name of an environment variable to check for the setting
277 :param callable system_hook: a function that will attempt to look up a value for the setting
278 :param default: an implicit default value for the setting
279 :type default: any
280 :param callable convert: a function to convert values before they are returned
281 """
282
283 def __init__(
284 self,
285 name: str,
286 env_var: Optional[str] = None,
287 system_hook: Optional[Callable[[], ValidInputType]] = None,
288 default: Union[ValidInputType, _Unset] = _unset,
289 convert: Optional[Callable[[Union[ValidInputType, str]], ValueType]] = None,
290 ):
291
292 self._name = name
293 self._env_var = env_var
294 self._system_hook = system_hook
295 self._default = default
296 noop_convert: Callable[[Any], Any] = lambda x: x
297 self._convert: Callable[[Union[ValidInputType, str]], ValueType] = convert if convert else noop_convert
298 self._user_value: Union[ValidInputType, _Unset] = _unset
299
300 def __repr__(self) -> str:
301 return "PrioritizedSetting(%r)" % self._name
302
303 def __call__(self, value: Optional[ValidInputType] = None) -> ValueType:
304 """Return the setting value according to the standard precedence.
305
306 :param value: value
307 :type value: str or int or float or None
308 :returns: the value of the setting
309 :rtype: str or int or float
310 :raises RuntimeError: if no value can be determined
311 """
312
313 # 4. immediate values
314 if value is not None:
315 return self._convert(value)
316
317 # 3. previously user-set value
318 if not isinstance(self._user_value, _Unset):
319 return self._convert(self._user_value)
320
321 # 2. environment variable
322 if self._env_var and self._env_var in os.environ:
323 return self._convert(os.environ[self._env_var])
324
325 # 1. system setting
326 if self._system_hook:
327 return self._convert(self._system_hook())
328
329 # 0. implicit default
330 if not isinstance(self._default, _Unset):
331 return self._convert(self._default)
332
333 raise RuntimeError("No configured value found for setting %r" % self._name)
334
335 def __get__(self, instance: Any, owner: Optional[Any] = None) -> PrioritizedSetting[ValidInputType, ValueType]:
336 return self
337
338 def __set__(self, instance: Any, value: ValidInputType) -> None:
339 self.set_value(value)
340
341 def set_value(self, value: ValidInputType) -> None:
342 """Specify a value for this setting programmatically.
343
344 A value set this way takes precedence over all other methods except
345 immediate values.
346
347 :param value: a user-set value for this setting
348 :type value: str or int or float
349 """
350 self._user_value = value
351
352 def unset_value(self) -> None:
353 """Unset the previous user value such that the priority is reset."""
354 self._user_value = _unset
355
356 @property
357 def env_var(self) -> Optional[str]:
358 return self._env_var
359
360 @property
361 def default(self) -> Union[ValidInputType, _Unset]:
362 return self._default
363
364
365class Settings:
366 """Settings for globally used Azure configuration values.
367
368 You probably don't want to create an instance of this class, but call the singleton instance:
369
370 .. code-block:: python
371
372 from azure.core.settings import settings
373 settings.log_level = log_level = logging.DEBUG
374
375 The following methods are searched in order for a setting:
376
377 4. immediate values
378 3. previously user-set value
379 2. environment variable
380 1. system setting
381 0. implicit default
382
383 An implicit default is (optionally) defined by the setting attribute itself.
384
385 A system setting value can be obtained from registries or other OS configuration
386 for settings that support that method.
387
388 An environment variable value is obtained from ``os.environ``
389
390 User-set values many be specified by assigning to the attribute:
391
392 .. code-block:: python
393
394 settings.log_level = log_level = logging.DEBUG
395
396 Immediate values are (optionally) provided when the setting is retrieved:
397
398 .. code-block:: python
399
400 settings.log_level(logging.DEBUG())
401
402 Immediate values are most often useful to provide from optional arguments
403 to client functions. If the argument value is not None, it will be returned
404 as-is. Otherwise, the setting searches other methods according to the
405 precedence rules.
406
407 Immutable configuration snapshots can be created with the following methods:
408
409 * settings.defaults returns the base defaultsvalues , ignoring any environment or system
410 or user settings
411
412 * settings.current returns the current computation of settings including prioritization
413 of configuration sources, unless defaults_only is set to True (in which case the result
414 is identical to settings.defaults)
415
416 * settings.config can be called with specific values to override what settings.current
417 would provide
418
419 .. code-block:: python
420
421 # return current settings with log level overridden
422 settings.config(log_level=logging.DEBUG)
423
424 :cvar log_level: a log level to use across all Azure client SDKs (AZURE_LOG_LEVEL)
425 :type log_level: PrioritizedSetting
426 :cvar tracing_enabled: Whether tracing should be enabled across Azure SDKs (AZURE_TRACING_ENABLED)
427 :type tracing_enabled: PrioritizedSetting
428 :cvar tracing_implementation: The tracing implementation to use (AZURE_SDK_TRACING_IMPLEMENTATION)
429 :type tracing_implementation: PrioritizedSetting
430
431 :Example:
432
433 >>> import logging
434 >>> from azure.core.settings import settings
435 >>> settings.log_level = logging.DEBUG
436 >>> settings.log_level()
437 10
438
439 >>> settings.log_level(logging.WARN)
440 30
441
442 """
443
444 def __init__(self) -> None:
445 self._defaults_only: bool = False
446
447 @property
448 def defaults_only(self) -> bool:
449 """Whether to ignore environment and system settings and return only base default values.
450
451 :rtype: bool
452 :returns: Whether to ignore environment and system settings and return only base default values.
453 """
454 return self._defaults_only
455
456 @defaults_only.setter
457 def defaults_only(self, value: bool) -> None:
458 self._defaults_only = value
459
460 @property
461 def defaults(self) -> Tuple[Any, ...]:
462 """Return implicit default values for all settings, ignoring environment and system.
463
464 :rtype: namedtuple
465 :returns: The implicit default values for all settings
466 """
467 props = {k: v.default for (k, v) in self.__class__.__dict__.items() if isinstance(v, PrioritizedSetting)}
468 return self._config(props)
469
470 @property
471 def current(self) -> Tuple[Any, ...]:
472 """Return the current values for all settings.
473
474 :rtype: namedtuple
475 :returns: The current values for all settings
476 """
477 if self.defaults_only:
478 return self.defaults
479 return self.config()
480
481 def config(self, **kwargs: Any) -> Tuple[Any, ...]:
482 """Return the currently computed settings, with values overridden by parameter values.
483
484 :rtype: namedtuple
485 :returns: The current values for all settings, with values overridden by parameter values
486
487 Examples:
488
489 .. code-block:: python
490
491 # return current settings with log level overridden
492 settings.config(log_level=logging.DEBUG)
493
494 """
495 props = {k: v() for (k, v) in self.__class__.__dict__.items() if isinstance(v, PrioritizedSetting)}
496 props.update(kwargs)
497 return self._config(props)
498
499 def _config(self, props: Mapping[str, Any]) -> Tuple[Any, ...]:
500 keys: List[str] = list(props.keys())
501 # https://github.com/python/mypy/issues/4414
502 Config = namedtuple("Config", keys) # type: ignore
503 return Config(**props)
504
505 log_level: PrioritizedSetting[Union[str, int], int] = PrioritizedSetting(
506 "log_level",
507 env_var="AZURE_LOG_LEVEL",
508 convert=convert_logging,
509 default=logging.INFO,
510 )
511
512 tracing_enabled: PrioritizedSetting[Optional[Union[str, bool]], bool] = PrioritizedSetting(
513 "tracing_enabled",
514 env_var="AZURE_TRACING_ENABLED",
515 convert=convert_tracing_enabled,
516 default=None,
517 )
518
519 tracing_implementation: PrioritizedSetting[
520 Optional[Union[str, Type[AbstractSpan]]], Optional[Type[AbstractSpan]]
521 ] = PrioritizedSetting(
522 "tracing_implementation",
523 env_var="AZURE_SDK_TRACING_IMPLEMENTATION",
524 convert=convert_tracing_impl,
525 default=None,
526 )
527
528 azure_cloud: PrioritizedSetting[Union[str, AzureClouds], AzureClouds] = PrioritizedSetting(
529 "azure_cloud",
530 env_var="AZURE_CLOUD",
531 convert=convert_azure_cloud,
532 default=AzureClouds.AZURE_PUBLIC_CLOUD,
533 )
534
535
536settings: Settings = Settings()
537"""The settings unique instance.
538
539:type settings: Settings
540"""