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