This file is indexed.

/usr/share/pyshared/gdata/client.py is in python-gdata 2.0.18+dfsg-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

   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
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
#!/usr/bin/env python
#
# Copyright (C) 2008, 2009 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.


# This module is used for version 2 of the Google Data APIs.


"""Provides a client to interact with Google Data API servers.

This module is used for version 2 of the Google Data APIs. The primary class
in this module is GDClient.

  GDClient: handles auth and CRUD operations when communicating with servers.
  GDataClient: deprecated client for version one services. Will be removed.
"""


__author__ = 'j.s@google.com (Jeff Scudder)'


import re
import atom.client
import atom.core
import atom.http_core
import gdata.gauth
import gdata.data


class Error(Exception):
  pass


class RequestError(Error):
  status = None
  reason = None
  body = None
  headers = None


class RedirectError(RequestError):
  pass


class CaptchaChallenge(RequestError):
  captcha_url = None
  captcha_token = None


class ClientLoginTokenMissing(Error):
  pass


class MissingOAuthParameters(Error):
  pass


class ClientLoginFailed(RequestError):
  pass


class UnableToUpgradeToken(RequestError):
  pass


class Unauthorized(Error):
  pass


class BadAuthenticationServiceURL(RedirectError):
  pass


class BadAuthentication(RequestError):
  pass


class NotModified(RequestError):
  pass

class NotImplemented(RequestError):
  pass


def error_from_response(message, http_response, error_class,
                        response_body=None):

  """Creates a new exception and sets the HTTP information in the error.

  Args:
   message: str human readable message to be displayed if the exception is
            not caught.
   http_response: The response from the server, contains error information.
   error_class: The exception to be instantiated and populated with
                information from the http_response
   response_body: str (optional) specify if the response has already been read
                  from the http_response object.
  """
  if response_body is None:
    body = http_response.read()
  else:
    body = response_body
  error = error_class('%s: %i, %s' % (message, http_response.status, body))
  error.status = http_response.status
  error.reason = http_response.reason
  error.body = body
  error.headers = atom.http_core.get_headers(http_response)
  return error


def get_xml_version(version):
  """Determines which XML schema to use based on the client API version.

  Args:
    version: string which is converted to an int. The version string is in
             the form 'Major.Minor.x.y.z' and only the major version number
             is considered. If None is provided assume version 1.
  """
  if version is None:
    return 1
  return int(version.split('.')[0])


