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.20 2006/12/20 14:08:40 tmack 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
153 def help(self, indent = " "):
155 Text documentation for the method.
158 (min_args, max_args, defaults) = self.args()
160 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
162 text += "Description:\n\n"
163 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
164 text += "\n".join(lines) + "\n\n"
166 text += "Allowed Roles:\n\n"
171 text += indent + ", ".join(roles) + "\n\n"
173 def param_text(name, param, indent, step):
175 Format a method parameter.
180 # Print parameter name
183 text += name.ljust(param_offset - len(indent))
185 param_offset = len(indent)
187 # Print parameter type
188 param_type = python_type(param)
189 text += xmlrpc_type(param_type) + "\n"
191 # Print parameter documentation right below type
192 if isinstance(param, Parameter):
193 wrapper = textwrap.TextWrapper(width = 70,
194 initial_indent = " " * param_offset,
195 subsequent_indent = " " * param_offset)
196 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
201 # Indent struct fields and mixed types
202 if isinstance(param, dict):
203 for name, subparam in param.iteritems():
204 text += param_text(name, subparam, indent + step, step)
205 elif isinstance(param, Mixed):
206 for subparam in param:
207 text += param_text(name, subparam, indent + step, step)
208 elif isinstance(param, (list, tuple, set)):
209 for subparam in param:
210 text += param_text("", subparam, indent + step, step)
214 text += "Parameters:\n\n"
215 for name, param in zip(max_args, self.accepts):
216 text += param_text(name, param, indent, indent)
218 text += "Returns:\n\n"
219 text += param_text("", self.returns, indent, indent)
227 ((arg1_name, arg2_name, ...),
228 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
229 (None, None, ..., optional1_default, optional2_default, ...))
231 That represents the minimum and maximum sets of arguments that
232 this function accepts and the defaults for the optional arguments.
235 # Inspect call. Remove self from the argument list.
236 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
237 defaults = self.call.func_defaults
241 min_args = max_args[0:len(max_args) - len(defaults)]
242 defaults = tuple([None for arg in min_args]) + defaults
244 return (min_args, max_args, defaults)
246 def type_check(self, name, value, expected, args):
248 Checks the type of the named value against the expected type,
249 which may be a Python type, a typed value, a Parameter, a
250 Mixed type, or a list or dictionary of possibly mixed types,
251 values, Parameters, or Mixed types.
253 Extraneous members of lists must be of the same type as the
254 last specified type. For example, if the expected argument
255 type is [int, bool], then [1, False] and [14, True, False,
256 True] are valid, but [1], [False, 1] and [14, True, 1] are
259 Extraneous members of dictionaries are ignored.
262 # If any of a number of types is acceptable
263 if isinstance(expected, Mixed):
264 for item in expected:
266 self.type_check(name, value, item, args)
268 except PLCInvalidArgument, fault:
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)
320 elif expected_type in (list, tuple, set):
321 if min is not None and len(value) < min:
322 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
323 if max is not None and len(value) > max:
324 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
326 if min is not None and value < min:
327 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
328 if max is not None and value > max:
329 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
331 # If a list with particular types of items is expected
332 if isinstance(expected, (list, tuple, set)):
333 for i in range(len(value)):
334 if i >= len(expected):
335 j = len(expected) - 1
338 self.type_check(name + "[]", value[i], expected[j], args)
340 # If a struct with particular (or required) types of items is
342 elif isinstance(expected, dict):
343 for key in value.keys():
345 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
346 for key, subparam in expected.iteritems():
347 if isinstance(subparam, Parameter) and \
348 subparam.optional is not None and \
349 not subparam.optional and key not in value.keys():
350 raise PLCInvalidArgument("'%s' not specified" % key, name)
353 auth.check(self, *args)