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.26 2007/05/16 18:54:12 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']
134 if auth_method in auth_methods:
135 event['auth_type'] = auth_method
136 for password in 'AuthString', 'session':
137 if args[0].has_key(password):
138 auth = args[0].copy()
139 auth[password] = "Removed by API"
140 args = (auth,) + args[1:]
142 # Log call representation
143 # XXX Truncate to avoid DoS
144 event['call'] = self.name + pprint.saferepr(args)
145 event['call_name'] = self.name
147 # Both users and nodes can call some methods
148 if isinstance(self.caller, Person):
149 event['person_id'] = self.caller['person_id']
150 elif isinstance(self.caller, Node):
151 event['node_id'] = self.caller['node_id']
153 event.sync(commit = False)
155 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
156 for key in self.event_objects.keys():
157 for object_id in self.event_objects[key]:
158 event.add_object(key, object_id, commit = False)
161 # Set the message for this event
162 if hasattr(self, 'message'):
163 event['message'] = self.message
168 def help(self, indent = " "):
170 Text documentation for the method.
173 (min_args, max_args, defaults) = self.args()
175 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
177 text += "Description:\n\n"
178 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
179 text += "\n".join(lines) + "\n\n"
181 text += "Allowed Roles:\n\n"
186 text += indent + ", ".join(roles) + "\n\n"
188 def param_text(name, param, indent, step):
190 Format a method parameter.
195 # Print parameter name
198 text += name.ljust(param_offset - len(indent))
200 param_offset = len(indent)
202 # Print parameter type
203 param_type = python_type(param)
204 text += xmlrpc_type(param_type) + "\n"
206 # Print parameter documentation right below type
207 if isinstance(param, Parameter):
208 wrapper = textwrap.TextWrapper(width = 70,
209 initial_indent = " " * param_offset,
210 subsequent_indent = " " * param_offset)
211 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
216 # Indent struct fields and mixed types
217 if isinstance(param, dict):
218 for name, subparam in param.iteritems():
219 text += param_text(name, subparam, indent + step, step)
220 elif isinstance(param, Mixed):
221 for subparam in param:
222 text += param_text(name, subparam, indent + step, step)
223 elif isinstance(param, (list, tuple, set)):
224 for subparam in param:
225 text += param_text("", subparam, indent + step, step)
229 text += "Parameters:\n\n"
230 for name, param in zip(max_args, self.accepts):
231 text += param_text(name, param, indent, indent)
233 text += "Returns:\n\n"
234 text += param_text("", self.returns, indent, indent)
242 ((arg1_name, arg2_name, ...),
243 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
244 (None, None, ..., optional1_default, optional2_default, ...))
246 That represents the minimum and maximum sets of arguments that
247 this function accepts and the defaults for the optional arguments.
250 # Inspect call. Remove self from the argument list.
251 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
252 defaults = self.call.func_defaults
256 min_args = max_args[0:len(max_args) - len(defaults)]
257 defaults = tuple([None for arg in min_args]) + defaults
259 return (min_args, max_args, defaults)
261 def type_check(self, name, value, expected, args):
263 Checks the type of the named value against the expected type,
264 which may be a Python type, a typed value, a Parameter, a
265 Mixed type, or a list or dictionary of possibly mixed types,
266 values, Parameters, or Mixed types.
268 Extraneous members of lists must be of the same type as the
269 last specified type. For example, if the expected argument
270 type is [int, bool], then [1, False] and [14, True, False,
271 True] are valid, but [1], [False, 1] and [14, True, 1] are
274 Extraneous members of dictionaries are ignored.
277 # If any of a number of types is acceptable
278 if isinstance(expected, Mixed):
279 for item in expected:
281 self.type_check(name, value, item, args)
283 except PLCInvalidArgument, fault:
287 # If an authentication structure is expected, save it and
288 # authenticate after basic type checking is done.
289 if isinstance(expected, Auth):
294 # Get actual expected type from within the Parameter structure
295 if isinstance(expected, Parameter):
298 nullok = expected.nullok
299 expected = expected.type
305 expected_type = python_type(expected)
307 # If value can be NULL
308 if value is None and nullok:
311 # Strings are a special case. Accept either unicode or str
312 # types if a string is expected.
313 if expected_type in StringTypes and isinstance(value, StringTypes):
316 # Integers and long integers are also special types. Accept
317 # either int or long types if an int or long is expected.
318 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
321 elif not isinstance(value, expected_type):
322 raise PLCInvalidArgument("expected %s, got %s" % \
323 (xmlrpc_type(expected_type),
324 xmlrpc_type(type(value))),
327 # If a minimum or maximum (length, value) has been specified
328 if expected_type in StringTypes:
329 if min is not None and \
330 len(value.encode(self.api.encoding)) < min:
331 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
332 if max is not None and \
333 len(value.encode(self.api.encoding)) > max:
334 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
335 elif expected_type in (list, tuple, set):
336 if min is not None and len(value) < min:
337 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
338 if max is not None and len(value) > max:
339 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
341 if min is not None and value < min:
342 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
343 if max is not None and value > max:
344 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
346 # If a list with particular types of items is expected
347 if isinstance(expected, (list, tuple, set)):
348 for i in range(len(value)):
349 if i >= len(expected):
350 j = len(expected) - 1
353 self.type_check(name + "[]", value[i], expected[j], args)
355 # If a struct with particular (or required) types of items is
357 elif isinstance(expected, dict):
358 for key in value.keys():
360 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
361 for key, subparam in expected.iteritems():
362 if isinstance(subparam, Parameter) and \
363 subparam.optional is not None and \
364 not subparam.optional and key not in value.keys():
365 raise PLCInvalidArgument("'%s' not specified" % key, name)
368 auth.check(self, *args)