- add **kwds paramater to __call__()
[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.6 2006/10/17 15:28:39 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             (min_args, max_args, defaults) = self.args()
78                                 
79             # Check that the right number of arguments were passed in
80             if len(args) < len(min_args) or len(args) > len(max_args):
81                 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
82
83             for name, value, expected in zip(max_args, args, self.accepts):
84                 self.type_check(name, value, expected)
85         
86             # The first argument to all methods that require
87             # authentication, should be an Auth structure. The rest of the
88             # arguments to the call may also be used in the authentication
89             # check. For example, calls made by the Boot Manager are
90             # verified by comparing a hash of the message parameters to
91             # the value in the authentication structure.        
92
93             if len(self.accepts):
94                 auth = None
95                 if isinstance(self.accepts[0], Auth):
96                     auth = self.accepts[0]
97                 elif isinstance(self.accepts[0], Mixed):
98                     for auth in self.accepts[0]:
99                         if isinstance(auth, Auth):
100                             break
101                 if isinstance(auth, Auth):
102                     auth.check(self, *args)
103
104             start = time.time() 
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 = 0
129         object_ids = []
130         call_name = self.name
131         call_args = ", ".join([unicode(arg) for arg in list(args)[1:]]).replace('\'', '\\\'')
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         # get next event_id
151         rows = self.api.db.selectall("SELECT nextval('events_event_id_seq')", hashref = False)
152         event_id =  rows[0][0]
153         
154         sql_event = "INSERT INTO events " \
155               " (event_id, person_id, event_type, object_type, fault_code, call, runtime) VALUES" \
156               " (%(event_id)d, %(person_id)d, '%(event_type)s', '%(object_type)s'," \
157               "  %(fault_code)d, '%(call)s', %(runtime)f)" %  \
158               (locals())
159         self.api.db.do(sql_event)
160                         
161         # log objects affected
162         for object_id in object_ids:
163                 sql_objects = "INSERT INTO event_object (event_id, object_id) VALUES" \
164                         " (%(event_id)d, %(object_id)d) "  % (locals()) 
165                 self.api.db.do(sql_objects)
166                         
167         self.api.db.commit()            
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