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.5 2006/10/16 20:41:02 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 result = self.call(*args)
106 if self.api.config.PLC_API_DEBUG:
111 except PLCFault, fault:
112 # Prepend method name to expected faults
113 fault.faultString = self.name + ": " + fault.faultString
114 self.log(fault.faultCode, *args)
118 def log(self, fault_code, *args):
122 # Gather necessary logging variables
123 event_type = 'Unknown'
124 object_type = 'Unknown'
127 call_name = self.name
128 call_args = ", ".join([unicode(arg) for arg in list(args)[1:]]).replace('\'', '\\\'')
129 call = "%s(%s)" % (call_name, call_args)
131 if hasattr(self, 'event_type'):
132 event_type = self.event_type
133 if hasattr(self, 'object_type'):
134 object_type = self.object_type
136 person_id = self.caller['person_id']
137 if hasattr(self, 'object_ids'):
138 object_ids = self.object_ids
140 # make sure this is an api call
141 if call_name in ['system.listMethods', 'system.methodHelp', 'system.multicall', 'system.methodSignature']:
145 # XX get real event id
147 rows = self.api.db.selectall("SELECT nextval('events_event_id_seq')", hashref = False)
148 event_id = rows[0][0]
150 sql_event = "INSERT INTO events " \
151 " (event_id, person_id, event_type, object_type, fault_code, call) VALUES" \
152 " (%(event_id)d, %(person_id)d, '%(event_type)s', '%(object_type)s', %(fault_code)d, '%(call)s')" % \
154 self.api.db.do(sql_event)
156 # log objects affected
157 for object_id in object_ids:
158 sql_objects = "INSERT INTO event_objects (event_id, object_id) VALUES" \
159 " (%(event_id)d, %(object_id)d) " % (locals())
160 self.api.db.do(sql_objects)
165 def help(self, indent = " "):
167 Text documentation for the method.
170 (min_args, max_args, defaults) = self.args()
172 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
174 text += "Description:\n\n"
175 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
176 text += "\n".join(lines) + "\n\n"
178 text += "Allowed Roles:\n\n"
183 text += indent + ", ".join(roles) + "\n\n"
185 def param_text(name, param, indent, step):
187 Format a method parameter.
192 # Print parameter name
195 text += name.ljust(param_offset - len(indent))
197 param_offset = len(indent)
199 # Print parameter type
200 param_type = python_type(param)
201 text += xmlrpc_type(param_type) + "\n"
203 # Print parameter documentation right below type
204 if isinstance(param, Parameter):
205 wrapper = textwrap.TextWrapper(width = 70,
206 initial_indent = " " * param_offset,
207 subsequent_indent = " " * param_offset)
208 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
213 # Indent struct fields and mixed types
214 if isinstance(param, dict):
215 for name, subparam in param.iteritems():
216 text += param_text(name, subparam, indent + step, step)
217 elif isinstance(param, Mixed):
218 for subparam in param:
219 text += param_text(name, subparam, indent + step, step)
220 elif isinstance(param, (list, tuple)):
221 for subparam in param:
222 text += param_text("", subparam, indent + step, step)
226 text += "Parameters:\n\n"
227 for name, param in zip(max_args, self.accepts):
228 text += param_text(name, param, indent, indent)
230 text += "Returns:\n\n"
231 text += param_text("", self.returns, indent, indent)
239 ((arg1_name, arg2_name, ...),
240 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
241 (None, None, ..., optional1_default, optional2_default, ...))
243 That represents the minimum and maximum sets of arguments that
244 this function accepts and the defaults for the optional arguments.
247 # Inspect call. Remove self from the argument list.
248 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
249 defaults = self.call.func_defaults
253 min_args = max_args[0:len(max_args) - len(defaults)]
254 defaults = tuple([None for arg in min_args]) + defaults
256 return (min_args, max_args, defaults)
258 def type_check(self, name, value, expected, min = None, max = None):
260 Checks the type of the named value against the expected type,
261 which may be a Python type, a typed value, a Parameter, a
262 Mixed type, or a list or dictionary of possibly mixed types,
263 values, Parameters, or Mixed types.
265 Extraneous members of lists must be of the same type as the
266 last specified type. For example, if the expected argument
267 type is [int, bool], then [1, False] and [14, True, False,
268 True] are valid, but [1], [False, 1] and [14, True, 1] are
271 Extraneous members of dictionaries are ignored.
274 # If any of a number of types is acceptable
275 if isinstance(expected, Mixed):
276 for item in expected:
278 self.type_check(name, value, item)
281 except PLCInvalidArgument, fault:
284 xmlrpc_types = [xmlrpc_type(item) for item in expected]
285 raise PLCInvalidArgument("expected %s, got %s" % \
286 (" or ".join(xmlrpc_types),
287 xmlrpc_type(type(value))),
290 # Get actual expected type from within the Parameter structure
291 elif isinstance(expected, Parameter):
294 expected = expected.type
296 expected_type = python_type(expected)
298 # Strings are a special case. Accept either unicode or str
299 # types if a string is expected.
300 if expected_type in StringTypes and isinstance(value, StringTypes):
303 # Integers and long integers are also special types. Accept
304 # either int or long types if an int or long is expected.
305 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
308 elif not isinstance(value, expected_type):
309 raise PLCInvalidArgument("expected %s, got %s" % \
310 (xmlrpc_type(expected_type),
311 xmlrpc_type(type(value))),
314 # If a minimum or maximum (length, value) has been specified
315 if expected_type in StringTypes:
316 if min is not None and \
317 len(value.encode(self.api.encoding)) < min:
318 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
319 if max is not None and \
320 len(value.encode(self.api.encoding)) > max:
321 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
323 if min is not None and value < min:
324 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
325 if max is not None and value > max:
326 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
328 # If a list with particular types of items is expected
329 if isinstance(expected, (list, tuple)):
330 for i in range(len(value)):
331 if i >= len(expected):
332 i = len(expected) - 1
333 self.type_check(name + "[]", value[i], expected[i])
335 # If a struct with particular (or required) types of items is
337 elif isinstance(expected, dict):
338 for key in value.keys():
340 self.type_check(name + "['%s']" % key, value[key], expected[key])
341 for key, subparam in expected.iteritems():
342 if isinstance(subparam, Parameter) and \
343 not subparam.optional and key not in value.keys():
344 raise PLCInvalidArgument("'%s' not specified" % key, name)
346 def python_type(arg):
348 Returns the Python type of the specified argument, which may be a
349 Python type, a typed value, or a Parameter.
352 if isinstance(arg, Parameter):
355 if isinstance(arg, type):
360 def xmlrpc_type(arg):
362 Returns the XML-RPC type of the specified argument, which may be a
363 Python type, a typed value, or a Parameter.
366 arg_type = python_type(arg)
368 if arg_type == NoneType:
370 elif arg_type == IntType or arg_type == LongType:
372 elif arg_type == bool:
374 elif arg_type == FloatType:
376 elif arg_type in StringTypes:
378 elif arg_type == ListType or arg_type == TupleType:
380 elif arg_type == DictType:
382 elif arg_type == Mixed:
383 # Not really an XML-RPC type but return "mixed" for
384 # documentation purposes.
387 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type