Package pyamf :: Package remoting :: Package client
[hide private]
[frames] | no frames]

Source Code for Package pyamf.remoting.client

  1  # Copyright (c) 2007-2008 The PyAMF Project. 
  2  # See LICENSE for details. 
  3   
  4  """ 
  5  Remoting client implementation. 
  6   
  7  @author: U{Nick Joyce<mailto:nick@boxdesign.co.uk>} 
  8   
  9  @since: 0.1.0 
 10  """ 
 11   
 12  import httplib, urlparse 
 13   
 14  import pyamf 
 15  from pyamf import remoting, logging 
 16   
 17  #: Default AMF client type. 
 18  #: @see: L{ClientTypes<pyamf.ClientTypes>} 
 19  DEFAULT_CLIENT_TYPE = pyamf.ClientTypes.Flash6 
 20   
 21  HTTP_OK = 200 
 22   
23 -def convert_args(args):
24 if args == (tuple(),): 25 return [] 26 else: 27 return [x for x in args]
28
29 -class ServiceMethodProxy(object):
30 """ 31 Serves as a proxy for calling a service method. 32 33 @ivar service: The parent service. 34 @type service: L{ServiceProxy} 35 @ivar name: The name of the method. 36 @type name: C{str} or C{None} 37 38 @see: L{ServiceProxy.__getattr__} 39 """ 40
41 - def __init__(self, service, name):
42 self.service = service 43 self.name = name
44
45 - def __call__(self, *args):
46 """ 47 Inform the proxied service that this function has been called. 48 """ 49 return self.service._call(self, *args)
50
51 - def __str__(self):
52 """ 53 Returns the full service name, including the method name if there is 54 one. 55 """ 56 service_name = str(self.service) 57 58 if self.name is not None: 59 service_name = '%s.%s' % (service_name, self.name) 60 61 return service_name
62
63 -class ServiceProxy(object):
64 """ 65 Serves as a service object proxy for RPC calls. Generates 66 L{ServiceMethodProxy} objects for method calls. 67 68 @see: L{RequestWrapper} for more info. 69 70 @ivar _gw: The parent gateway 71 @type _gw: L{RemotingService} 72 @ivar _name: The name of the service 73 @type _name: C{str} 74 @ivar _auto_execute: If set to C{True}, when a service method is called, 75 the AMF request is immediately sent to the remote gateway and a 76 response is returned. If set to C{False}, a L{RequestWrapper} is 77 returned, waiting for the underlying gateway to fire the 78 L{execute<RemotingService.execute>} method. 79 """ 80
81 - def __init__(self, gw, name, auto_execute=True):
82 self._gw = gw 83 self._name = name 84 self._auto_execute = auto_execute
85
86 - def __getattr__(self, name):
87 return ServiceMethodProxy(self, name)
88
89 - def _call(self, method_proxy, *args):
90 """ 91 Executed when a L{ServiceMethodProxy} is called. Adds a request to the 92 underlying gateway. If C{_auto_execute} is set to C{True}, then the 93 request is immediately called on the remote gateway. 94 """ 95 96 request = self._gw.addRequest(method_proxy, *args) 97 98 if self._auto_execute: 99 response = self._gw.execute_single(request) 100 101 # XXX nick: What to do about Fault objects here? 102 return response.body 103 104 return request
105
106 - def __call__(self, *args):
107 """ 108 This allows services to be 'called' without a method name. 109 """ 110 return self._call(ServiceMethodProxy(self, None), *args)
111
112 - def __str__(self):
113 """ 114 Returns a string representation of the name of the service. 115 """ 116 return self._name
117
118 -class RequestWrapper(object):
119 """ 120 A container object that wraps a service method request. 121 122 @ivar gw: The underlying gateway. 123 @type gw: L{RemotingService} 124 @ivar id: The id of the request. 125 @type id: C{str} 126 @ivar service: The service proxy. 127 @type service: L{ServiceProxy} 128 @ivar args: The args used to invoke the call. 129 @type args: C{list} 130 """ 131
132 - def __init__(self, gw, id_, service, *args):
133 self.gw = gw 134 self.id = id_ 135 self.service = service 136 self.args = args
137
138 - def __str__(self):
139 return str(self.id)
140
141 - def setResponse(self, response):
142 """ 143 A response has been received by the gateway 144 """ 145 # XXX nick: What to do about Fault objects here? 146 self.response = response 147 self.result = self.response.body 148 149 if isinstance(self.result, remoting.ErrorFault): 150 self.result.raiseException()
151
152 - def _get_result(self):
153 """ 154 Returns the result of the called remote request. If the request has not 155 yet been called, an exception is raised. 156 """ 157 if not hasattr(self, '_result'): 158 raise AttributeError, "'RequestWrapper' object has no attribute 'result'" 159 160 return self._result
161
162 - def _set_result(self, result):
163 self._result = result
164 165 result = property(_get_result, _set_result)
166
167 -class RemotingService(object):
168 """ 169 Acts as a client for AMF calls. 170 171 @ivar url: The url of the remote gateway. Accepts C{http} or C{https} as schemes. 172 @type url: C{str} 173 @ivar requests: The list of pending requests to process. 174 @type requests: C{list} 175 @ivar request_number: A unique identifier for an tracking the number of 176 requests. 177 @ivar amf_version: The AMF version to use. 178 See L{ENCODING_TYPES<pyamf.ENCODING_TYPES>}. 179 @type amf_version: C{int} 180 @ivar client_type: The client type. See L{ClientTypes<pyamf.ClientTypes>}. 181 @ivar connection: The underlying connection to the remoting server. 182 @type connection: C{httplib.HTTPConnection} or C{httplib.HTTPSConnection} 183 @ivar headers: A list of persistent headers to send with each request. 184 @type headers: L{HeaderCollection<pyamf.remoting.HeaderCollection>} 185 """ 186
187 - def __init__(self, url, amf_version=pyamf.AMF0, client_type=DEFAULT_CLIENT_TYPE):
188 self.logger = logging.instance_logger(self) 189 self.original_url = url 190 self.requests = [] 191 self.request_number = 1 192 193 self.amf_version = amf_version 194 self.client_type = client_type 195 self.headers = remoting.HeaderCollection() 196 197 self._setUrl(url)
198
199 - def _setUrl(self, url):
200 self.url = urlparse.urlparse(url) 201 self._root_url = urlparse.urlunparse(['', ''] + list(self.url[2:])) 202 203 port = None 204 hostname = None 205 206 if hasattr(self.url, 'port'): 207 if self.url.port is not None: 208 port = self.url.port 209 else: 210 if ':' not in self.url[1]: 211 hostname = self.url[1] 212 port = None 213 else: 214 sp = self.url[1].split(':') 215 216 hostname, port = sp[0], sp[1] 217 port = int(port) 218 219 if hostname is None: 220 if hasattr(self.url, 'hostname'): 221 hostname = self.url.hostname 222 223 if self.url[0] == 'http': 224 if port is None: 225 port = httplib.HTTP_PORT 226 227 self.connection = httplib.HTTPConnection(hostname, port) 228 elif self.url[0] == 'https': 229 if port is None: 230 port = httplib.HTTPS_PORT 231 232 self.connection = httplib.HTTPSConnection(hostname, port) 233 else: 234 raise ValueError, 'Unknown scheme' 235 236 self.logger.debug('creating connection to %s://%s:%s' % (self.url[0], hostname, port))
237
238 - def addHeader(self, name, value, must_understand=False):
239 """ 240 Sets a persistent header to send with each request. 241 """ 242 self.headers[name] = value 243 self.headers.set_required(name, must_understand)
244
245 - def getService(self, name, auto_execute=True):
246 """ 247 Returns a L{ServiceProxy} for the supplied name. Sets up an object that 248 can have method calls made to it that build the AMF requests. 249 250 @raise TypeError: C{string} type required for C{name}. 251 @rtype: L{ServiceProxy} 252 """ 253 if not isinstance(name, basestring): 254 raise TypeError, 'string type required' 255 256 return ServiceProxy(self, name, auto_execute)
257
258 - def getRequest(self, id_):
259 """ 260 Gets a request based on the id. 261 262 @raise LookupError: Request not found. 263 """ 264 for request in self.requests: 265 if request.id == id_: 266 return request 267 268 raise LookupError, "request %s not found" % id_
269
270 - def addRequest(self, service, *args):
271 """ 272 Adds a request to be sent to the remoting gateway. 273 """ 274 wrapper = RequestWrapper(self, '/%d' % self.request_number, 275 service, *args) 276 277 self.request_number += 1 278 self.requests.append(wrapper) 279 self.logger.debug('adding request %s%r' % (wrapper.service, args)) 280 281 return wrapper
282
283 - def removeRequest(self, service, *args):
284 """ 285 Removes a request from the pending request list. 286 287 @raise LookupError: Request not found. 288 """ 289 if isinstance(service, RequestWrapper): 290 del self.requests[self.requests.index(service)] 291 292 return 293 294 for request in self.requests: 295 if request.service == service and request.args == args: 296 del self.requests[self.requests.index(request)] 297 298 return 299 300 raise LookupError, "request not found"
301
302 - def getAMFRequest(self, requests):
303 """ 304 Builds an AMF request L{envelope<pyamf.remoting.Envelope>} from a 305 supplied list of requests. 306 307 @param requests: List of requests 308 @type requests: C{list} 309 @rtype: L{Envelope<pyamf.remoting.Envelope>} 310 """ 311 envelope = remoting.Envelope(self.amf_version, self.client_type) 312 313 for request in requests: 314 service = request.service 315 args = list(request.args) 316 317 envelope[request.id] = remoting.Request(str(service), args) 318 319 envelope.headers = self.headers 320 321 return envelope
322
323 - def execute_single(self, request):
324 """ 325 Builds, sends and handles the response to a single request, returning 326 the response. 327 328 @param request: 329 @type request: 330 @rtype: 331 """ 332 self.logger.debug('executing single request') 333 body = remoting.encode(self.getAMFRequest([request])) 334 335 self.logger.debug('sending POST request to %s' % self._root_url) 336 self.connection.request('POST', self._root_url, body.getvalue()) 337 338 envelope = self._getResponse() 339 self.removeRequest(request) 340 341 return envelope[request.id]
342
343 - def execute(self):
344 """ 345 Builds, sends and handles the responses to all requests listed in 346 C{self.requests}. 347 """ 348 body = remoting.encode(self.getAMFRequest(self.requests)) 349 350 self.connection.request('POST', self._root_url, body.getvalue()) 351 352 envelope = self._getResponse() 353 354 for response in envelope: 355 request = self.getRequest(response[0]) 356 response = response[1] 357 358 request.setResponse(response) 359 360 self.removeRequest(request)
361
362 - def _getResponse(self):
363 """ 364 Gets and handles the HTTP response from the remote gateway. 365 """ 366 self.logger.debug('waiting for response ...') 367 http_response = self.connection.getresponse() 368 self.logger.debug('got response status=%s' % http_response.status) 369 370 if http_response.status != HTTP_OK: 371 self.logger.debug('content-type = %s' % http_response.getheader('Content-Type')) 372 self.logger.debug('body = %s' % http_response.read()) 373 374 if hasattr(httplib, 'responses'): 375 raise remoting.RemotingError, "HTTP Gateway reported status %d %s" % ( 376 http_response.status, httplib.responses[http_response.status]) 377 378 raise remoting.RemotingError, "HTTP Gateway reported status %d" % ( 379 http_response.status,) 380 381 content_type = http_response.getheader('Content-Type') 382 383 if content_type != remoting.CONTENT_TYPE: 384 self.logger.debug('content-type = %s' % http_response.getheader('Content-Type')) 385 self.logger.debug('body = %s' % http_response.read()) 386 387 raise remoting.RemotingError, "Incorrect MIME type received. (got: %s)" % content_type 388 389 content_length = http_response.getheader('Content-Length') 390 bytes = '' 391 392 if content_length is None: 393 bytes = http_response.read() 394 else: 395 bytes = http_response.read(content_length) 396 397 self.logger.debug('read %d bytes for the response' % len(bytes)) 398 399 response = remoting.decode(bytes) 400 self.logger.debug('response = %s' % response) 401 402 if remoting.APPEND_TO_GATEWAY_URL in response.headers: 403 self.original_url += response.headers[remoting.APPEND_TO_GATEWAY_URL] 404 405 self._setUrl(self.original_url) 406 elif remoting.REPLACE_GATEWAY_URL in response.headers: 407 self.original_url = response.headers[remoting.REPLACE_GATEWAY_URL] 408 409 self._setUrl(self.original_url) 410 411 if remoting.REQUEST_PERSISTENT_HEADER in response.headers: 412 data = response.headers[remoting.REQUEST_PERSISTENT_HEADER] 413 414 for k, v in data.iteritems(): 415 self.headers[k] = v 416 417 http_response.close() 418 419 return response
420
421 - def setCredentials(self, username, password):
422 """ 423 Sets authentication credentials for accessing the remote gateway. 424 """ 425 self.addHeader('Credentials', dict(userid=unicode(username), 426 password=unicode(password)), True)
427