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.14 2006/10/31 21:47:21 mlhuang Exp $
17 from PLC.Faults import *
18 from PLC.Parameter import Parameter, Mixed
19 from PLC.Auth import Auth
20 from PLC.Debug import profile, log
21 from PLC.Events import Event, Events
22 from PLC.Nodes import Node, Nodes
23 from PLC.Persons import Person, Persons
27 Base class for all PLCAPI functions. At a minimum, all PLCAPI
28 functions must define:
30 roles = [list of roles]
31 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
32 returns = Parameter(return_type, return_doc)
33 call(arg1, arg2, ...): method body
35 Argument types may be Python types (e.g., int, bool, etc.), typed
36 values (e.g., 1, True, etc.), a Parameter, or lists or
37 dictionaries of possibly mixed types, values, and/or Parameters
38 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
40 Once function decorators in Python 2.4 are fully supported,
41 consider wrapping calls with accepts() and returns() functions
42 instead of performing type checking manually.
45 # Defaults. Could implement authentication and type checking with
46 # decorators, but they are not supported in Python 2.3 and it
47 # would be hard to generate documentation without writing a code
55 def call(self, *args):
57 Method body for all PLCAPI functions. Must override.
62 def __init__(self, api):
63 self.name = self.__class__.__name__
66 # Auth may set this to a Person instance (if an anonymous
67 # method, will remain None).
70 # API may set this to a (addr, port) tuple if known
73 def __call__(self, *args, **kwds):
75 Main entry point for all PLCAPI functions. Type checks
76 arguments, authenticates, and executes call().
81 (min_args, max_args, defaults) = self.args()
83 # Check that the right number of arguments were passed in
84 if len(args) < len(min_args) or len(args) > len(max_args):
85 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
87 for name, value, expected in zip(max_args, args, self.accepts):
88 self.type_check(name, value, expected, args)
90 result = self.call(*args, **kwds)
91 runtime = time.time() - start
93 if self.api.config.PLC_API_DEBUG:
94 self.log(0, runtime, *args)
98 except PLCFault, fault:
99 # Prepend method name to expected faults
100 fault.faultString = self.name + ": " + fault.faultString
101 runtime = time.time() - start
102 self.log(fault.faultCode, runtime, *args)
105 def log(self, fault_code, runtime, *args):
110 # Do not log system or Get calls
111 if self.name.startswith('system') or self.name.startswith('Get'):
115 event = Event(self.api)
116 event['fault_code'] = fault_code
117 event['runtime'] = runtime
119 # Redact passwords and sessions
120 if args and isinstance(args[0], dict):
121 for password in 'AuthString', 'session':
122 if args[0].has_key(password):
123 auth = args[0].copy()
124 auth[password] = "Removed by API"
125 args = (auth,) + args[1:]
127 # Log call representation
128 # XXX Truncate to avoid DoS
129 event['call'] = self.name + pprint.saferepr(args)
131 # Both users and nodes can call some methods
132 if isinstance(self.caller, Person):
133 event['person_id'] = self.caller['person_id']
134 elif isinstance(self.caller, Node):
135 event['node_id'] = self.caller['node_id']
137 event.sync(commit = False)
139 # XXX object_ids is currently defined as a class variable
140 if hasattr(self, 'object_ids'):
141 for object_id in self.object_ids:
142 event.add_object(object_id, commit = False)
145 event.sync(commit = True)
147 def help(self, indent = " "):
149 Text documentation for the method.
152 (min_args, max_args, defaults) = self.args()
154 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
156 text += "Description:\n\n"
157 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
158 text += "\n".join(lines) + "\n\n"
160 text += "Allowed Roles:\n\n"
165 text += indent + ", ".join(roles) + "\n\n"
167 def param_text(name, param, indent, step):
169 Format a method parameter.
174 # Print parameter name
177 text += name.ljust(param_offset - len(indent))
179 param_offset = len(indent)
181 # Print parameter type
182 param_type = python_type(param)
183 text += xmlrpc_type(param_type) + "\n"
185 # Print parameter documentation right below type
186 if isinstance(param, Parameter):
187 wrapper = textwrap.TextWrapper(width = 70,
188 initial_indent = " " * param_offset,
189 subsequent_indent = " " * param_offset)
190 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
195 # Indent struct fields and mixed types
196 if isinstance(param, dict):
197 for name, subparam in param.iteritems():
198 text += param_text(name, subparam, indent + step, step)
199 elif isinstance(param, Mixed):
200 for subparam in param:
201 text += param_text(name, subparam, indent + step, step)
202 elif isinstance(param, (list, tuple)):
203 for subparam in param:
204 text += param_text("", subparam, indent + step, step)
208 text += "Parameters:\n\n"
209 for name, param in zip(max_args, self.accepts):
210 text += param_text(name, param, indent, indent)
212 text += "Returns:\n\n"
213 text += param_text("", self.returns, indent, indent)
221 ((arg1_name, arg2_name, ...),
222 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
223 (None, None, ..., optional1_default, optional2_default, ...))
225 That represents the minimum and maximum sets of arguments that
226 this function accepts and the defaults for the optional arguments.
229 # Inspect call. Remove self from the argument list.
230 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
231 defaults = self.call.func_defaults
235 min_args = max_args[0:len(max_args) - len(defaults)]
236 defaults = tuple([None for arg in min_args]) + defaults
238 return (min_args, max_args, defaults)
240 def type_check(self, name, value, expected, args):
242 Checks the type of the named value against the expected type,
243 which may be a Python type, a typed value, a Parameter, a
244 Mixed type, or a list or dictionary of possibly mixed types,
245 values, Parameters, or Mixed types.
247 Extraneous members of lists must be of the same type as the
248 last specified type. For example, if the expected argument
249 type is [int, bool], then [1, False] and [14, True, False,
250 True] are valid, but [1], [False, 1] and [14, True, 1] are
253 Extraneous members of dictionaries are ignored.
256 # If any of a number of types is acceptable
257 if isinstance(expected, Mixed):
258 for item in expected:
260 self.type_check(name, value, item, args)
262 except PLCInvalidArgument, fault:
264 xmlrpc_types = [xmlrpc_type(item) for item in expected]
265 raise PLCInvalidArgument("expected %s, got %s" % \
266 (" or ".join(xmlrpc_types),
267 xmlrpc_type(type(value))),
270 # If an authentication structure is expected, save it and
271 # authenticate after basic type checking is done.
272 if isinstance(expected, Auth):
277 # Get actual expected type from within the Parameter structure
278 if isinstance(expected, Parameter):
281 nullok = expected.nullok
282 expected = expected.type
288 expected_type = python_type(expected)
290 # If value can be NULL
291 if value is None and nullok:
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 j = len(expected) - 1
331 self.type_check(name + "[]", value[i], expected[j], args)
333 # If a struct with particular (or required) types of items is
335 elif isinstance(expected, dict):
336 for key in value.keys():
338 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
339 for key, subparam in expected.iteritems():
340 if isinstance(subparam, Parameter) and \
341 subparam.optional is not None and \
342 not subparam.optional and key not in value.keys():
343 raise PLCInvalidArgument("'%s' not specified" % key, name)
346 auth.check(self, *args)
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