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.18 2006/11/08 22:11:26 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)
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 # XXX object_ids is currently defined as a class variable
143 if hasattr(self, 'object_ids'):
144 for object_id in self.object_ids:
145 event.add_object(object_id, commit = False)
148 event.sync(commit = True)
150 def help(self, indent = " "):
152 Text documentation for the method.
155 (min_args, max_args, defaults) = self.args()
157 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
159 text += "Description:\n\n"
160 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
161 text += "\n".join(lines) + "\n\n"
163 text += "Allowed Roles:\n\n"
168 text += indent + ", ".join(roles) + "\n\n"
170 def param_text(name, param, indent, step):
172 Format a method parameter.
177 # Print parameter name
180 text += name.ljust(param_offset - len(indent))
182 param_offset = len(indent)
184 # Print parameter type
185 param_type = python_type(param)
186 text += xmlrpc_type(param_type) + "\n"
188 # Print parameter documentation right below type
189 if isinstance(param, Parameter):
190 wrapper = textwrap.TextWrapper(width = 70,
191 initial_indent = " " * param_offset,
192 subsequent_indent = " " * param_offset)
193 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
198 # Indent struct fields and mixed types
199 if isinstance(param, dict):
200 for name, subparam in param.iteritems():
201 text += param_text(name, subparam, indent + step, step)
202 elif isinstance(param, Mixed):
203 for subparam in param:
204 text += param_text(name, subparam, indent + step, step)
205 elif isinstance(param, (list, tuple, set)):
206 for subparam in param:
207 text += param_text("", subparam, indent + step, step)
211 text += "Parameters:\n\n"
212 for name, param in zip(max_args, self.accepts):
213 text += param_text(name, param, indent, indent)
215 text += "Returns:\n\n"
216 text += param_text("", self.returns, indent, indent)
224 ((arg1_name, arg2_name, ...),
225 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
226 (None, None, ..., optional1_default, optional2_default, ...))
228 That represents the minimum and maximum sets of arguments that
229 this function accepts and the defaults for the optional arguments.
232 # Inspect call. Remove self from the argument list.
233 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
234 defaults = self.call.func_defaults
238 min_args = max_args[0:len(max_args) - len(defaults)]
239 defaults = tuple([None for arg in min_args]) + defaults
241 return (min_args, max_args, defaults)
243 def type_check(self, name, value, expected, args):
245 Checks the type of the named value against the expected type,
246 which may be a Python type, a typed value, a Parameter, a
247 Mixed type, or a list or dictionary of possibly mixed types,
248 values, Parameters, or Mixed types.
250 Extraneous members of lists must be of the same type as the
251 last specified type. For example, if the expected argument
252 type is [int, bool], then [1, False] and [14, True, False,
253 True] are valid, but [1], [False, 1] and [14, True, 1] are
256 Extraneous members of dictionaries are ignored.
259 # If any of a number of types is acceptable
260 if isinstance(expected, Mixed):
261 for item in expected:
263 self.type_check(name, value, item, args)
265 except PLCInvalidArgument, fault:
269 # If an authentication structure is expected, save it and
270 # authenticate after basic type checking is done.
271 if isinstance(expected, Auth):
276 # Get actual expected type from within the Parameter structure
277 if isinstance(expected, Parameter):
280 nullok = expected.nullok
281 expected = expected.type
287 expected_type = python_type(expected)
289 # If value can be NULL
290 if value is None and nullok:
293 # Strings are a special case. Accept either unicode or str
294 # types if a string is expected.
295 if expected_type in StringTypes and isinstance(value, StringTypes):
298 # Integers and long integers are also special types. Accept
299 # either int or long types if an int or long is expected.
300 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
303 elif not isinstance(value, expected_type):
304 raise PLCInvalidArgument("expected %s, got %s" % \
305 (xmlrpc_type(expected_type),
306 xmlrpc_type(type(value))),
309 # If a minimum or maximum (length, value) has been specified
310 if expected_type in StringTypes:
311 if min is not None and \
312 len(value.encode(self.api.encoding)) < min:
313 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
314 if max is not None and \
315 len(value.encode(self.api.encoding)) > max:
316 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
317 elif expected_type in (list, tuple, set):
318 if min is not None and len(value) < min:
319 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
320 if max is not None and len(value) > max:
321 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
323 if min is not None and value < min:
324 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
325 if max is not None and value > max:
326 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
328 # If a list with particular types of items is expected
329 if isinstance(expected, (list, tuple, set)):
330 for i in range(len(value)):
331 if i >= len(expected):
332 j = len(expected) - 1
335 self.type_check(name + "[]", value[i], expected[j], args)
337 # If a struct with particular (or required) types of items is
339 elif isinstance(expected, dict):
340 for key in value.keys():
342 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
343 for key, subparam in expected.iteritems():
344 if isinstance(subparam, Parameter) and \
345 subparam.optional is not None and \
346 not subparam.optional and key not in value.keys():
347 raise PLCInvalidArgument("'%s' not specified" % key, name)
350 auth.check(self, *args)