4cda6bb0a556383c6cb3da2cbce06c33f09919be
[plcapi.git] / PLC / Method.py
1 #
2 # Base class for all PLCAPI functions
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id: Method.py,v 1.14 2006/10/31 21:47:21 mlhuang Exp $
8 #
9
10 import xmlrpclib
11 from types import *
12 import textwrap
13 import os
14 import time
15 import pprint
16
17 from PLC.Faults import *
18 from PLC.Parameter import Parameter, Mixed
19 from PLC.Auth import Auth
20 from PLC.Debug import profile, log
21 from PLC.Events import Event, Events
22 from PLC.Nodes import Node, Nodes
23 from PLC.Persons import Person, Persons
24
25 class Method:
26     """
27     Base class for all PLCAPI functions. At a minimum, all PLCAPI
28     functions must define:
29
30     roles = [list of roles]
31     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
32     returns = Parameter(return_type, return_doc)
33     call(arg1, arg2, ...): method body
34
35     Argument types may be Python types (e.g., int, bool, etc.), typed
36     values (e.g., 1, True, etc.), a Parameter, or lists or
37     dictionaries of possibly mixed types, values, and/or Parameters
38     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
39
40     Once function decorators in Python 2.4 are fully supported,
41     consider wrapping calls with accepts() and returns() functions
42     instead of performing type checking manually.
43     """
44
45     # Defaults. Could implement authentication and type checking with
46     # decorators, but they are not supported in Python 2.3 and it
47     # would be hard to generate documentation without writing a code
48     # parser.
49
50     roles = []
51     accepts = []
52     returns = bool
53     status = "current"
54
55     def call(self, *args):
56         """
57         Method body for all PLCAPI functions. Must override.
58         """
59
60         return True
61
62     def __init__(self, api):
63         self.name = self.__class__.__name__
64         self.api = api
65
66         # Auth may set this to a Person instance (if an anonymous
67         # method, will remain None).
68         self.caller = None
69
70         # API may set this to a (addr, port) tuple if known
71         self.source = None
72         
73     def __call__(self, *args, **kwds):
74         """
75         Main entry point for all PLCAPI functions. Type checks
76         arguments, authenticates, and executes call().
77         """
78
79         try:
80             start = time.time()
81             (min_args, max_args, defaults) = self.args()
82                                 
83             # Check that the right number of arguments were passed in
84             if len(args) < len(min_args) or len(args) > len(max_args):
85                 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
86
87             for name, value, expected in zip(max_args, args, self.accepts):
88                 self.type_check(name, value, expected, args)
89         
90             result = self.call(*args, **kwds)
91             runtime = time.time() - start
92
93             if self.api.config.PLC_API_DEBUG:
94                 self.log(0, runtime, *args)
95                 
96             return result
97
98         except PLCFault, fault:
99             # Prepend method name to expected faults
100             fault.faultString = self.name + ": " + fault.faultString
101             runtime = time.time() - start
102             self.log(fault.faultCode, runtime, *args)
103             raise fault
104
105     def log(self, fault_code, runtime, *args):
106         """
107         Log the transaction 
108         """     
109
110         # Do not log system or Get calls
111         if self.name.startswith('system') or self.name.startswith('Get'):
112             return False
113
114         # Create a new event
115         event = Event(self.api)
116         event['fault_code'] = fault_code
117         event['runtime'] = runtime
118
119         # Redact passwords and sessions
120         if args and isinstance(args[0], dict):
121             for password in 'AuthString', 'session':
122                 if args[0].has_key(password):
123                     auth = args[0].copy()
124                     auth[password] = "Removed by API"
125                     args = (auth,) + args[1:]
126
127         # Log call representation
128         # XXX Truncate to avoid DoS
129         event['call'] = self.name + pprint.saferepr(args)
130
131         # Both users and nodes can call some methods
132         if isinstance(self.caller, Person):
133             event['person_id'] = self.caller['person_id']
134         elif isinstance(self.caller, Node):
135             event['node_id'] = self.caller['node_id']
136
137         event.sync(commit = False)
138
139         # XXX object_ids is currently defined as a class variable
140         if hasattr(self, 'object_ids'):
141             for object_id in self.object_ids:
142                 event.add_object(object_id, commit = False)
143
144         # Commit
145         event.sync(commit = True)
146
147     def help(self, indent = "  "):
148         """
149         Text documentation for the method.
150         """
151
152         (min_args, max_args, defaults) = self.args()
153
154         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
155
156         text += "Description:\n\n"
157         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
158         text += "\n".join(lines) + "\n\n"
159
160         text += "Allowed Roles:\n\n"
161         if not self.roles:
162             roles = ["any"]
163         else:
164             roles = self.roles
165         text += indent + ", ".join(roles) + "\n\n"
166
167         def param_text(name, param, indent, step):
168             """
169             Format a method parameter.
170             """
171
172             text = indent
173
174             # Print parameter name
175             if name:
176                 param_offset = 32
177                 text += name.ljust(param_offset - len(indent))
178             else:
179                 param_offset = len(indent)
180
181             # Print parameter type
182             param_type = python_type(param)
183             text += xmlrpc_type(param_type) + "\n"
184
185             # Print parameter documentation right below type
186             if isinstance(param, Parameter):
187                 wrapper = textwrap.TextWrapper(width = 70,
188                                                initial_indent = " " * param_offset,
189                                                subsequent_indent = " " * param_offset)
190                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
191                 param = param.type
192
193             text += "\n"
194
195             # Indent struct fields and mixed types
196             if isinstance(param, dict):
197                 for name, subparam in param.iteritems():
198                     text += param_text(name, subparam, indent + step, step)
199             elif isinstance(param, Mixed):
200                 for subparam in param:
201                     text += param_text(name, subparam, indent + step, step)
202             elif isinstance(param, (list, tuple)):
203                 for subparam in param:
204                     text += param_text("", subparam, indent + step, step)
205
206             return text
207
208         text += "Parameters:\n\n"
209         for name, param in zip(max_args, self.accepts):
210             text += param_text(name, param, indent, indent)
211
212         text += "Returns:\n\n"
213         text += param_text("", self.returns, indent, indent)
214
215         return text
216
217     def args(self):
218         """
219         Returns a tuple:
220
221         ((arg1_name, arg2_name, ...),
222          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
223          (None, None, ..., optional1_default, optional2_default, ...))
224
225         That represents the minimum and maximum sets of arguments that
226         this function accepts and the defaults for the optional arguments.
227         """
228
229         # Inspect call. Remove self from the argument list.
230         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
231         defaults = self.call.func_defaults
232         if defaults is None:
233             defaults = ()
234
235         min_args = max_args[0:len(max_args) - len(defaults)]
236         defaults = tuple([None for arg in min_args]) + defaults
237         
238         return (min_args, max_args, defaults)
239
240     def type_check(self, name, value, expected, args):
241         """
242         Checks the type of the named value against the expected type,
243         which may be a Python type, a typed value, a Parameter, a
244         Mixed type, or a list or dictionary of possibly mixed types,
245         values, Parameters, or Mixed types.
246         
247         Extraneous members of lists must be of the same type as the
248         last specified type. For example, if the expected argument
249         type is [int, bool], then [1, False] and [14, True, False,
250         True] are valid, but [1], [False, 1] and [14, True, 1] are
251         not.
252
253         Extraneous members of dictionaries are ignored.
254         """
255
256         # If any of a number of types is acceptable
257         if isinstance(expected, Mixed):
258             for item in expected:
259                 try:
260                     self.type_check(name, value, item, args)
261                     return
262                 except PLCInvalidArgument, fault:
263                     pass
264             xmlrpc_types = [xmlrpc_type(item) for item in expected]
265             raise PLCInvalidArgument("expected %s, got %s" % \
266                                      (" or ".join(xmlrpc_types),
267                                       xmlrpc_type(type(value))),
268                                      name)
269
270         # If an authentication structure is expected, save it and
271         # authenticate after basic type checking is done.
272         if isinstance(expected, Auth):
273             auth = expected
274         else:
275             auth = None
276
277         # Get actual expected type from within the Parameter structure
278         if isinstance(expected, Parameter):
279             min = expected.min
280             max = expected.max
281             nullok = expected.nullok
282             expected = expected.type
283         else:
284             min = None
285             max = None
286             nullok = False
287
288         expected_type = python_type(expected)
289
290         # If value can be NULL
291         if value is None and nullok:
292             return
293
294         # Strings are a special case. Accept either unicode or str
295         # types if a string is expected.
296         if expected_type in StringTypes and isinstance(value, StringTypes):
297             pass
298
299         # Integers and long integers are also special types. Accept
300         # either int or long types if an int or long is expected.
301         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
302             pass
303
304         elif not isinstance(value, expected_type):
305             raise PLCInvalidArgument("expected %s, got %s" % \
306                                      (xmlrpc_type(expected_type),
307                                       xmlrpc_type(type(value))),
308                                      name)
309
310         # If a minimum or maximum (length, value) has been specified
311         if expected_type in StringTypes:
312             if min is not None and \
313                len(value.encode(self.api.encoding)) < min:
314                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
315             if max is not None and \
316                len(value.encode(self.api.encoding)) > max:
317                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
318         else:
319             if min is not None and value < min:
320                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
321             if max is not None and value > max:
322                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
323
324         # If a list with particular types of items is expected
325         if isinstance(expected, (list, tuple)):
326             for i in range(len(value)):
327                 if i >= len(expected):
328                     j = len(expected) - 1
329                 else:
330                     j = i
331                 self.type_check(name + "[]", value[i], expected[j], args)
332
333         # If a struct with particular (or required) types of items is
334         # expected.
335         elif isinstance(expected, dict):
336             for key in value.keys():
337                 if key in expected:
338                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
339             for key, subparam in expected.iteritems():
340                 if isinstance(subparam, Parameter) and \
341                    subparam.optional is not None and \
342                    not subparam.optional and key not in value.keys():
343                     raise PLCInvalidArgument("'%s' not specified" % key, name)
344
345         if auth is not None:
346             auth.check(self, *args)
347
348 def python_type(arg):
349     """
350     Returns the Python type of the specified argument, which may be a
351     Python type, a typed value, or a Parameter.
352     """
353
354     if isinstance(arg, Parameter):
355         arg = arg.type
356
357     if isinstance(arg, type):
358         return arg
359     else:
360         return type(arg)
361
362 def xmlrpc_type(arg):
363     """
364     Returns the XML-RPC type of the specified argument, which may be a
365     Python type, a typed value, or a Parameter.
366     """
367
368     arg_type = python_type(arg)
369
370     if arg_type == NoneType:
371         return "nil"
372     elif arg_type == IntType or arg_type == LongType:
373         return "int"
374     elif arg_type == bool:
375         return "boolean"
376     elif arg_type == FloatType:
377         return "double"
378     elif arg_type in StringTypes:
379         return "string"
380     elif arg_type == ListType or arg_type == TupleType:
381         return "array"
382     elif arg_type == DictType:
383         return "struct"
384     elif arg_type == Mixed:
385         # Not really an XML-RPC type but return "mixed" for
386         # documentation purposes.
387         return "mixed"
388     else:
389         raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type