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