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.21 2007/01/16 17:04:08 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 or hasattr(self, 'message'):
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)
132 event['call_name'] = self.name
134 # Both users and nodes can call some methods
135 if isinstance(self.caller, Person):
136 event['person_id'] = self.caller['person_id']
137 elif isinstance(self.caller, Node):
138 event['node_id'] = self.caller['node_id']
140 event.sync(commit = False)
142 if hasattr(self, 'object_ids'):
143 for object_id in self.object_ids:
144 event.add_object(object_id, commit = False)
146 # Set the message for this event
147 if hasattr(self, 'message'):
148 event['message'] = self.message
150 if hasattr(self, 'object_type'):
151 event['object_type'] = self.object_type
156 def help(self, indent = " "):
158 Text documentation for the method.
161 (min_args, max_args, defaults) = self.args()
163 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
165 text += "Description:\n\n"
166 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
167 text += "\n".join(lines) + "\n\n"
169 text += "Allowed Roles:\n\n"
174 text += indent + ", ".join(roles) + "\n\n"
176 def param_text(name, param, indent, step):
178 Format a method parameter.
183 # Print parameter name
186 text += name.ljust(param_offset - len(indent))
188 param_offset = len(indent)
190 # Print parameter type
191 param_type = python_type(param)
192 text += xmlrpc_type(param_type) + "\n"
194 # Print parameter documentation right below type
195 if isinstance(param, Parameter):
196 wrapper = textwrap.TextWrapper(width = 70,
197 initial_indent = " " * param_offset,
198 subsequent_indent = " " * param_offset)
199 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
204 # Indent struct fields and mixed types
205 if isinstance(param, dict):
206 for name, subparam in param.iteritems():
207 text += param_text(name, subparam, indent + step, step)
208 elif isinstance(param, Mixed):
209 for subparam in param:
210 text += param_text(name, subparam, indent + step, step)
211 elif isinstance(param, (list, tuple, set)):
212 for subparam in param:
213 text += param_text("", subparam, indent + step, step)
217 text += "Parameters:\n\n"
218 for name, param in zip(max_args, self.accepts):
219 text += param_text(name, param, indent, indent)
221 text += "Returns:\n\n"
222 text += param_text("", self.returns, indent, indent)
230 ((arg1_name, arg2_name, ...),
231 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
232 (None, None, ..., optional1_default, optional2_default, ...))
234 That represents the minimum and maximum sets of arguments that
235 this function accepts and the defaults for the optional arguments.
238 # Inspect call. Remove self from the argument list.
239 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
240 defaults = self.call.func_defaults
244 min_args = max_args[0:len(max_args) - len(defaults)]
245 defaults = tuple([None for arg in min_args]) + defaults
247 return (min_args, max_args, defaults)
249 def type_check(self, name, value, expected, args):
251 Checks the type of the named value against the expected type,
252 which may be a Python type, a typed value, a Parameter, a
253 Mixed type, or a list or dictionary of possibly mixed types,
254 values, Parameters, or Mixed types.
256 Extraneous members of lists must be of the same type as the
257 last specified type. For example, if the expected argument
258 type is [int, bool], then [1, False] and [14, True, False,
259 True] are valid, but [1], [False, 1] and [14, True, 1] are
262 Extraneous members of dictionaries are ignored.
265 # If any of a number of types is acceptable
266 if isinstance(expected, Mixed):
267 for item in expected:
269 self.type_check(name, value, item, args)
271 except PLCInvalidArgument, fault:
275 # If an authentication structure is expected, save it and
276 # authenticate after basic type checking is done.
277 if isinstance(expected, Auth):
282 # Get actual expected type from within the Parameter structure
283 if isinstance(expected, Parameter):
286 nullok = expected.nullok
287 expected = expected.type
293 expected_type = python_type(expected)
295 # If value can be NULL
296 if value is None and nullok:
299 # Strings are a special case. Accept either unicode or str
300 # types if a string is expected.
301 if expected_type in StringTypes and isinstance(value, StringTypes):
304 # Integers and long integers are also special types. Accept
305 # either int or long types if an int or long is expected.
306 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
309 elif not isinstance(value, expected_type):
310 raise PLCInvalidArgument("expected %s, got %s" % \
311 (xmlrpc_type(expected_type),
312 xmlrpc_type(type(value))),
315 # If a minimum or maximum (length, value) has been specified
316 if expected_type in StringTypes:
317 if min is not None and \
318 len(value.encode(self.api.encoding)) < min:
319 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
320 if max is not None and \
321 len(value.encode(self.api.encoding)) > max:
322 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
323 elif expected_type in (list, tuple, set):
324 if min is not None and len(value) < min:
325 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
326 if max is not None and len(value) > max:
327 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
329 if min is not None and value < min:
330 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
331 if max is not None and value > max:
332 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
334 # If a list with particular types of items is expected
335 if isinstance(expected, (list, tuple, set)):
336 for i in range(len(value)):
337 if i >= len(expected):
338 j = len(expected) - 1
341 self.type_check(name + "[]", value[i], expected[j], args)
343 # If a struct with particular (or required) types of items is
345 elif isinstance(expected, dict):
346 for key in value.keys():
348 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
349 for key, subparam in expected.iteritems():
350 if isinstance(subparam, Parameter) and \
351 subparam.optional is not None and \
352 not subparam.optional and key not in value.keys():
353 raise PLCInvalidArgument("'%s' not specified" % key, name)
356 auth.check(self, *args)