2 # Base class for all SfaAPI functions
7 from types import IntType, LongType, StringTypes
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
17 Base class for all SfaAPI functions. At a minimum, all SfaAPI
18 functions must define:
20 interfaces = [allowed interfaces]
21 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
22 returns = Parameter(return_type, return_doc)
23 call(arg1, arg2, ...): method body
25 Argument types may be Python types (e.g., int, bool, etc.), typed
26 values (e.g., 1, True, etc.), a Parameter, or lists or
27 dictionaries of possibly mixed types, values, and/or Parameters
28 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
30 Once function decorators in Python 2.4 are fully supported,
31 consider wrapping calls with accepts() and returns() functions
32 instead of performing type checking manually.
40 def call(self, *args):
42 Method body for all SfaAPI functions. Must override.
46 def __init__(self, api):
47 self.name = self.__class__.__name__
50 # Auth may set this to a Person instance (if an anonymous
51 # method, will remain None).
54 # API may set this to a (addr, port) tuple if known
57 def __call__(self, *args, **kwds):
59 Main entry point for all SfaAPI functions. Type checks
60 arguments, authenticates, and executes call().
65 methodname = self.name
66 if not self.api.interface or self.api.interface not in self.interfaces:
67 raise SfaInvalidAPIMethod(methodname, self.api.interface)
69 (min_args, max_args, defaults) = self.args()
71 # Check that the right number of arguments were passed in
72 if len(args) < len(min_args) or len(args) > len(max_args):
73 raise SfaInvalidArgumentCount(len(args), len(min_args), len(max_args))
75 for name, value, expected in zip(max_args, args, self.accepts):
76 self.type_check(name, value, expected, args)
78 if self.api.config.SFA_API_DEBUG:
79 logger.debug("method.__call__ [%s] : BEG %s"%(self.api.interface,methodname))
80 result = self.call(*args, **kwds)
82 runtime = time.time() - start
83 if self.api.config.SFA_API_DEBUG or hasattr(self, 'message'):
84 logger.debug("method.__call__ [%s] : END %s in %02f s (%s)"%\
85 (self.api.interface,methodname,runtime,getattr(self,'message',"[no-msg]")))
89 except SfaFault, fault:
93 # Prepend caller and method name to expected faults
94 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
95 runtime = time.time() - start
96 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, ", ".join(max_args), xmlrpc_type(self.returns))
109 text += "Description:\n\n"
110 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
111 text += "\n".join(lines) + "\n\n"
113 def param_text(name, param, indent, step):
115 Format a method parameter.
120 # Print parameter name
123 text += name.ljust(param_offset - len(indent))
125 param_offset = len(indent)
127 # Print parameter type
128 param_type = python_type(param)
129 text += xmlrpc_type(param_type) + "\n"
131 # Print parameter documentation right below type
132 if isinstance(param, Parameter):
133 wrapper = textwrap.TextWrapper(width = 70,
134 initial_indent = " " * param_offset,
135 subsequent_indent = " " * param_offset)
136 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
141 # Indent struct fields and mixed types
142 if isinstance(param, dict):
143 for name, subparam in param.iteritems():
144 text += param_text(name, subparam, indent + step, step)
145 elif isinstance(param, Mixed):
146 for subparam in param:
147 text += param_text(name, subparam, indent + step, step)
148 elif isinstance(param, (list, tuple, set)):
149 for subparam in param:
150 text += param_text("", subparam, indent + step, step)
154 text += "Parameters:\n\n"
155 for name, param in zip(max_args, self.accepts):
156 text += param_text(name, param, indent, indent)
158 text += "Returns:\n\n"
159 text += param_text("", self.returns, indent, indent)
167 ((arg1_name, arg2_name, ...),
168 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
169 (None, None, ..., optional1_default, optional2_default, ...))
171 That represents the minimum and maximum sets of arguments that
172 this function accepts and the defaults for the optional arguments.
175 # Inspect call. Remove self from the argument list.
176 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
177 defaults = self.call.func_defaults
181 min_args = max_args[0:len(max_args) - len(defaults)]
182 defaults = tuple([None for arg in min_args]) + defaults
184 return (min_args, max_args, defaults)
186 def type_check(self, name, value, expected, args):
188 Checks the type of the named value against the expected type,
189 which may be a Python type, a typed value, a Parameter, a
190 Mixed type, or a list or dictionary of possibly mixed types,
191 values, Parameters, or Mixed types.
193 Extraneous members of lists must be of the same type as the
194 last specified type. For example, if the expected argument
195 type is [int, bool], then [1, False] and [14, True, False,
196 True] are valid, but [1], [False, 1] and [14, True, 1] are
199 Extraneous members of dictionaries are ignored.
202 # If any of a number of types is acceptable
203 if isinstance(expected, Mixed):
204 for item in expected:
206 self.type_check(name, value, item, args)
208 except SfaInvalidArgument, fault:
212 # If an authentication structure is expected, save it and
213 # authenticate after basic type checking is done.
214 #if isinstance(expected, Auth):
219 # Get actual expected type from within the Parameter structure
220 if isinstance(expected, Parameter):
223 nullok = expected.nullok
224 expected = expected.type
230 expected_type = python_type(expected)
232 # If value can be NULL
233 if value is None and nullok:
236 # Strings are a special case. Accept either unicode or str
237 # types if a string is expected.
238 if expected_type in StringTypes and isinstance(value, StringTypes):
241 # Integers and long integers are also special types. Accept
242 # either int or long types if an int or long is expected.
243 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
246 elif not isinstance(value, expected_type):
247 raise SfaInvalidArgument("expected %s, got %s" % \
248 (xmlrpc_type(expected_type),
249 xmlrpc_type(type(value))),
252 # If a minimum or maximum (length, value) has been specified
253 if expected_type in StringTypes:
254 if min is not None and \
255 len(value.encode(self.api.encoding)) < min:
256 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
257 if max is not None and \
258 len(value.encode(self.api.encoding)) > max:
259 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
260 elif expected_type in (list, tuple, set):
261 if min is not None and len(value) < min:
262 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
263 if max is not None and len(value) > max:
264 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
266 if min is not None and value < min:
267 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
268 if max is not None and value > max:
269 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
271 # If a list with particular types of items is expected
272 if isinstance(expected, (list, tuple, set)):
273 for i in range(len(value)):
274 if i >= len(expected):
275 j = len(expected) - 1
278 self.type_check(name + "[]", value[i], expected[j], args)
280 # If a struct with particular (or required) types of items is
282 elif isinstance(expected, dict):
283 for key in value.keys():
285 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
286 for key, subparam in expected.iteritems():
287 if isinstance(subparam, Parameter) and \
288 subparam.optional is not None and \
289 not subparam.optional and key not in value.keys():
290 raise SfaInvalidArgument("'%s' not specified" % key, name)
292 #if auth is not None:
293 # auth.check(self, *args)