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.7 2006/10/18 19:42:46 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 print self.api.db.last_insert_id('events', 'event_id')
157 # log objects affected
158 for object_id in object_ids:
159 event_id = self.api.db.last_insert_id('events', 'event_id')
160 sql_objects = "INSERT INTO event_object (event_id, object_id) VALUES" \
161 " (%(event_id)d, %(object_id)d) " % (locals())
162 self.api.db.do(sql_objects)
167 def help(self, indent = " "):
169 Text documentation for the method.
172 (min_args, max_args, defaults) = self.args()
174 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
176 text += "Description:\n\n"
177 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
178 text += "\n".join(lines) + "\n\n"
180 text += "Allowed Roles:\n\n"
185 text += indent + ", ".join(roles) + "\n\n"
187 def param_text(name, param, indent, step):
189 Format a method parameter.
194 # Print parameter name
197 text += name.ljust(param_offset - len(indent))
199 param_offset = len(indent)
201 # Print parameter type
202 param_type = python_type(param)
203 text += xmlrpc_type(param_type) + "\n"
205 # Print parameter documentation right below type
206 if isinstance(param, Parameter):
207 wrapper = textwrap.TextWrapper(width = 70,
208 initial_indent = " " * param_offset,
209 subsequent_indent = " " * param_offset)
210 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
215 # Indent struct fields and mixed types
216 if isinstance(param, dict):
217 for name, subparam in param.iteritems():
218 text += param_text(name, subparam, indent + step, step)
219 elif isinstance(param, Mixed):
220 for subparam in param:
221 text += param_text(name, subparam, indent + step, step)
222 elif isinstance(param, (list, tuple)):
223 for subparam in param:
224 text += param_text("", subparam, indent + step, step)
228 text += "Parameters:\n\n"
229 for name, param in zip(max_args, self.accepts):
230 text += param_text(name, param, indent, indent)
232 text += "Returns:\n\n"
233 text += param_text("", self.returns, indent, indent)
241 ((arg1_name, arg2_name, ...),
242 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
243 (None, None, ..., optional1_default, optional2_default, ...))
245 That represents the minimum and maximum sets of arguments that
246 this function accepts and the defaults for the optional arguments.
249 # Inspect call. Remove self from the argument list.
250 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
251 defaults = self.call.func_defaults
255 min_args = max_args[0:len(max_args) - len(defaults)]
256 defaults = tuple([None for arg in min_args]) + defaults
258 return (min_args, max_args, defaults)
260 def type_check(self, name, value, expected, min = None, max = None):
262 Checks the type of the named value against the expected type,
263 which may be a Python type, a typed value, a Parameter, a
264 Mixed type, or a list or dictionary of possibly mixed types,
265 values, Parameters, or Mixed types.
267 Extraneous members of lists must be of the same type as the
268 last specified type. For example, if the expected argument
269 type is [int, bool], then [1, False] and [14, True, False,
270 True] are valid, but [1], [False, 1] and [14, True, 1] are
273 Extraneous members of dictionaries are ignored.
276 # If any of a number of types is acceptable
277 if isinstance(expected, Mixed):
278 for item in expected:
280 self.type_check(name, value, item)
283 except PLCInvalidArgument, fault:
286 xmlrpc_types = [xmlrpc_type(item) for item in expected]
287 raise PLCInvalidArgument("expected %s, got %s" % \
288 (" or ".join(xmlrpc_types),
289 xmlrpc_type(type(value))),
292 # Get actual expected type from within the Parameter structure
293 elif isinstance(expected, Parameter):
296 expected = expected.type
298 expected_type = python_type(expected)
300 # Strings are a special case. Accept either unicode or str
301 # types if a string is expected.
302 if expected_type in StringTypes and isinstance(value, StringTypes):
305 # Integers and long integers are also special types. Accept
306 # either int or long types if an int or long is expected.
307 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
310 elif not isinstance(value, expected_type):
311 raise PLCInvalidArgument("expected %s, got %s" % \
312 (xmlrpc_type(expected_type),
313 xmlrpc_type(type(value))),
316 # If a minimum or maximum (length, value) has been specified
317 if expected_type in StringTypes:
318 if min is not None and \
319 len(value.encode(self.api.encoding)) < min:
320 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
321 if max is not None and \
322 len(value.encode(self.api.encoding)) > max:
323 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
325 if min is not None and value < min:
326 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
327 if max is not None and value > max:
328 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
330 # If a list with particular types of items is expected
331 if isinstance(expected, (list, tuple)):
332 for i in range(len(value)):
333 if i >= len(expected):
334 i = len(expected) - 1
335 self.type_check(name + "[]", value[i], expected[i])
337 # If a struct with particular (or required) types of items is
339 elif isinstance(expected, dict):
340 for key in value.keys():
342 self.type_check(name + "['%s']" % key, value[key], expected[key])
343 for key, subparam in expected.iteritems():
344 if isinstance(subparam, Parameter) and \
345 not subparam.optional and key not in value.keys():
346 raise PLCInvalidArgument("'%s' not specified" % key, name)
348 def python_type(arg):
350 Returns the Python type of the specified argument, which may be a
351 Python type, a typed value, or a Parameter.
354 if isinstance(arg, Parameter):
357 if isinstance(arg, type):
362 def xmlrpc_type(arg):
364 Returns the XML-RPC type of the specified argument, which may be a
365 Python type, a typed value, or a Parameter.
368 arg_type = python_type(arg)
370 if arg_type == NoneType:
372 elif arg_type == IntType or arg_type == LongType:
374 elif arg_type == bool:
376 elif arg_type == FloatType:
378 elif arg_type in StringTypes:
380 elif arg_type == ListType or arg_type == TupleType:
382 elif arg_type == DictType:
384 elif arg_type == Mixed:
385 # Not really an XML-RPC type but return "mixed" for
386 # documentation purposes.
389 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type