Source

ytmanager / atom / service.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
#!/usr/bin/python
#
# Copyright (C) 2006, 2007, 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""AtomService provides CRUD ops. in line with the Atom Publishing Protocol.

  AtomService: Encapsulates the ability to perform insert, update and delete
               operations with the Atom Publishing Protocol on which GData is
               based. An instance can perform query, insertion, deletion, and
               update.

  HttpRequest: Function that performs a GET, POST, PUT, or DELETE HTTP request
       to the specified end point. An AtomService object or a subclass can be
       used to specify information about the request.
"""

__author__ = 'api.jscudder (Jeff Scudder)'


import atom.http_interface
import atom.url
import atom.http
import atom.token_store

import os
import httplib
import urllib
import re
import base64
import socket
import warnings
try:
  from xml.etree import cElementTree as ElementTree
except ImportError:
  try:
    import cElementTree as ElementTree
  except ImportError:
    try:
      from xml.etree import ElementTree
    except ImportError:
      from elementtree import ElementTree
import atom


class AtomService(object):
  """Performs Atom Publishing Protocol CRUD operations.
  
  The AtomService contains methods to perform HTTP CRUD operations. 
  """

  # Default values for members
  port = 80
  ssl = False 
  # Set the current_token to force the AtomService to use this token
  # instead of searching for an appropriate token in the token_store.
  current_token = None
  auto_store_tokens = True
  auto_set_current_token = True

  def _get_override_token(self):
    return self.current_token

  def _set_override_token(self, token):
    self.current_token = token

  override_token = property(_get_override_token, _set_override_token)

  #@atom.v1_deprecated('Please use atom.client.AtomPubClient instead.')
  def __init__(self, server=None, additional_headers=None, 
      application_name='', http_client=None, token_store=None):
    """Creates a new AtomService client.
    
    Args:
      server: string (optional) The start of a URL for the server
              to which all operations should be directed. Example: 
              'www.google.com'
      additional_headers: dict (optional) Any additional HTTP headers which
                          should be included with CRUD operations.
      http_client: An object responsible for making HTTP requests using a
                   request method. If none is provided, a new instance of
                   atom.http.ProxiedHttpClient will be used.
      token_store: Keeps a collection of authorization tokens which can be
                   applied to requests for a specific URLs. Critical methods are
                   find_token based on a URL (atom.url.Url or a string), add_token,
                   and remove_token.
    """
    self.http_client = http_client or atom.http.ProxiedHttpClient()
    self.token_store = token_store or atom.token_store.TokenStore()
    self.server = server
    self.additional_headers = additional_headers or {}
    self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % (
        application_name,)
    # If debug is True, the HTTPConnection will display debug information
    self._set_debug(False)

  __init__ = atom.v1_deprecated(
      'Please use atom.client.AtomPubClient instead.')(
          __init__)

  def _get_debug(self):
    return self.http_client.debug

  def _set_debug(self, value):
    self.http_client.debug = value

  debug = property(_get_debug, _set_debug, 
      doc='If True, HTTP debug information is printed.')

  def use_basic_auth(self, username, password, scopes=None):
    if username is not None and password is not None:
      if scopes is None:
        scopes = [atom.token_store.SCOPE_ALL]
      base_64_string = base64.encodestring('%s:%s' % (username, password))
      token = BasicAuthToken('Basic %s' % base_64_string.strip(), 
          scopes=[atom.token_store.SCOPE_ALL])
      if self.auto_set_current_token:
        self.current_token = token
      if self.auto_store_tokens:
        return self.token_store.add_token(token)
      return True
    return False

  def UseBasicAuth(self, username, password, for_proxy=False):
    """Sets an Authenticaiton: Basic HTTP header containing plaintext.

    Deprecated, use use_basic_auth instead.
    
    The username and password are base64 encoded and added to an HTTP header
    which will be included in each request. Note that your username and 
    password are sent in plaintext.

    Args:
      username: str
      password: str
    """
    self.use_basic_auth(username, password)

  #@atom.v1_deprecated('Please use atom.client.AtomPubClient for requests.')
  def request(self, operation, url, data=None, headers=None, 
      url_params=None):
    if isinstance(url, (str, unicode)):
      if url.startswith('http:') and self.ssl:
        # Force all requests to be https if self.ssl is True.
        url = atom.url.parse_url('https:' + url[5:])
      elif not url.startswith('http') and self.ssl: 
        url = atom.url.parse_url('https://%s%s' % (self.server, url))
      elif not url.startswith('http'):
        url = atom.url.parse_url('http://%s%s' % (self.server, url))
      else:
        url = atom.url.parse_url(url)

    if url_params:
      for name, value in url_params.iteritems():
        url.params[name] = value

    all_headers = self.additional_headers.copy()
    if headers:
      all_headers.update(headers)

    # If the list of headers does not include a Content-Length, attempt to
    # calculate it based on the data object.
    if data and 'Content-Length' not in all_headers:
      content_length = CalculateDataLength(data)
      if content_length:
        all_headers['Content-Length'] = str(content_length)

    # Find an Authorization token for this URL if one is available.
    if self.override_token:
      auth_token = self.override_token
    else:
      auth_token = self.token_store.find_token(url)
    return auth_token.perform_request(self.http_client, operation, url, 
        data=data, headers=all_headers)

  request = atom.v1_deprecated(
      'Please use atom.client.AtomPubClient for requests.')(
          request)

  # CRUD operations
  def Get(self, uri, extra_headers=None, url_params=None, escape_params=True):
    """Query the APP server with the given URI

    The uri is the portion of the URI after the server value 
    (server example: 'www.google.com').

    Example use:
    To perform a query against Google Base, set the server to 
    'base.google.com' and set the uri to '/base/feeds/...', where ... is 
    your query. For example, to find snippets for all digital cameras uri 
    should be set to: '/base/feeds/snippets?bq=digital+camera'

    Args:
      uri: string The query in the form of a URI. Example:
           '/base/feeds/snippets?bq=digital+camera'.
      extra_headers: dicty (optional) Extra HTTP headers to be included
                     in the GET request. These headers are in addition to 
                     those stored in the client's additional_headers property.
                     The client automatically sets the Content-Type and 
                     Authorization headers.
      url_params: dict (optional) Additional URL parameters to be included
                  in the query. These are translated into query arguments
                  in the form '&dict_key=value&...'.
                  Example: {'max-results': '250'} becomes &max-results=250
      escape_params: boolean (optional) If false, the calling code has already
                     ensured that the query will form a valid URL (all
                     reserved characters have been escaped). If true, this
                     method will escape the query and any URL parameters
                     provided.

    Returns:
      httplib.HTTPResponse The server's response to the GET request.
    """
    return self.request('GET', uri, data=None, headers=extra_headers, 
                        url_params=url_params)

  def Post(self, data, uri, extra_headers=None, url_params=None, 
           escape_params=True, content_type='application/atom+xml'):
    """Insert data into an APP server at the given URI.

    Args:
      data: string, ElementTree._Element, or something with a __str__ method 
            The XML to be sent to the uri. 
      uri: string The location (feed) to which the data should be inserted. 
           Example: '/base/feeds/items'. 
      extra_headers: dict (optional) HTTP headers which are to be included. 
                     The client automatically sets the Content-Type,
                     Authorization, and Content-Length headers.
      url_params: dict (optional) Additional URL parameters to be included
                  in the URI. These are translated into query arguments
                  in the form '&dict_key=value&...'.
                  Example: {'max-results': '250'} becomes &max-results=250
      escape_params: boolean (optional) If false, the calling code has already
                     ensured that the query will form a valid URL (all
                     reserved characters have been escaped). If true, this
                     method will escape the query and any URL parameters
                     provided.

    Returns:
      httplib.HTTPResponse Server's response to the POST request.
    """
    if extra_headers is None:
      extra_headers = {}
    if content_type:
      extra_headers['Content-Type'] = content_type
    return self.request('POST', uri, data=data, headers=extra_headers, 
                        url_params=url_params)

  def Put(self, data, uri, extra_headers=None, url_params=None, 
           escape_params=True, content_type='application/atom+xml'):
    """Updates an entry at the given URI.
     
    Args:
      data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The 
            XML containing the updated data.
      uri: string A URI indicating entry to which the update will be applied.
           Example: '/base/feeds/items/ITEM-ID'
      extra_headers: dict (optional) HTTP headers which are to be included.
                     The client automatically sets the Content-Type,
                     Authorization, and Content-Length headers.
      url_params: dict (optional) Additional URL parameters to be included
                  in the URI. These are translated into query arguments
                  in the form '&dict_key=value&...'.
                  Example: {'max-results': '250'} becomes &max-results=250
      escape_params: boolean (optional) If false, the calling code has already
                     ensured that the query will form a valid URL (all
                     reserved characters have been escaped). If true, this
                     method will escape the query and any URL parameters
                     provided.
  
    Returns:
      httplib.HTTPResponse Server's response to the PUT request.
    """
    if extra_headers is None:
      extra_headers = {}
    if content_type:
      extra_headers['Content-Type'] = content_type
    return self.request('PUT', uri, data=data, headers=extra_headers, 
                        url_params=url_params)

  def Delete(self, uri, extra_headers=None, url_params=None, 
             escape_params=True):
    """Deletes the entry at the given URI.

    Args:
      uri: string The URI of the entry to be deleted. Example: 
           '/base/feeds/items/ITEM-ID'
      extra_headers: dict (optional) HTTP headers which are to be included.
                     The client automatically sets the Content-Type and
                     Authorization headers.
      url_params: dict (optional) Additional URL parameters to be included
                  in the URI. These are translated into query arguments
                  in the form '&dict_key=value&...'.
                  Example: {'max-results': '250'} becomes &max-results=250
      escape_params: boolean (optional) If false, the calling code has already
                     ensured that the query will form a valid URL (all
                     reserved characters have been escaped). If true, this
                     method will escape the query and any URL parameters
                     provided.

    Returns:
      httplib.HTTPResponse Server's response to the DELETE request.
    """
    return self.request('DELETE', uri, data=None, headers=extra_headers, 
                        url_params=url_params)


class BasicAuthToken(atom.http_interface.GenericToken):
  def __init__(self, auth_header, scopes=None):
    """Creates a token used to add Basic Auth headers to HTTP requests.

    Args:
      auth_header: str The value for the Authorization header.
      scopes: list of str or atom.url.Url specifying the beginnings of URLs
          for which this token can be used. For example, if scopes contains
          'http://example.com/foo', then this token can be used for a request to
          'http://example.com/foo/bar' but it cannot be used for a request to
          'http://example.com/baz'
    """
    self.auth_header = auth_header
    self.scopes = scopes or []

  def perform_request(self, http_client, operation, url, data=None,
                      headers=None):
    """Sets the Authorization header to the basic auth string."""
    if headers is None:
      headers = {'Authorization':self.auth_header}
    else:
      headers['Authorization'] = self.auth_header
    return http_client.request(operation, url, data=data, headers=headers)

  def __str__(self):
    return self.auth_header

  def valid_for_scope(self, url):
    """Tells the caller if the token authorizes access to the desired URL.
    """
    if isinstance(url, (str, unicode)):
      url = atom.url.parse_url(url)
    for scope in self.scopes:
      if scope == atom.token_store.SCOPE_ALL:
        return True
      if isinstance(scope, (str, unicode)):
        scope = atom.url.parse_url(scope)
      if scope == url:
        return True
      # Check the host and the path, but ignore the port and protocol.
      elif scope.host == url.host and not scope.path:
        return True
      elif scope.host == url.host and scope.path and not url.path:
        continue
      elif scope.host == url.host and url.path.startswith(scope.path):
        return True
    return False


def PrepareConnection(service, full_uri):
  """Opens a connection to the server based on the full URI.

  This method is deprecated, instead use atom.http.HttpClient.request.

  Examines the target URI and the proxy settings, which are set as
  environment variables, to open a connection with the server. This
  connection is used to make an HTTP request.

  Args:
    service: atom.AtomService or a subclass. It must have a server string which
      represents the server host to which the request should be made. It may also
      have a dictionary of additional_headers to send in the HTTP request.
    full_uri: str Which is the target relative (lacks protocol and host) or
    absolute URL to be opened. Example:
    'https://www.google.com/accounts/ClientLogin' or
    'base/feeds/snippets' where the server is set to www.google.com.

  Returns:
    A tuple containing the httplib.HTTPConnection and the full_uri for the
    request.
  """
  deprecation('calling deprecated function PrepareConnection')
  (server, port, ssl, partial_uri) = ProcessUrl(service, full_uri)
  if ssl:
    # destination is https
    proxy = os.environ.get('https_proxy')
    if proxy:
      (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service, proxy, True)
      proxy_username = os.environ.get('proxy-username')
      if not proxy_username:
        proxy_username = os.environ.get('proxy_username')
      proxy_password = os.environ.get('proxy-password')
      if not proxy_password:
        proxy_password = os.environ.get('proxy_password')
      if proxy_username:
        user_auth = base64.encodestring('%s:%s' % (proxy_username,
                                                   proxy_password))
        proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % (
            user_auth.strip()))
      else:
        proxy_authorization = ''
      proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server, port)
      user_agent = 'User-Agent: %s\r\n' % (
          service.additional_headers['User-Agent'])
      proxy_pieces = (proxy_connect + proxy_authorization + user_agent
                       + '\r\n')

      #now connect, very simple recv and error checking
      p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      p_sock.connect((p_server,p_port))
      p_sock.sendall(proxy_pieces)
      response = ''

      # Wait for the full response.
      while response.find("\r\n\r\n") == -1:
        response += p_sock.recv(8192)
       
      p_status=response.split()[1]
      if p_status!=str(200):
        raise 'Error status=',str(p_status)

      # Trivial setup for ssl socket.
      ssl = socket.ssl(p_sock, None, None)
      fake_sock = httplib.FakeSocket(p_sock, ssl)

      # Initalize httplib and replace with the proxy socket.
      connection = httplib.HTTPConnection(server)
      connection.sock=fake_sock
      full_uri = partial_uri

    else:
      connection = httplib.HTTPSConnection(server, port)
      full_uri = partial_uri

  else:
    # destination is http
    proxy = os.environ.get('http_proxy')
    if proxy:
      (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service.server, proxy, True)
      proxy_username = os.environ.get('proxy-username')
      if not proxy_username:
        proxy_username = os.environ.get('proxy_username')
      proxy_password = os.environ.get('proxy-password')
      if not proxy_password:
        proxy_password = os.environ.get('proxy_password')
      if proxy_username:
        UseBasicAuth(service, proxy_username, proxy_password, True)
      connection = httplib.HTTPConnection(p_server, p_port)
      if not full_uri.startswith("http://"):
        if full_uri.startswith("/"):
          full_uri = "http://%s%s" % (service.server, full_uri)
        else:
          full_uri = "http://%s/%s" % (service.server, full_uri)
    else:
      connection = httplib.HTTPConnection(server, port)
      full_uri = partial_uri

  return (connection, full_uri)


def UseBasicAuth(service, username, password, for_proxy=False):
  """Sets an Authenticaiton: Basic HTTP header containing plaintext.

  Deprecated, use AtomService.use_basic_auth insread.
  
  The username and password are base64 encoded and added to an HTTP header
  which will be included in each request. Note that your username and 
  password are sent in plaintext. The auth header is added to the 
  additional_headers dictionary in the service object.

  Args:
    service: atom.AtomService or a subclass which has an 
        additional_headers dict as a member.
    username: str
    password: str
  """
  deprecation('calling deprecated function UseBasicAuth')
  base_64_string = base64.encodestring('%s:%s' % (username, password))
  base_64_string = base_64_string.strip()
  if for_proxy:
    header_name = 'Proxy-Authorization'
  else:
    header_name = 'Authorization'
  service.additional_headers[header_name] = 'Basic %s' % (base_64_string,)


def ProcessUrl(service, url, for_proxy=False):
  """Processes a passed URL.  If the URL does not begin with https?, then
  the default value for server is used

  This method is deprecated, use atom.url.parse_url instead.
  """
  if not isinstance(url, atom.url.Url):
    url = atom.url.parse_url(url)

  server = url.host
  ssl = False
  port = 80

  if not server:
    if hasattr(service, 'server'):
      server = service.server
    else:
      server = service
    if not url.protocol and hasattr(service, 'ssl'):
      ssl = service.ssl
    if hasattr(service, 'port'):
      port = service.port
  else:
    if url.protocol == 'https':
      ssl = True
    elif url.protocol == 'http':
      ssl = False
    if url.port:
      port = int(url.port)
    elif port == 80 and ssl:
      port = 443

  return (server, port, ssl, url.get_request_uri())

def DictionaryToParamList(url_parameters, escape_params=True):
  """Convert a dictionary of URL arguments into a URL parameter string.

  This function is deprcated, use atom.url.Url instead.

  Args:
    url_parameters: The dictionaty of key-value pairs which will be converted
                    into URL parameters. For example,
                    {'dry-run': 'true', 'foo': 'bar'}
                    will become ['dry-run=true', 'foo=bar'].

  Returns:
    A list which contains a string for each key-value pair. The strings are
    ready to be incorporated into a URL by using '&'.join([] + parameter_list)
  """
  # Choose which function to use when modifying the query and parameters.
  # Use quote_plus when escape_params is true.
  transform_op = [str, urllib.quote_plus][bool(escape_params)]
  # Create a list of tuples containing the escaped version of the
  # parameter-value pairs.
  parameter_tuples = [(transform_op(param), transform_op(value))
                     for param, value in (url_parameters or {}).items()]
  # Turn parameter-value tuples into a list of strings in the form
  # 'PARAMETER=VALUE'.
  return ['='.join(x) for x in parameter_tuples]


def BuildUri(uri, url_params=None, escape_params=True):
  """Converts a uri string and a collection of parameters into a URI.

  This function is deprcated, use atom.url.Url instead.

  Args:
    uri: string
    url_params: dict (optional)
    escape_params: boolean (optional)
    uri: string The start of the desired URI. This string can alrady contain
         URL parameters. Examples: '/base/feeds/snippets', 
         '/base/feeds/snippets?bq=digital+camera'
    url_parameters: dict (optional) Additional URL parameters to be included
                    in the query. These are translated into query arguments
                    in the form '&dict_key=value&...'.
                    Example: {'max-results': '250'} becomes &max-results=250
    escape_params: boolean (optional) If false, the calling code has already
                   ensured that the query will form a valid URL (all
                   reserved characters have been escaped). If true, this
                   method will escape the query and any URL parameters
                   provided.

  Returns:
    string The URI consisting of the escaped URL parameters appended to the
    initial uri string.
  """
  # Prepare URL parameters for inclusion into the GET request.
  parameter_list = DictionaryToParamList(url_params, escape_params)

  # Append the URL parameters to the URL.
  if parameter_list:
    if uri.find('?') != -1:
      # If there are already URL parameters in the uri string, add the
      # parameters after a new & character.
      full_uri = '&'.join([uri] + parameter_list)
    else:
      # The uri string did not have any URL parameters (no ? character)
      # so put a ? between the uri and URL parameters.
      full_uri = '%s%s' % (uri, '?%s' % ('&'.join([] + parameter_list)))  
  else:
    full_uri = uri
        
  return full_uri

  
def HttpRequest(service, operation, data, uri, extra_headers=None, 
    url_params=None, escape_params=True, content_type='application/atom+xml'):
  """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
  
  This method is deprecated, use atom.http.HttpClient.request instead.

  Usage example, perform and HTTP GET on http://www.google.com/:
    import atom.service
    client = atom.service.AtomService()
    http_response = client.Get('http://www.google.com/')
  or you could set the client.server to 'www.google.com' and use the 
  following:
    client.server = 'www.google.com'
    http_response = client.Get('/')

  Args:
    service: atom.AtomService object which contains some of the parameters 
        needed to make the request. The following members are used to 
        construct the HTTP call: server (str), additional_headers (dict), 
        port (int), and ssl (bool).
    operation: str The HTTP operation to be performed. This is usually one of
        'GET', 'POST', 'PUT', or 'DELETE'
    data: ElementTree, filestream, list of parts, or other object which can be 
        converted to a string. 
        Should be set to None when performing a GET or PUT.
        If data is a file-like object which can be read, this method will read
        a chunk of 100K bytes at a time and send them. 
        If the data is a list of parts to be sent, each part will be evaluated
        and sent.
    uri: The beginning of the URL to which the request should be sent. 
        Examples: '/', '/base/feeds/snippets', 
        '/m8/feeds/contacts/default/base'
    extra_headers: dict of strings. HTTP headers which should be sent
        in the request. These headers are in addition to those stored in 
        service.additional_headers.
    url_params: dict of strings. Key value pairs to be added to the URL as
        URL parameters. For example {'foo':'bar', 'test':'param'} will 
        become ?foo=bar&test=param.
    escape_params: bool default True. If true, the keys and values in 
        url_params will be URL escaped when the form is constructed 
        (Special characters converted to %XX form.)
    content_type: str The MIME type for the data being sent. Defaults to
        'application/atom+xml', this is only used if data is set.
  """
  deprecation('call to deprecated function HttpRequest')
  full_uri = BuildUri(uri, url_params, escape_params)
  (connection, full_uri) = PrepareConnection(service, full_uri)

  if extra_headers is None:
    extra_headers = {}

  # Turn on debug mode if the debug member is set.
  if service.debug:
    connection.debuglevel = 1

  connection.putrequest(operation, full_uri)

  # If the list of headers does not include a Content-Length, attempt to 
  # calculate it based on the data object.
  if (data and not service.additional_headers.has_key('Content-Length') and 
      not extra_headers.has_key('Content-Length')):
    content_length = CalculateDataLength(data)
    if content_length:
      extra_headers['Content-Length'] = str(content_length)

  if content_type:
    extra_headers['Content-Type'] = content_type 

  # Send the HTTP headers.
  if isinstance(service.additional_headers, dict):
    for header in service.additional_headers:
      connection.putheader(header, service.additional_headers[header])
  if isinstance(extra_headers, dict):
    for header in extra_headers:
      connection.putheader(header, extra_headers[header])
  connection.endheaders()

  # If there is data, send it in the request.
  if data:
    if isinstance(data, list):
      for data_part in data:
        __SendDataPart(data_part, connection)
    else:
      __SendDataPart(data, connection)

  # Return the HTTP Response from the server.
  return connection.getresponse()
  

def __SendDataPart(data, connection):
  """This method is deprecated, use atom.http._send_data_part"""
  deprecated('call to deprecated function __SendDataPart')
  if isinstance(data, str):
    #TODO add handling for unicode.
    connection.send(data)
    return
  elif ElementTree.iselement(data):
    connection.send(ElementTree.tostring(data))
    return
  # Check to see if data is a file-like object that has a read method.
  elif hasattr(data, 'read'):
    # Read the file and send it a chunk at a time.
    while 1:
      binarydata = data.read(100000)
      if binarydata == '': break
      connection.send(binarydata)
    return
  else:
    # The data object was not a file.
    # Try to convert to a string and send the data.
    connection.send(str(data))
    return


def CalculateDataLength(data):
  """Attempts to determine the length of the data to send. 
  
  This method will respond with a length only if the data is a string or
  and ElementTree element.

  Args:
    data: object If this is not a string or ElementTree element this funtion
        will return None.
  """
  if isinstance(data, str):
    return len(data)
  elif isinstance(data, list):
    return None
  elif ElementTree.iselement(data):
    return len(ElementTree.tostring(data))
  elif hasattr(data, 'read'):
    # If this is a file-like object, don't try to guess the length.
    return None
  else:
    return len(str(data))
    

def deprecation(message):
  warnings.warn(message, DeprecationWarning, stacklevel=2)