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.12 2006/10/25 14:26:08 mlhuang 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, args)
87 result = self.call(*args, **kwds)
88 runtime = time.time() - start
90 if self.api.config.PLC_API_DEBUG:
91 self.log(0, runtime, *args)
95 except PLCFault, fault:
96 # Prepend method name to expected faults
97 fault.faultString = self.name + ": " + fault.faultString
98 runtime = time.time() - start
99 self.log(fault.faultCode, runtime, *args)
103 def log(self, fault_code, runtime, *args):
107 # Gather necessary logging variables
108 event_type = 'Unknown'
109 object_type = 'Unknown'
112 call_name = self.name
113 call_args = ", ".join([unicode(arg) for arg in list(args)[1:]])
114 call = "%s(%s)" % (call_name, call_args)
116 if hasattr(self, 'event_type'):
117 event_type = self.event_type
118 if hasattr(self, 'object_type'):
119 object_type = self.object_type
121 person_id = self.caller['person_id']
122 if hasattr(self, 'object_ids'):
123 object_ids = self.object_ids
125 # do not log system calls
126 if call_name.startswith('system'):
128 # do not log get calls
129 if call_name.startswith('Get'):
132 sql_event = "INSERT INTO events " \
133 " (person_id, event_type, object_type, fault_code, call, runtime) VALUES" \
134 " (%(person_id)s, %(event_type)s, %(object_type)s," \
135 " %(fault_code)d, %(call)s, %(runtime)f)"
136 self.api.db.do(sql_event, locals())
138 # log objects affected
139 event_id = self.api.db.last_insert_id('events', 'event_id')
140 for object_id in object_ids:
141 sql_objects = "INSERT INTO event_object (event_id, object_id) VALUES" \
142 " (%(event_id)d, %(object_id)d) "
143 self.api.db.do(sql_objects, locals())
148 def help(self, indent = " "):
150 Text documentation for the method.
153 (min_args, max_args, defaults) = self.args()
155 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
157 text += "Description:\n\n"
158 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
159 text += "\n".join(lines) + "\n\n"
161 text += "Allowed Roles:\n\n"
166 text += indent + ", ".join(roles) + "\n\n"
168 def param_text(name, param, indent, step):
170 Format a method parameter.
175 # Print parameter name
178 text += name.ljust(param_offset - len(indent))
180 param_offset = len(indent)
182 # Print parameter type
183 param_type = python_type(param)
184 text += xmlrpc_type(param_type) + "\n"
186 # Print parameter documentation right below type
187 if isinstance(param, Parameter):
188 wrapper = textwrap.TextWrapper(width = 70,
189 initial_indent = " " * param_offset,
190 subsequent_indent = " " * param_offset)
191 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
196 # Indent struct fields and mixed types
197 if isinstance(param, dict):
198 for name, subparam in param.iteritems():
199 text += param_text(name, subparam, indent + step, step)
200 elif isinstance(param, Mixed):
201 for subparam in param:
202 text += param_text(name, subparam, indent + step, step)
203 elif isinstance(param, (list, tuple)):
204 for subparam in param:
205 text += param_text("", subparam, indent + step, step)
209 text += "Parameters:\n\n"
210 for name, param in zip(max_args, self.accepts):
211 text += param_text(name, param, indent, indent)
213 text += "Returns:\n\n"
214 text += param_text("", self.returns, indent, indent)
222 ((arg1_name, arg2_name, ...),
223 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
224 (None, None, ..., optional1_default, optional2_default, ...))
226 That represents the minimum and maximum sets of arguments that
227 this function accepts and the defaults for the optional arguments.
230 # Inspect call. Remove self from the argument list.
231 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
232 defaults = self.call.func_defaults
236 min_args = max_args[0:len(max_args) - len(defaults)]
237 defaults = tuple([None for arg in min_args]) + defaults
239 return (min_args, max_args, defaults)
241 def type_check(self, name, value, expected, args):
243 Checks the type of the named value against the expected type,
244 which may be a Python type, a typed value, a Parameter, a
245 Mixed type, or a list or dictionary of possibly mixed types,
246 values, Parameters, or Mixed types.
248 Extraneous members of lists must be of the same type as the
249 last specified type. For example, if the expected argument
250 type is [int, bool], then [1, False] and [14, True, False,
251 True] are valid, but [1], [False, 1] and [14, True, 1] are
254 Extraneous members of dictionaries are ignored.
257 # If any of a number of types is acceptable
258 if isinstance(expected, Mixed):
259 for item in expected:
261 self.type_check(name, value, item, args)
263 except PLCInvalidArgument, fault:
265 xmlrpc_types = [xmlrpc_type(item) for item in expected]
266 raise PLCInvalidArgument("expected %s, got %s" % \
267 (" or ".join(xmlrpc_types),
268 xmlrpc_type(type(value))),
271 # If an authentication structure is expected, save it and
272 # authenticate after basic type checking is done.
273 if isinstance(expected, Auth):
278 # Get actual expected type from within the Parameter structure
279 if isinstance(expected, Parameter):
282 expected = expected.type
287 expected_type = python_type(expected)
289 # Strings are a special case. Accept either unicode or str
290 # types if a string is expected.
291 if expected_type in StringTypes and isinstance(value, StringTypes):
294 # Integers and long integers are also special types. Accept
295 # either int or long types if an int or long is expected.
296 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
299 elif not isinstance(value, expected_type):
300 raise PLCInvalidArgument("expected %s, got %s" % \
301 (xmlrpc_type(expected_type),
302 xmlrpc_type(type(value))),
305 # If a minimum or maximum (length, value) has been specified
306 if expected_type in StringTypes:
307 if min is not None and \
308 len(value.encode(self.api.encoding)) < min:
309 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
310 if max is not None and \
311 len(value.encode(self.api.encoding)) > max:
312 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
314 if min is not None and value < min:
315 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
316 if max is not None and value > max:
317 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
319 # If a list with particular types of items is expected
320 if isinstance(expected, (list, tuple)):
321 for i in range(len(value)):
322 if i >= len(expected):
323 j = len(expected) - 1
326 self.type_check(name + "[]", value[i], expected[j], args)
328 # If a struct with particular (or required) types of items is
330 elif isinstance(expected, dict):
331 for key in value.keys():
333 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
334 for key, subparam in expected.iteritems():
335 if isinstance(subparam, Parameter) and \
336 subparam.optional is not None and \
337 not subparam.optional and key not in value.keys():
338 raise PLCInvalidArgument("'%s' not specified" % key, name)
341 auth.check(self, *args)
343 def python_type(arg):
345 Returns the Python type of the specified argument, which may be a
346 Python type, a typed value, or a Parameter.
349 if isinstance(arg, Parameter):
352 if isinstance(arg, type):
357 def xmlrpc_type(arg):
359 Returns the XML-RPC type of the specified argument, which may be a
360 Python type, a typed value, or a Parameter.
363 arg_type = python_type(arg)
365 if arg_type == NoneType:
367 elif arg_type == IntType or arg_type == LongType:
369 elif arg_type == bool:
371 elif arg_type == FloatType:
373 elif arg_type in StringTypes:
375 elif arg_type == ListType or arg_type == TupleType:
377 elif arg_type == DictType:
379 elif arg_type == Mixed:
380 # Not really an XML-RPC type but return "mixed" for
381 # documentation purposes.
384 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type