2 # Base class for all PLCAPI functions
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
7 # $Id: Method.py,v 1.3 2006/10/13 15:11:31 tmack Exp $
16 from PLC.Faults import *
17 from PLC.Parameter import Parameter, Mixed
18 from PLC.Auth import Auth
19 from PLC.Debug import profile, log
23 Base class for all PLCAPI functions. At a minimum, all PLCAPI
24 functions must define:
26 roles = [list of roles]
27 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
28 returns = Parameter(return_type, return_doc)
29 call(arg1, arg2, ...): method body
31 Argument types may be Python types (e.g., int, bool, etc.), typed
32 values (e.g., 1, True, etc.), a Parameter, or lists or
33 dictionaries of possibly mixed types, values, and/or Parameters
34 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
36 Once function decorators in Python 2.4 are fully supported,
37 consider wrapping calls with accepts() and returns() functions
38 instead of performing type checking manually.
41 # Defaults. Could implement authentication and type checking with
42 # decorators, but they are not supported in Python 2.3 and it
43 # would be hard to generate documentation without writing a code
51 def call(self, *args):
53 Method body for all PLCAPI functions. Must override.
58 def __init__(self, api):
59 self.name = self.__class__.__name__
62 # Auth may set this to a Person instance (if an anonymous
63 # method, will remain None).
66 # API may set this to a (addr, port) tuple if known
68 self.__call__ = self.log(self.__call__)
71 def __call__(self, *args):
73 Main entry point for all PLCAPI functions. Type checks
74 arguments, authenticates, and executes call().
78 (min_args, max_args, defaults) = self.args()
80 # Check that the right number of arguments were passed in
81 if len(args) < len(min_args) or len(args) > len(max_args):
82 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
84 for name, value, expected in zip(max_args, args, self.accepts):
85 self.type_check(name, value, expected)
87 # The first argument to all methods that require
88 # authentication, should be an Auth structure. The rest of the
89 # arguments to the call may also be used in the authentication
90 # check. For example, calls made by the Boot Manager are
91 # verified by comparing a hash of the message parameters to
92 # the value in the authentication structure.
96 if isinstance(self.accepts[0], Auth):
97 auth = self.accepts[0]
98 elif isinstance(self.accepts[0], Mixed):
99 for auth in self.accepts[0]:
100 if isinstance(auth, Auth):
102 if isinstance(auth, Auth):
103 auth.check(self, *args)
105 return self.call(*args)
107 except PLCFault, fault:
108 # Prepend method name to expected faults
109 fault.faultString = self.name + ": " + fault.faultString
113 def log(self, callable):
119 Commit the transaction
123 if vars['call_name'] in ['listMethods', 'methodSignature']:
126 sql = "INSERT INTO events " \
127 " (person_id, event_type, object_type, fault_code, call, runtime)" \
128 " VALUES (%d, '%s', '%s', %d, '%s', %f)" % \
129 (vars['person_id'], vars['event_type'], vars['object_type'],
130 vars['fault_code'], vars['call'], vars['runtime'])
135 def wrapper(*args, **kwds):
137 # Gather necessary logging vars
140 event_type = 'Unknown'
141 object_type = 'Unknown'
142 call_name = callable.im_class.__module__.split('.')[-1:][0]
143 call_args = ", ".join([str(arg) for arg in list(args)[1:]]).replace('\'', '\\\'')
144 call = "%s(%s)" % (call_name, call_args)
146 if hasattr(self, 'event_type'):
147 event_type = self.event_type
148 if hasattr(self, 'object_type'):
149 object_type = self.object_type
151 person_id = self.caller['person_id']
156 result = callable(*args, **kwds)
157 runtime = time.time() - start
161 except PLCFault, fault:
162 fault_code = fault.faultCode
163 runtime = time.time() - start
170 def help(self, indent = " "):
172 Text documentation for the method.
175 (min_args, max_args, defaults) = self.args()
177 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
179 text += "Description:\n\n"
180 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
181 text += "\n".join(lines) + "\n\n"
183 text += "Allowed Roles:\n\n"
188 text += indent + ", ".join(roles) + "\n\n"
190 def param_text(name, param, indent, step):
192 Format a method parameter.
197 # Print parameter name
200 text += name.ljust(param_offset - len(indent))
202 param_offset = len(indent)
204 # Print parameter type
205 param_type = python_type(param)
206 text += xmlrpc_type(param_type) + "\n"
208 # Print parameter documentation right below type
209 if isinstance(param, Parameter):
210 wrapper = textwrap.TextWrapper(width = 70,
211 initial_indent = " " * param_offset,
212 subsequent_indent = " " * param_offset)
213 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
218 # Indent struct fields and mixed types
219 if isinstance(param, dict):
220 for name, subparam in param.iteritems():
221 text += param_text(name, subparam, indent + step, step)
222 elif isinstance(param, Mixed):
223 for subparam in param:
224 text += param_text(name, subparam, indent + step, step)
225 elif isinstance(param, (list, tuple)):
226 for subparam in param:
227 text += param_text("", subparam, indent + step, step)
231 text += "Parameters:\n\n"
232 for name, param in zip(max_args, self.accepts):
233 text += param_text(name, param, indent, indent)
235 text += "Returns:\n\n"
236 text += param_text("", self.returns, indent, indent)
244 ((arg1_name, arg2_name, ...),
245 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
246 (None, None, ..., optional1_default, optional2_default, ...))
248 That represents the minimum and maximum sets of arguments that
249 this function accepts and the defaults for the optional arguments.
252 # Inspect call. Remove self from the argument list.
253 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
254 defaults = self.call.func_defaults
258 min_args = max_args[0:len(max_args) - len(defaults)]
259 defaults = tuple([None for arg in min_args]) + defaults
261 return (min_args, max_args, defaults)
263 def type_check(self, name, value, expected, min = None, max = None):
265 Checks the type of the named value against the expected type,
266 which may be a Python type, a typed value, a Parameter, a
267 Mixed type, or a list or dictionary of possibly mixed types,
268 values, Parameters, or Mixed types.
270 Extraneous members of lists must be of the same type as the
271 last specified type. For example, if the expected argument
272 type is [int, bool], then [1, False] and [14, True, False,
273 True] are valid, but [1], [False, 1] and [14, True, 1] are
276 Extraneous members of dictionaries are ignored.
279 # If any of a number of types is acceptable
280 if isinstance(expected, Mixed):
281 for item in expected:
283 self.type_check(name, value, item)
286 except PLCInvalidArgument, fault:
289 xmlrpc_types = [xmlrpc_type(item) for item in expected]
290 raise PLCInvalidArgument("expected %s, got %s" % \
291 (" or ".join(xmlrpc_types),
292 xmlrpc_type(type(value))),
295 # Get actual expected type from within the Parameter structure
296 elif isinstance(expected, Parameter):
299 expected = expected.type
301 expected_type = python_type(expected)
303 # Strings are a special case. Accept either unicode or str
304 # types if a string is expected.
305 if expected_type in StringTypes and isinstance(value, StringTypes):
308 # Integers and long integers are also special types. Accept
309 # either int or long types if an int or long is expected.
310 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
313 elif not isinstance(value, expected_type):
314 raise PLCInvalidArgument("expected %s, got %s" % \
315 (xmlrpc_type(expected_type),
316 xmlrpc_type(type(value))),
319 # If a minimum or maximum (length, value) has been specified
320 if expected_type in StringTypes:
321 if min is not None and \
322 len(value.encode(self.api.encoding)) < min:
323 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
324 if max is not None and \
325 len(value.encode(self.api.encoding)) > max:
326 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
328 if min is not None and value < min:
329 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
330 if max is not None and value > max:
331 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
333 # If a list with particular types of items is expected
334 if isinstance(expected, (list, tuple)):
335 for i in range(len(value)):
336 if i >= len(expected):
337 i = len(expected) - 1
338 self.type_check(name + "[]", value[i], expected[i])
340 # If a struct with particular (or required) types of items is
342 elif isinstance(expected, dict):
343 for key in value.keys():
345 self.type_check(name + "['%s']" % key, value[key], expected[key])
346 for key, subparam in expected.iteritems():
347 if isinstance(subparam, Parameter) and \
348 not subparam.optional and key not in value.keys():
349 raise PLCInvalidArgument("'%s' not specified" % key, name)
351 def python_type(arg):
353 Returns the Python type of the specified argument, which may be a
354 Python type, a typed value, or a Parameter.
357 if isinstance(arg, Parameter):
360 if isinstance(arg, type):
365 def xmlrpc_type(arg):
367 Returns the XML-RPC type of the specified argument, which may be a
368 Python type, a typed value, or a Parameter.
371 arg_type = python_type(arg)
373 if arg_type == NoneType:
375 elif arg_type == IntType or arg_type == LongType:
377 elif arg_type == bool:
379 elif arg_type == FloatType:
381 elif arg_type in StringTypes:
383 elif arg_type == ListType or arg_type == TupleType:
385 elif arg_type == DictType:
387 elif arg_type == Mixed:
388 # Not really an XML-RPC type but return "mixed" for
389 # documentation purposes.
392 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type