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