class GDClient(atom.client.AtomPubClient):
  """Communicates with Google Data servers to perform CRUD operations.

  This class is currently experimental and may change in backwards
  incompatible ways.

  This class exists to simplify the following three areas involved in using
  the Google Data APIs.

  CRUD Operations:

  The client provides a generic 'request' method for making HTTP requests.
  There are a number of convenience methods which are built on top of
  request, which include get_feed, get_entry, get_next, post, update, and
  delete. These methods contact the Google Data servers.

  Auth:

  Reading user-specific private data requires authorization from the user as
  do any changes to user data. An auth_token object can be passed into any
  of the HTTP requests to set the Authorization header in the request.

  You may also want to set the auth_token member to a an object which can
  use modify_request to set the Authorization header in the HTTP request.

  If you are authenticating using the email address and password, you can
  use the client_login method to obtain an auth token and set the
  auth_token member.

  If you are using browser redirects, specifically AuthSub, you will want
  to use gdata.gauth.AuthSubToken.from_url to obtain the token after the
  redirect, and you will probably want to updgrade this since use token
  to a multiple use (session) token using the upgrade_token method.

  API Versions:

  This client is multi-version capable and can be used with Google Data API
  version 1 and version 2. The version should be specified by setting the
  api_version member to a string, either '1' or '2'.
  """

  # The gsessionid is used by Google Calendar to prevent redirects.
  __gsessionid = None
  api_version = None
  # Name of the Google Data service when making a ClientLogin request.
  auth_service = None
  # URL prefixes which should be requested for AuthSub and OAuth.
  auth_scopes = None
  # Name of alternate auth service to use in certain cases
  alt_auth_service = None

  def request(self, method=None, uri=None, auth_token=None,
              http_request=None, converter=None, desired_class=None,
              redirects_remaining=4, **kwargs):
    """Make an HTTP request to the server.

    See also documentation for atom.client.AtomPubClient.request.

    If a 302 redirect is sent from the server to the client, this client
    assumes that the redirect is in the form used by the Google Calendar API.
    The same request URI and method will be used as in the original request,
    but a gsessionid URL parameter will be added to the request URI with
    the value provided in the server's 302 redirect response. If the 302
    redirect is not in the format specified by the Google Calendar API, a
    RedirectError will be raised containing the body of the server's
    response.

    The method calls the client's modify_request method to make any changes
    required by the client before the request is made. For example, a
    version 2 client could add a GData-Version: 2 header to the request in
    its modify_request method.

    Args:
      method: str The HTTP verb for this request, usually 'GET', 'POST',
              'PUT', or 'DELETE'
      uri: atom.http_core.Uri, str, or unicode The URL being requested.
      auth_token: An object which sets the Authorization HTTP header in its
                  modify_request method. Recommended classes include
                  gdata.gauth.ClientLoginToken and gdata.gauth.AuthSubToken
                  among others.
      http_request: (optional) atom.http_core.HttpRequest
      converter: function which takes the body of the response as its only
                 argument and returns the desired object.
      desired_class: class descended from atom.core.XmlElement to which a
                     successful response should be converted. If there is no
                     converter function specified (converter=None) then the
                     desired_class will be used in calling the
                     atom.core.parse function. If neither
                     the desired_class nor the converter is specified, an
                     HTTP reponse object will be returned.
      redirects_remaining: (optional) int, if this number is 0 and the
                           server sends a 302 redirect, the request method
                           will raise an exception. This parameter is used in
                           recursive request calls to avoid an infinite loop.

    Any additional arguments are passed through to
    atom.client.AtomPubClient.request.

    Returns:
      An HTTP response object (see atom.http_core.HttpResponse for a
      description of the object's interface) if no converter was
      specified and no desired_class was specified. If a converter function
      was provided, the results of calling the converter are returned. If no
      converter was specified but a desired_class was provided, the response
      body will be converted to the class using
      atom.core.parse.
    """
    if isinstance(uri, (str, unicode)):
      uri = atom.http_core.Uri.parse_uri(uri)

    # Add the gsession ID to the URL to prevent further redirects.
    # TODO: If different sessions are using the same client, there will be a
    # multitude of redirects and session ID shuffling.
    # If the gsession ID is in the URL, adopt it as the standard location.
    if uri is not None and uri.query is not None and 'gsessionid' in uri.query:
      self.__gsessionid = uri.query['gsessionid']
    # The gsession ID could also be in the HTTP request.
    elif (http_request is not None and http_request.uri is not None
          and http_request.uri.query is not None
          and 'gsessionid' in http_request.uri.query):
      self.__gsessionid = http_request.uri.query['gsessionid']
    # If the gsession ID is stored in the client, and was not present in the
    # URI then add it to the URI.
    elif self.__gsessionid is not None:
      uri.query['gsessionid'] = self.__gsessionid

    # The AtomPubClient should call this class' modify_request before
    # performing the HTTP request.
    #http_request = self.modify_request(http_request)

    response = atom.client.AtomPubClient.request(self, method=method,
        uri=uri, auth_token=auth_token, http_request=http_request, **kwargs)
    # On success, convert the response body using the desired converter
    # function if present.
    if response is None:
      return None
    if response.status == 200 or response.status == 201:
      if converter is not None:
        return converter(response)
      elif desired_class is not None:
        if self.api_version is not None:
          return atom.core.parse(response.read(), desired_class,
                                 version=get_xml_version(self.api_version))
        else:
          # No API version was specified, so allow parse to
          # use the default version.
          return atom.core.parse(response.read(), desired_class)
      else:
        return response
    # TODO: move the redirect logic into the Google Calendar client once it
    # exists since the redirects are only used in the calendar API.
    elif response.status == 302:
      if redirects_remaining > 0:
        location = (response.getheader('Location')
                    or response.getheader('location'))
        if location is not None:
          # Make a recursive call with the gsession ID in the URI to follow
          # the redirect.
          return self.request(method=method, uri=location, 
                              auth_token=auth_token, http_request=http_request,
                              converter=converter, desired_class=desired_class,
                              redirects_remaining=redirects_remaining-1,
                              **kwargs)
        else:
          raise error_from_response('302 received without Location header',
                                    response, RedirectError)
      else:
        raise error_from_response('Too many redirects from server',
                                  response, RedirectError)
    elif response.status == 401:
      raise error_from_response('Unauthorized - Server responded with',
                                response, Unauthorized)
    elif response.status == 304:
      raise error_from_response('Entry Not Modified - Server responded with',
                                response, NotModified)
    elif response.status == 501:
      raise error_from_response(
          'This API operation is not implemented. - Server responded with',
          response, NotImplemented)
    # If the server's response was not a 200, 201, 302, 304, 401, or 501, raise
    # an exception.
    else:
      raise error_from_response('Server responded with', response,
                                RequestError)

  Request = request

  def request_client_login_token(
      self, email, password, source, service=None,
      account_type='HOSTED_OR_GOOGLE',
      auth_url=atom.http_core.Uri.parse_uri(
          'https://www.google.com/accounts/ClientLogin'),
      captcha_token=None, captcha_response=None):
    service = service or self.auth_service
    # Set the target URL.
    http_request = atom.http_core.HttpRequest(uri=auth_url, method='POST')
    http_request.add_body_part(
        gdata.gauth.generate_client_login_request_body(email=email,
            password=password, service=service, source=source,
            account_type=account_type, captcha_token=captcha_token,
            captcha_response=captcha_response),
        'application/x-www-form-urlencoded')

    # Use the underlying http_client to make the request.
    response = self.http_client.request(http_request)

    response_body = response.read()
    if response.status == 200:
      token_string = gdata.gauth.get_client_login_token_string(response_body)
      if token_string is not None:
        return gdata.gauth.ClientLoginToken(token_string)
      else:
        raise ClientLoginTokenMissing(
            'Recieved a 200 response to client login request,'
            ' but no token was present. %s' % (response_body,))
    elif response.status == 403:
      captcha_challenge = gdata.gauth.get_captcha_challenge(response_body)
      if captcha_challenge:
        challenge = CaptchaChallenge('CAPTCHA required')
        challenge.captcha_url = captcha_challenge['url']
        challenge.captcha_token = captcha_challenge['token']
        raise challenge
      elif response_body.splitlines()[0] == 'Error=BadAuthentication':
        raise BadAuthentication('Incorrect username or password')
      else:
        raise error_from_response('Server responded with a 403 code',
                                  response, RequestError, response_body)
    elif response.status == 302:
      # Google tries to redirect all bad URLs back to
      # http://www.google.<locale>. If a redirect
      # attempt is made, assume the user has supplied an incorrect
      # authentication URL
      raise error_from_response('Server responded with a redirect',
                                response, BadAuthenticationServiceURL,
                                response_body)
    else:
      raise error_from_response('Server responded to ClientLogin request',
                                response, ClientLoginFailed, response_body)

  RequestClientLoginToken = request_client_login_token

  def client_login(self, email, password, source, service=None,
                   account_type='HOSTED_OR_GOOGLE',
                   auth_url=atom.http_core.Uri.parse_uri(
                       'https://www.google.com/accounts/ClientLogin'),
                   captcha_token=None, captcha_response=None):
    """Performs an auth request using the user's email address and password.
    
    In order to modify user specific data and read user private data, your
    application must be authorized by the user. One way to demonstrage
    authorization is by including a Client Login token in the Authorization
    HTTP header of all requests. This method requests the Client Login token
    by sending the user's email address, password, the name of the
    application, and the service code for the service which will be accessed
    by the application. If the username and password are correct, the server
    will respond with the client login code and a new ClientLoginToken
    object will be set in the client's auth_token member. With the auth_token
    set, future requests from this client will include the Client Login
    token.
    
    For a list of service names, see 
    http://code.google.com/apis/gdata/faq.html#clientlogin
    For more information on Client Login, see:
    http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html

    Args:
      email: str The user's email address or username.
      password: str The password for the user's account.
      source: str The name of your application. This can be anything you
              like but should should give some indication of which app is
              making the request.
      service: str The service code for the service you would like to access.
               For example, 'cp' for contacts, 'cl' for calendar. For a full
               list see
               http://code.google.com/apis/gdata/faq.html#clientlogin
               If you are using a subclass of the gdata.client.GDClient, the
               service will usually be filled in for you so you do not need
               to specify it. For example see BloggerClient,
               SpreadsheetsClient, etc.
      account_type: str (optional) The type of account which is being
                    authenticated. This can be either 'GOOGLE' for a Google
                    Account, 'HOSTED' for a Google Apps Account, or the
                    default 'HOSTED_OR_GOOGLE' which will select the Google
                    Apps Account if the same email address is used for both
                    a Google Account and a Google Apps Account.
      auth_url: str (optional) The URL to which the login request should be
                sent.
      captcha_token: str (optional) If a previous login attempt was reponded
                     to with a CAPTCHA challenge, this is the token which
                     identifies the challenge (from the CAPTCHA's URL).
      captcha_response: str (optional) If a previous login attempt was
                        reponded to with a CAPTCHA challenge, this is the
                        response text which was contained in the challenge.

      Returns:
        Generated token, which is also stored in this object.

      Raises:
        A RequestError or one of its suclasses: BadAuthentication,
        BadAuthenticationServiceURL, ClientLoginFailed,
        ClientLoginTokenMissing, or CaptchaChallenge
    """
    service = service or self.auth_service
    self.auth_token = self.request_client_login_token(email, password,
        source, service=service, account_type=account_type, auth_url=auth_url,
        captcha_token=captcha_token, captcha_response=captcha_response)
    if self.alt_auth_service is not None:
      self.alt_auth_token = self.request_client_login_token(
          email, password, source, service=self.alt_auth_service,
          account_type=account_type, auth_url=auth_url,
          captcha_token=captcha_token, captcha_response=captcha_response)
    return self.auth_token

  ClientLogin = client_login

  def upgrade_token(self, token=None, url=atom.http_core.Uri.parse_uri(
      'https://www.google.com/accounts/AuthSubSessionToken')):
    """Asks the Google auth server for a multi-use AuthSub token.

    For details on AuthSub, see:
    http://code.google.com/apis/accounts/docs/AuthSub.html

    Args:
      token: gdata.gauth.AuthSubToken or gdata.gauth.SecureAuthSubToken
          (optional) If no token is passed in, the client's auth_token member
          is used to request the new token. The token object will be modified
          to contain the new session token string.
      url: str or atom.http_core.Uri (optional) The URL to which the token
          upgrade request should be sent. Defaults to:
          https://www.google.com/accounts/AuthSubSessionToken

    Returns:
      The upgraded gdata.gauth.AuthSubToken object.
    """
    # Default to using the auth_token member if no token is provided.
    if token is None:
      token = self.auth_token
    # We cannot upgrade a None token.
    if token is None:
      raise UnableToUpgradeToken('No token was provided.')
    if not isinstance(token, gdata.gauth.AuthSubToken):
      raise UnableToUpgradeToken(
          'Cannot upgrade the token because it is not an AuthSubToken object.')
    http_request = atom.http_core.HttpRequest(uri=url, method='GET')
    token.modify_request(http_request)
    # Use the lower level HttpClient to make the request.
    response = self.http_client.request(http_request)
    if response.status == 200:
      token._upgrade_token(response.read())
      return token
    else:
      raise UnableToUpgradeToken(
          'Server responded to token upgrade request with %s: %s' % (
              response.status, response.read()))

  UpgradeToken = upgrade_token

  def revoke_token(self, token=None, url=atom.http_core.Uri.parse_uri(
      'https://www.google.com/accounts/AuthSubRevokeToken')):
    """Requests that the token be invalidated.
    
    This method can be used for both AuthSub and OAuth tokens (to invalidate
    a ClientLogin token, the user must change their password).

    Returns:
      True if the server responded with a 200.

    Raises:
      A RequestError if the server responds with a non-200 status.
    """
    # Default to using the auth_token member if no token is provided.
    if token is None:
      token = self.auth_token

    http_request = atom.http_core.HttpRequest(uri=url, method='GET')
    token.modify_request(http_request)
    response = self.http_client.request(http_request)
    if response.status != 200:
      raise error_from_response('Server sent non-200 to revoke token',
                                response, RequestError, response.read())

    return True

  RevokeToken = revoke_token

  def get_oauth_token(self, scopes, next, consumer_key, consumer_secret=None,
                      rsa_private_key=None,
                      url=gdata.gauth.REQUEST_TOKEN_URL):
    """Obtains an OAuth request token to allow the user to authorize this app.

    Once this client has a request token, the user can authorize the request
    token by visiting the authorization URL in their browser. After being
    redirected back to this app at the 'next' URL, this app can then exchange
    the authorized request token for an access token.

    For more information see the documentation on Google Accounts with OAuth:
    http://code.google.com/apis/accounts/docs/OAuth.html#AuthProcess

    Args:
      scopes: list of strings or atom.http_core.Uri objects which specify the
          URL prefixes which this app will be accessing. For example, to access
          the Google Calendar API, you would want to use scopes:
          ['https://www.google.com/calendar/feeds/',
           'http://www.google.com/calendar/feeds/']
      next: str or atom.http_core.Uri object, The URL which the user's browser
          should be sent to after they authorize access to their data. This
          should be a URL in your application which will read the token
          information from the URL and upgrade the request token to an access
          token.
      consumer_key: str This is the identifier for this application which you
          should have received when you registered your application with Google
          to use OAuth.
      consumer_secret: str (optional) The shared secret between your app and
          Google which provides evidence that this request is coming from you
          application and not another app. If present, this libraries assumes
          you want to use an HMAC signature to verify requests. Keep this data
          a secret.
      rsa_private_key: str (optional) The RSA private key which is used to
          generate a digital signature which is checked by Google's server. If
          present, this library assumes that you want to use an RSA signature
          to verify requests. Keep this data a secret.
      url: The URL to which a request for a token should be made. The default
          is Google's OAuth request token provider.
    """
    http_request = None
    if rsa_private_key is not None:
      http_request = gdata.gauth.generate_request_for_request_token(
          consumer_key, gdata.gauth.RSA_SHA1, scopes,
          rsa_key=rsa_private_key, auth_server_url=url, next=next)
    elif consumer_secret is not None:
      http_request = gdata.gauth.generate_request_for_request_token(
          consumer_key, gdata.gauth.HMAC_SHA1, scopes,
          consumer_secret=consumer_secret, auth_server_url=url, next=next)
    else:
      raise MissingOAuthParameters(
          'To request an OAuth token, you must provide your consumer secret'
          ' or your private RSA key.')

    response = self.http_client.request(http_request)
    response_body = response.read()

    if response.status != 200:
      raise error_from_response('Unable to obtain OAuth request token',
                                response, RequestError, response_body)

    if rsa_private_key is not None:
      return gdata.gauth.rsa_token_from_body(response_body, consumer_key,
                                             rsa_private_key,
                                             gdata.gauth.REQUEST_TOKEN)
    elif consumer_secret is not None:
      return gdata.gauth.hmac_token_from_body(response_body, consumer_key,
                                              consumer_secret,
                                              gdata.gauth.REQUEST_TOKEN)

  GetOAuthToken = get_oauth_token

  def get_access_token(self, request_token,
                       url=gdata.gauth.ACCESS_TOKEN_URL):
    """Exchanges an authorized OAuth request token for an access token.

    Contacts the Google OAuth server to upgrade a previously authorized
    request token. Once the request token is upgraded to an access token,
    the access token may be used to access the user's data.

    For more details, see the Google Accounts OAuth documentation:
    http://code.google.com/apis/accounts/docs/OAuth.html#AccessToken

    Args:
      request_token: An OAuth token which has been authorized by the user.
      url: (optional) The URL to which the upgrade request should be sent.
          Defaults to: https://www.google.com/accounts/OAuthAuthorizeToken
    """
    http_request = gdata.gauth.generate_request_for_access_token(
        request_token, auth_server_url=url)
    response = self.http_client.request(http_request)
    response_body = response.read()
    if response.status != 200:
      raise error_from_response(
          'Unable to upgrade OAuth request token to access token',
          response, RequestError, response_body)

    return gdata.gauth.upgrade_to_access_token(request_token, response_body)

  GetAccessToken = get_access_token

  def modify_request(self, http_request):
    """Adds or changes request before making the HTTP request.

    This client will add the API version if it is specified.
    Subclasses may override this method to add their own request
    modifications before the request is made.
    """
    http_request = atom.client.AtomPubClient.modify_request(self,
                                                            http_request)
    if self.api_version is not None:
      http_request.headers['GData-Version'] = self.api_version
    return http_request

  ModifyRequest = modify_request

  def get_feed(self, uri, auth_token=None, converter=None,
               desired_class=gdata.data.GDFeed, **kwargs):
    return self.request(method='GET', uri=uri, auth_token=auth_token,
                        converter=converter, desired_class=desired_class,
                        **kwargs)

  GetFeed = get_feed

  def get_entry(self, uri, auth_token=None, converter=None,
                desired_class=gdata.data.GDEntry, etag=None, **kwargs):
    http_request = atom.http_core.HttpRequest()
    # Conditional retrieval
    if etag is not None:
      http_request.headers['If-None-Match'] = etag
    return self.request(method='GET', uri=uri, auth_token=auth_token,
                        http_request=http_request, converter=converter,
                        desired_class=desired_class, **kwargs)

  GetEntry = get_entry

  def get_next(self, feed, auth_token=None, converter=None,
               desired_class=None, **kwargs):
    """Fetches the next set of results from the feed.

    When requesting a feed, the number of entries returned is capped at a
    service specific default limit (often 25 entries). You can specify your
    own entry-count cap using the max-results URL query parameter. If there
    are more results than could fit under max-results, the feed will contain
    a next link. This method performs a GET against this next results URL.

    Returns:
      A new feed object containing the next set of entries in this feed.
    """
    if converter is None and desired_class is None:
      desired_class = feed.__class__
    return self.get_feed(feed.find_next_link(), auth_token=auth_token,
                         converter=converter, desired_class=desired_class,
                         **kwargs)

  GetNext = get_next

  # TODO: add a refresh method to re-fetch the entry/feed from the server
  # if it has been updated.

  def post(self, entry, uri, auth_token=None, converter=None,
           desired_class=None, **kwargs):
    if converter is None and desired_class is None:
      desired_class = entry.__class__
    http_request = atom.http_core.HttpRequest()
    http_request.add_body_part(
        entry.to_string(get_xml_version(self.api_version)),
        'application/atom+xml')
    return self.request(method='POST', uri=uri, auth_token=auth_token,
                        http_request=http_request, converter=converter,
                        desired_class=desired_class, **kwargs)

  Post = post

  def update(self, entry, auth_token=None, force=False, uri=None, **kwargs):
    """Edits the entry on the server by sending the XML for this entry.

    Performs a PUT and converts the response to a new entry object with a
    matching class to the entry passed in.

    Args:
      entry:
      auth_token:
      force: boolean stating whether an update should be forced. Defaults to
             False. Normally, if a change has been made since the passed in
             entry was obtained, the server will not overwrite the entry since
             the changes were based on an obsolete version of the entry.
             Setting force to True will cause the update to silently
             overwrite whatever version is present.
      uri: The uri to put to. If provided, this uri is PUT to rather than the
           inferred uri from the entry's edit link.

    Returns:
      A new Entry object of a matching type to the entry which was passed in.
    """
    http_request = atom.http_core.HttpRequest()
    http_request.add_body_part(
        entry.to_string(get_xml_version(self.api_version)),
        'application/atom+xml')
    # Include the ETag in the request if present.
    if force:
      http_request.headers['If-Match'] = '*'
    elif hasattr(entry, 'etag') and entry.etag:
      http_request.headers['If-Match'] = entry.etag

    if uri is None:
      uri = entry.find_edit_link()

    return self.request(method='PUT', uri=uri, auth_token=auth_token,
                        http_request=http_request,
                        desired_class=entry.__class__, **kwargs)

  Update = update

  def delete(self, entry_or_uri, auth_token=None, force=False, **kwargs):
    http_request = atom.http_core.HttpRequest()
      
    # Include the ETag in the request if present.
    if force:
      http_request.headers['If-Match'] = '*'
    elif hasattr(entry_or_uri, 'etag') and entry_or_uri.etag:
      http_request.headers['If-Match'] = entry_or_uri.etag

    # If the user passes in a URL, just delete directly, may not work as
    # the service might require an ETag.
    if isinstance(entry_or_uri, (str, unicode, atom.http_core.Uri)):
      return self.request(method='DELETE', uri=entry_or_uri,
                          http_request=http_request, auth_token=auth_token,
                          **kwargs)

    return self.request(method='DELETE', uri=entry_or_uri.find_edit_link(),
                        http_request=http_request, auth_token=auth_token,
                        **kwargs)

  Delete = delete

  def batch(self, feed, uri=None, force=False, auth_token=None, **kwargs):
    """Sends a batch request to the server to execute operation entries.

    Args:
      feed: A batch feed containing batch entries, each is an operation.
      uri: (optional) The uri to which the batch request feed should be POSTed.
          If none is provided, then the feed's edit link will be used.
      force: (optional) boolean set to True if you want the batch update to
          clobber all data. If False, the version in the information in the
          feed object will cause the server to check to see that no changes
          intervened between when you fetched the data and when you sent the
          changes.
      auth_token: (optional) An object which sets the Authorization HTTP header
          in its modify_request method. Recommended classes include
          gdata.gauth.ClientLoginToken and gdata.gauth.AuthSubToken
          among others.
    """
    http_request = atom.http_core.HttpRequest()
    http_request.add_body_part(
        feed.to_string(get_xml_version(self.api_version)),
        'application/atom+xml')
    if force:
      http_request.headers['If-Match'] = '*'
    elif hasattr(feed, 'etag') and feed.etag:
      http_request.headers['If-Match'] = feed.etag

    if uri is None:
      uri = feed.find_edit_link()

    return self.request(method='POST', uri=uri, auth_token=auth_token,
                        http_request=http_request,
                        desired_class=feed.__class__, **kwargs)

  Batch = batch

  # TODO: add a refresh method to request a conditional update to an entry
  # or feed.


