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