PageRenderTime 61ms CodeModel.GetById 19ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 0ms

/atom/service.py

http://radioappz.googlecode.com/
Python | 740 lines | 681 code | 16 blank | 43 comment | 6 complexity | 4dfc1acbbc1a42a1d4226c2edf18155e MD5 | raw file
  1#!/usr/bin/python
  2#
  3# Copyright (C) 2006, 2007, 2008 Google Inc.
  4#
  5# Licensed under the Apache License, Version 2.0 (the "License");
  6# you may not use this file except in compliance with the License.
  7# You may obtain a copy of the License at
  8#
  9#      http://www.apache.org/licenses/LICENSE-2.0
 10#
 11# Unless required by applicable law or agreed to in writing, software
 12# distributed under the License is distributed on an "AS IS" BASIS,
 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14# See the License for the specific language governing permissions and
 15# limitations under the License.
 16
 17
 18"""AtomService provides CRUD ops. in line with the Atom Publishing Protocol.
 19
 20  AtomService: Encapsulates the ability to perform insert, update and delete
 21               operations with the Atom Publishing Protocol on which GData is
 22               based. An instance can perform query, insertion, deletion, and
 23               update.
 24
 25  HttpRequest: Function that performs a GET, POST, PUT, or DELETE HTTP request
 26       to the specified end point. An AtomService object or a subclass can be
 27       used to specify information about the request.
 28"""
 29
 30__author__ = 'api.jscudder (Jeff Scudder)'
 31
 32
 33import atom.http_interface
 34import atom.url
 35import atom.http
 36import atom.token_store
 37
 38import os
 39import httplib
 40import urllib
 41import re
 42import base64
 43import socket
 44import warnings
 45try:
 46  from xml.etree import cElementTree as ElementTree
 47except ImportError:
 48  try:
 49    import cElementTree as ElementTree
 50  except ImportError:
 51    try:
 52      from xml.etree import ElementTree
 53    except ImportError:
 54      from elementtree import ElementTree
 55import atom
 56
 57
 58class AtomService(object):
 59  """Performs Atom Publishing Protocol CRUD operations.
 60  
 61  The AtomService contains methods to perform HTTP CRUD operations. 
 62  """
 63
 64  # Default values for members
 65  port = 80
 66  ssl = False
 67  # Set the current_token to force the AtomService to use this token
 68  # instead of searching for an appropriate token in the token_store.
 69  current_token = None
 70  auto_store_tokens = True
 71  auto_set_current_token = True
 72
 73  def _get_override_token(self):
 74    return self.current_token
 75
 76  def _set_override_token(self, token):
 77    self.current_token = token
 78
 79  override_token = property(_get_override_token, _set_override_token)
 80
 81  #@atom.v1_deprecated('Please use atom.client.AtomPubClient instead.')
 82  def __init__(self, server=None, additional_headers=None, 
 83      application_name='', http_client=None, token_store=None):
 84    """Creates a new AtomService client.
 85    
 86    Args:
 87      server: string (optional) The start of a URL for the server
 88              to which all operations should be directed. Example: 
 89              'www.google.com'
 90      additional_headers: dict (optional) Any additional HTTP headers which
 91                          should be included with CRUD operations.
 92      http_client: An object responsible for making HTTP requests using a
 93                   request method. If none is provided, a new instance of
 94                   atom.http.ProxiedHttpClient will be used.
 95      token_store: Keeps a collection of authorization tokens which can be
 96                   applied to requests for a specific URLs. Critical methods are
 97                   find_token based on a URL (atom.url.Url or a string), add_token,
 98                   and remove_token.
 99    """
