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.25 2007/05/16 16:11:10 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:
103 if isinstance(self.caller, Person):
104 caller = 'person_id %s' % self.caller['person_id']
105 elif isinstance(self.caller, Node):
106 caller = 'node_id %s' % self.caller['node_id']
108 # Prepend caller and method name to expected faults
109 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
110 runtime = time.time() - start
111 self.log(fault.faultCode, runtime, *args)
114 def log(self, fault_code, runtime, *args):
119 # Do not log system or Get calls
120 #if self.name.startswith('system') or self.name.startswith('Get'):
124 event = Event(self.api)
125 event['fault_code'] = fault_code
126 event['runtime'] = runtime
128 # Redact passwords and sessions
129 if args and isinstance(args[0], dict):
130 # what type of auth this is
131 if args[0].has_key('AuthMethod'):
132 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
133 auth_method = args[0]['AuthMethod']
135 if auth_method in auth_methods:
136 event['auth_type'] = auth_method
137 for password in 'AuthString', 'session':
138 if args[0].has_key(password):
139 auth = args[0].copy()
140 auth[password] = "Removed by API"
141 args = (auth,) + args[1:]
143 # Log call representation
144 # XXX Truncate to avoid DoS
145 event['call'] = self.name + pprint.saferepr(args)
146 event['call_name'] = self.name
148 # Both users and nodes can call some methods
149 if isinstance(self.caller, Person):
150 event['person_id'] = self.caller['person_id']
151 elif isinstance(self.caller, Node):
152 event['node_id'] = self.caller['node_id']
154 event.sync(commit = False)
156 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
157 for key in self.event_objects.keys():
158 for object_id in self.event_objects[key]:
159 event.add_object(key, object_id, commit = False)
162 # Set the message for this event
163 if hasattr(self, 'message'):
164 event['message'] = self.message
169 def help(self, indent = " "):
171 Text documentation for the method.
174 (min_args, max_args, defaults) = self.args()
176 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
178 text += "Description:\n\n"
179 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
180 text += "\n".join(lines) + "\n\n"
182 text += "Allowed Roles:\n\n"
187 text += indent + ", ".join(roles) + "\n\n"
189 def param_text(name, param, indent, step):
191 Format a method parameter.
196 # Print parameter name
199 text += name.ljust(param_offset - len(indent))
201 param_offset = len(indent)
203 # Print parameter type
204 param_type = python_type(param)
205 text += xmlrpc_type(param_type) + "\n"
207 # Print parameter documentation right below type
208 if isinstance(param, Parameter):
209 wrapper = textwrap.TextWrapper(width = 70,
210 initial_indent = " " * param_offset,
211 subsequent_indent = " " * param_offset)
212 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
217 # Indent struct fields and mixed types
218 if isinstance(param, dict):
219 for name, subparam in param.iteritems():
220 text += param_text(name, subparam, indent + step, step)
221 elif isinstance(param, Mixed):
222 for subparam in param:
223 text += param_text(name, subparam, indent + step, step)
224 elif isinstance(param, (list, tuple, set)):
225 for subparam in param:
226 text += param_text("", subparam, indent + step, step)
230 text += "Parameters:\n\n"
231 for name, param in zip(max_args, self.accepts):
232 text += param_text(name, param, indent, indent)
234 text += "Returns:\n\n"
235 text += param_text("", self.returns, indent, indent)
243 ((arg1_name, arg2_name, ...),
244 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
245 (None, None, ..., optional1_default, optional2_default, ...))
247 That represents the minimum and maximum sets of arguments that
248 this function accepts and the defaults for the optional arguments.
251 # Inspect call. Remove self from the argument list.
252 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
253 defaults = self.call.func_defaults
257 min_args = max_args[0:len(max_args) - len(defaults)]
258 defaults = tuple([None for arg in min_args]) + defaults
260 return (min_args, max_args, defaults)
262 def type_check(self, name, value, expected, args):
264 Checks the type of the named value against the expected type,
265 which may be a Python type, a typed value, a Parameter, a
266 Mixed type, or a list or dictionary of possibly mixed types,
267 values, Parameters, or Mixed types.
269 Extraneous members of lists must be of the same type as the
270 last specified type. For example, if the expected argument
271 type is [int, bool], then [1, False] and [14, True, False,
272 True] are valid, but [1], [False, 1] and [14, True, 1] are
275 Extraneous members of dictionaries are ignored.
278 # If any of a number of types is acceptable
279 if isinstance(expected, Mixed):
280 for item in expected:
282 self.type_check(name, value, item, args)
284 except PLCInvalidArgument, fault:
288 # If an authentication structure is expected, save it and
289 # authenticate after basic type checking is done.
290 if isinstance(expected, Auth):
295 # Get actual expected type from within the Parameter structure
296 if isinstance(expected, Parameter):
299 nullok = expected.nullok
300 expected = expected.type
306 expected_type = python_type(expected)
308 # If value can be NULL
309 if value is None and nullok:
312 # Strings are a special case. Accept either unicode or str
313 # types if a string is expected.
314 if expected_type in StringTypes and isinstance(value, StringTypes):
317 # Integers and long integers are also special types. Accept
318 # either int or long types if an int or long is expected.
319 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
322 elif not isinstance(value, expected_type):
323 raise PLCInvalidArgument("expected %s, got %s" % \
324 (xmlrpc_type(expected_type),
325 xmlrpc_type(type(value))),
328 # If a minimum or maximum (length, value) has been specified
329 if expected_type in StringTypes:
330 if min is not None and \
331 len(value.encode(self.api.encoding)) < min:
332 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
333 if max is not None and \
334 len(value.encode(self.api.encoding)) > max:
335 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
336 elif expected_type in (list, tuple, set):
337 if min is not None and len(value) < min:
338 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
339 if max is not None and len(value) > max:
340 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
342 if min is not None and value < min:
343 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
344 if max is not None and value > max:
345 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
347 # If a list with particular types of items is expected
348 if isinstance(expected, (list, tuple, set)):
349 for i in range(len(value)):
350 if i >= len(expected):
351 j = len(expected) - 1
354 self.type_check(name + "[]", value[i], expected[j], args)
356 # If a struct with particular (or required) types of items is
358 elif isinstance(expected, dict):
359 for key in value.keys():
361 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
362 for key, subparam in expected.iteritems():
363 if isinstance(subparam, Parameter) and \
364 subparam.optional is not None and \
365 not subparam.optional and key not in value.keys():
366 raise PLCInvalidArgument("'%s' not specified" % key, name)
369 auth.check(self, *args)