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.10 2006/10/19 19:32:48 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, **kwds):
72 Main entry point for all PLCAPI functions. Type checks
73 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 result = self.call(*args, **kwds)
106 runtime = time.time() - start
108 if self.api.config.PLC_API_DEBUG:
109 self.log(0, runtime, *args)
113 except PLCFault, fault:
114 # Prepend method name to expected faults
115 fault.faultString = self.name + ": " + fault.faultString
116 runtime = time.time() - start
117 self.log(fault.faultCode, runtime, *args)
121 def log(self, fault_code, runtime, *args):
125 # Gather necessary logging variables
126 event_type = 'Unknown'
127 object_type = 'Unknown'
130 call_name = self.name
131 call_args = ", ".join([unicode(arg) for arg in list(args)[1:]])
132 call = "%s(%s)" % (call_name, call_args)
134 if hasattr(self, 'event_type'):
135 event_type = self.event_type
136 if hasattr(self, 'object_type'):
137 object_type = self.object_type
139 person_id = self.caller['person_id']
140 if hasattr(self, 'object_ids'):
141 object_ids = self.object_ids
143 # do not log system calls
144 if call_name.startswith('system'):
146 # do not log get calls
147 if call_name.startswith('Get'):
150 sql_event = "INSERT INTO events " \
151 " (person_id, event_type, object_type, fault_code, call, runtime) VALUES" \
152 " (%(person_id)s, %(event_type)s, %(object_type)s," \
153 " %(fault_code)d, %(call)s, %(runtime)f)"
154 self.api.db.do(sql_event, locals())
156 # log objects affected
157 event_id = self.api.db.last_insert_id('events', 'event_id')
158 for object_id in object_ids:
159 sql_objects = "INSERT INTO event_object (event_id, object_id) VALUES" \
160 " (%(event_id)d, %(object_id)d) "
161 self.api.db.do(sql_objects, locals())
166 def help(self, indent = " "):
168 Text documentation for the method.
171 (min_args, max_args, defaults) = self.args()
173 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
175 text += "Description:\n\n"
176 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
177 text += "\n".join(lines) + "\n\n"
179 text += "Allowed Roles:\n\n"
184 text += indent + ", ".join(roles) + "\n\n"
186 def param_text(name, param, indent, step):
188 Format a method parameter.
193 # Print parameter name
196 text += name.ljust(param_offset - len(indent))
198 param_offset = len(indent)
200 # Print parameter type
201 param_type = python_type(param)
202 text += xmlrpc_type(param_type) + "\n"
204 # Print parameter documentation right below type
205 if isinstance(param, Parameter):
206 wrapper = textwrap.TextWrapper(width = 70,
207 initial_indent = " " * param_offset,
208 subsequent_indent = " " * param_offset)
209 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
214 # Indent struct fields and mixed types
215 if isinstance(param, dict):
216 for name, subparam in param.iteritems():
217 text += param_text(name, subparam, indent + step, step)
218 elif isinstance(param, Mixed):
219 for subparam in param:
220 text += param_text(name, subparam, indent + step, step)
221 elif isinstance(param, (list, tuple)):
222 for subparam in param:
223 text += param_text("", subparam, indent + step, step)
227 text += "Parameters:\n\n"
228 for name, param in zip(max_args, self.accepts):
229 text += param_text(name, param, indent, indent)
231 text += "Returns:\n\n"
232 text += param_text("", self.returns, indent, indent)
240 ((arg1_name, arg2_name, ...),
241 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
242 (None, None, ..., optional1_default, optional2_default, ...))
244 That represents the minimum and maximum sets of arguments that
245 this function accepts and the defaults for the optional arguments.
248 # Inspect call. Remove self from the argument list.
249 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
250 defaults = self.call.func_defaults
254 min_args = max_args[0:len(max_args) - len(defaults)]
255 defaults = tuple([None for arg in min_args]) + defaults
257 return (min_args, max_args, defaults)
259 def type_check(self, name, value, expected, min = None, max = None):
261 Checks the type of the named value against the expected type,
262 which may be a Python type, a typed value, a Parameter, a
263 Mixed type, or a list or dictionary of possibly mixed types,
264 values, Parameters, or Mixed types.
266 Extraneous members of lists must be of the same type as the
267 last specified type. For example, if the expected argument
268 type is [int, bool], then [1, False] and [14, True, False,
269 True] are valid, but [1], [False, 1] and [14, True, 1] are
272 Extraneous members of dictionaries are ignored.
275 # If any of a number of types is acceptable
276 if isinstance(expected, Mixed):
277 for item in expected:
279 self.type_check(name, value, item)
282 except PLCInvalidArgument, fault:
285 xmlrpc_types = [xmlrpc_type(item) for item in expected]
286 raise PLCInvalidArgument("expected %s, got %s" % \
287 (" or ".join(xmlrpc_types),
288 xmlrpc_type(type(value))),
291 # Get actual expected type from within the Parameter structure
292 elif isinstance(expected, Parameter):
295 expected = expected.type
297 expected_type = python_type(expected)
299 # Strings are a special case. Accept either unicode or str
300 # types if a string is expected.
301 if expected_type in StringTypes and isinstance(value, StringTypes):
304 # Integers and long integers are also special types. Accept
305 # either int or long types if an int or long is expected.
306 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
309 elif not isinstance(value, expected_type):
310 raise PLCInvalidArgument("expected %s, got %s" % \
311 (xmlrpc_type(expected_type),
312 xmlrpc_type(type(value))),
315 # If a minimum or maximum (length, value) has been specified
316 if expected_type in StringTypes:
317 if min is not None and \
318 len(value.encode(self.api.encoding)) < min:
319 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
320 if max is not None and \
321 len(value.encode(self.api.encoding)) > max:
322 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
324 if min is not None and value < min:
325 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
326 if max is not None and value > max:
327 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
329 # If a list with particular types of items is expected
330 if isinstance(expected, (list, tuple)):
331 for i in range(len(value)):
332 if i >= len(expected):
333 i = len(expected) - 1
334 self.type_check(name + "[]", value[i], expected[i])
336 # If a struct with particular (or required) types of items is
338 elif isinstance(expected, dict):
339 for key in value.keys():
341 self.type_check(name + "['%s']" % key, value[key], expected[key])
342 for key, subparam in expected.iteritems():
343 if isinstance(subparam, Parameter) and \
344 not subparam.optional and key not in value.keys():
345 raise PLCInvalidArgument("'%s' not specified" % key, name)
347 def python_type(arg):
349 Returns the Python type of the specified argument, which may be a
350 Python type, a typed value, or a Parameter.
353 if isinstance(arg, Parameter):
356 if isinstance(arg, type):
361 def xmlrpc_type(arg):
363 Returns the XML-RPC type of the specified argument, which may be a
364 Python type, a typed value, or a Parameter.
367 arg_type = python_type(arg)
369 if arg_type == NoneType:
371 elif arg_type == IntType or arg_type == LongType:
373 elif arg_type == bool:
375 elif arg_type == FloatType:
377 elif arg_type in StringTypes:
379 elif arg_type == ListType or arg_type == TupleType:
381 elif arg_type == DictType:
383 elif arg_type == Mixed:
384 # Not really an XML-RPC type but return "mixed" for
385 # documentation purposes.
388 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type