100    self.http_client = http_client or atom.http.ProxiedHttpClient()
101    self.token_store = token_store or atom.token_store.TokenStore()
102    self.server = server
103    self.additional_headers = additional_headers or {}
104    self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % (
105        application_name,)
106    # If debug is True, the HTTPConnection will display debug information
107    self._set_debug(False)
108
109  __init__ = atom.v1_deprecated(
110      'Please use atom.client.AtomPubClient instead.')(
111          __init__)
112
113  def _get_debug(self):
114    return self.http_client.debug
115
116  def _set_debug(self, value):
117    self.http_client.debug = value
118
119  debug = property(_get_debug, _set_debug, 
120      doc='If True, HTTP debug information is printed.')
121
122  def use_basic_auth(self, username, password, scopes=None):
123    if username is not None and password is not None:
124      if scopes is None:
125        scopes = [atom.token_store.SCOPE_ALL]
126      base_64_string = base64.encodestring('%s:%s' % (username, password))
127      token = BasicAuthToken('Basic %s' % base_64_string.strip(), 
128          scopes=[atom.token_store.SCOPE_ALL])
129      if self.auto_set_current_token:
130        self.current_token = token
131      if self.auto_store_tokens:
132        return self.token_store.add_token(token)
133      return True
134    return False
135
136  def UseBasicAuth(self, username, password, for_proxy=False):
137    """Sets an Authenticaiton: Basic HTTP header containing plaintext.
138
139    Deprecated, use use_basic_auth instead.
140    
141    The username and password are base64 encoded and added to an HTTP header
142    which will be included in each request. Note that your username and 
143    password are sent in plaintext.
144
145    Args:
146      username: str
147      password: str
148    """
149    self.use_basic_auth(username, password)
150
151  #@atom.v1_deprecated('Please use atom.client.AtomPubClient for requests.')
152  def request(self, operation, url, data=None, headers=None, 
153      url_params=None):
154    if isinstance(url, (str, unicode)):
155      if url.startswith('http:') and self.ssl:
156        # Force all requests to be https if self.ssl is True.
157        url = atom.url.parse_url('https:' + url[5:])
158      elif not url.startswith('http') and self.ssl: 
159        url = atom.url.parse_url('https://%s%s' % (self.server, url))
160      elif not url.startswith('http'):
161        url = atom.url.parse_url('http://%s%s' % (self.server, url))
162      else:
163        url = atom.url.parse_url(url)
164
165    if url_params:
166      for name, value in url_params.iteritems():
167        url.params[name] = value
168
169    all_headers = self.additional_headers.copy()
170    if headers:
171      all_headers.update(headers)
172
173    # If the list of headers does not include a Content-Length, attempt to
174    # calculate it based on the data object.
175    if data and 'Content-Length' not in all_headers:
176      content_length = CalculateDataLength(data)
177      if content_length:
178        all_headers['Content-Length'] = str(content_length)
179
180    # Find an Authorization token for this URL if one is available.
181    if self.override_token:
182      auth_token = self.override_token
183    else:
184      auth_token = self.token_store.find_token(url)
185    return auth_token.perform_request(self.http_client, operation, url, 
186        data=data, headers=all_headers)
187
188  request = atom.v1_deprecated(
189      'Please use atom.client.AtomPubClient for requests.')(
190          request)
191
192  # CRUD operations
193  def Get(self, uri, extra_headers=None, url_params=None, escape_params=True):
194    """Query the APP server with the given URI
195
196    The uri is the portion of the URI after the server value 
197    (server example: 'www.google.com').
198
199    Example use:
200    To perform a query against Google Base, set the server to 
201    'base.google.com' and set the uri to '/base/feeds/...', where ... is 
202    your query. For example, to find snippets for all digital cameras uri 
203    should be set to: '/base/feeds/snippets?bq=digital+camera'
204
205    Args:
206      uri: string The query in the form of a URI. Example:
207           '/base/feeds/snippets?bq=digital+camera'.
208      extra_headers: dicty (optional) Extra HTTP headers to be included
209                     in the GET request. These headers are in addition to 
210                     those stored in the client's additional_headers property.
211                     The client automatically sets the Content-Type and 
212                     Authorization headers.
213      url_params: dict (optional) Additional URL parameters to be included
214                  in the query. These are translated into query arguments
215                  in the form '&dict_key=value&...'.
216                  Example: {'max-results': '250'} becomes &max-results=250
217      escape_params: boolean (optional) If false, the calling code has already
218                     ensured that the query will form a valid URL (all
219                     reserved characters have been escaped). If true, this
220                     method will escape the query and any URL parameters
221                     provided.
222
223    Returns:
224      httplib.HTTPResponse The server's response to the GET request.
225    """
226    return self.request('GET', uri, data=None, headers=extra_headers, 
227                        url_params=url_params)
228
229  def Post(self, data, uri, extra_headers=None, url_params=None, 
230           escape_params=True, content_type='application/atom+xml'):
231    """Insert data into an APP server at the given URI.
232
233    Args:
234      data: string, ElementTree._Element, or something with a __str__ method 
235            The XML to be sent to the uri. 
236      uri: string The location (feed) to which the data should be inserted. 
237           Example: '/base/feeds/items'. 
238      extra_headers: dict (optional) HTTP headers which are to be included. 
239                     The client automatically sets the Content-Type,
240                     Authorization, and Content-Length headers.
241      url_params: dict (optional) Additional URL parameters to be included
242                  in the URI. These are translated into query arguments
243                  in the form '&dict_key=value&...'.
244                  Example: {'max-results': '250'} becomes &max-results=250
245      escape_params: boolean (optional) If false, the calling code has already
246                     ensured that the query will form a valid URL (all
247                     reserved characters have been escaped). If true, this
248                     method will escape the query and any URL parameters
249                     provided.
250
251    Returns:
252      httplib.HTTPResponse Server's response to the POST request.
253    """
254    if extra_headers is None:
255      extra_headers = {}
256    if content_type:
257      extra_headers['Content-Type'] = content_type
258    return self.request('POST', uri, data=data, headers=extra_headers, 
259                        url_params=url_params)
260
261  def Put(self, data, uri, extra_headers=None, url_params=None, 
262           escape_params=True, content_type='application/atom+xml'):
263    """Updates an entry at the given URI.
264     
265    Args:
266      data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The 
267            XML containing the updated data.
268      uri: string A URI indicating entry to which the update will be applied.
269           Example: '/base/feeds/items/ITEM-ID'
270      extra_headers: dict (optional) HTTP headers which are to be included.
271                     The client automatically sets the Content-Type,
272                     Authorization, and Content-Length headers.
273      url_params: dict (optional) Additional URL parameters to be included
274                  in the URI. These are translated into query arguments
275                  in the form '&dict_key=value&...'.
276                  Example: {'max-results': '250'} becomes &max-results=250
277      escape_params: boolean (optional) If false, the calling code has already
278                     ensured that the query will form a valid URL (all
279                     reserved characters have been escaped). If true, this
280                     method will escape the query and any URL parameters
281                     provided.
282  
283    Returns:
284      httplib.HTTPResponse Server's response to the PUT request.
285    """
286    if extra_headers is None:
287      extra_headers = {}
288    if content_type:
289      extra_headers['Content-Type'] = content_type
290    return self.request('PUT', uri, data=data, headers=extra_headers, 
291                        url_params=url_params)
292
293  def Delete(self, uri, extra_headers=None, url_params=None, 
294             escape_params=True):
295    """Deletes the entry at the given URI.
296
297    Args:
298      uri: string The URI of the entry to be deleted. Example: 
299           '/base/feeds/items/ITEM-ID'
300      extra_headers: dict (optional) HTTP headers which are to be included.
301                     The client automatically sets the Content-Type and
302                     Authorization headers.
303      url_params: dict (optional) Additional URL parameters to be included
304                  in the URI. These are translated into query arguments
305                  in the form '&dict_key=value&...'.
306                  Example: {'max-results': '250'} becomes &max-results=250
307      escape_params: boolean (optional) If false, the calling code has already
308                     ensured that the query will form a valid URL (all
309                     reserved characters have been escaped). If true, this
310                     method will escape the query and any URL parameters
311                     provided.
312
313    Returns:
314      httplib.HTTPResponse Server's response to the DELETE request.
315    """
316    return self.request('DELETE', uri, data=None, headers=extra_headers, 
317                        url_params=url_params)
318
319
320class BasicAuthToken(atom.http_interface.GenericToken):
321  def __init__(self, auth_header, scopes=None):
322    """Creates a token used to add Basic Auth headers to HTTP requests.
323
324    Args:
325      auth_header: str The value for the Authorization header.
326      scopes: list of str or atom.url.Url specifying the beginnings of URLs
327          for which this token can be used. For example, if scopes contains
328          'http://example.com/foo', then this token can be used for a request to
329          'http://example.com/foo/bar' but it cannot be used for a request to
330          'http://example.com/baz'
331    """
332    self.auth_header = auth_header
333    self.scopes = scopes or []
334
335  def perform_request(self, http_client, operation, url, data=None,
336                      headers=None):
337    """Sets the Authorization header to the basic auth string."""
338    if headers is None:
339      headers = {'Authorization':self.auth_header}
340    else:
341      headers['Authorization'] = self.auth_header
342    return http_client.request(operation, url, data=data, headers=headers)
343
344  def __str__(self):
345    return self.auth_header
346
347  def valid_for_scope(self, url):
348    """Tells the caller if the token authorizes access to the desired URL.
349    """
350    if isinstance(url, (str, unicode)):
351      url = atom.url.parse_url(url)
352    for scope in self.scopes:
353      if scope == atom.token_store.SCOPE_ALL:
354        return True
355      if isinstance(scope, (str, unicode)):
356        scope = atom.url.parse_url(scope)
357      if scope == url:
358        return True
359      # Check the host and the path, but ignore the port and protocol.
360      elif scope.host == url.host and not scope.path:
361        return True
362      elif scope.host == url.host and scope.path and not url.path:
363        continue
364      elif scope.host == url.host and url.path.startswith(scope.path):
365        return True
366    return False
367
368
369def PrepareConnection(service, full_uri):
370  """Opens a connection to the server based on the full URI.
371
372  This method is deprecated, instead use atom.http.HttpClient.request.
373
374  Examines the target URI and the proxy settings, which are set as
375  environment variables, to open a connection with the server. This
376  connection is used to make an HTTP request.
377
378  Args:
379    service: atom.AtomService or a subclass. It must have a server string which
380      represents the server host to which the request should be made. It may also
381      have a dictionary of additional_headers to send in the HTTP request.
382    full_uri: str Which is the target relative (lacks protocol and host) or
383    absolute URL to be opened. Example:
384    'https://www.google.com/accounts/ClientLogin' or
385    'base/feeds/snippets' where the server is set to www.google.com.
386
387  Returns:
388    A tuple containing the httplib.HTTPConnection and the full_uri for the
389    request.
390  """
391  deprecation('calling deprecated function PrepareConnection')
392  (server, port, ssl, partial_uri) = ProcessUrl(service, full_uri)
393  if ssl:
394    # destination is https
395    proxy = os.environ.get('https_proxy')
396    if proxy:
397      (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service, proxy, True)
398      proxy_username = os.environ.get('proxy-username')
399      if not proxy_username:
400        proxy_username = os.environ.get('proxy_username')
401      proxy_password = os.environ.get('proxy-password')
402      if not proxy_password:
403        proxy_password = os.environ.get('proxy_password')
404      if proxy_username:
405        user_auth = base64.encodestring('%s:%s' % (proxy_username,
406                                                   proxy_password))
407        proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % (
408            user_auth.strip()))
409      else:
410        proxy_authorization = ''
411      proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server, port)
412      user_agent = 'User-Agent: %s\r\n' % (
413          service.additional_headers['User-Agent'])
414      proxy_pieces = (proxy_connect + proxy_authorization + user_agent
415                       + '\r\n')
416
417      #now connect, very simple recv and error checking
418      p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
419      p_sock.connect((p_server,p_port))
420      p_sock.sendall(proxy_pieces)
421      response = ''
422
423      # Wait for the full response.
424      while response.find("\r\n\r\n") == -1:
425        response += p_sock.recv(8192)
426       
427      p_status=response.split()[1]
428      if p_status!=str(200):
429        raise 'Error status=',str(p_status)
430
431      # Trivial setup for ssl socket.
432      ssl = socket.ssl(p_sock, None, None)
433      fake_sock = httplib.FakeSocket(p_sock, ssl)
434
435      # Initalize httplib and replace with the proxy socket.
436      connection = httplib.HTTPConnection(server)
437      connection.sock=fake_sock
438      full_uri = partial_uri
439
440    else:
441      connection = httplib.HTTPSConnection(server, port)
442      full_uri = partial_uri
443
444  else:
445    # destination is http
446    proxy = os.environ.get('http_proxy')
447    if proxy:
448      (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service.server, proxy, True)
449      proxy_username = os.environ.get('proxy-username')
450      if not proxy_username:
451        proxy_username = os.environ.get('proxy_username')
452      proxy_password = os.environ.get('proxy-password')
453      if not proxy_password:
454        proxy_password = os.environ.get('proxy_password')
455      if proxy_username:
456        UseBasicAuth(service, proxy_username, proxy_password, True)
457      connection = httplib.HTTPConnection(p_server, p_port)
458      if not full_uri.startswith("http://"):
459        if full_uri.startswith("/"):
460          full_uri = "http://%s%s" % (service.server, full_uri)
461        else:
462          full_uri = "http://%s/%s" % (service.server, full_uri)
463    else:
464      connection = httplib.HTTPConnection(server, port)
465      full_uri = partial_uri
466
467  return (connection, full_uri)
468
469
470def UseBasicAuth(service, username, password, for_proxy=False):
471  """Sets an Authenticaiton: Basic HTTP header containing plaintext.
472
473  Deprecated, use AtomService.use_basic_auth insread.
474  
475  The username and password are base64 encoded and added to an HTTP header
476  which will be included in each request. Note that your username and 
477  password are sent in plaintext. The auth header is added to the 
478  additional_headers dictionary in the service object.
479
480  Args:
481    service: atom.AtomService or a subclass which has an 
482        additional_headers dict as a member.
483    username: str
484    password: str
485  """
486  deprecation('calling deprecated function UseBasicAuth')
487  base_64_string = base64.encodestring('%s:%s' % (username, password))
488  base_64_string = base_64_string.strip()
489  if for_proxy:
490    header_name = 'Proxy-Authorization'
491  else:
492    header_name = 'Authorization'
493  service.additional_headers[header_name] = 'Basic %s' % (base_64_string,)
494
495
496def ProcessUrl(service, url, for_proxy=False):
497  """Processes a passed URL.  If the URL does not begin with https?, then
498  the default value for server is used
499
500  This method is deprecated, use atom.url.parse_url instead.
501  """
502  if not isinstance(url, atom.url.Url):
503    url = atom.url.parse_url(url)
504
505  server = url.host
506  ssl = False
507  port = 80
508
509  if not server:
510    if hasattr(service, 'server'):
511      server = service.server
512    else:
513      server = service
514    if not url.protocol and hasattr(service, 'ssl'):
515      ssl = service.ssl
516    if hasattr(service, 'port'):
517      port = service.port
518  else:
519    if url.protocol == 'https':
520      ssl = True
521    elif url.protocol == 'http':
522      ssl = False
523    if url.port:
524      port = int(url.port)
525    elif port == 80 and ssl:
526      port = 443
527
528  return (server, port, ssl, url.get_request_uri())
529
530def DictionaryToParamList(url_parameters, escape_params=True):
531  """Convert a dictionary of URL arguments into a URL parameter string.
532
533  This function is deprcated, use atom.url.Url instead.
534
535  Args:
536    url_parameters: The dictionaty of key-value pairs which will be converted
537                    into URL parameters. For example,
538                    {'dry-run': 'true', 'foo': 'bar'}
539                    will become ['dry-run=true', 'foo=bar'].
540
541  Returns:
542    A list which contains a string for each key-value pair. The strings are
543    ready to be incorporated into a URL by using '&'.join([] + parameter_list)
544  """
545  # Choose which function to use when modifying the query and parameters.
546  # Use quote_plus when escape_params is true.
547  transform_op = [str, urllib.quote_plus][bool(escape_params)]
548  # Create a list of tuples containing the escaped version of the
549  # parameter-value pairs.
550  parameter_tuples = [(transform_op(param), transform_op(value))
551                     for param, value in (url_parameters or {}).items()]
552  # Turn parameter-value tuples into a list of strings in the form
553  # 'PARAMETER=VALUE'.
554  return ['='.join(x) for x in parameter_tuples]
555
556
557def BuildUri(uri, url_params=None, escape_params=True):
558  """Converts a uri string and a collection of parameters into a URI.
559
560  This function is deprcated, use atom.url.Url instead.
561
562  Args:
563    uri: string
564    url_params: dict (optional)
565    escape_params: boolean (optional)
566    uri: string The start of the desired URI. This string can alrady contain
567         URL parameters. Examples: '/base/feeds/snippets', 
568         '/base/feeds/snippets?bq=digital+camera'
569    url_parameters: dict (optional) Additional URL parameters to be included
570                    in the query. These are translated into query arguments
571                    in the form '&dict_key=value&...'.
572                    Example: {'max-results': '250'} becomes &max-results=250
573    escape_params: boolean (optional) If false, the calling code has already
574                   ensured that the query will form a valid URL (all
575                   reserved characters have been escaped). If true, this
576                   method will escape the query and any URL parameters
577                   provided.
578
579  Returns:
580    string The URI consisting of the escaped URL parameters appended to the
581    initial uri string.
582  """
583  # Prepare URL parameters for inclusion into the GET request.
584  parameter_list = DictionaryToParamList(url_params, escape_params)
585
586  # Append the URL parameters to the URL.
587  if parameter_list:
588    if uri.find('?') != -1:
589      # If there are already URL parameters in the uri string, add the
590      # parameters after a new & character.
591      full_uri = '&'.join([uri] + parameter_list)
592    else:
593      # The uri string did not have any URL parameters (no ? character)
594      # so put a ? between the uri and URL parameters.
595      full_uri = '%s%s' % (uri, '?%s' % ('&'.join([] + parameter_list)))  
596  else:
597    full_uri = uri
598        
599  return full_uri
600
601  
602def HttpRequest(service, operation, data, uri, extra_headers=None, 
603    url_params=None, escape_params=True, content_type='application/atom+xml'):
604  """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
605  
606  This method is deprecated, use atom.http.HttpClient.request instead.
607
608  Usage example, perform and HTTP GET on http://www.google.com/:
609    import atom.service
610    client = atom.service.AtomService()
611    http_response = client.Get('http://www.google.com/')
612  or you could set the client.server to 'www.google.com' and use the 
613  following:
614    client.server = 'www.google.com'
615    http_response = client.Get('/')
616
617  Args:
618    service: atom.AtomService object which contains some of the parameters 
619        needed to make the request. The following members are used to 
620        construct the HTTP call: server (str), additional_headers (dict), 
621        port (int), and ssl (bool).
622    operation: str The HTTP operation to be performed. This is usually one of
623        'GET', 'POST', 'PUT', or 'DELETE'
624    data: ElementTree, filestream, list of parts, or other object which can be 
625        converted to a string. 
626        Should be set to None when performing a GET or PUT.
627        If data is a file-like object which can be read, this method will read
628        a chunk of 100K bytes at a time and send them. 
629        If the data is a list of parts to be sent, each part will be evaluated
630        and sent.
631    uri: The beginning of the URL to which the request should be sent. 
632        Examples: '/', '/base/feeds/snippets', 
633        '/m8/feeds/contacts/default/base'
634    extra_headers: dict of strings. HTTP headers which should be sent
635        in the request. These headers are in addition to those stored in 
636        service.additional_headers.
637    url_params: dict of strings. Key value pairs to be added to the URL as
638        URL parameters. For example {'foo':'bar', 'test':'param'} will 
639        become ?foo=bar&test=param.
640    escape_params: bool default True. If true, the keys and values in 
641        url_params will be URL escaped when the form is constructed 
642        (Special characters converted to %XX form.)
643    content_type: str The MIME type for the data being sent. Defaults to
644        'application/atom+xml', this is only used if data is set.
645  """
646  deprecation('call to deprecated function HttpRequest')
647  full_uri = BuildUri(uri, url_params, escape_params)
648  (connection, full_uri) = PrepareConnection(service, full_uri)
649
650  if extra_headers is None:
651    extra_headers = {}
652
653  # Turn on debug mode if the debug member is set.
654  if service.debug:
655    connection.debuglevel = 1
656
657  connection.putrequest(operation, full_uri)
658
659  # If the list of headers does not include a Content-Length, attempt to 
660  # calculate it based on the data object.
661  if (data and not service.additional_headers.has_key('Content-Length') and 
662      not extra_headers.has_key('Content-Length')):
663    content_length = CalculateDataLength(data)
664    if content_length:
665      extra_headers['Content-Length'] = str(content_length)
666
667  if content_type:
668    extra_headers['Content-Type'] = content_type 
669
670  # Send the HTTP headers.
671  if isinstance(service.additional_headers, dict):
672    for header in service.additional_headers:
673      connection.putheader(header, service.additional_headers[header])
674  if isinstance(extra_headers, dict):
675    for header in extra_headers:
676      connection.putheader(header, extra_headers[header])
677  connection.endheaders()
678
679  # If there is data, send it in the request.
680  if data:
681    if isinstance(data, list):
682      for data_part in data:
683        __SendDataPart(data_part, connection)
684    else:
685      __SendDataPart(data, connection)
686
687  # Return the HTTP Response from the server.
688  return connection.getresponse()
689  
690
691def __SendDataPart(data, connection):
692  """This method is deprecated, use atom.http._send_data_part"""
693  deprecated('call to deprecated function __SendDataPart')
694  if isinstance(data, str):
695    #TODO add handling for unicode.
696    connection.send(data)
697    return
698  elif ElementTree.iselement(data):
699    connection.send(ElementTree.tostring(data))
700    return
701  # Check to see if data is a file-like object that has a read method.
702  elif hasattr(data, 'read'):
703    # Read the file and send it a chunk at a time.
704    while 1:
705      binarydata = data.read(100000)
706      if binarydata == '': break
707      connection.send(binarydata)
708    return
709  else:
710    # The data object was not a file.
711    # Try to convert to a string and send the data.
712    connection.send(str(data))
713    return
714
715
716def CalculateDataLength(data):
717  """Attempts to determine the length of the data to send. 
718  
719  This method will respond with a length only if the data is a string or
720  and ElementTree element.
721
722  Args:
723    data: object If this is not a string or ElementTree element this funtion
724        will return None.
725  """
726  if isinstance(data, str):
727    return len(data)
728  elif isinstance(data, list):
729    return None
730  elif ElementTree.iselement(data):
731    return len(ElementTree.tostring(data))
732  elif hasattr(data, 'read'):
733    # If this is a file-like object, don't try to guess the length.
734    return None
735  else:
736    return len(str(data))
737    
738
739def deprecation(message):
740  warnings.warn(message, DeprecationWarning, stacklevel=2)