2 # Base class for all SfaAPI functions
7 from types import IntType, LongType
10 from sfa.util.sfalogging import logger
11 from sfa.util.faults import SfaFault, SfaInvalidAPIMethod, SfaInvalidArgumentCount, SfaInvalidArgument
13 from sfa.storage.parameter import Parameter, Mixed, python_type, xmlrpc_type
18 Base class for all SfaAPI functions. At a minimum, all SfaAPI
19 functions must define:
21 interfaces = [allowed interfaces]
22 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
23 returns = Parameter(return_type, return_doc)
24 call(arg1, arg2, ...): method body
26 Argument types may be Python types (e.g., int, bool, etc.), typed
27 values (e.g., 1, True, etc.), a Parameter, or lists or
28 dictionaries of possibly mixed types, values, and/or Parameters
29 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
31 Once function decorators in Python 2.4 are fully supported,
32 consider wrapping calls with accepts() and returns() functions
33 instead of performing type checking manually.
41 def call(self, *args):
43 Method body for all SfaAPI functions. Must override.
47 def __init__(self, api):
48 self.name = self.__class__.__name__
51 # Auth may set this to a Person instance (if an anonymous
52 # method, will remain None).
55 # API may set this to a (addr, port) tuple if known
58 def __call__(self, *args, **kwds):
60 Main entry point for all SFA API functions. Type checks
61 arguments, authenticates, and executes call().
66 methodname = self.name
67 if not self.api.interface or self.api.interface not in self.interfaces:
68 raise SfaInvalidAPIMethod(methodname, self.api.interface)
70 (min_args, max_args, defaults) = self.args()
72 # Check that the right number of arguments were passed in
73 if len(args) < len(min_args) or len(args) > len(max_args):
74 raise SfaInvalidArgumentCount(
75 len(args), len(min_args), len(max_args))
77 for name, value, expected in zip(max_args, args, self.accepts):
78 self.type_check(name, value, expected, args)
80 logger.debug("method.__call__ [%s] : BEG %s" % (
81 self.api.interface, methodname))
82 result = self.call(*args, **kwds)
84 runtime = time.time() - start
85 logger.debug("method.__call__ [%s] : END %s in %02f s (%s)" %
86 (self.api.interface, methodname, runtime, getattr(self, 'message', "[no-msg]")))
90 except SfaFault as fault:
94 # Prepend caller and method name to expected faults
95 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
96 runtime = time.time() - start
97 logger.log_exc("Method %s raised an exception" % self.name)
100 def help(self, indent=" "):
102 Text documentation for the method.
105 (min_args, max_args, defaults) = self.args()
107 text = "%s(%s) -> %s\n\n" % (self.name,
108 ", ".join(max_args), xmlrpc_type(self.returns))
110 text += "Description:\n\n"
111 lines = [indent + line.strip()
112 for line in self.__doc__.strip().split("\n")]
113 text += "\n".join(lines) + "\n\n"
115 def param_text(name, param, indent, step):
117 Format a method parameter.
122 # Print parameter name
125 text += name.ljust(param_offset - len(indent))
127 param_offset = len(indent)
129 # Print parameter type
130 param_type = python_type(param)
131 text += xmlrpc_type(param_type) + "\n"
133 # Print parameter documentation right below type
134 if isinstance(param, Parameter):
135 wrapper = textwrap.TextWrapper(width=70,
136 initial_indent=" " * param_offset,
137 subsequent_indent=" " * param_offset)
138 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
143 # Indent struct fields and mixed types
144 if isinstance(param, dict):
145 for name, subparam in param.items():
146 text += param_text(name, subparam, indent + step, step)
147 elif isinstance(param, Mixed):
148 for subparam in param:
149 text += param_text(name, subparam, indent + step, step)
150 elif isinstance(param, (list, tuple, set)):
151 for subparam in param:
152 text += param_text("", subparam, indent + step, step)
156 text += "Parameters:\n\n"
157 for name, param in zip(max_args, self.accepts):
158 text += param_text(name, param, indent, indent)
160 text += "Returns:\n\n"
161 text += param_text("", self.returns, indent, indent)
169 ((arg1_name, arg2_name, ...),
170 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
171 (None, None, ..., optional1_default, optional2_default, ...))
173 That represents the minimum and maximum sets of arguments that
174 this function accepts and the defaults for the optional arguments.
177 # Inspect call. Remove self from the argument list.
178 max_args = self.call.__code__.co_varnames[
179 1:self.call.__code__.co_argcount]
180 defaults = self.call.__defaults__
184 min_args = max_args[0:len(max_args) - len(defaults)]
185 defaults = tuple([None for arg in min_args]) + defaults
187 return (min_args, max_args, defaults)
189 def type_check(self, name, value, expected, args):
191 Checks the type of the named value against the expected type,
192 which may be a Python type, a typed value, a Parameter, a
193 Mixed type, or a list or dictionary of possibly mixed types,
194 values, Parameters, or Mixed types.
196 Extraneous members of lists must be of the same type as the
197 last specified type. For example, if the expected argument
198 type is [int, bool], then [1, False] and [14, True, False,
199 True] are valid, but [1], [False, 1] and [14, True, 1] are
202 Extraneous members of dictionaries are ignored.
205 # If any of a number of types is acceptable
206 if isinstance(expected, Mixed):
207 for item in expected:
209 self.type_check(name, value, item, args)
211 except SfaInvalidArgument as fault:
215 # If an authentication structure is expected, save it and
216 # authenticate after basic type checking is done.
217 # if isinstance(expected, Auth):
222 # Get actual expected type from within the Parameter structure
223 if isinstance(expected, Parameter):
226 nullok = expected.nullok
227 expected = expected.type
233 expected_type = python_type(expected)
235 # If value can be NULL
236 if value is None and nullok:
239 # Strings are a special case. Accept either unicode or str
240 # types if a string is expected.
241 if issubclass(expected_type, str) and isinstance(value, str):
244 # Integers and long integers are also special types. Accept
245 # either int or long types if an int or long is expected.
246 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
249 elif not isinstance(value, expected_type):
250 raise SfaInvalidArgument("expected %s, got %s" %
251 (xmlrpc_type(expected_type),
252 xmlrpc_type(type(value))),
255 # If a minimum or maximum (length, value) has been specified
256 if issubclass(expected_type, str):
257 if min is not None and \
258 len(value.encode(self.api.encoding)) < min:
259 raise SfaInvalidArgument(
260 "%s must be at least %d bytes long" % (name, min))
261 if max is not None and \
262 len(value.encode(self.api.encoding)) > max:
263 raise SfaInvalidArgument(
264 "%s must be at most %d bytes long" % (name, max))
265 elif expected_type in (list, tuple, set):
266 if min is not None and len(value) < min:
267 raise SfaInvalidArgument(
268 "%s must contain at least %d items" % (name, min))
269 if max is not None and len(value) > max:
270 raise SfaInvalidArgument(
271 "%s must contain at most %d items" % (name, max))
273 if min is not None and value < min:
274 raise SfaInvalidArgument("%s must be > %s" % (name, str(min)))
275 if max is not None and value > max:
276 raise SfaInvalidArgument("%s must be < %s" % (name, str(max)))
278 # If a list with particular types of items is expected
279 if isinstance(expected, (list, tuple, set)):
280 for i in range(len(value)):
281 if i >= len(expected):
282 j = len(expected) - 1
285 self.type_check(name + "[]", value[i], expected[j], args)
287 # If a struct with particular (or required) types of items is
289 elif isinstance(expected, dict):
290 for key in list(value.keys()):
292 self.type_check(name + "['%s']" %
293 key, value[key], expected[key], args)
294 for key, subparam in expected.items():
295 if isinstance(subparam, Parameter) and \
296 subparam.optional is not None and \
297 not subparam.optional and key not in list(value.keys()):
298 raise SfaInvalidArgument("'%s' not specified" % key, name)
300 # if auth is not None:
301 # auth.check(self, *args)