7329036e400a55b03c3ff5f777eebc9124c19c80
[sfa.git] / sfa / util / method.py
1 #
2 # Base class for all SfaAPI functions
3 #
4 #
5
6 import time
7 import textwrap
8
9 from sfa.util.sfalogging import logger
10 from sfa.util.faults import SfaFault, SfaInvalidAPIMethod, SfaInvalidArgumentCount, SfaInvalidArgument
11
12 from sfa.storage.parameter import Parameter, Mixed, python_type, xmlrpc_type
13
14
15 class Method:
16     """
17     Base class for all SfaAPI functions. At a minimum, all SfaAPI
18     functions must define:
19
20     interfaces = [allowed interfaces]
21     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
22     returns = Parameter(return_type, return_doc)
23     call(arg1, arg2, ...): method body
24
25     Argument types may be Python types (e.g., int, bool, etc.), typed
26     values (e.g., 1, True, etc.), a Parameter, or lists or
27     dictionaries of possibly mixed types, values, and/or Parameters
28     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
29
30     Once function decorators in Python 2.4 are fully supported,
31     consider wrapping calls with accepts() and returns() functions
32     instead of performing type checking manually.
33     """
34
35     interfaces = []
36     accepts = []
37     returns = bool
38     status = "current"
39
40     def call(self, *args):
41         """
42         Method body for all SfaAPI functions. Must override.
43         """
44         return None
45
46     def __init__(self, api):
47         self.name = self.__class__.__name__
48         self.api = api
49
50         # Auth may set this to a Person instance (if an anonymous
51         # method, will remain None).
52         self.caller = None
53
54         # API may set this to a (addr, port) tuple if known
55         self.source = None
56
57     def __call__(self, *args, **kwds):
58         """
59         Main entry point for all SFA API functions. Type checks
60         arguments, authenticates, and executes call().
61         """
62
63         try:
64             start = time.time()
65             methodname = self.name
66             if not self.api.interface or self.api.interface not in self.interfaces:
67                 raise SfaInvalidAPIMethod(methodname, self.api.interface)
68
69             (min_args, max_args, defaults) = self.args()
70
71             # Check that the right number of arguments were passed in
72             if len(args) < len(min_args) or len(args) > len(max_args):
73                 raise SfaInvalidArgumentCount(
74                     len(args), len(min_args), len(max_args))
75
76             for name, value, expected in zip(max_args, args, self.accepts):
77                 self.type_check(name, value, expected, args)
78
79             logger.debug("method.__call__ [%s] : BEG %s" % (
80                 self.api.interface, methodname))
81             result = self.call(*args, **kwds)
82
83             runtime = time.time() - start
84             logger.debug("method.__call__ [%s] : END %s in %02f s (%s)" %
85                          (self.api.interface, methodname, runtime, getattr(self, 'message', "[no-msg]")))
86
87             return result
88
89         except SfaFault as fault:
90
91             caller = ""
92
93             # Prepend caller and method name to expected faults
94             fault.faultString = caller + ": " + self.name + ": " + fault.faultString
95             runtime = time.time() - start
96             logger.log_exc("Method %s raised an exception" % self.name)
97             raise fault
98
99     def help(self, indent="  "):
100         """
101         Text documentation for the method.
102         """
103
104         (min_args, max_args, defaults) = self.args()
105
106         text = "%s(%s) -> %s\n\n" % (self.name,
107                                      ", ".join(max_args), xmlrpc_type(self.returns))
108
109         text += "Description:\n\n"
110         lines = [indent + line.strip()
111                  for line in self.__doc__.strip().split("\n")]
112         text += "\n".join(lines) + "\n\n"
113
114         def param_text(name, param, indent, step):
115             """
116             Format a method parameter.
117             """
118
119             text = indent
120
121             # Print parameter name
122             if name:
123                 param_offset = 32
124                 text += name.ljust(param_offset - len(indent))
125             else:
126                 param_offset = len(indent)
127
128             # Print parameter type
129             param_type = python_type(param)
130             text += xmlrpc_type(param_type) + "\n"
131
132             # Print parameter documentation right below type
133             if isinstance(param, Parameter):
134                 wrapper = textwrap.TextWrapper(width=70,
135                                                initial_indent=" " * param_offset,
136                                                subsequent_indent=" " * param_offset)
137                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
138                 param = param.type
139
140             text += "\n"
141
142             # Indent struct fields and mixed types
143             if isinstance(param, dict):
144                 for name, subparam in param.items():
145                     text += param_text(name, subparam, indent + step, step)
146             elif isinstance(param, Mixed):
147                 for subparam in param:
148                     text += param_text(name, subparam, indent + step, step)
149             elif isinstance(param, (list, tuple, set)):
150                 for subparam in param:
151                     text += param_text("", subparam, indent + step, step)
152
153             return text
154
155         text += "Parameters:\n\n"
156         for name, param in zip(max_args, self.accepts):
157             text += param_text(name, param, indent, indent)
158
159         text += "Returns:\n\n"
160         text += param_text("", self.returns, indent, indent)
161
162         return text
163
164     def args(self):
165         """
166         Returns a tuple:
167
168         ((arg1_name, arg2_name, ...),
169          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
170          (None, None, ..., optional1_default, optional2_default, ...))
171
172         That represents the minimum and maximum sets of arguments that
173         this function accepts and the defaults for the optional arguments.
174         """
175
176         # Inspect call. Remove self from the argument list.
177         max_args = self.call.__code__.co_varnames[
178             1:self.call.__code__.co_argcount]
179         defaults = self.call.__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 as 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 issubclass(expected_type, str) and isinstance(value, str):
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 is int and isinstance(value, int):
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 issubclass(expected_type, str):
256             if min is not None and \
257                len(value.encode(self.api.encoding)) < min:
258                 raise SfaInvalidArgument(
259                     "%s must be at least %d bytes long" % (name, min))
260             if max is not None and \
261                len(value.encode(self.api.encoding)) > max:
262                 raise SfaInvalidArgument(
263                     "%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(
267                     "%s must contain at least %d items" % (name, min))
268             if max is not None and len(value) > max:
269                 raise SfaInvalidArgument(
270                     "%s must contain at most %d items" % (name, max))
271         else:
272             if min is not None and value < min:
273                 raise SfaInvalidArgument("%s must be > %s" % (name, str(min)))
274             if max is not None and value > max:
275                 raise SfaInvalidArgument("%s must be < %s" % (name, str(max)))
276
277         # If a list with particular types of items is expected
278         if isinstance(expected, (list, tuple, set)):
279             for i in range(len(value)):
280                 if i >= len(expected):
281                     j = len(expected) - 1
282                 else:
283                     j = i
284                 self.type_check(name + "[]", value[i], expected[j], args)
285
286         # If a struct with particular (or required) types of items is
287         # expected.
288         elif isinstance(expected, dict):
289             for key in list(value.keys()):
290                 if key in expected:
291                     self.type_check(name + "['%s']" %
292                                     key, value[key], expected[key], args)
293             for key, subparam in expected.items():
294                 if isinstance(subparam, Parameter) and \
295                    subparam.optional is not None and \
296                    not subparam.optional and key not in list(value.keys()):
297                     raise SfaInvalidArgument("'%s' not specified" % key, name)
298
299         # if auth is not None:
300         #    auth.check(self, *args)