forked from irods/python-irodsclient
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
489 lines (376 loc) · 16.1 KB
/
__init__.py
File metadata and controls
489 lines (376 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
import ast
import collections
import contextlib
import copy
import io
import logging
import os
import re
import sys
import types
# Duplicate here for convenience
from .. import DEFAULT_CONFIG_PATH
logger = logging.getLogger(__name__)
class iRODSConfiguration:
__slots__ = ()
def getter(category, setting):
"""A programmatic way of allowing the current value of the specified setting to be
given indirectly (through an extra call indirection) as the default value of a parameter.
Returns a lambda that, when called, will yield the setting's value. In the closure of
that lambda, the Python builtin function globals() is used to access (in a read-only
capacity) the namespace dict of the irods.client_configuration module.
See the irods.manager.data_object_manager.DataObjectManager.open(...) method signature
for a usage example.
"""
return lambda: getattr(globals()[category], setting)
class iRODSConfigAliasMetaclass(type):
def __new__(meta, name, bases, attrs):
cls = type.__new__(meta, name, bases, attrs)
cls.writeable_properties = tuple(
k
for k, v in attrs.items()
if isinstance(v, property) and v.fset is not None
)
return cls
class ConnectionsProperties(iRODSConfiguration, metaclass=iRODSConfigAliasMetaclass):
__slots__ = ()
@property
def xml_parser_default(self):
from irods.message import get_default_XML_by_name
return get_default_XML_by_name()
@xml_parser_default.setter
def xml_parser_default(self, str_value):
from irods.message import set_default_XML_by_name
return set_default_XML_by_name(str_value)
connections = ConnectionsProperties()
class ConfigurationError(BaseException): pass
class ConfigurationValueError(ValueError,ConfigurationError): pass
class Genquery1_Properties(iRODSConfiguration, metaclass=iRODSConfigAliasMetaclass):
__slots__ = ()
@property
def irods_query_limit(self):
import irods.query
return irods.query.IRODS_QUERY_LIMIT
@irods_query_limit.setter
def irods_query_limit(self, target_value):
import irods.query
requested = int(target_value)
if requested <= 0:
raise ConfigurationValueError(f'Error setting IRODS_QUERY_LIMIT to [{requested}]. Use positive values only.')
irods.query.IRODS_QUERY_LIMIT = requested
genquery1 = Genquery1_Properties()
# #############################################################################
#
# Classes for building client configuration categories
# (irods.client_configuration.data_objects is one such category):
class DataObjects(iRODSConfiguration):
__slots__ = (
"auto_close",
"allow_redirect",
"force_create_by_default",
"force_put_by_default",
)
def __init__(self):
# Setting it in the constructor lets the attribute be a
# configurable one and allows a default value of False.
#
# Running following code will opt in to the the auto-closing
# behavior for any data objects opened subsequently.
#
# >>> import irods.client_configuration as config
# >>> irods.client_configuration.data_objects.auto_close = True
self.auto_close = False
self.allow_redirect = False
self.force_create_by_default = True
self.force_put_by_default = True
# #############################################################################
#
# Instantiations of client-configuration categories:
# The usage "irods.client_configuration.data_objects" reflects the commonly used
# manager name (session.data_objects) and is thus understood to influence the
# behavior of data objects.
#
# By design, valid configurable targets (e.g. auto_close) are limited to the names
# listed in the __slots__ member of the category class.
data_objects = DataObjects()
class LegacyAuth(iRODSConfiguration):
__slots__ = ("pam", "force_legacy_auth")
class Pam(iRODSConfiguration):
__slots__ = (
"time_to_live_in_hours",
"password_for_auto_renew",
"store_password_to_environment",
"force_use_of_dedicated_pam_api",
)
def __init__(self):
self.time_to_live_in_hours = (
0 # -> We default to the server's TTL preference.
)
self.password_for_auto_renew = ""
self.store_password_to_environment = False
self.force_use_of_dedicated_pam_api = False
def __init__(self):
self.pam = self.Pam()
self.force_legacy_auth = False
legacy_auth = LegacyAuth()
# Exposes the significant settable attributes of an iRODSConfiguration object:
def _config_names(root):
slots = getattr(root, "__slots__", ())
properties = getattr(root, "writeable_properties", ())
return tuple(slots) + tuple(properties)
# Exposes one level of the configuration hierarchy from the given ("root") node:
def _var_items(root, leaf_flag=False):
if leaf_flag:
flag = lambda _: (_,)
else:
flag = lambda _: ()
if isinstance(root, types.ModuleType):
return [
((i, v) + flag(False))
for i, v in vars(root).items()
if isinstance(v, iRODSConfiguration)
]
if isinstance(root, iRODSConfiguration):
return [(i, getattr(root, i)) + flag(True) for i in _config_names(root)]
return []
# Recurses through an entire configuration hierarchy:
def _var_items_as_generator(root=sys.modules[__name__], dotted=""):
_v = _var_items(root, leaf_flag=True)
for name, sub_node, is_config in _v:
dn = dotted + ("." if dotted else "") + name
yield dn, sub_node, is_config
# # TODO: (#480) When Python2 support is removed, we can instead use the simpler construction:
# yield from _var_items_as_generator(root = sub_node, dotted = dn)
for _dotted, _root, _is_config in _var_items_as_generator(
root=sub_node, dotted=dn
):
yield _dotted, _root, _is_config
VarItemTuple = collections.namedtuple("VarItemTuple", ["dotted", "root", "is_config"])
def _var_item_tuples_as_generator(root=sys.modules[__name__]):
for _ in _var_items_as_generator(root):
yield VarItemTuple(*_)
def save(root=None, string="", file=""):
"""Save the current configuration.
When called simply as save(), this function simply writes all client settings into
a configuration file.
The 'root' and 'string' parameters are not likely to be overridden when called from an
application. They should usually only vary from the defaults when save() recurses into itself.
However, for due explanation's sake: 'root' specifies at which subtree node to start writing,
None denoting the top level; and 'string' specifies a prefix for the dotted prefix name,
which should be empty for an invocation that references the settings' top level namespace.
Both of these defaults are in effect when calling save() without explicit parameters.
The configuration file path will normally be the value of DEFAULT_CONFIG_PATH,
but this can be overridden by supplying a non-empty string in the 'file' parameter.
"""
_file = None
auto_close_settings = False
try:
if not file:
from .. import get_settings_path
file = get_settings_path()
if isinstance(file, str):
_file = open(file, "w")
auto_close_settings = True
else:
_file = file # assume file-like object if not a string
if root is None:
root = sys.modules[__name__]
for k, v in _var_items(root):
dotted_string = string + ("." if string else "") + k
if isinstance(v, iRODSConfiguration):
save(root=v, string=dotted_string, file=_file)
else:
print(dotted_string, repr(v), sep="\t\t", file=_file)
return file
finally:
if _file and auto_close_settings:
_file.close()
@contextlib.contextmanager
def loadlines(entries, common_root=sys.modules[__name__]):
"""Temporarily change the values for one or more settings in the PRC's configuration. Useful for test code.
Parameters:
entries: list of dict objects of the form dict(setting="dotted.path.to.setting", value=<temp_value>).
common_root: root point in configuration tree (best left to its default value).
Sample usage:
with loadlines(entries=[dict(setting='legacy_auth.pam.password_for_auto_renew',value='my-pam-password'),
dict(setting='legacy_auth.pam.store_password_to_environment',value=True)]):
# ... test code for which the altered setting(s) should be in force
"""
root_item = [("root", common_root)]
entries_ = copy.deepcopy(entries)
identity = lambda _: _
try:
# Load config values.
entries_ = []
for e in entries:
e_ = dict(root_item + list(e.items()))
L = []
_load_config_line(eval_func=identity, return_old=L, **e_)
e_["value"] = L[0]
entries_.append(e_)
yield
finally:
# Restore old values.
for e_ in entries_:
_load_config_line(eval_func=identity, **e_)
def _load_config_line(
root, setting, value, return_old=None, eval_func=ast.literal_eval
):
"""Low-level utility function for loading a line of settings, with the option to return the old (displaced) value.
The 'root' refers to the starting point in the configuration tree. Its meaning is the same as in loadlines().
The 'setting' is a string containing the dotted name for the configuration setting.
The 'value' is the new value to be loaded. This will be evaluated via 'eval_func' (see below).
The 'return_old' is either None or a list which returns the displaced value back to the caller.
The 'eval_func' is a function for making the supplied 'value' parameter into a Pythonic value to be assigned to the given 'setting'.
"""
arr = [_.strip() for _ in setting.split(".")]
loadexc = None
# Compute the object referred to by the dotted name.
try:
attr = ""
for i in filter(None, arr):
if attr:
root = getattr(root, attr)
attr = i
# Assign into the current setting of the dotted name (effectively <root>.<attr>)
# using the loaded value.
if attr:
if isinstance(return_old, list):
# Return, in the provided list, the old value of the setting.
return_old.append(getattr(root, attr))
return setattr(root, attr, eval_func(value))
except Exception as e:
loadexc = e
# If we get this far, there's a problem loading the configuration setting. Raise an exception or log it.
error_message = (
"Bad setting: root = {root!r}, setting = {setting!r}, value = {value!r}".format(
**locals()
)
)
if loadexc:
error_message += " [{loadexc!r}]".format(**locals())
if allow_config_load_errors:
raise RuntimeError(error_message)
else:
logging.getLogger(__name__).log(logging.ERROR, "%s", error_message)
allow_config_load_errors = ast.literal_eval(
os.environ.get("PYTHON_IRODSCLIENT_CONFIGURATION_LOAD_ERRORS_FATAL", "False")
)
# The following regular expression is used to match a configuration file line of the form:
# ---------------------------------------------------------------
# <optional whitespace>
# key: <dotted-name specification>
# <whitespace of length 1 or more>
# value: <A Python value which can be given to ast.literal_eval(); e.g. 5, True, or 'some_string'>
# <optional whitespace>
_key_value_pattern = re.compile(r"\s*(?P<key>\w+(\.\w+)+)\s+(?P<value>\S.*?)\s*$")
class _ConfigLoadError:
"""
Exceptions that subclass this type can be thrown by the load() function if
their classes are listed in the failure_modes parameter of that function.
"""
class NoConfigError(Exception, _ConfigLoadError):
pass
class BadConfigError(Exception, _ConfigLoadError):
pass
def load(
root=None,
file="",
failure_modes=(),
verify_only=False,
logging_level=logging.WARNING,
use_environment_variables=False,
):
"""Load the current configuration.
An example of a valid line in a configuration file is this:
data_objects.auto_close True
When this function is called without parameters, it reads all client settings from
a configuration file (the path given by DEFAULT_CONFIG_PATH, since file = '' in such
an invocation) and assigns the repr()-style Python value given into the dotted-string
configuration entry given.
The 'file' parameter, when set to a non-empty string, provides an override for
the config-file path default.
As with save(), 'root' refers to the starting location in the settings tree, with
a value of None denoting the top tree node (ie the namespace containing *all* settings).
There are as yet no imagined use-cases for an application developer to pass in an
explicit 'root' override.
'failure_modes' is an iterable containing desired exception types to be thrown if,
for example, the configuration file is missing (NoConfigError) or contains an improperly
formatted line (BadConfigError).
'logging_level' governs the internally logged messages and can be used to e.g. quiet the
call's logging output.
"""
def _existing_config(path):
if os.path.isfile(path):
return open(path, "r")
message = "Config file not available at %r" % (path,)
logging.getLogger(__name__).log(logging_level, message)
if NoConfigError in failure_modes:
raise NoConfigError(message)
return io.StringIO()
_file = None
try:
if not file:
from .. import get_settings_path
file = get_settings_path()
_file = _existing_config(file)
if root is None:
root = sys.modules[__name__]
if verify_only:
return
for line_number, line in enumerate(_file.readlines()):
line = line.strip()
match = _key_value_pattern.match(line)
if not match:
if line != "":
# Log only the invalid lines that contain non-whitespace characters.
message = "Invalid configuration format at line %d: %r" % (
line_number + 1,
line,
)
logging.getLogger(__name__).log(logging_level, message)
if BadConfigError in failure_modes:
raise BadConfigError(message)
continue
_load_config_line(root, match.group("key"), match.group("value"))
if use_environment_variables:
_load_settings_from_environment(root)
finally:
if _file:
_file.close()
default_config_dict = {}
def _load_settings_from_environment(root=None):
if root is None:
root = sys.modules[__name__]
for key, variable in _calculate_overriding_environment_variables().items():
value = os.environ.get(variable)
if value is not None:
_load_config_line(root, key, value)
def preserve_defaults():
default_config_dict.update(
(k, copy.deepcopy(v))
for k, v in globals().items()
if isinstance(v, iRODSConfiguration)
)
def autoload(_file_to_load):
if _file_to_load is None:
_load_settings_from_environment()
else:
load(file=_file_to_load, use_environment_variables=True)
def new_default_config():
module = types.ModuleType("_")
module.__dict__.update(default_config_dict)
return module
def overriding_environment_variables():
uppercase_and_dot_split = lambda _: _.upper().split(".")
return {
_tuple.dotted: "__".join(
["PYTHON_IRODSCLIENT_CONFIG"] + uppercase_and_dot_split(_tuple.dotted)
)
for _tuple in _var_item_tuples_as_generator()
if _tuple.is_config
}
def _calculate_overriding_environment_variables(
memo=overriding_environment_variables(),
):
return memo