1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: Apache-2.0
14 FILTER_STR_RE = re.compile(r'''
20 ''', re.ASCII | re.VERBOSE)
25 raise ValueError("can't accept negative value: %s" % (num,))
28 retry_opt = argparse.ArgumentParser(add_help=False)
29 retry_opt.add_argument('--retries', type=_pos_int, default=10, help="""
30 Maximum number of times to retry server requests that encounter temporary
31 failures (e.g., server down). Default 10.""")
33 def _ignore_error(error):
36 def _raise_error(error):
39 CAUGHT_SIGNALS = [signal.SIGINT, signal.SIGQUIT, signal.SIGTERM]
41 def exit_signal_handler(sigcode, frame):
42 logging.getLogger('arvados').error("Caught signal {}, exiting.".format(sigcode))
45 def install_signal_handlers():
46 global orig_signal_handlers
47 orig_signal_handlers = {sigcode: signal.signal(sigcode, exit_signal_handler)
48 for sigcode in CAUGHT_SIGNALS}
50 def restore_signal_handlers():
51 for sigcode, orig_handler in orig_signal_handlers.items():
52 signal.signal(sigcode, orig_handler)
54 def validate_filters(filters):
55 """Validate user-provided filters
57 This function validates that a user-defined object represents valid
58 Arvados filters that can be passed to an API client: that it's a list of
59 3-element lists with the field name and operator given as strings. If any
60 of these conditions are not true, it raises a ValueError with details about
63 It returns validated filters. Currently the provided filters are returned
64 unmodified. Future versions of this function may clean up the filters with
65 "obvious" type conversions, so callers SHOULD use the returned value for
68 if not isinstance(filters, list):
69 raise ValueError(f"filters are not a list: {filters!r}")
70 for index, f in enumerate(filters):
71 if isinstance(f, str):
72 match = FILTER_STR_RE.fullmatch(f)
74 raise ValueError(f"filter at index {index} has invalid syntax: {f!r}")
75 s, op, o = match.groups()
77 raise ValueError(f"filter at index {index} has invalid syntax: bad field name {s!r}")
79 raise ValueError(f"filter at index {index} has invalid syntax: bad field name {o!r}")
81 elif not isinstance(f, list):
82 raise ValueError(f"filter at index {index} is not a string or list: {f!r}")
87 f"filter at index {index} does not have three items (field name, operator, operand): {f!r}",
89 if not isinstance(s, str):
90 raise ValueError(f"filter at index {index} field name is not a string: {s!r}")
91 if not isinstance(op, str):
92 raise ValueError(f"filter at index {index} operator is not a string: {op!r}")
97 """Parse a JSON file from a command line argument string or path
99 JSONArgument objects can be called with a string and return an arbitrary
100 object. First it will try to decode the string as JSON. If that fails, it
101 will try to open a file at the path named by the string, and decode it as
102 JSON. If that fails, it raises ValueError with more detail.
104 This is designed to be used as an argparse argument type.
105 Typical usage looks like:
107 parser = argparse.ArgumentParser()
108 parser.add_argument('--object', type=JSONArgument(), ...)
110 You can construct JSONArgument with an optional validation function. If
111 given, it is called with the object decoded from user input, and its
112 return value replaces it. It should raise ValueError if there is a problem
113 with the input. (argparse turns ValueError into a useful error message.)
115 filters_type = JSONArgument(validate_filters)
116 parser.add_argument('--filters', type=filters_type, ...)
118 def __init__(self, validator=None):
119 self.validator = validator
121 def __call__(self, value):
123 retval = json.loads(value)
124 except json.JSONDecodeError:
126 with open(value, 'rb') as json_file:
127 retval = json.load(json_file)
128 except json.JSONDecodeError as error:
129 raise ValueError(f"error decoding JSON from file {value!r}: {error}") from None
130 except (FileNotFoundError, ValueError):
131 raise ValueError(f"not a valid JSON string or file path: {value!r}") from None
132 except OSError as error:
133 raise ValueError(f"error reading JSON file path {value!r}: {error.strerror}") from None
134 if self.validator is not None:
135 retval = self.validator(retval)