- fixed bug in logging of event_objects
[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.10 2006/10/19 19:32:48 tmack 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
69         
70     def __call__(self, *args, **kwds):
71         """
72         Main entry point for all PLCAPI functions. Type checks
73         arguments, authenticates, and executes call().
74         """
75
76         try:
77             start = time.time()
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             result = self.call(*args, **kwds)
106             runtime = time.time() - start
107
108             if self.api.config.PLC_API_DEBUG:
109                 self.log(0, runtime, *args)
110                 
111             return result
112
113         except PLCFault, fault:
114             # Prepend method name to expected faults
115             fault.faultString = self.name + ": " + fault.faultString
116             runtime = time.time() - start
117             self.log(fault.faultCode, runtime, *args)
118             raise fault
119
120
121     def log(self, fault_code, runtime, *args):
122         """
123         Log the transaction 
124         """     
125         # Gather necessary logging variables
126         event_type = 'Unknown'
127         object_type = 'Unknown'
128         person_id = None
129         object_ids = []
130         call_name = self.name
131         call_args = ", ".join([unicode(arg) for arg in list(args)[1:]])
132         call = "%s(%s)" % (call_name, call_args)
133                 
134         if hasattr(self, 'event_type'):
135                 event_type = self.event_type
136         if hasattr(self, 'object_type'):
137                 object_type = self.object_type
138         if self.caller:
139                 person_id = self.caller['person_id']
140         if hasattr(self, 'object_ids'):
141                 object_ids = self.object_ids 
142
143         # do not log system calls
144         if call_name.startswith('system'):
145                 return False
146         # do not log get calls
147         if call_name.startswith('Get'):
148                 return False
149         
150         sql_event = "INSERT INTO events " \
151               " (person_id, event_type, object_type, fault_code, call, runtime) VALUES" \
152               " (%(person_id)s, %(event_type)s, %(object_type)s," \
153               "  %(fault_code)d, %(call)s, %(runtime)f)" 
154         self.api.db.do(sql_event, locals())     
155
156         # log objects affected
157         event_id =  self.api.db.last_insert_id('events', 'event_id')
158         for object_id in object_ids:
159                 sql_objects = "INSERT INTO event_object (event_id, object_id) VALUES" \
160                         " (%(event_id)d, %(object_id)d) " 
161                 self.api.db.do(sql_objects, locals())
162                         
163         self.api.db.commit()            
164         
165
166     def help(self, indent = "  "):
167         """
168         Text documentation for the method.
169         """
170
171         (min_args, max_args, defaults) = self.args()
172
173         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
174
175         text += "Description:\n\n"
176         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
177         text += "\n".join(lines) + "\n\n"
178
179         text += "Allowed Roles:\n\n"
180         if not self.roles:
181             roles = ["any"]
182         else:
183             roles = self.roles
184         text += indent + ", ".join(roles) + "\n\n"
185
186         def param_text(name, param, indent, step):
187             """
188             Format a method parameter.
189             """
190
191             text = indent
192
193             # Print parameter name
194             if name:
195                 param_offset = 32
196                 text += name.ljust(param_offset - len(indent))
197             else:
198                 param_offset = len(indent)
199
200             # Print parameter type
201             param_type = python_type(param)
202             text += xmlrpc_type(param_type) + "\n"
203
204             # Print parameter documentation right below type
205             if isinstance(param, Parameter):
206                 wrapper = textwrap.TextWrapper(width = 70,
207                                                initial_indent = " " * param_offset,
208                                                subsequent_indent = " " * param_offset)
209                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
210                 param = param.type
211
212             text += "\n"
213
214             # Indent struct fields and mixed types
215             if isinstance(param, dict):
216                 for name, subparam in param.iteritems():
217                     text += param_text(name, subparam, indent + step, step)
218             elif isinstance(param, Mixed):
219                 for subparam in param:
220                     text += param_text(name, subparam, indent + step, step)
221             elif isinstance(param, (list, tuple)):
222                 for subparam in param:
223                     text += param_text("", subparam, indent + step, step)
224
225             return text
226
227         text += "Parameters:\n\n"
228         for name, param in zip(max_args, self.accepts):
229             text += param_text(name, param, indent, indent)
230
231         text += "Returns:\n\n"
232         text += param_text("", self.returns, indent, indent)
233
234         return text
235
236     def args(self):
237         """
238         Returns a tuple:
239
240         ((arg1_name, arg2_name, ...),
241          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
242          (None, None, ..., optional1_default, optional2_default, ...))
243
244         That represents the minimum and maximum sets of arguments that
245         this function accepts and the defaults for the optional arguments.
246         """
247
248         # Inspect call. Remove self from the argument list.
249         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
250         defaults = self.call.func_defaults
251         if defaults is None:
252             defaults = ()
253
254         min_args = max_args[0:len(max_args) - len(defaults)]
255         defaults = tuple([None for arg in min_args]) + defaults
256         
257         return (min_args, max_args, defaults)
258
259     def type_check(self, name, value, expected, min = None, max = None):
260         """
261         Checks the type of the named value against the expected type,
262         which may be a Python type, a typed value, a Parameter, a
263         Mixed type, or a list or dictionary of possibly mixed types,
264         values, Parameters, or Mixed types.
265         
266         Extraneous members of lists must be of the same type as the
267         last specified type. For example, if the expected argument
268         type is [int, bool], then [1, False] and [14, True, False,
269         True] are valid, but [1], [False, 1] and [14, True, 1] are
270         not.
271
272         Extraneous members of dictionaries are ignored.
273         """
274
275         # If any of a number of types is acceptable
276         if isinstance(expected, Mixed):
277             for item in expected:
278                 try:
279                     self.type_check(name, value, item)
280                     expected = item
281                     break
282                 except PLCInvalidArgument, fault:
283                     pass
284             if expected != item:
285                 xmlrpc_types = [xmlrpc_type(item) for item in expected]
286                 raise PLCInvalidArgument("expected %s, got %s" % \
287                                          (" or ".join(xmlrpc_types),
288                                           xmlrpc_type(type(value))),
289                                          name)
290
291         # Get actual expected type from within the Parameter structure
292         elif isinstance(expected, Parameter):
293             min = expected.min
294             max = expected.max
295             expected = expected.type
296
297         expected_type = python_type(expected)
298
299         # Strings are a special case. Accept either unicode or str
300         # types if a string is expected.
301         if expected_type in StringTypes and isinstance(value, StringTypes):
302             pass
303
304         # Integers and long integers are also special types. Accept
305         # either int or long types if an int or long is expected.
306         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
307             pass
308
309         elif not isinstance(value, expected_type):
310             raise PLCInvalidArgument("expected %s, got %s" % \
311                                      (xmlrpc_type(expected_type),
312                                       xmlrpc_type(type(value))),
313                                      name)
314
315         # If a minimum or maximum (length, value) has been specified
316         if expected_type in StringTypes:
317             if min is not None and \
318                len(value.encode(self.api.encoding)) < min:
319                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
320             if max is not None and \
321                len(value.encode(self.api.encoding)) > max:
322                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
323         else:
324             if min is not None and value < min:
325                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
326             if max is not None and value > max:
327                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
328
329         # If a list with particular types of items is expected
330         if isinstance(expected, (list, tuple)):
331             for i in range(len(value)):
332                 if i >= len(expected):
333                     i = len(expected) - 1
334                 self.type_check(name + "[]", value[i], expected[i])
335
336         # If a struct with particular (or required) types of items is
337         # expected.
338         elif isinstance(expected, dict):
339             for key in value.keys():
340                 if key in expected:
341                     self.type_check(name + "['%s']" % key, value[key], expected[key])
342             for key, subparam in expected.iteritems():
343                 if isinstance(subparam, Parameter) and \
344                    not subparam.optional and key not in value.keys():
345                     raise PLCInvalidArgument("'%s' not specified" % key, name)
346
347 def python_type(arg):
348     """
349     Returns the Python type of the specified argument, which may be a
350     Python type, a typed value, or a Parameter.
351     """
352
353     if isinstance(arg, Parameter):
354         arg = arg.type
355
356     if isinstance(arg, type):
357         return arg
358     else:
359         return type(arg)
360
361 def xmlrpc_type(arg):
362     """
363     Returns the XML-RPC type of the specified argument, which may be a
364     Python type, a typed value, or a Parameter.
365     """
366
367     arg_type = python_type(arg)
368
369     if arg_type == NoneType:
370         return "nil"
371     elif arg_type == IntType or arg_type == LongType:
372         return "int"
373     elif arg_type == bool:
374         return "boolean"
375     elif arg_type == FloatType:
376         return "double"
377     elif arg_type in StringTypes:
378         return "string"
379     elif arg_type == ListType or arg_type == TupleType:
380         return "array"
381     elif arg_type == DictType:
382         return "struct"
383     elif arg_type == Mixed:
384         # Not really an XML-RPC type but return "mixed" for
385         # documentation purposes.
386         return "mixed"
387     else:
388         raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type