4c37c6766c1d32f30f78aded391921f4c1fa1817
[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 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             if self.api.config.SFA_API_DEBUG:
83                 logger.debug("method.__call__ [%s] : BEG %s"%(self.api.interface,methodname))
84             result = self.call(*args, **kwds)
85
86             runtime = time.time() - start
87             if self.api.config.SFA_API_DEBUG or hasattr(self, 'message'):
88                 logger.debug("method.__call__ [%s] : END %s in %02f s (%s)"%\
89                                        (self.api.interface,methodname,runtime,getattr(self,'message',"[no-msg]")))
90
91             return result
92
93         except SfaFault, fault:
94
95             caller = ""
96
97             # Prepend caller and method name to expected faults
98             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
99             runtime = time.time() - start
100             logger.log_exc("Method %s raised an exception"%self.name) 
101             raise fault
102
103
104     def help(self, indent = "  "):
105         """
106         Text documentation for the method.
107         """
108
109         (min_args, max_args, defaults) = self.args()
110
111         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
112
113         text += "Description:\n\n"
114         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
115         text += "\n".join(lines) + "\n\n"
116
117         def param_text(name, param, indent, step):
118             """
119             Format a method parameter.
120             """
121
122             text = indent
123
124             # Print parameter name
125             if name:
126                 param_offset = 32
127                 text += name.ljust(param_offset - len(indent))
128             else:
129                 param_offset = len(indent)
130
131             # Print parameter type
132             param_type = python_type(param)
133             text += xmlrpc_type(param_type) + "\n"
134
135             # Print parameter documentation right below type
136             if isinstance(param, Parameter):
137                 wrapper = textwrap.TextWrapper(width = 70,
138                                                initial_indent = " " * param_offset,
139                                                subsequent_indent = " " * param_offset)
140                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
141                 param = param.type
142
143             text += "\n"
144
145             # Indent struct fields and mixed types
146             if isinstance(param, dict):
147                 for name, subparam in param.iteritems():
148                     text += param_text(name, subparam, indent + step, step)
149             elif isinstance(param, Mixed):
150                 for subparam in param:
151                     text += param_text(name, subparam, indent + step, step)
152             elif isinstance(param, (list, tuple, set)):
153                 for subparam in param:
154                     text += param_text("", subparam, indent + step, step)
155
156             return text
157
158         text += "Parameters:\n\n"
159         for name, param in zip(max_args, self.accepts):
160             text += param_text(name, param, indent, indent)
161
162         text += "Returns:\n\n"
163         text += param_text("", self.returns, indent, indent)
164
165         return text
166
167     def args(self):
168         """
169         Returns a tuple:
170
171         ((arg1_name, arg2_name, ...),
172          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
173          (None, None, ..., optional1_default, optional2_default, ...))
174
175         That represents the minimum and maximum sets of arguments that
176         this function accepts and the defaults for the optional arguments.
177         """
178         
179         # Inspect call. Remove self from the argument list.
180         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
181         defaults = self.call.func_defaults
182         if defaults is None:
183             defaults = ()
184
185         min_args = max_args[0:len(max_args) - len(defaults)]
186         defaults = tuple([None for arg in min_args]) + defaults
187         
188         return (min_args, max_args, defaults)
189
190     def type_check(self, name, value, expected, args):
191         """
192         Checks the type of the named value against the expected type,
193         which may be a Python type, a typed value, a Parameter, a
194         Mixed type, or a list or dictionary of possibly mixed types,
195         values, Parameters, or Mixed types.
196         
197         Extraneous members of lists must be of the same type as the
198         last specified type. For example, if the expected argument
199         type is [int, bool], then [1, False] and [14, True, False,
200         True] are valid, but [1], [False, 1] and [14, True, 1] are
201         not.
202
203         Extraneous members of dictionaries are ignored.
204         """
205
206         # If any of a number of types is acceptable
207         if isinstance(expected, Mixed):
208             for item in expected:
209                 try:
210                     self.type_check(name, value, item, args)
211                     return
212                 except SfaInvalidArgument, fault:
213                     pass
214             raise fault
215
216         # If an authentication structure is expected, save it and
217         # authenticate after basic type checking is done.
218         #if isinstance(expected, Auth):
219         #    auth = expected
220         #else:
221         #    auth = None
222
223         # Get actual expected type from within the Parameter structure
224         if isinstance(expected, Parameter):
225             min = expected.min
226             max = expected.max
227             nullok = expected.nullok
228             expected = expected.type
229         else:
230             min = None
231             max = None
232             nullok = False
233
234         expected_type = python_type(expected)
235
236         # If value can be NULL
237         if value is None and nullok:
238             return
239
240         # Strings are a special case. Accept either unicode or str
241         # types if a string is expected.
242         if expected_type in StringTypes and isinstance(value, StringTypes):
243             pass
244
245         # Integers and long integers are also special types. Accept
246         # either int or long types if an int or long is expected.
247         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
248             pass
249
250         elif not isinstance(value, expected_type):
251             raise SfaInvalidArgument("expected %s, got %s" % \
252                                      (xmlrpc_type(expected_type),
253                                       xmlrpc_type(type(value))),
254                                      name)
255
256         # If a minimum or maximum (length, value) has been specified
257         if expected_type in StringTypes:
258             if min is not None and \
259                len(value.encode(self.api.encoding)) < min:
260                 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
261             if max is not None and \
262                len(value.encode(self.api.encoding)) > max:
263                 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
264         elif expected_type in (list, tuple, set):
265             if min is not None and len(value) < min:
266                 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
267             if max is not None and len(value) > max:
268                 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
269         else:
270             if min is not None and value < min:
271                 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
272             if max is not None and value > max:
273                 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
274
275         # If a list with particular types of items is expected
276         if isinstance(expected, (list, tuple, set)):
277             for i in range(len(value)):
278                 if i >= len(expected):
279                     j = len(expected) - 1
280                 else:
281                     j = i
282                 self.type_check(name + "[]", value[i], expected[j], args)
283
284         # If a struct with particular (or required) types of items is
285         # expected.
286         elif isinstance(expected, dict):
287             for key in value.keys():
288                 if key in expected:
289                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
290             for key, subparam in expected.iteritems():
291                 if isinstance(subparam, Parameter) and \
292                    subparam.optional is not None and \
293                    not subparam.optional and key not in value.keys():
294                     raise SfaInvalidArgument("'%s' not specified" % key, name)
295
296         #if auth is not None:
297         #    auth.check(self, *args)