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