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.15 2006/11/02 18:32:55 mlhuang Exp $
17 from types import StringTypes
19 from PLC.Faults import *
20 from PLC.Parameter import Parameter, Mixed
21 from PLC.Auth import Auth
22 from PLC.Debug import profile, log
23 from PLC.Events import Event, Events
24 from PLC.Nodes import Node, Nodes
25 from PLC.Persons import Person, Persons
29 Base class for all PLCAPI functions. At a minimum, all PLCAPI
30 functions must define:
32 roles = [list of roles]
33 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
34 returns = Parameter(return_type, return_doc)
35 call(arg1, arg2, ...): method body
37 Argument types may be Python types (e.g., int, bool, etc.), typed
38 values (e.g., 1, True, etc.), a Parameter, or lists or
39 dictionaries of possibly mixed types, values, and/or Parameters
40 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
42 Once function decorators in Python 2.4 are fully supported,
43 consider wrapping calls with accepts() and returns() functions
44 instead of performing type checking manually.
47 # Defaults. Could implement authentication and type checking with
48 # decorators, but they are not supported in Python 2.3 and it
49 # would be hard to generate documentation without writing a code
57 def call(self, *args):
59 Method body for all PLCAPI functions. Must override.
64 def __init__(self, api):
65 self.name = self.__class__.__name__
68 # Auth may set this to a Person instance (if an anonymous
69 # method, will remain None).
72 # API may set this to a (addr, port) tuple if known
75 def __call__(self, *args, **kwds):
77 Main entry point for all PLCAPI functions. Type checks
78 arguments, authenticates, and executes call().
83 (min_args, max_args, defaults) = self.args()
85 # Check that the right number of arguments were passed in
86 if len(args) < len(min_args) or len(args) > len(max_args):
87 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
89 for name, value, expected in zip(max_args, args, self.accepts):
90 self.type_check(name, value, expected, args)
92 result = self.call(*args, **kwds)
93 runtime = time.time() - start
95 if self.api.config.PLC_API_DEBUG:
96 self.log(0, runtime, *args)
100 except PLCFault, fault:
101 # Prepend method name to expected faults
102 fault.faultString = self.name + ": " + fault.faultString
103 runtime = time.time() - start
104 self.log(fault.faultCode, runtime, *args)
107 def log(self, fault_code, runtime, *args):
112 # Do not log system or Get calls
113 if self.name.startswith('system') or self.name.startswith('Get'):
117 event = Event(self.api)
118 event['fault_code'] = fault_code
119 event['runtime'] = runtime
121 # Redact passwords and sessions
122 if args and isinstance(args[0], dict):
123 for password in 'AuthString', 'session':
124 if args[0].has_key(password):
125 auth = args[0].copy()
126 auth[password] = "Removed by API"
127 args = (auth,) + args[1:]
129 # Log call representation
130 # XXX Truncate to avoid DoS
131 event['call'] = self.name + pprint.saferepr(args)
133 # Both users and nodes can call some methods
134 if isinstance(self.caller, Person):
135 event['person_id'] = self.caller['person_id']
136 elif isinstance(self.caller, Node):
137 event['node_id'] = self.caller['node_id']
139 event.sync(commit = False)
141 # XXX object_ids is currently defined as a class variable
142 if hasattr(self, 'object_ids'):
143 for object_id in self.object_ids:
144 event.add_object(object_id, commit = False)
147 event.sync(commit = True)
149 def help(self, indent = " "):
151 Text documentation for the method.
154 (min_args, max_args, defaults) = self.args()
156 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
158 text += "Description:\n\n"
159 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
160 text += "\n".join(lines) + "\n\n"
162 text += "Allowed Roles:\n\n"
167 text += indent + ", ".join(roles) + "\n\n"
169 def param_text(name, param, indent, step):
171 Format a method parameter.
176 # Print parameter name
179 text += name.ljust(param_offset - len(indent))
181 param_offset = len(indent)
183 # Print parameter type
184 param_type = python_type(param)
185 text += xmlrpc_type(param_type) + "\n"
187 # Print parameter documentation right below type
188 if isinstance(param, Parameter):
189 wrapper = textwrap.TextWrapper(width = 70,
190 initial_indent = " " * param_offset,
191 subsequent_indent = " " * param_offset)
192 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
197 # Indent struct fields and mixed types
198 if isinstance(param, dict):
199 for name, subparam in param.iteritems():
200 text += param_text(name, subparam, indent + step, step)
201 elif isinstance(param, Mixed):
202 for subparam in param:
203 text += param_text(name, subparam, indent + step, step)
204 elif isinstance(param, (list, tuple)):
205 for subparam in param:
206 text += param_text("", subparam, indent + step, step)
210 text += "Parameters:\n\n"
211 for name, param in zip(max_args, self.accepts):
212 text += param_text(name, param, indent, indent)
214 text += "Returns:\n\n"
215 text += param_text("", self.returns, indent, indent)
223 ((arg1_name, arg2_name, ...),
224 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
225 (None, None, ..., optional1_default, optional2_default, ...))
227 That represents the minimum and maximum sets of arguments that
228 this function accepts and the defaults for the optional arguments.
231 # Inspect call. Remove self from the argument list.
232 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
233 defaults = self.call.func_defaults
237 min_args = max_args[0:len(max_args) - len(defaults)]
238 defaults = tuple([None for arg in min_args]) + defaults
240 return (min_args, max_args, defaults)
242 def type_check(self, name, value, expected, args):
244 Checks the type of the named value against the expected type,
245 which may be a Python type, a typed value, a Parameter, a
246 Mixed type, or a list or dictionary of possibly mixed types,
247 values, Parameters, or Mixed types.
249 Extraneous members of lists must be of the same type as the
250 last specified type. For example, if the expected argument
251 type is [int, bool], then [1, False] and [14, True, False,
252 True] are valid, but [1], [False, 1] and [14, True, 1] are
255 Extraneous members of dictionaries are ignored.
258 # If any of a number of types is acceptable
259 if isinstance(expected, Mixed):
260 for item in expected:
262 self.type_check(name, value, item, args)
264 except PLCInvalidArgument, fault:
266 xmlrpc_types = [xmlrpc_type(item) for item in expected]
267 raise PLCInvalidArgument("expected %s, got %s" % \
268 (" or ".join(xmlrpc_types),
269 xmlrpc_type(type(value))),
272 # If an authentication structure is expected, save it and
273 # authenticate after basic type checking is done.
274 if isinstance(expected, Auth):
279 # Get actual expected type from within the Parameter structure
280 if isinstance(expected, Parameter):
283 nullok = expected.nullok
284 expected = expected.type
290 expected_type = python_type(expected)
292 # If value can be NULL
293 if value is None and nullok:
296 # Strings are a special case. Accept either unicode or str
297 # types if a string is expected.
298 if expected_type in StringTypes and isinstance(value, StringTypes):
301 # Integers and long integers are also special types. Accept
302 # either int or long types if an int or long is expected.
303 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
306 elif not isinstance(value, expected_type):
307 raise PLCInvalidArgument("expected %s, got %s" % \
308 (xmlrpc_type(expected_type),
309 xmlrpc_type(type(value))),
312 # If a minimum or maximum (length, value) has been specified
313 if expected_type in StringTypes:
314 if min is not None and \
315 len(value.encode(self.api.encoding)) < min:
316 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
317 if max is not None and \
318 len(value.encode(self.api.encoding)) > max:
319 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
321 if min is not None and value < min:
322 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
323 if max is not None and value > max:
324 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
326 # If a list with particular types of items is expected
327 if isinstance(expected, (list, tuple)):
328 for i in range(len(value)):
329 if i >= len(expected):
330 j = len(expected) - 1
333 self.type_check(name + "[]", value[i], expected[j], args)
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], args)
341 for key, subparam in expected.iteritems():
342 if isinstance(subparam, Parameter) and \
343 subparam.optional is not None and \
344 not subparam.optional and key not in value.keys():
345 raise PLCInvalidArgument("'%s' not specified" % key, name)
348 auth.check(self, *args)
350 def python_type(arg):
352 Returns the Python type of the specified argument, which may be a
353 Python type, a typed value, or a Parameter.
356 if isinstance(arg, Parameter):
359 if isinstance(arg, type):
364 def xmlrpc_type(arg):
366 Returns the XML-RPC type of the specified argument, which may be a
367 Python type, a typed value, or a Parameter.
370 arg_type = python_type(arg)
372 if arg_type == NoneType:
374 elif arg_type == IntType or arg_type == LongType:
376 elif arg_type == bool:
378 elif arg_type == FloatType:
380 elif arg_type in StringTypes:
382 elif arg_type == ListType or arg_type == TupleType:
384 elif arg_type == DictType:
386 elif arg_type == Mixed:
387 # Not really an XML-RPC type but return "mixed" for
388 # documentation purposes.
391 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type