2 # Base class for all SfaAPI functions
7 from types import IntType, LongType
10 from sfa.util.sfalogging import logger
11 from sfa.util.py23 import StringType
12 from sfa.util.faults import SfaFault, SfaInvalidAPIMethod, SfaInvalidArgumentCount, SfaInvalidArgument
14 from sfa.storage.parameter import Parameter, Mixed, python_type, xmlrpc_type
19 Base class for all SfaAPI functions. At a minimum, all SfaAPI
20 functions must define:
22 interfaces = [allowed interfaces]
23 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
24 returns = Parameter(return_type, return_doc)
25 call(arg1, arg2, ...): method body
27 Argument types may be Python types (e.g., int, bool, etc.), typed
28 values (e.g., 1, True, etc.), a Parameter, or lists or
29 dictionaries of possibly mixed types, values, and/or Parameters
30 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
32 Once function decorators in Python 2.4 are fully supported,
33 consider wrapping calls with accepts() and returns() functions
34 instead of performing type checking manually.
42 def call(self, *args):
44 Method body for all SfaAPI functions. Must override.
48 def __init__(self, api):
49 self.name = self.__class__.__name__
52 # Auth may set this to a Person instance (if an anonymous
53 # method, will remain None).
56 # API may set this to a (addr, port) tuple if known
59 def __call__(self, *args, **kwds):
61 Main entry point for all SFA API functions. Type checks
62 arguments, authenticates, and executes call().
67 methodname = self.name
68 if not self.api.interface or self.api.interface not in self.interfaces:
69 raise SfaInvalidAPIMethod(methodname, self.api.interface)
71 (min_args, max_args, defaults) = self.args()
73 # Check that the right number of arguments were passed in
74 if len(args) < len(min_args) or len(args) > len(max_args):
75 raise SfaInvalidArgumentCount(
76 len(args), len(min_args), len(max_args))
78 for name, value, expected in zip(max_args, args, self.accepts):
79 self.type_check(name, value, expected, args)
81 logger.debug("method.__call__ [%s] : BEG %s" % (
82 self.api.interface, methodname))
83 result = self.call(*args, **kwds)
85 runtime = time.time() - start
86 logger.debug("method.__call__ [%s] : END %s in %02f s (%s)" %
87 (self.api.interface, methodname, runtime, getattr(self, 'message', "[no-msg]")))
91 except SfaFault as fault:
95 # Prepend caller and method name to expected faults
96 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
97 runtime = time.time() - start
98 logger.log_exc("Method %s raised an exception" % self.name)
101 def help(self, indent=" "):
103 Text documentation for the method.
106 (min_args, max_args, defaults) = self.args()
108 text = "%s(%s) -> %s\n\n" % (self.name,
109 ", ".join(max_args), xmlrpc_type(self.returns))
111 text += "Description:\n\n"
112 lines = [indent + line.strip()
113 for line in self.__doc__.strip().split("\n")]
114 text += "\n".join(lines) + "\n\n"
116 def param_text(name, param, indent, step):
118 Format a method parameter.
123 # Print parameter name
126 text += name.ljust(param_offset - len(indent))
128 param_offset = len(indent)
130 # Print parameter type
131 param_type = python_type(param)
132 text += xmlrpc_type(param_type) + "\n"
134 # Print parameter documentation right below type
135 if isinstance(param, Parameter):
136 wrapper = textwrap.TextWrapper(width=70,
137 initial_indent=" " * param_offset,
138 subsequent_indent=" " * param_offset)
139 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
144 # Indent struct fields and mixed types
145 if isinstance(param, dict):
146 for name, subparam in param.iteritems():
147 text += param_text(name, subparam, indent + step, step)
148 elif isinstance(param, Mixed):
149 for subparam in param:
150 text += param_text(name, subparam, indent + step, step)
151 elif isinstance(param, (list, tuple, set)):
152 for subparam in param:
153 text += param_text("", subparam, indent + step, step)
157 text += "Parameters:\n\n"
158 for name, param in zip(max_args, self.accepts):
159 text += param_text(name, param, indent, indent)
161 text += "Returns:\n\n"
162 text += param_text("", self.returns, indent, indent)
170 ((arg1_name, arg2_name, ...),
171 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
172 (None, None, ..., optional1_default, optional2_default, ...))
174 That represents the minimum and maximum sets of arguments that
175 this function accepts and the defaults for the optional arguments.
178 # Inspect call. Remove self from the argument list.
179 max_args = self.call.func_code.co_varnames[
180 1:self.call.func_code.co_argcount]
181 defaults = self.call.func_defaults
185 min_args = max_args[0:len(max_args) - len(defaults)]
186 defaults = tuple([None for arg in min_args]) + defaults
188 return (min_args, max_args, defaults)
190 def type_check(self, name, value, expected, args):
192 Checks the type of the named value against the expected type,
193 which may be a Python type, a typed value, a Parameter, a
194 Mixed type, or a list or dictionary of possibly mixed types,
195 values, Parameters, or Mixed types.
197 Extraneous members of lists must be of the same type as the
198 last specified type. For example, if the expected argument
199 type is [int, bool], then [1, False] and [14, True, False,
200 True] are valid, but [1], [False, 1] and [14, True, 1] are
203 Extraneous members of dictionaries are ignored.
206 # If any of a number of types is acceptable
207 if isinstance(expected, Mixed):
208 for item in expected:
210 self.type_check(name, value, item, args)
212 except SfaInvalidArgument as fault:
216 # If an authentication structure is expected, save it and
217 # authenticate after basic type checking is done.
218 # if isinstance(expected, Auth):
223 # Get actual expected type from within the Parameter structure
224 if isinstance(expected, Parameter):
227 nullok = expected.nullok
228 expected = expected.type
234 expected_type = python_type(expected)
236 # If value can be NULL
237 if value is None and nullok:
240 # Strings are a special case. Accept either unicode or str
241 # types if a string is expected.
242 if issubclass(expected_type, StringType) and isinstance(value, StringType):
245 # Integers and long integers are also special types. Accept
246 # either int or long types if an int or long is expected.
247 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
250 elif not isinstance(value, expected_type):
251 raise SfaInvalidArgument("expected %s, got %s" %
252 (xmlrpc_type(expected_type),
253 xmlrpc_type(type(value))),
256 # If a minimum or maximum (length, value) has been specified
257 if issubclass(expected_type, StringType):
258 if min is not None and \
259 len(value.encode(self.api.encoding)) < min:
260 raise SfaInvalidArgument(
261 "%s must be at least %d bytes long" % (name, min))
262 if max is not None and \
263 len(value.encode(self.api.encoding)) > max:
264 raise SfaInvalidArgument(
265 "%s must be at most %d bytes long" % (name, max))
266 elif expected_type in (list, tuple, set):
267 if min is not None and len(value) < min:
268 raise SfaInvalidArgument(
269 "%s must contain at least %d items" % (name, min))
270 if max is not None and len(value) > max:
271 raise SfaInvalidArgument(
272 "%s must contain at most %d items" % (name, max))
274 if min is not None and value < min:
275 raise SfaInvalidArgument("%s must be > %s" % (name, str(min)))
276 if max is not None and value > max:
277 raise SfaInvalidArgument("%s must be < %s" % (name, str(max)))
279 # If a list with particular types of items is expected
280 if isinstance(expected, (list, tuple, set)):
281 for i in range(len(value)):
282 if i >= len(expected):
283 j = len(expected) - 1
286 self.type_check(name + "[]", value[i], expected[j], args)
288 # If a struct with particular (or required) types of items is
290 elif isinstance(expected, dict):
291 for key in value.keys():
293 self.type_check(name + "['%s']" %
294 key, value[key], expected[key], args)
295 for key, subparam in expected.iteritems():
296 if isinstance(subparam, Parameter) and \
297 subparam.optional is not None and \
298 not subparam.optional and key not in value.keys():
299 raise SfaInvalidArgument("'%s' not specified" % key, name)
301 # if auth is not None:
302 # auth.check(self, *args)