def _add_query_param(param_string, value, http_request):
  if value:
    http_request.uri.query[param_string] = value


class Query(object):

  def __init__(self, text_query=None, categories=None, author=None, alt=None,
               updated_min=None, updated_max=None, pretty_print=False,
               published_min=None, published_max=None, start_index=None,
               max_results=None, strict=False, **custom_parameters):
    """Constructs a Google Data Query to filter feed contents serverside.

    Args:
      text_query: Full text search str (optional)
      categories: list of strings (optional). Each string is a required
          category. To include an 'or' query, put a | in the string between
          terms. For example, to find everything in the Fitz category and
          the Laurie or Jane category (Fitz and (Laurie or Jane)) you would
          set categories to ['Fitz', 'Laurie|Jane'].
      author: str (optional) The service returns entries where the author
          name and/or email address match your query string.
      alt: str (optional) for the Alternative representation type you'd like
          the feed in. If you don't specify an alt parameter, the service
          returns an Atom feed. This is equivalent to alt='atom'.
          alt='rss' returns an RSS 2.0 result feed.
          alt='json' returns a JSON representation of the feed.
          alt='json-in-script' Requests a response that wraps JSON in a script
          tag.
          alt='atom-in-script' Requests an Atom response that wraps an XML
          string in a script tag.
          alt='rss-in-script' Requests an RSS response that wraps an XML
          string in a script tag.
      updated_min: str (optional), RFC 3339 timestamp format, lower bounds.
          For example: 2005-08-09T10:57:00-08:00
      updated_max: str (optional) updated time must be earlier than timestamp.
      pretty_print: boolean (optional) If True the server's XML response will
          be indented to make it more human readable. Defaults to False.
      published_min: str (optional), Similar to updated_min but for published
          time.
      published_max: str (optional), Similar to updated_max but for published
          time.
      start_index: int or str (optional) 1-based index of the first result to
          be retrieved. Note that this isn't a general cursoring mechanism.
          If you first send a query with ?start-index=1&max-results=10 and
          then send another query with ?start-index=11&max-results=10, the
          service cannot guarantee that the results are equivalent to
          ?start-index=1&max-results=20, because insertions and deletions
          could have taken place in between the two queries.
      max_results: int or str (optional) Maximum number of results to be
          retrieved. Each service has a default max (usually 25) which can
          vary from service to service. There is also a service-specific
          limit to the max_results you can fetch in a request.
      strict: boolean (optional) If True, the server will return an error if
          the server does not recognize any of the parameters in the request
          URL. Defaults to False.
      custom_parameters: other query parameters that are not explicitly defined.
    """
    self.text_query = text_query
    self.categories = categories or []
    self.author = author
    self.alt = alt
    self.updated_min = updated_min
    self.updated_max = updated_max
    self.pretty_print = pretty_print
    self.published_min = published_min
    self.published_max = published_max
    self.start_index = start_index
    self.max_results = max_results
    self.strict = strict
    self.custom_parameters = custom_parameters

  def add_custom_parameter(self, key, value):
    self.custom_parameters[key] = value

  AddCustomParameter = add_custom_parameter

  def modify_request(self, http_request):
    _add_query_param('q', self.text_query, http_request)
    if self.categories:
      http_request.uri.query['category'] = ','.join(self.categories)
    _add_query_param('author', self.author, http_request)
    _add_query_param('alt', self.alt, http_request)
    _add_query_param('updated-min', self.updated_min, http_request)
    _add_query_param('updated-max', self.updated_max, http_request)
    if self.pretty_print:
      http_request.uri.query['prettyprint'] = 'true'
    _add_query_param('published-min', self.published_min, http_request)
    _add_query_param('published-max', self.published_max, http_request)
    if self.start_index is not None:
      http_request.uri.query['start-index'] = str(self.start_index)
    if self.max_results is not None:
      http_request.uri.query['max-results'] = str(self.max_results)
    if self.strict:
      http_request.uri.query['strict'] = 'true'
    http_request.uri.query.update(self.custom_parameters)

  ModifyRequest = modify_request


