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.19 2006/11/29 17:57:27 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:
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)
147 # Get message for this event
148 if hasattr(self, 'message'):
149 event['message'] = self.message
152 event.sync(commit = True)
154 def help(self, indent = " "):
156 Text documentation for the method.
159 (min_args, max_args, defaults) = self.args()
161 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
163 text += "Description:\n\n"
164 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
165 text += "\n".join(lines) + "\n\n"
167 text += "Allowed Roles:\n\n"
172 text += indent + ", ".join(roles) + "\n\n"
174 def param_text(name, param, indent, step):
176 Format a method parameter.
181 # Print parameter name
184 text += name.ljust(param_offset - len(indent))
186 param_offset = len(indent)
188 # Print parameter type
189 param_type = python_type(param)
190 text += xmlrpc_type(param_type) + "\n"
192 # Print parameter documentation right below type
193 if isinstance(param, Parameter):
194 wrapper = textwrap.TextWrapper(width = 70,
195 initial_indent = " " * param_offset,
196 subsequent_indent = " " * param_offset)
197 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
202 # Indent struct fields and mixed types
203 if isinstance(param, dict):
204 for name, subparam in param.iteritems():
205 text += param_text(name, subparam, indent + step, step)
206 elif isinstance(param, Mixed):
207 for subparam in param:
208 text += param_text(name, subparam, indent + step, step)
209 elif isinstance(param, (list, tuple, set)):
210 for subparam in param:
211 text += param_text("", subparam, indent + step, step)
215 text += "Parameters:\n\n"
216 for name, param in zip(max_args, self.accepts):
217 text += param_text(name, param, indent, indent)
219 text += "Returns:\n\n"
220 text += param_text("", self.returns, indent, indent)
228 ((arg1_name, arg2_name, ...),
229 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
230 (None, None, ..., optional1_default, optional2_default, ...))
232 That represents the minimum and maximum sets of arguments that
233 this function accepts and the defaults for the optional arguments.
236 # Inspect call. Remove self from the argument list.
237 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
238 defaults = self.call.func_defaults
242 min_args = max_args[0:len(max_args) - len(defaults)]
243 defaults = tuple([None for arg in min_args]) + defaults
245 return (min_args, max_args, defaults)
247 def type_check(self, name, value, expected, args):
249 Checks the type of the named value against the expected type,
250 which may be a Python type, a typed value, a Parameter, a
251 Mixed type, or a list or dictionary of possibly mixed types,
252 values, Parameters, or Mixed types.
254 Extraneous members of lists must be of the same type as the
255 last specified type. For example, if the expected argument
256 type is [int, bool], then [1, False] and [14, True, False,
257 True] are valid, but [1], [False, 1] and [14, True, 1] are
260 Extraneous members of dictionaries are ignored.
263 # If any of a number of types is acceptable
264 if isinstance(expected, Mixed):
265 for item in expected:
267 self.type_check(name, value, item, args)
269 except PLCInvalidArgument, fault:
273 # If an authentication structure is expected, save it and
274 # authenticate after basic type checking is done.
275 if isinstance(expected, Auth):
280 # Get actual expected type from within the Parameter structure
281 if isinstance(expected, Parameter):
284 nullok = expected.nullok
285 expected = expected.type
291 expected_type = python_type(expected)
293 # If value can be NULL
294 if value is None and nullok:
297 # Strings are a special case. Accept either unicode or str
298 # types if a string is expected.
299 if expected_type in StringTypes and isinstance(value, StringTypes):
302 # Integers and long integers are also special types. Accept
303 # either int or long types if an int or long is expected.
304 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
307 elif not isinstance(value, expected_type):
308 raise PLCInvalidArgument("expected %s, got %s" % \
309 (xmlrpc_type(expected_type),
310 xmlrpc_type(type(value))),
313 # If a minimum or maximum (length, value) has been specified
314 if expected_type in StringTypes:
315 if min is not None and \
316 len(value.encode(self.api.encoding)) < min:
317 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
318 if max is not None and \
319 len(value.encode(self.api.encoding)) > max:
320 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
321 elif expected_type in (list, tuple, set):
322 if min is not None and len(value) < min:
323 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
324 if max is not None and len(value) > max:
325 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
327 if min is not None and value < min:
328 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
329 if max is not None and value > max:
330 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
332 # If a list with particular types of items is expected
333 if isinstance(expected, (list, tuple, set)):
334 for i in range(len(value)):
335 if i >= len(expected):
336 j = len(expected) - 1
339 self.type_check(name + "[]", value[i], expected[j], args)
341 # If a struct with particular (or required) types of items is
343 elif isinstance(expected, dict):
344 for key in value.keys():
346 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
347 for key, subparam in expected.iteritems():
348 if isinstance(subparam, Parameter) and \
349 subparam.optional is not None and \
350 not subparam.optional and key not in value.keys():
351 raise PLCInvalidArgument("'%s' not specified" % key, name)
354 auth.check(self, *args)