- removed anything having to with event_type/event_object
[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.18 2006/11/08 22:11:26 mlhuang 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:
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         # XXX object_ids is currently defined as a class variable
143         if hasattr(self, 'object_ids'):
144             for object_id in self.object_ids:
145                 event.add_object(object_id, commit = False)
146
147         # Commit
148         event.sync(commit = True)
149
150     def help(self, indent = "  "):
151         """
152         Text documentation for the method.
153         """
154
155         (min_args, max_args, defaults) = self.args()
156
157         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
158
159         text += "Description:\n\n"
160         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
161         text += "\n".join(lines) + "\n\n"
162
163         text += "Allowed Roles:\n\n"
164         if not self.roles:
165             roles = ["any"]
166         else:
167             roles = self.roles
168         text += indent + ", ".join(roles) + "\n\n"
169
170         def param_text(name, param, indent, step):
171             """
172             Format a method parameter.
173             """
174
175             text = indent
176
177             # Print parameter name
178             if name:
179                 param_offset = 32
180                 text += name.ljust(param_offset - len(indent))
181             else:
182                 param_offset = len(indent)
183
184             # Print parameter type
185             param_type = python_type(param)
186             text += xmlrpc_type(param_type) + "\n"
187
188             # Print parameter documentation right below type
189             if isinstance(param, Parameter):
190                 wrapper = textwrap.TextWrapper(width = 70,
191                                                initial_indent = " " * param_offset,
192                                                subsequent_indent = " " * param_offset)
193                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
194                 param = param.type
195
196             text += "\n"
197
198             # Indent struct fields and mixed types
199             if isinstance(param, dict):
200                 for name, subparam in param.iteritems():
201                     text += param_text(name, subparam, indent + step, step)
202             elif isinstance(param, Mixed):
203                 for subparam in param:
204                     text += param_text(name, subparam, indent + step, step)
205             elif isinstance(param, (list, tuple, set)):
206                 for subparam in param:
207                     text += param_text("", subparam, indent + step, step)
208
209             return text
210
211         text += "Parameters:\n\n"
212         for name, param in zip(max_args, self.accepts):
213             text += param_text(name, param, indent, indent)
214
215         text += "Returns:\n\n"
216         text += param_text("", self.returns, indent, indent)
217
218         return text
219
220     def args(self):
221         """
222         Returns a tuple:
223
224         ((arg1_name, arg2_name, ...),
225          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
226          (None, None, ..., optional1_default, optional2_default, ...))
227
228         That represents the minimum and maximum sets of arguments that
229         this function accepts and the defaults for the optional arguments.
230         """
231
232         # Inspect call. Remove self from the argument list.
233         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
234         defaults = self.call.func_defaults
235         if defaults is None:
236             defaults = ()
237
238         min_args = max_args[0:len(max_args) - len(defaults)]
239         defaults = tuple([None for arg in min_args]) + defaults
240         
241         return (min_args, max_args, defaults)
242
243     def type_check(self, name, value, expected, args):
244         """
245         Checks the type of the named value against the expected type,
246         which may be a Python type, a typed value, a Parameter, a
247         Mixed type, or a list or dictionary of possibly mixed types,
248         values, Parameters, or Mixed types.
249         
250         Extraneous members of lists must be of the same type as the
251         last specified type. For example, if the expected argument
252         type is [int, bool], then [1, False] and [14, True, False,
253         True] are valid, but [1], [False, 1] and [14, True, 1] are
254         not.
255
256         Extraneous members of dictionaries are ignored.
257         """
258
259         # If any of a number of types is acceptable
260         if isinstance(expected, Mixed):
261             for item in expected:
262                 try:
263                     self.type_check(name, value, item, args)
264                     return
265                 except PLCInvalidArgument, fault:
266                     pass
267             raise fault
268
269         # If an authentication structure is expected, save it and
270         # authenticate after basic type checking is done.
271         if isinstance(expected, Auth):
272             auth = expected
273         else:
274             auth = None
275
276         # Get actual expected type from within the Parameter structure
277         if isinstance(expected, Parameter):
278             min = expected.min
279             max = expected.max
280             nullok = expected.nullok
281             expected = expected.type
282         else:
283             min = None
284             max = None
285             nullok = False
286
287         expected_type = python_type(expected)
288
289         # If value can be NULL
290         if value is None and nullok:
291             return
292
293         # Strings are a special case. Accept either unicode or str
294         # types if a string is expected.
295         if expected_type in StringTypes and isinstance(value, StringTypes):
296             pass
297
298         # Integers and long integers are also special types. Accept
299         # either int or long types if an int or long is expected.
300         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
301             pass
302
303         elif not isinstance(value, expected_type):
304             raise PLCInvalidArgument("expected %s, got %s" % \
305                                      (xmlrpc_type(expected_type),
306                                       xmlrpc_type(type(value))),
307                                      name)
308
309         # If a minimum or maximum (length, value) has been specified
310         if expected_type in StringTypes:
311             if min is not None and \
312                len(value.encode(self.api.encoding)) < min:
313                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
314             if max is not None and \
315                len(value.encode(self.api.encoding)) > max:
316                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
317         elif expected_type in (list, tuple, set):
318             if min is not None and len(value) < min:
319                 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
320             if max is not None and len(value) > max:
321                 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
322         else:
323             if min is not None and value < min:
324                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
325             if max is not None and value > max:
326                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
327
328         # If a list with particular types of items is expected
329         if isinstance(expected, (list, tuple, set)):
330             for i in range(len(value)):
331                 if i >= len(expected):
332                     j = len(expected) - 1
333                 else:
334                     j = i
335                 self.type_check(name + "[]", value[i], expected[j], args)
336
337         # If a struct with particular (or required) types of items is
338         # expected.
339         elif isinstance(expected, dict):
340             for key in value.keys():
341                 if key in expected:
342                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
343             for key, subparam in expected.iteritems():
344                 if isinstance(subparam, Parameter) and \
345                    subparam.optional is not None and \
346                    not subparam.optional and key not in value.keys():
347                     raise PLCInvalidArgument("'%s' not specified" % key, name)
348
349         if auth is not None:
350             auth.check(self, *args)