* tried to put some sense in the way things get logged, at least on server-side for now
[sfa.git] / sfa / util / method.py
1 #
2 # Base class for all SfaAPI functions
3 #
4 #
5
6 ### $Id$
7 ### $URL$
8
9 import os, time
10 from types import *
11 from types import StringTypes
12 import traceback
13 import textwrap
14 import xmlrpclib
15
16
17 from sfa.util.sfalogging import sfa_logger
18 from sfa.util.faults import * 
19 from sfa.util.parameter import Parameter, Mixed, python_type, xmlrpc_type
20 from sfa.trust.auth import Auth
21
22 # we inherit object because we use new-style classes for legacy methods
23 class Method (object):
24     """
25     Base class for all SfaAPI functions. At a minimum, all SfaAPI
26     functions must define:
27
28     interfaces = [allowed interfaces]
29     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
30     returns = Parameter(return_type, return_doc)
31     call(arg1, arg2, ...): method body
32
33     Argument types may be Python types (e.g., int, bool, etc.), typed
34     values (e.g., 1, True, etc.), a Parameter, or lists or
35     dictionaries of possibly mixed types, values, and/or Parameters
36     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
37
38     Once function decorators in Python 2.4 are fully supported,
39     consider wrapping calls with accepts() and returns() functions
40     instead of performing type checking manually.
41     """
42
43     interfaces = []
44     accepts = []
45     returns = bool
46     status = "current"
47
48     def call(self, *args):
49         """
50         Method body for all SfaAPI functions. Must override.
51
52         """
53
54         return True
55
56     def __init__(self, api):
57         self.name = self.__class__.__name__
58         self.api = api
59
60         # Auth may set this to a Person instance (if an anonymous
61         # method, will remain None).
62         self.caller = None
63
64         # API may set this to a (addr, port) tuple if known
65         self.source = None
66         
67     def __call__(self, *args, **kwds):
68         """
69         Main entry point for all SfaAPI functions. Type checks
70         arguments, authenticates, and executes call().
71         """
72
73         try:
74             start = time.time()
75             methodname = self.name
76             if not self.api.interface or self.api.interface not in self.interfaces:
77                 raise SfaInvalidAPIMethod(methodname, self.api.interface) 
78
79             # legacy code cannot be type-checked, due to the way Method.args() works
80             if not hasattr(self,"skip_typecheck"):
81                 (min_args, max_args, defaults) = self.args()
82                                 
83                 # Check that the right number of arguments were passed in
84                 if len(args) < len(min_args) or len(args) > len(max_args):
85                     raise SfaInvalidArgumentCount(len(args), len(min_args), len(max_args))
86
87                 for name, value, expected in zip(max_args, args, self.accepts):
88                     self.type_check(name, value, expected, args)
89
90             result = self.call(*args, **kwds)
91             runtime = time.time() - start
92
93             if self.api.config.SFA_API_DEBUG or hasattr(self, 'message'):
94                 msg=getattr(self,'message',"method %s completed"%methodname)
95                 sfa_logger.info(msg)
96
97             return result
98
99         except SfaFault, fault:
100
101             caller = ""
102
103             # Prepend caller and method name to expected faults
104             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
105             runtime = time.time() - start
106             sfa_logger.log_exc("Method %s raised an exception"%self.name) 
107             raise fault
108
109
110     def help(self, indent = "  "):
111         """
112         Text documentation for the method.
113         """
114
115         (min_args, max_args, defaults) = self.args()
116
117         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
118
119         text += "Description:\n\n"
120         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
121         text += "\n".join(lines) + "\n\n"
122
123         def param_text(name, param, indent, step):
124             """
125             Format a method parameter.
126             """
127
128             text = indent
129
130             # Print parameter name
131             if name:
132                 param_offset = 32
133                 text += name.ljust(param_offset - len(indent))
134             else:
135                 param_offset = len(indent)
136
137             # Print parameter type
138             param_type = python_type(param)
139             text += xmlrpc_type(param_type) + "\n"
140
141             # Print parameter documentation right below type
142             if isinstance(param, Parameter):
143                 wrapper = textwrap.TextWrapper(width = 70,
144                                                initial_indent = " " * param_offset,
145                                                subsequent_indent = " " * param_offset)
146                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
147                 param = param.type
148
149             text += "\n"
150
151             # Indent struct fields and mixed types
152             if isinstance(param, dict):
153                 for name, subparam in param.iteritems():
154                     text += param_text(name, subparam, indent + step, step)
155             elif isinstance(param, Mixed):
156                 for subparam in param:
157                     text += param_text(name, subparam, indent + step, step)
158             elif isinstance(param, (list, tuple, set)):
159                 for subparam in param:
160                     text += param_text("", subparam, indent + step, step)
161
162             return text
163
164         text += "Parameters:\n\n"
165         for name, param in zip(max_args, self.accepts):
166             text += param_text(name, param, indent, indent)
167
168         text += "Returns:\n\n"
169         text += param_text("", self.returns, indent, indent)
170
171         return text
172
173     def args(self):
174         """
175         Returns a tuple:
176
177         ((arg1_name, arg2_name, ...),
178          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
179          (None, None, ..., optional1_default, optional2_default, ...))
180
181         That represents the minimum and maximum sets of arguments that
182         this function accepts and the defaults for the optional arguments.
183         """
184         
185         # Inspect call. Remove self from the argument list.
186         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
187         defaults = self.call.func_defaults
188         if defaults is None:
189             defaults = ()
190
191         min_args = max_args[0:len(max_args) - len(defaults)]
192         defaults = tuple([None for arg in min_args]) + defaults
193         
194         return (min_args, max_args, defaults)
195
196     def type_check(self, name, value, expected, args):
197         """
198         Checks the type of the named value against the expected type,
199         which may be a Python type, a typed value, a Parameter, a
200         Mixed type, or a list or dictionary of possibly mixed types,
201         values, Parameters, or Mixed types.
202         
203         Extraneous members of lists must be of the same type as the
204         last specified type. For example, if the expected argument
205         type is [int, bool], then [1, False] and [14, True, False,
206         True] are valid, but [1], [False, 1] and [14, True, 1] are
207         not.
208
209         Extraneous members of dictionaries are ignored.
210         """
211
212         # If any of a number of types is acceptable
213         if isinstance(expected, Mixed):
214             for item in expected:
215                 try:
216                     self.type_check(name, value, item, args)
217                     return
218                 except SfaInvalidArgument, fault:
219                     pass
220             raise fault
221
222         # If an authentication structure is expected, save it and
223         # authenticate after basic type checking is done.
224         #if isinstance(expected, Auth):
225         #    auth = expected
226         #else:
227         #    auth = None
228
229         # Get actual expected type from within the Parameter structure
230         if isinstance(expected, Parameter):
231             min = expected.min
232             max = expected.max
233             nullok = expected.nullok
234             expected = expected.type
235         else:
236             min = None
237             max = None
238             nullok = False
239
240         expected_type = python_type(expected)
241
242         # If value can be NULL
243         if value is None and nullok:
244             return
245
246         # Strings are a special case. Accept either unicode or str
247         # types if a string is expected.
248         if expected_type in StringTypes and isinstance(value, StringTypes):
249             pass
250
251         # Integers and long integers are also special types. Accept
252         # either int or long types if an int or long is expected.
253         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
254             pass
255
256         elif not isinstance(value, expected_type):
257             raise SfaInvalidArgument("expected %s, got %s" % \
258                                      (xmlrpc_type(expected_type),
259                                       xmlrpc_type(type(value))),
260                                      name)
261
262         # If a minimum or maximum (length, value) has been specified
263         if expected_type in StringTypes:
264             if min is not None and \
265                len(value.encode(self.api.encoding)) < min:
266                 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
267             if max is not None and \
268                len(value.encode(self.api.encoding)) > max:
269                 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
270         elif expected_type in (list, tuple, set):
271             if min is not None and len(value) < min:
272                 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
273             if max is not None and len(value) > max:
274                 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
275         else:
276             if min is not None and value < min:
277                 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
278             if max is not None and value > max:
279                 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
280
281         # If a list with particular types of items is expected
282         if isinstance(expected, (list, tuple, set)):
283             for i in range(len(value)):
284                 if i >= len(expected):
285                     j = len(expected) - 1
286                 else:
287                     j = i
288                 self.type_check(name + "[]", value[i], expected[j], args)
289
290         # If a struct with particular (or required) types of items is
291         # expected.
292         elif isinstance(expected, dict):
293             for key in value.keys():
294                 if key in expected:
295                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
296             for key, subparam in expected.iteritems():
297                 if isinstance(subparam, Parameter) and \
298                    subparam.optional is not None and \
299                    not subparam.optional and key not in value.keys():
300                     raise SfaInvalidArgument("'%s' not specified" % key, name)
301
302         #if auth is not None:
303         #    auth.check(self, *args)