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.4 2006/10/13 21:42:25 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
70 def __call__(self, *args):
72 Main entry point for all PLCAPI functions. Type checks
73 arguments, authenticates, and executes call().
77 (min_args, max_args, defaults) = self.args()
79 # Check that the right number of arguments were passed in
80 if len(args) < len(min_args) or len(args) > len(max_args):
81 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
83 for name, value, expected in zip(max_args, args, self.accepts):
84 self.type_check(name, value, expected)
86 # The first argument to all methods that require
87 # authentication, should be an Auth structure. The rest of the
88 # arguments to the call may also be used in the authentication
89 # check. For example, calls made by the Boot Manager are
90 # verified by comparing a hash of the message parameters to
91 # the value in the authentication structure.
95 if isinstance(self.accepts[0], Auth):
96 auth = self.accepts[0]
97 elif isinstance(self.accepts[0], Mixed):
98 for auth in self.accepts[0]:
99 if isinstance(auth, Auth):
101 if isinstance(auth, Auth):
102 auth.check(self, *args)
104 if self.api.config.PLC_API_DEBUG:
107 return self.call(*args)
109 except PLCFault, fault:
110 # Prepend method name to expected faults
111 fault.faultString = self.name + ": " + fault.faultString
112 self.log(fault.faultCode, *args)
116 def log(self, fault_code, *args):
120 # Gather necessary logging variables
121 event_type = 'Unknown'
122 object_type = 'Unknown'
125 call_name = self.name
126 call_args = ", ".join([unicode(arg) for arg in list(args)[1:]]).replace('\'', '\\\'')
127 call = "%s(%s)" % (call_name, call_args)
129 if hasattr(self, 'event_type'):
130 event_type = self.event_type
131 if hasattr(self, 'object_type'):
132 object_type = self.object_type
134 person_id = self.caller['person_id']
135 if hasattr(self, 'object_ids'):
136 object_ids = self.object_ids
138 # make sure this is an api call
139 if call_name in ['system.listMethods', 'system.methodHelp', 'system.multicall', 'system.methodSignature']:
142 sql_event = "INSERT INTO events " \
143 " (person_id, event_type, object_type, fault_code, call) VALUES" \
144 " (%(person_id)d, '%(event_type)s', '%(object_type)s', %(fault_code)d, '%(call)s')" % \
147 # XX get real event id
149 # log objects affected
150 # XX this probably wont work (we only know objects affected after call is made. Find another way
152 affected_id_list = zip([event_id for object in object_ids], object_ids)
153 affected_ids = ", ".join(map(str,affected_id_list))
154 sql_objects = "INSERT INTO event_objects (event_id, object_id) VALUES" \
155 " %s " % affected_ids
157 self.api.db.do(sql_event)
161 def help(self, indent = " "):
163 Text documentation for the method.
166 (min_args, max_args, defaults) = self.args()
168 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
170 text += "Description:\n\n"
171 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
172 text += "\n".join(lines) + "\n\n"
174 text += "Allowed Roles:\n\n"
179 text += indent + ", ".join(roles) + "\n\n"
181 def param_text(name, param, indent, step):
183 Format a method parameter.
188 # Print parameter name
191 text += name.ljust(param_offset - len(indent))
193 param_offset = len(indent)
195 # Print parameter type
196 param_type = python_type(param)
197 text += xmlrpc_type(param_type) + "\n"
199 # Print parameter documentation right below type
200 if isinstance(param, Parameter):
201 wrapper = textwrap.TextWrapper(width = 70,
202 initial_indent = " " * param_offset,
203 subsequent_indent = " " * param_offset)
204 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
209 # Indent struct fields and mixed types
210 if isinstance(param, dict):
211 for name, subparam in param.iteritems():
212 text += param_text(name, subparam, indent + step, step)
213 elif isinstance(param, Mixed):
214 for subparam in param:
215 text += param_text(name, subparam, indent + step, step)
216 elif isinstance(param, (list, tuple)):
217 for subparam in param:
218 text += param_text("", subparam, indent + step, step)
222 text += "Parameters:\n\n"
223 for name, param in zip(max_args, self.accepts):
224 text += param_text(name, param, indent, indent)
226 text += "Returns:\n\n"
227 text += param_text("", self.returns, indent, indent)
235 ((arg1_name, arg2_name, ...),
236 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
237 (None, None, ..., optional1_default, optional2_default, ...))
239 That represents the minimum and maximum sets of arguments that
240 this function accepts and the defaults for the optional arguments.
243 # Inspect call. Remove self from the argument list.
244 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
245 defaults = self.call.func_defaults
249 min_args = max_args[0:len(max_args) - len(defaults)]
250 defaults = tuple([None for arg in min_args]) + defaults
252 return (min_args, max_args, defaults)
254 def type_check(self, name, value, expected, min = None, max = None):
256 Checks the type of the named value against the expected type,
257 which may be a Python type, a typed value, a Parameter, a
258 Mixed type, or a list or dictionary of possibly mixed types,
259 values, Parameters, or Mixed types.
261 Extraneous members of lists must be of the same type as the
262 last specified type. For example, if the expected argument
263 type is [int, bool], then [1, False] and [14, True, False,
264 True] are valid, but [1], [False, 1] and [14, True, 1] are
267 Extraneous members of dictionaries are ignored.
270 # If any of a number of types is acceptable
271 if isinstance(expected, Mixed):
272 for item in expected:
274 self.type_check(name, value, item)
277 except PLCInvalidArgument, fault:
280 xmlrpc_types = [xmlrpc_type(item) for item in expected]
281 raise PLCInvalidArgument("expected %s, got %s" % \
282 (" or ".join(xmlrpc_types),
283 xmlrpc_type(type(value))),
286 # Get actual expected type from within the Parameter structure
287 elif isinstance(expected, Parameter):
290 expected = expected.type
292 expected_type = python_type(expected)
294 # Strings are a special case. Accept either unicode or str
295 # types if a string is expected.
296 if expected_type in StringTypes and isinstance(value, StringTypes):
299 # Integers and long integers are also special types. Accept
300 # either int or long types if an int or long is expected.
301 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
304 elif not isinstance(value, expected_type):
305 raise PLCInvalidArgument("expected %s, got %s" % \
306 (xmlrpc_type(expected_type),
307 xmlrpc_type(type(value))),
310 # If a minimum or maximum (length, value) has been specified
311 if expected_type in StringTypes:
312 if min is not None and \
313 len(value.encode(self.api.encoding)) < min:
314 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
315 if max is not None and \
316 len(value.encode(self.api.encoding)) > max:
317 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
319 if min is not None and value < min:
320 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
321 if max is not None and value > max:
322 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
324 # If a list with particular types of items is expected
325 if isinstance(expected, (list, tuple)):
326 for i in range(len(value)):
327 if i >= len(expected):
328 i = len(expected) - 1
329 self.type_check(name + "[]", value[i], expected[i])
331 # If a struct with particular (or required) types of items is
333 elif isinstance(expected, dict):
334 for key in value.keys():
336 self.type_check(name + "['%s']" % key, value[key], expected[key])
337 for key, subparam in expected.iteritems():
338 if isinstance(subparam, Parameter) and \
339 not subparam.optional and key not in value.keys():
340 raise PLCInvalidArgument("'%s' not specified" % key, name)
342 def python_type(arg):
344 Returns the Python type of the specified argument, which may be a
345 Python type, a typed value, or a Parameter.
348 if isinstance(arg, Parameter):
351 if isinstance(arg, type):
356 def xmlrpc_type(arg):
358 Returns the XML-RPC type of the specified argument, which may be a
359 Python type, a typed value, or a Parameter.
362 arg_type = python_type(arg)
364 if arg_type == NoneType:
366 elif arg_type == IntType or arg_type == LongType:
368 elif arg_type == bool:
370 elif arg_type == FloatType:
372 elif arg_type in StringTypes:
374 elif arg_type == ListType or arg_type == TupleType:
376 elif arg_type == DictType:
378 elif arg_type == Mixed:
379 # Not really an XML-RPC type but return "mixed" for
380 # documentation purposes.
383 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type