Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
singlestore-labs
GitHub Repository: singlestore-labs/singlestoredb-python
Path: blob/main/singlestoredb/utils/config.py
469 views
1
#!/usr/bin/env python
2
#
3
# Copyright SAS Institute
4
#
5
# Licensed under the Apache License, Version 2.0 (the License);
6
# you may not use this file except in compliance with the License.
7
# You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
#
17
# This file was originally copied from https://github.com/sassoftware/python-swat.
18
#
19
"""
20
Generalized interface for configuring, setting, and getting options.
21
22
Options can be set and retrieved using set_option(...), get_option(...), and
23
reset_option(...). The describe_option(...) function can be used to display
24
a description of one or more options.
25
26
"""
27
import contextlib
28
import os
29
import re
30
from typing import Any
31
from typing import Callable
32
from typing import Dict
33
from typing import Iterator
34
from typing import List
35
from typing import Mapping
36
from typing import Optional
37
from typing import Tuple
38
from typing import Union
39
from urllib.parse import urlparse
40
41
from .xdict import xdict
42
43
44
# Container for options
45
_config = xdict()
46
47
items_types = (list, tuple, set)
48
49
50
def _getenv(names: Union[str, List[str]], *args: Any) -> str:
51
"""
52
Check for multiple environment variable values.
53
54
Two forms of the environment variable name will be checked,
55
both with and without underscores. This allows for aliases
56
such as CAS_HOST and CASHOST.
57
58
Parameters
59
----------
60
names : str or list of str
61
Names of environment variables to look for
62
*args : any, optional
63
The default return value if no matching environment
64
variables exist
65
66
Returns
67
-------
68
string or default value
69
70
"""
71
if not isinstance(names, items_types):
72
names = [names]
73
for name in names:
74
if name in os.environ:
75
return os.environ[name]
76
name = name.replace('_', '')
77
if name in os.environ:
78
return os.environ[name]
79
if args:
80
return args[0]
81
raise KeyError(names[0])
82
83
84
def _setenv(names: Union[str, List[str]], value: Any) -> None:
85
"""
86
Set environment variable.
87
88
The environment is first checked for an existing variable
89
that is set. If it finds one, it uses that name.
90
If no variable is found, the first one in the `names`
91
list is used.
92
93
Just as with _getenv, the variable name is checked both
94
with and without underscores to allow aliases.
95
96
Parameters
97
----------
98
names : str or list of str
99
Names of environment variable to look for
100
value : Any
101
The value to set
102
103
"""
104
if not isinstance(names, items_types):
105
names = [names]
106
for name in names:
107
if name in os.environ:
108
os.environ[name] = value
109
name = name.replace('_', '')
110
if name in os.environ:
111
os.environ[name] = value
112
113
114
def _delenv(names: Union[str, List[str]]) -> None:
115
"""Delete given environment variables."""
116
if not isinstance(names, items_types):
117
names = [names]
118
for name in names:
119
os.environ.pop(name, None)
120
os.environ.pop(name.replace('_', ''), None)
121
122
123
def iteroptions(*args: Any, **kwargs: Any) -> Iterator[Tuple[str, Any]]:
124
"""
125
Iterate through name / value pairs of options
126
127
Options can come in several forms. They can be consecutive arguments
128
where the first argument is the name and the following argument is
129
the value. They can be two-element tuples (or lists) where the first
130
element is the name and the second element is the value. You can
131
also pass in a dictionary of key / value pairs. And finally, you can
132
use keyword arguments.
133
134
Parameters
135
----------
136
*args : any, optional
137
See description above.
138
**kwargs : key / value pairs, optional
139
Arbitrary keyword arguments.
140
141
Returns
142
-------
143
generator
144
Each iteration returns a name / value pair in a tuple
145
146
"""
147
items = list(args)
148
while items:
149
item = items.pop(0)
150
if isinstance(item, (list, tuple)):
151
yield item[0], item[1]
152
elif isinstance(item, dict):
153
for key, value in item.items():
154
yield key, value
155
else:
156
yield item, items.pop(0)
157
for key, value in kwargs.items():
158
yield key, value
159
160
161
@contextlib.contextmanager
162
def option_context(*args: Any, **kwargs: Any) -> Iterator[None]:
163
"""
164
Create a context for setting option temporarily.
165
166
Parameters
167
----------
168
*args : str / any pairs
169
Name / value pairs in consecutive arguments (not tuples)
170
**kwargs : dict
171
Key / value pairs of options
172
173
"""
174
# Save old state and set new option values
175
oldstate = {}
176
for key, value in iteroptions(*args, **kwargs):
177
key = key.lower()
178
oldstate[key] = get_option(key)
179
set_option(key, value)
180
181
# Yield control
182
yield
183
184
# Set old state back
185
for key, value in oldstate.items():
186
set_option(key, value)
187
188
189
def _get_option_leaf_node(key: str) -> str:
190
"""
191
Find full option name of given key.
192
193
Parameters
194
----------
195
key : str
196
Either a partial key or full key name of an option
197
198
Returns
199
-------
200
str
201
The full key name of the option
202
203
Raises
204
------
205
KeyError
206
If more than one option matches
207
208
"""
209
flatkeys = list(_config.flatkeys())
210
key = key.lower()
211
if key in flatkeys:
212
return key
213
keys = [k for k in flatkeys if k.endswith('.' + key)]
214
if len(keys) > 1:
215
raise KeyError('There is more than one option with the name %s.' % key)
216
if not keys:
217
if '.' in key:
218
raise KeyError('%s is not a valid option name.' % key)
219
else:
220
raise TypeError('%s is not a valid option name.' % key)
221
return keys[0]
222
223
224
def set_option(*args: Any, **kwargs: Any) -> None:
225
"""
226
Set the value of an option.
227
228
Parameters
229
----------
230
*args : str or Any
231
The name and value of an option in consecutive arguments (not tuples)
232
**kwargs : dict
233
Arbitrary keyword / value pairs
234
235
"""
236
for key, value in iteroptions(*args, **kwargs):
237
key = _get_option_leaf_node(key)
238
opt = _config[key]
239
if not isinstance(opt, Option):
240
raise TypeError('%s is not a valid option name' % key)
241
opt.set(value)
242
243
244
set_options = set_option
245
246
247
def get_option(key: str) -> Any:
248
"""
249
Get the value of an option.
250
251
Parameters
252
----------
253
key : str
254
The name of the option
255
256
Returns
257
-------
258
Any
259
The value of the option
260
261
"""
262
key = _get_option_leaf_node(key)
263
opt = _config[key]
264
if not isinstance(opt, Option):
265
raise TypeError('%s is not a valid option name' % key)
266
return opt.get()
267
268
269
def get_suboptions(key: str) -> Dict[str, Any]:
270
"""
271
Get the dictionary of options at the level `key`.
272
273
Parameters
274
----------
275
key : str
276
The name of the option collection
277
278
Returns
279
-------
280
dict
281
The dictionary of options at level `key`
282
283
"""
284
if key not in _config:
285
raise KeyError('%s is not a valid option name' % key)
286
opt = _config[key]
287
if isinstance(opt, Option):
288
raise TypeError('%s does not have sub-options' % key)
289
return opt
290
291
292
def get_default(key: str) -> Any:
293
"""
294
Get the default value of an option.
295
296
Parameters
297
----------
298
key : str
299
The name of the option
300
301
Returns
302
-------
303
Any
304
The default value of the option
305
306
"""
307
key = _get_option_leaf_node(key)
308
opt = _config[key]
309
if not isinstance(opt, Option):
310
raise TypeError('%s is not a valid option name' % key)
311
return opt.get_default()
312
313
314
get_default_val = get_default
315
316
317
def describe_option(*keys: str, **kwargs: Any) -> Optional[str]:
318
"""
319
Print the description of one or more options.
320
321
To print the descriptions of all options, execute this function
322
with no parameters.
323
324
Parameters
325
----------
326
*keys : one or more strings
327
Names of the options
328
329
"""
330
_print_desc = kwargs.get('_print_desc', True)
331
332
out = []
333
334
if not keys:
335
keys = tuple(sorted(_config.flatkeys()))
336
else:
337
newkeys = []
338
for k in keys:
339
try:
340
newkeys.append(_get_option_leaf_node(k))
341
except (KeyError, TypeError):
342
newkeys.append(k)
343
344
for key in keys:
345
346
if key not in _config:
347
raise KeyError('%s is not a valid option name' % key)
348
349
opt = _config[key]
350
if isinstance(opt, xdict):
351
desc = describe_option(
352
*[
353
'%s.%s' % (key, x)
354
for x in opt.flatkeys()
355
], _print_desc=_print_desc,
356
)
357
if desc is not None:
358
out.append(desc)
359
continue
360
361
if _print_desc:
362
print(opt.__doc__)
363
print('')
364
else:
365
out.append(opt.__doc__)
366
367
if not _print_desc:
368
return '\n'.join(out)
369
370
return None
371
372
373
def reset_option(*keys: str) -> None:
374
"""
375
Reset one or more options back to their default value.
376
377
Parameters
378
----------
379
*keys : one or more strings
380
Names of options to reset
381
382
"""
383
if not keys:
384
keys = tuple(sorted(_config.flatkeys()))
385
else:
386
keys = tuple([_get_option_leaf_node(k) for k in keys])
387
388
for key in keys:
389
390
if key not in _config:
391
raise KeyError('%s is not a valid option name' % key)
392
393
opt = _config[key]
394
if not isinstance(opt, Option):
395
raise TypeError('%s is not a valid option name' % key)
396
397
# Reset options
398
set_option(key, get_default(key))
399
400
401
def check_int(
402
value: Union[int, float, str],
403
minimum: Optional[int] = None,
404
maximum: Optional[int] = None,
405
exclusive_minimum: bool = False,
406
exclusive_maximum: bool = False,
407
multiple_of: Optional[int] = None,
408
) -> int:
409
"""
410
Validate an integer value.
411
412
Parameters
413
----------
414
value : int or float
415
Value to validate
416
minimum : int, optional
417
The minimum value allowed
418
maximum : int, optional
419
The maximum value allowed
420
exclusive_minimum : bool, optional
421
Should the minimum value be excluded as an endpoint?
422
exclusive_maximum : bool, optional
423
Should the maximum value be excluded as an endpoint?
424
multiple_of : int, optional
425
If specified, the value must be a multple of it in order for
426
the value to be considered valid.
427
428
Returns
429
-------
430
int
431
The validated integer value
432
433
"""
434
out = int(value)
435
436
if minimum is not None:
437
if out < minimum:
438
raise ValueError(
439
'%s is smaller than the minimum value of %s' %
440
(out, minimum),
441
)
442
if exclusive_minimum and out == minimum:
443
raise ValueError(
444
'%s is equal to the exclusive nimum value of %s' %
445
(out, minimum),
446
)
447
448
if maximum is not None:
449
if out > maximum:
450
raise ValueError(
451
'%s is larger than the maximum value of %s' %
452
(out, maximum),
453
)
454
if exclusive_maximum and out == maximum:
455
raise ValueError(
456
'%s is equal to the exclusive maximum value of %s' %
457
(out, maximum),
458
)
459
460
if multiple_of is not None and (out % int(multiple_of)) != 0:
461
raise ValueError('%s is not a multiple of %s' % (out, multiple_of))
462
463
return out
464
465
466
def check_float(
467
value: Union[float, int, str],
468
minimum: Optional[Union[float, int]] = None,
469
maximum: Optional[Union[float, int]] = None,
470
exclusive_minimum: bool = False,
471
exclusive_maximum: bool = False,
472
multiple_of: Optional[Union[float, int]] = None,
473
) -> float:
474
"""
475
Validate a floating point value.
476
477
Parameters
478
----------
479
value : int or float
480
Value to validate
481
minimum : int or float, optional
482
The minimum value allowed
483
maximum : int or float, optional
484
The maximum value allowed
485
exclusive_minimum : bool, optional
486
Should the minimum value be excluded as an endpoint?
487
exclusive_maximum : bool, optional
488
Should the maximum value be excluded as an endpoint?
489
multiple_of : int or float, optional
490
If specified, the value must be a multple of it in order for
491
the value to be considered valid.
492
493
Returns
494
-------
495
float
496
The validated floating point value
497
498
"""
499
out = float(value)
500
501
if minimum is not None:
502
if out < minimum:
503
raise ValueError(
504
'%s is smaller than the minimum value of %s' %
505
(out, minimum),
506
)
507
if exclusive_minimum and out == minimum:
508
raise ValueError(
509
'%s is equal to the exclusive nimum value of %s' %
510
(out, minimum),
511
)
512
513
if maximum is not None:
514
if out > maximum:
515
raise ValueError(
516
'%s is larger than the maximum value of %s' %
517
(out, maximum),
518
)
519
if exclusive_maximum and out == maximum:
520
raise ValueError(
521
'%s is equal to the exclusive maximum value of %s' %
522
(out, maximum),
523
)
524
525
if multiple_of is not None and (out % int(multiple_of)) != 0:
526
raise ValueError('%s is not a multiple of %s' % (out, multiple_of))
527
528
return out
529
530
531
def check_bool(value: Union[bool, int]) -> bool:
532
"""
533
Validate a bool value.
534
535
Parameters
536
----------
537
value : int or bool
538
The value to validate. If specified as an integer, it must
539
be either 0 for False or 1 for True.
540
541
Returns
542
-------
543
bool
544
The validated bool
545
546
"""
547
if value is False or value is True:
548
return value
549
550
if isinstance(value, int):
551
if value == 1:
552
return True
553
if value == 0:
554
return False
555
556
if isinstance(value, (str, bytes)):
557
value = str(value)
558
if value.lower() in ['y', 'yes', 'on', 't', 'true', 'enable', 'enabled', '1']:
559
return True
560
if value.lower() in ['n', 'no', 'off', 'f', 'false', 'disable', 'disabled', '0']:
561
return False
562
563
raise ValueError('%s is not a recognized bool value')
564
565
566
def check_optional_bool(value: Optional[Union[bool, int]]) -> Optional[bool]:
567
"""
568
Validate an optional bool value.
569
570
Parameters
571
----------
572
value : int or bool or None
573
The value to validate. If specified as an integer, it must
574
be either 0 for False or 1 for True.
575
576
Returns
577
-------
578
bool
579
The validated bool
580
581
"""
582
if value is None:
583
return None
584
585
return check_bool(value)
586
587
588
def check_str(
589
value: Any,
590
pattern: Optional[str] = None,
591
max_length: Optional[int] = None,
592
min_length: Optional[int] = None,
593
valid_values: Optional[List[str]] = None,
594
) -> Optional[str]:
595
"""
596
Validate a string value.
597
598
Parameters
599
----------
600
value : string
601
The value to validate
602
pattern : regular expression string, optional
603
A regular expression used to validate string values
604
max_length : int, optional
605
The maximum length of the string
606
min_length : int, optional
607
The minimum length of the string
608
valid_values : list of strings, optional
609
List of the only possible values
610
611
Returns
612
-------
613
string
614
The validated string value
615
616
"""
617
if value is None:
618
return None
619
620
if isinstance(value, str):
621
out = value
622
else:
623
out = str(value, 'utf-8')
624
625
if max_length is not None and len(out) > max_length:
626
raise ValueError(
627
'%s is longer than the maximum length of %s' %
628
(out, max_length),
629
)
630
631
if min_length is not None and len(out) < min_length:
632
raise ValueError(
633
'%s is shorter than the minimum length of %s' %
634
(out, min_length),
635
)
636
637
if pattern is not None and not re.search(pattern, out):
638
raise ValueError('%s does not match pattern %s' % (out, pattern))
639
640
if valid_values is not None and out not in valid_values:
641
raise ValueError(
642
'%s is not one of the possible values: %s' %
643
(out, ', '.join(valid_values)),
644
)
645
646
return out
647
648
649
def check_dict_str_str(
650
value: Any,
651
) -> Optional[Dict[str, str]]:
652
"""
653
Validate a string value.
654
655
Parameters
656
----------
657
value : dict
658
The value to validate. Keys and values must be strings.
659
660
Returns
661
-------
662
dict
663
The validated dict value
664
"""
665
if value is None:
666
return None
667
668
if not isinstance(value, Mapping):
669
raise ValueError(
670
'value {} must be of type dict'.format(value),
671
)
672
673
out = {}
674
for k, v in value.items():
675
if not isinstance(k, str) or not isinstance(v, str):
676
raise ValueError(
677
'keys and values in {} must be strings'.format(value),
678
)
679
out[k] = v
680
681
return out
682
683
684
def check_url(
685
value: str,
686
pattern: Optional[str] = None,
687
max_length: Optional[int] = None,
688
min_length: Optional[int] = None,
689
valid_values: Optional[List[str]] = None,
690
) -> Optional[str]:
691
"""
692
Validate a URL value.
693
694
Parameters
695
----------
696
value : any
697
The value to validate. This value will be cast to a string
698
and converted to unicode.
699
pattern : regular expression string, optional
700
A regular expression used to validate string values
701
max_length : int, optional
702
The maximum length of the string
703
min_length : int, optional
704
The minimum length of the string
705
valid_values : list of strings, optional
706
List of the only possible values
707
708
Returns
709
-------
710
string
711
The validated URL value
712
713
"""
714
if value is None:
715
return None
716
717
out = check_str(
718
value, pattern=pattern, max_length=max_length,
719
min_length=min_length, valid_values=valid_values,
720
)
721
try:
722
urlparse(out)
723
except Exception:
724
raise TypeError('%s is not a valid URL' % value)
725
return out
726
727
728
class Option(object):
729
"""
730
Configuration option.
731
732
Parameters
733
----------
734
name : str
735
The name of the option
736
typedesc : str
737
Description of the option data type (e.g., int, float, string)
738
validator : callable
739
A callable object that validates the option value and returns
740
the validated value.
741
default : any
742
The default value of the option
743
doc : str
744
The documentation string for the option
745
environ : str or list of strs, optional
746
If specified, the value should be specified in an environment
747
variable of that name.
748
749
"""
750
751
def __init__(
752
self,
753
name: str,
754
typedesc: str,
755
validator: Callable[[str], Any],
756
default: Any,
757
doc: str,
758
environ: Optional[Union[str, List[str]]] = None,
759
):
760
self._name = name
761
self._typedesc = typedesc
762
self._validator = validator
763
if environ is not None:
764
self._default = validator(_getenv(environ, default))
765
else:
766
self._default = validator(default)
767
self._environ = environ
768
self._value = self._default
769
self._doc = doc
770
771
@property
772
def __doc__(self) -> str: # type: ignore
773
"""Generate documentation string."""
774
separator = ' '
775
if isinstance(self._value, (str, bytes)) and len(self._value) > 40:
776
separator = '\n '
777
return '''%s : %s\n %s\n [default: %s]%s[currently: %s]\n''' % \
778
(
779
self._name, self._typedesc, self._doc.rstrip().replace('\n', '\n '),
780
self._default, separator, str(self._value),
781
)
782
783
def set(self, value: Any) -> None:
784
"""
785
Set the value of the option.
786
787
Parameters
788
----------
789
value : any
790
The value to set
791
792
"""
793
value = self._validator(value)
794
_config[self._name]._value = value
795
796
if self._environ is not None:
797
if value is None:
798
_delenv(self._environ)
799
else:
800
_setenv(self._environ, str(value))
801
802
def get(self) -> Any:
803
"""
804
Get the value of the option.
805
806
Returns
807
-------
808
Any
809
The value of the option
810
811
"""
812
if self._environ is not None:
813
try:
814
_config[self._name]._value = self._validator(_getenv(self._environ))
815
except KeyError:
816
pass
817
return _config[self._name]._value
818
819
def get_default(self) -> Any:
820
"""
821
Get the default value of the option.
822
823
Returns
824
-------
825
Any
826
The default value of the option
827
828
"""
829
return _config[self._name]._default
830
831
832
def register_option(
833
key: str,
834
typedesc: str,
835
validator: Callable[[Any], Any],
836
default: Any,
837
doc: str,
838
environ: Optional[Union[str, List[str]]] = None,
839
) -> None:
840
"""
841
Register a new option.
842
843
Parameters
844
----------
845
key : str
846
The name of the option
847
typedesc : str
848
Description of option data type (e.g., int, float, string)
849
validator : callable
850
A callable object that validates the value and returns
851
a validated value.
852
default : any
853
The default value of the option
854
doc : str
855
The documentation string for the option
856
environ : str or list of strs, optional
857
If specified, the value should be specified in an environment
858
variable of that name.
859
860
"""
861
import warnings
862
with warnings.catch_warnings():
863
warnings.simplefilter('ignore')
864
_config[key] = Option(key, typedesc, validator, default, doc, environ=environ)
865
866
867
class AttrOption(object):
868
"""
869
Attribute-style access of options.
870
871
Parameters
872
----------
873
name : str
874
Name of the option
875
876
"""
877
878
def __init__(self, name: str):
879
object.__setattr__(self, '_name', name)
880
881
def __dir__(self) -> List[str]:
882
"""Return list of flattened keys."""
883
if self._name in _config:
884
return _config[self._name].flatkeys()
885
return _config.flatkeys()
886
887
@property
888
def __doc__(self) -> Optional[str]: # type: ignore
889
if self._name:
890
return describe_option(self._name, _print_desc=False)
891
return describe_option(_print_desc=False)
892
893
def __getattr__(self, name: str) -> Any:
894
"""
895
Retieve option as an attribute.
896
897
Parameters
898
----------
899
name : str
900
Name of the option
901
902
Returns
903
-------
904
Any
905
906
"""
907
name = name.lower()
908
if self._name:
909
fullname = self._name + '.' + name
910
else:
911
fullname = name
912
if fullname not in _config:
913
fullname = _get_option_leaf_node(fullname)
914
out = _config[fullname]
915
if not isinstance(out, Option):
916
return type(self)(fullname)
917
return out.get()
918
919
def __setattr__(self, name: str, value: Any) -> Any:
920
"""
921
Set an attribute value.
922
923
Parameters
924
----------
925
name : str
926
Name of the option
927
value : Any
928
Value of the option
929
930
"""
931
name = name.lower()
932
if self._name:
933
fullname = self._name + '.' + name
934
else:
935
fullname = name
936
if fullname not in _config:
937
fullname = _get_option_leaf_node(fullname)
938
out = _config[fullname]
939
if not isinstance(out, Option):
940
return type(self)(fullname)
941
_config[fullname].set(value)
942
return
943
944
def __call__(self, *args: Any, **kwargs: Any) -> Iterator[None]:
945
"""Shortcut for option context."""
946
return option_context(*args, **kwargs) # type: ignore
947
948
949
# Object for setting and getting options using attribute syntax
950
options = AttrOption('')
951
952