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.22 2007/01/19 17:49:02 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, 'event_objects') and isinstance(self.event_objects, dict):
143 for key in self.event_objects.keys():
144 for object_id in self.event_objects[key]:
145 event.add_object(key, object_id, commit = False)
148 # Set the message for this event
149 if hasattr(self, 'message'):
150 event['message'] = self.message
155 def help(self, indent = " "):
157 Text documentation for the method.
160 (min_args, max_args, defaults) = self.args()
162 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
164 text += "Description:\n\n"
165 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
166 text += "\n".join(lines) + "\n\n"
168 text += "Allowed Roles:\n\n"
173 text += indent + ", ".join(roles) + "\n\n"
175 def param_text(name, param, indent, step):
177 Format a method parameter.
182 # Print parameter name
185 text += name.ljust(param_offset - len(indent))
187 param_offset = len(indent)
189 # Print parameter type
190 param_type = python_type(param)
191 text += xmlrpc_type(param_type) + "\n"
193 # Print parameter documentation right below type
194 if isinstance(param, Parameter):
195 wrapper = textwrap.TextWrapper(width = 70,
196 initial_indent = " " * param_offset,
197 subsequent_indent = " " * param_offset)
198 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
203 # Indent struct fields and mixed types
204 if isinstance(param, dict):
205 for name, subparam in param.iteritems():
206 text += param_text(name, subparam, indent + step, step)
207 elif isinstance(param, Mixed):
208 for subparam in param:
209 text += param_text(name, subparam, indent + step, step)
210 elif isinstance(param, (list, tuple, set)):
211 for subparam in param:
212 text += param_text("", subparam, indent + step, step)
216 text += "Parameters:\n\n"
217 for name, param in zip(max_args, self.accepts):
218 text += param_text(name, param, indent, indent)
220 text += "Returns:\n\n"
221 text += param_text("", self.returns, indent, indent)
229 ((arg1_name, arg2_name, ...),
230 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
231 (None, None, ..., optional1_default, optional2_default, ...))
233 That represents the minimum and maximum sets of arguments that
234 this function accepts and the defaults for the optional arguments.
237 # Inspect call. Remove self from the argument list.
238 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
239 defaults = self.call.func_defaults
243 min_args = max_args[0:len(max_args) - len(defaults)]
244 defaults = tuple([None for arg in min_args]) + defaults
246 return (min_args, max_args, defaults)
248 def type_check(self, name, value, expected, args):
250 Checks the type of the named value against the expected type,
251 which may be a Python type, a typed value, a Parameter, a
252 Mixed type, or a list or dictionary of possibly mixed types,
253 values, Parameters, or Mixed types.
255 Extraneous members of lists must be of the same type as the
256 last specified type. For example, if the expected argument
257 type is [int, bool], then [1, False] and [14, True, False,
258 True] are valid, but [1], [False, 1] and [14, True, 1] are
261 Extraneous members of dictionaries are ignored.
264 # If any of a number of types is acceptable
265 if isinstance(expected, Mixed):
266 for item in expected:
268 self.type_check(name, value, item, args)
270 except PLCInvalidArgument, fault:
274 # If an authentication structure is expected, save it and
275 # authenticate after basic type checking is done.
276 if isinstance(expected, Auth):
281 # Get actual expected type from within the Parameter structure
282 if isinstance(expected, Parameter):
285 nullok = expected.nullok
286 expected = expected.type
292 expected_type = python_type(expected)
294 # If value can be NULL
295 if value is None and nullok:
298 # Strings are a special case. Accept either unicode or str
299 # types if a string is expected.
300 if expected_type in StringTypes and isinstance(value, StringTypes):
303 # Integers and long integers are also special types. Accept
304 # either int or long types if an int or long is expected.
305 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
308 elif not isinstance(value, expected_type):
309 raise PLCInvalidArgument("expected %s, got %s" % \
310 (xmlrpc_type(expected_type),
311 xmlrpc_type(type(value))),
314 # If a minimum or maximum (length, value) has been specified
315 if expected_type in StringTypes:
316 if min is not None and \
317 len(value.encode(self.api.encoding)) < min:
318 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
319 if max is not None and \
320 len(value.encode(self.api.encoding)) > max:
321 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
322 elif expected_type in (list, tuple, set):
323 if min is not None and len(value) < min:
324 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
325 if max is not None and len(value) > max:
326 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
328 if min is not None and value < min:
329 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
330 if max is not None and value > max:
331 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
333 # If a list with particular types of items is expected
334 if isinstance(expected, (list, tuple, set)):
335 for i in range(len(value)):
336 if i >= len(expected):
337 j = len(expected) - 1
340 self.type_check(name + "[]", value[i], expected[j], args)
342 # If a struct with particular (or required) types of items is
344 elif isinstance(expected, dict):
345 for key in value.keys():
347 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
348 for key, subparam in expected.iteritems():
349 if isinstance(subparam, Parameter) and \
350 subparam.optional is not None and \
351 not subparam.optional and key not in value.keys():
352 raise PLCInvalidArgument("'%s' not specified" % key, name)
355 auth.check(self, *args)