class GDQuery(atom.http_core.Uri):

  def _get_text_query(self):
    return self.query['q']

  def _set_text_query(self, value):
    self.query['q'] = value

  text_query = property(_get_text_query, _set_text_query,
      doc='The q parameter for searching for an exact text match on content')


class ResumableUploader(object):
  """Resumable upload helper for the Google Data protocol."""

  DEFAULT_CHUNK_SIZE = 5242880  # 5MB
  # Initial chunks which are smaller than 256KB might be dropped. The last
  # chunk for a file can be smaller tan this.
  MIN_CHUNK_SIZE = 262144 # 256KB

  def __init__(self, client, file_handle, content_type, total_file_size,
               chunk_size=None, desired_class=None):
    """Starts a resumable upload to a service that supports the protocol.

    Args:
      client: gdata.client.GDClient A Google Data API service.
      file_handle: object A file-like object containing the file to upload.
      content_type: str The mimetype of the file to upload.
      total_file_size: int The file's total size in bytes.
      chunk_size: int The size of each upload chunk. If None, the
          DEFAULT_CHUNK_SIZE will be used.
      desired_class: object (optional) The type of gdata.data.GDEntry to parse
          the completed entry as. This should be specific to the API.
    """
    self.client = client
    self.file_handle = file_handle
    self.content_type = content_type
    self.total_file_size = total_file_size
    self.chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZE
    if self.chunk_size < self.MIN_CHUNK_SIZE:
      self.chunk_size = self.MIN_CHUNK_SIZE
    self.desired_class = desired_class or gdata.data.GDEntry
    self.upload_uri = None

    # Send the entire file if the chunk size is less than fize's total size.
    if self.total_file_size <= self.chunk_size:
      self.chunk_size = total_file_size

  def _init_session(self, resumable_media_link, entry=None, headers=None,
                    auth_token=None, method='POST'):
    """Starts a new resumable upload to a service that supports the protocol.

    The method makes a request to initiate a new upload session. The unique
    upload uri returned by the server (and set in this method) should be used
    to send upload chunks to the server.

    Args:
      resumable_media_link: str The full URL for the #resumable-create-media or
          #resumable-edit-media link for starting a resumable upload request or
          updating media using a resumable PUT.
      entry: A (optional) gdata.data.GDEntry containging metadata to create the 
          upload from.
      headers: dict (optional) Additional headers to send in the initial request
          to create the resumable upload request. These headers will override
          any default headers sent in the request. For example:
          headers={'Slug': 'MyTitle'}.
      auth_token: (optional) An object which sets the Authorization HTTP header
          in its modify_request method. Recommended classes include
          gdata.gauth.ClientLoginToken and gdata.gauth.AuthSubToken
          among others.
      method: (optional) Type of HTTP request to start the session with.
          Defaults to 'POST', but may also be 'PUT'.

    Returns:
      Result of HTTP request to intialize the session. See atom.client.request.

    Raises:
      RequestError if the unique upload uri is not set or the
      server returns something other than an HTTP 308 when the upload is
      incomplete.
    """
    http_request = atom.http_core.HttpRequest()
    
    # Send empty body if Atom XML wasn't specified.
    if entry is None:
      http_request.add_body_part('', self.content_type, size=0)
    else:
      http_request.add_body_part(str(entry), 'application/atom+xml',
                                 size=len(str(entry)))
    http_request.headers['X-Upload-Content-Type'] = self.content_type
    http_request.headers['X-Upload-Content-Length'] = str(self.total_file_size)

    if headers is not None:
      http_request.headers.update(headers)

    response = self.client.request(method=method,
                                   uri=resumable_media_link,
                                   auth_token=auth_token,
                                   http_request=http_request)

    self.upload_uri = (response.getheader('location') or
                       response.getheader('Location'))

    return response

  _InitSession = _init_session

  def upload_chunk(self, start_byte, content_bytes):
    """Uploads a byte range (chunk) to the resumable upload server.

    Args:
      start_byte: int The byte offset of the total file where the byte range
          passed in lives.
      content_bytes: str The file contents of this chunk.

    Returns:
      The final Atom entry created on the server. The entry object's type will
      be the class specified in self.desired_class.

    Raises:
      RequestError if the unique upload uri is not set or the
      server returns something other than an HTTP 308 when the upload is
      incomplete.
    """
    if self.upload_uri is None:
      raise RequestError('Resumable upload request not initialized.')

    # Adjustment if last byte range is less than defined chunk size.
    chunk_size = self.chunk_size
    if len(content_bytes) <= chunk_size:
      chunk_size = len(content_bytes)

    http_request = atom.http_core.HttpRequest()
    http_request.add_body_part(content_bytes, self.content_type,
                               size=len(content_bytes))
    http_request.headers['Content-Range'] = ('bytes %s-%s/%s'
                                             % (start_byte,
                                                start_byte + chunk_size - 1,
                                                self.total_file_size))

    try:
      response = self.client.request(method='PUT', uri=self.upload_uri,
                                     http_request=http_request,
                                     desired_class=self.desired_class)
      return response
    except RequestError, error:
      if error.status == 308:
        return None
      else:
        raise error

  UploadChunk = upload_chunk

  def upload_file(self, resumable_media_link, entry=None, headers=None,
                  auth_token=None, **kwargs):
    """Uploads an entire file in chunks using the resumable upload protocol.

    If you are interested in pausing an upload or controlling the chunking
    yourself, use the upload_chunk() method instead.

    Args:
      resumable_media_link: str The full URL for the #resumable-create-media for
          starting a resumable upload request.
      entry: A (optional) gdata.data.GDEntry containging metadata to create the 
          upload from.
      headers: dict Additional headers to send in the initial request to create
          the resumable upload request. These headers will override any default
          headers sent in the request. For example: headers={'Slug': 'MyTitle'}.
      auth_token: (optional) An object which sets the Authorization HTTP header
          in its modify_request method. Recommended classes include
          gdata.gauth.ClientLoginToken and gdata.gauth.AuthSubToken
          among others.
      kwargs: (optional) Other args to pass to self._init_session.

    Returns:
      The final Atom entry created on the server. The entry object's type will
      be the class specified in self.desired_class.

    Raises:
      RequestError if anything other than a HTTP 308 is returned
      when the request raises an exception.
    """
    self._init_session(resumable_media_link, headers=headers,
                       auth_token=auth_token, entry=entry, **kwargs)

    start_byte = 0
    entry = None

    while not entry:
      entry = self.upload_chunk(
          start_byte, self.file_handle.read(self.chunk_size))
      start_byte += self.chunk_size

    return entry

  UploadFile = upload_file

  def update_file(self, entry_or_resumable_edit_link, headers=None, force=False,
                  auth_token=None, update_metadata=False, uri_params=None):
    """Updates the contents of an existing file using the resumable protocol.

    If you are interested in pausing an upload or controlling the chunking
    yourself, use the upload_chunk() method instead.

    Args:
      entry_or_resumable_edit_link: object or string A gdata.data.GDEntry for
          the entry/file to update or the full uri of the link with rel
          #resumable-edit-media.
      headers: dict Additional headers to send in the initial request to create
          the resumable upload request. These headers will override any default
          headers sent in the request. For example: headers={'Slug': 'MyTitle'}.
      force boolean (optional) True to force an update and set the If-Match
          header to '*'. If False and entry_or_resumable_edit_link is a
          gdata.data.GDEntry object, its etag value is used. Otherwise this
          parameter should be set to True to force the update.
      auth_token: (optional) An object which sets the Authorization HTTP header
          in its modify_request method. Recommended classes include
          gdata.gauth.ClientLoginToken and gdata.gauth.AuthSubToken
          among others.
      update_metadata: (optional) True to also update the entry's metadata
          with that in the given GDEntry object in entry_or_resumable_edit_link.
      uri_params: (optional) Dict of additional parameters to attach to the URI.
          Some non-dict types are valid here, too, like list of tuple pairs.

    Returns:
      The final Atom entry created on the server. The entry object's type will
      be the class specified in self.desired_class.

    Raises:
      RequestError if anything other than a HTTP 308 is returned when the
      request raises an exception.
    """

    custom_headers = {}
    if headers is not None:
      custom_headers.update(headers)

    uri = None
    entry = None
    if isinstance(entry_or_resumable_edit_link, gdata.data.GDEntry):
      uri = entry_or_resumable_edit_link.find_url(
          'http://schemas.google.com/g/2005#resumable-edit-media')
      custom_headers['If-Match'] = entry_or_resumable_edit_link.etag
      if update_metadata:
        entry = entry_or_resumable_edit_link
    else:
      uri = entry_or_resumable_edit_link

    uri = atom.http_core.parse_uri(uri)
    if uri_params is not None:
      uri.query.update(uri_params)

    if force:
      custom_headers['If-Match'] = '*'

    return self.upload_file(str(uri), entry=entry, headers=custom_headers,
                            auth_token=auth_token, method='PUT')

  UpdateFile = update_file

  def query_upload_status(self, uri=None):
    """Queries the current status of a resumable upload request.

    Args:
      uri: str (optional) A resumable upload uri to query and override the one
          that is set in this object.

    Returns:
      An integer representing the file position (byte) to resume the upload from
      or True if the upload is complete.

    Raises:
      RequestError if anything other than a HTTP 308 is returned
      when the request raises an exception.
    """
    # Override object's unique upload uri.
    if uri is None:
      uri = self.upload_uri

    http_request = atom.http_core.HttpRequest()
    http_request.headers['Content-Length'] = '0'
    http_request.headers['Content-Range'] = 'bytes */%s' % self.total_file_size

    try:
      response = self.client.request(
          method='POST', uri=uri, http_request=http_request)
      if response.status == 201:
        return True
      else:
        raise error_from_response(
            '%s returned by server' % response.status, response, RequestError)
    except RequestError, error:
      if error.status == 308:
        for pair in error.headers:
          if pair[0].capitalize() == 'Range':
            return int(pair[1].split('-')[1]) + 1
      else:
        raise error

  QueryUploadStatus = query_upload_status