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
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(len(args), len(min_args), len(max_args))
76 for name, value, expected in zip(max_args, args, self.accepts):
77 self.type_check(name, value, expected, args)
79 logger.debug("method.__call__ [%s] : BEG %s"%(self.api.interface,methodname))
80 result = self.call(*args, **kwds)
82 runtime = time.time() - start
83 logger.debug("method.__call__ [%s] : END %s in %02f s (%s)"%\
84 (self.api.interface,methodname,runtime,getattr(self,'message',"[no-msg]")))
88 except SfaFault as fault:
92 # Prepend caller and method name to expected faults
93 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
94 runtime = time.time() - start
95 logger.log_exc("Method %s raised an exception"%self.name)
99 def help(self, indent = " "):
101 Text documentation for the method.
104 (min_args, max_args, defaults) = self.args()
106 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
108 text += "Description:\n\n"
109 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
110 text += "\n".join(lines) + "\n\n"
112 def param_text(name, param, indent, step):
114 Format a method parameter.
119 # Print parameter name
122 text += name.ljust(param_offset - len(indent))
124 param_offset = len(indent)
126 # Print parameter type
127 param_type = python_type(param)
128 text += xmlrpc_type(param_type) + "\n"
130 # Print parameter documentation right below type
131 if isinstance(param, Parameter):
132 wrapper = textwrap.TextWrapper(width = 70,
133 initial_indent = " " * param_offset,
134 subsequent_indent = " " * param_offset)
135 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
140 # Indent struct fields and mixed types
141 if isinstance(param, dict):
142 for name, subparam in param.iteritems():
143 text += param_text(name, subparam, indent + step, step)
144 elif isinstance(param, Mixed):
145 for subparam in param:
146 text += param_text(name, subparam, indent + step, step)
147 elif isinstance(param, (list, tuple, set)):
148 for subparam in param:
149 text += param_text("", subparam, indent + step, step)
153 text += "Parameters:\n\n"
154 for name, param in zip(max_args, self.accepts):
155 text += param_text(name, param, indent, indent)
157 text += "Returns:\n\n"
158 text += param_text("", self.returns, indent, indent)
166 ((arg1_name, arg2_name, ...),
167 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
168 (None, None, ..., optional1_default, optional2_default, ...))
170 That represents the minimum and maximum sets of arguments that
171 this function accepts and the defaults for the optional arguments.
174 # Inspect call. Remove self from the argument list.
175 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
176 defaults = self.call.func_defaults
180 min_args = max_args[0:len(max_args) - len(defaults)]
181 defaults = tuple([None for arg in min_args]) + defaults
183 return (min_args, max_args, defaults)
185 def type_check(self, name, value, expected, args):
187 Checks the type of the named value against the expected type,
188 which may be a Python type, a typed value, a Parameter, a
189 Mixed type, or a list or dictionary of possibly mixed types,
190 values, Parameters, or Mixed types.
192 Extraneous members of lists must be of the same type as the
193 last specified type. For example, if the expected argument
194 type is [int, bool], then [1, False] and [14, True, False,
195 True] are valid, but [1], [False, 1] and [14, True, 1] are
198 Extraneous members of dictionaries are ignored.
201 # If any of a number of types is acceptable
202 if isinstance(expected, Mixed):
203 for item in expected:
205 self.type_check(name, value, item, args)
207 except SfaInvalidArgument as fault:
211 # If an authentication structure is expected, save it and
212 # authenticate after basic type checking is done.
213 #if isinstance(expected, Auth):
218 # Get actual expected type from within the Parameter structure
219 if isinstance(expected, Parameter):
222 nullok = expected.nullok
223 expected = expected.type
229 expected_type = python_type(expected)
231 # If value can be NULL
232 if value is None and nullok:
235 # Strings are a special case. Accept either unicode or str
236 # types if a string is expected.
237 if issubclass(expected_type, StringType) and isinstance(value, StringType):
240 # Integers and long integers are also special types. Accept
241 # either int or long types if an int or long is expected.
242 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
245 elif not isinstance(value, expected_type):
246 raise SfaInvalidArgument("expected %s, got %s" % \
247 (xmlrpc_type(expected_type), xmlrpc_type(type(value))),
250 # If a minimum or maximum (length, value) has been specified
251 if issubclass(expected_type, StringType):
252 if min is not None and \
253 len(value.encode(self.api.encoding)) < min:
254 raise SfaInvalidArgument("%s must be at least %d bytes long" % (name, min))
255 if max is not None and \
256 len(value.encode(self.api.encoding)) > max:
257 raise SfaInvalidArgument("%s must be at most %d bytes long" % (name, max))
258 elif expected_type in (list, tuple, set):
259 if min is not None and len(value) < min:
260 raise SfaInvalidArgument("%s must contain at least %d items" % (name, min))
261 if max is not None and len(value) > max:
262 raise SfaInvalidArgument("%s must contain at most %d items" % (name, max))
264 if min is not None and value < min:
265 raise SfaInvalidArgument("%s must be > %s" % (name, str(min)))
266 if max is not None and value > max:
267 raise SfaInvalidArgument("%s must be < %s" % (name, str(max)))
269 # If a list with particular types of items is expected
270 if isinstance(expected, (list, tuple, set)):
271 for i in range(len(value)):
272 if i >= len(expected):
273 j = len(expected) - 1
276 self.type_check(name + "[]", value[i], expected[j], args)
278 # If a struct with particular (or required) types of items is
280 elif isinstance(expected, dict):
281 for key in value.keys():
283 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
284 for key, subparam in expected.iteritems():
285 if isinstance(subparam, Parameter) and \
286 subparam.optional is not None and \
287 not subparam.optional and key not in value.keys():
288 raise SfaInvalidArgument("'%s' not specified" % key, name)
290 #if auth is not None:
291 # auth.check(self, *args)