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.17 2006/11/03 23:44:51 mlhuang Exp $
17 from types import StringTypes
19 from PLC.Faults import *
20 from PLC.Parameter import Parameter, Mixed, python_type, xmlrpc_type
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, set)):
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:
268 # If an authentication structure is expected, save it and
269 # authenticate after basic type checking is done.
270 if isinstance(expected, Auth):
275 # Get actual expected type from within the Parameter structure
276 if isinstance(expected, Parameter):
279 nullok = expected.nullok
280 expected = expected.type
286 expected_type = python_type(expected)
288 # If value can be NULL
289 if value is None and nullok:
292 # Strings are a special case. Accept either unicode or str
293 # types if a string is expected.
294 if expected_type in StringTypes and isinstance(value, StringTypes):
297 # Integers and long integers are also special types. Accept
298 # either int or long types if an int or long is expected.
299 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
302 elif not isinstance(value, expected_type):
303 raise PLCInvalidArgument("expected %s, got %s" % \
304 (xmlrpc_type(expected_type),
305 xmlrpc_type(type(value))),
308 # If a minimum or maximum (length, value) has been specified
309 if expected_type in StringTypes:
310 if min is not None and \
311 len(value.encode(self.api.encoding)) < min:
312 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
313 if max is not None and \
314 len(value.encode(self.api.encoding)) > max:
315 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
316 elif expected_type in (list, tuple, set):
317 if min is not None and len(value) < min:
318 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
319 if max is not None and len(value) > max:
320 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
322 if min is not None and value < min:
323 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
324 if max is not None and value > max:
325 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
327 # If a list with particular types of items is expected
328 if isinstance(expected, (list, tuple, set)):
329 for i in range(len(value)):
330 if i >= len(expected):
331 j = len(expected) - 1
334 self.type_check(name + "[]", value[i], expected[j], args)
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], args)
342 for key, subparam in expected.iteritems():
343 if isinstance(subparam, Parameter) and \
344 subparam.optional is not None and \
345 not subparam.optional and key not in value.keys():
346 raise PLCInvalidArgument("'%s' not specified" % key, name)
349 auth.check(self, *args)