From 18a0df1363553c074cd08bf5c56c829f117107c6 Mon Sep 17 00:00:00 2001 From: Graham Percival Date: Thu, 6 Oct 2011 17:15:54 +0100 Subject: [PATCH] inital import of patch scripts --- patches/atom/__init__.py | 1484 +++++ patches/atom/__init__.pyc | Bin 0 -> 51858 bytes patches/atom/auth.py | 43 + patches/atom/client.py | 182 + patches/atom/client.pyc | Bin 0 -> 6054 bytes patches/atom/core.py | 550 ++ patches/atom/core.pyc | Bin 0 -> 16448 bytes patches/atom/data.py | 340 + patches/atom/data.pyc | Bin 0 -> 13479 bytes patches/atom/http.py | 364 ++ patches/atom/http_core.py | 597 ++ patches/atom/http_core.pyc | Bin 0 -> 18835 bytes patches/atom/http_interface.py | 156 + patches/atom/mock_http.py | 132 + patches/atom/mock_http_core.py | 323 + patches/atom/mock_service.py | 243 + patches/atom/service.py | 740 +++ patches/atom/token_store.py | 117 + patches/atom/url.py | 139 + patches/gdata/Crypto/Cipher/AES.pyd | Bin 0 -> 27648 bytes patches/gdata/Crypto/Cipher/ARC2.pyd | Bin 0 -> 15872 bytes patches/gdata/Crypto/Cipher/ARC4.pyd | Bin 0 -> 8704 bytes patches/gdata/Crypto/Cipher/Blowfish.pyd | Bin 0 -> 19968 bytes patches/gdata/Crypto/Cipher/CAST.pyd | Bin 0 -> 26112 bytes patches/gdata/Crypto/Cipher/DES.pyd | Bin 0 -> 20480 bytes patches/gdata/Crypto/Cipher/DES3.pyd | Bin 0 -> 20992 bytes patches/gdata/Crypto/Cipher/IDEA.pyd | Bin 0 -> 15360 bytes patches/gdata/Crypto/Cipher/RC5.pyd | Bin 0 -> 15872 bytes patches/gdata/Crypto/Cipher/XOR.pyd | Bin 0 -> 8704 bytes patches/gdata/Crypto/Cipher/__init__.py | 33 + patches/gdata/Crypto/Hash/HMAC.py | 108 + patches/gdata/Crypto/Hash/MD2.pyd | Bin 0 -> 8704 bytes patches/gdata/Crypto/Hash/MD4.pyd | Bin 0 -> 9728 bytes patches/gdata/Crypto/Hash/MD5.py | 13 + patches/gdata/Crypto/Hash/RIPEMD.pyd | Bin 0 -> 14336 bytes patches/gdata/Crypto/Hash/SHA.py | 11 + patches/gdata/Crypto/Hash/SHA256.pyd | Bin 0 -> 9216 bytes patches/gdata/Crypto/Hash/__init__.py | 24 + patches/gdata/Crypto/Protocol/AllOrNothing.py | 295 + patches/gdata/Crypto/Protocol/Chaffing.py | 229 + patches/gdata/Crypto/Protocol/__init__.py | 17 + patches/gdata/Crypto/PublicKey/DSA.py | 238 + patches/gdata/Crypto/PublicKey/ElGamal.py | 132 + patches/gdata/Crypto/PublicKey/RSA.py | 256 + patches/gdata/Crypto/PublicKey/__init__.py | 17 + patches/gdata/Crypto/PublicKey/pubkey.py | 172 + patches/gdata/Crypto/PublicKey/qNEW.py | 170 + patches/gdata/Crypto/Util/RFC1751.py | 342 + patches/gdata/Crypto/Util/__init__.py | 16 + patches/gdata/Crypto/Util/number.py | 201 + patches/gdata/Crypto/Util/randpool.py | 421 ++ patches/gdata/Crypto/Util/test.py | 453 ++ patches/gdata/Crypto/__init__.py | 25 + patches/gdata/Crypto/test.py | 38 + patches/gdata/__init__.py | 835 +++ patches/gdata/__init__.pyc | Bin 0 -> 32899 bytes patches/gdata/acl/__init__.py | 15 + patches/gdata/acl/data.py | 63 + patches/gdata/alt/__init__.py | 20 + patches/gdata/alt/app_engine.py | 101 + patches/gdata/alt/appengine.py | 321 + patches/gdata/analytics/__init__.py | 223 + patches/gdata/analytics/client.py | 313 + patches/gdata/analytics/data.py | 379 ++ patches/gdata/analytics/service.py | 331 + patches/gdata/apps/__init__.py | 526 ++ patches/gdata/apps/adminsettings/__init__.py | 16 + patches/gdata/apps/adminsettings/service.py | 471 ++ patches/gdata/apps/audit/__init__.py | 1 + patches/gdata/apps/audit/service.py | 277 + patches/gdata/apps/emailsettings/__init__.py | 15 + patches/gdata/apps/emailsettings/client.py | 400 ++ patches/gdata/apps/emailsettings/data.py | 1130 ++++ patches/gdata/apps/emailsettings/service.py | 264 + patches/gdata/apps/groups/__init__.py | 0 patches/gdata/apps/groups/service.py | 387 ++ patches/gdata/apps/migration/__init__.py | 212 + patches/gdata/apps/migration/service.py | 129 + patches/gdata/apps/organization/__init__.py | 0 patches/gdata/apps/organization/service.py | 297 + patches/gdata/apps/service.py | 552 ++ patches/gdata/apps_property.py | 39 + patches/gdata/auth.py | 952 +++ patches/gdata/base/__init__.py | 746 +++ patches/gdata/base/service.py | 256 + patches/gdata/blogger/__init__.py | 202 + patches/gdata/blogger/client.py | 175 + patches/gdata/blogger/data.py | 168 + patches/gdata/blogger/service.py | 142 + patches/gdata/books/__init__.py | 473 ++ patches/gdata/books/data.py | 90 + patches/gdata/books/service.py | 266 + patches/gdata/calendar/__init__.py | 1044 ++++ patches/gdata/calendar/client.py | 538 ++ patches/gdata/calendar/data.py | 327 + patches/gdata/calendar/service.py | 595 ++ patches/gdata/calendar_resource/__init__.py | 1 + patches/gdata/calendar_resource/client.py | 200 + patches/gdata/calendar_resource/data.py | 206 + patches/gdata/client.py | 1131 ++++ patches/gdata/client.pyc | Bin 0 -> 42940 bytes patches/gdata/codesearch/__init__.py | 136 + patches/gdata/codesearch/service.py | 110 + patches/gdata/contacts/__init__.py | 740 +++ patches/gdata/contacts/client.py | 497 ++ patches/gdata/contacts/data.py | 477 ++ patches/gdata/contacts/service.py | 427 ++ patches/gdata/contentforshopping/__init__.py | 21 + patches/gdata/contentforshopping/client.py | 237 + patches/gdata/contentforshopping/data.py | 1175 ++++ patches/gdata/core.py | 279 + patches/gdata/data.py | 1219 ++++ patches/gdata/data.pyc | Bin 0 -> 51152 bytes patches/gdata/docs/__init__.py | 269 + patches/gdata/docs/client.py | 644 ++ patches/gdata/docs/data.py | 280 + patches/gdata/docs/service.py | 618 ++ patches/gdata/dublincore/__init__.py | 15 + patches/gdata/dublincore/data.py | 78 + patches/gdata/exif/__init__.py | 217 + patches/gdata/finance/__init__.py | 486 ++ patches/gdata/finance/data.py | 156 + patches/gdata/finance/service.py | 243 + patches/gdata/gauth.py | 1306 ++++ patches/gdata/gauth.pyc | Bin 0 -> 49134 bytes patches/gdata/geo/__init__.py | 185 + patches/gdata/geo/data.py | 92 + patches/gdata/health/__init__.py | 229 + patches/gdata/health/service.py | 263 + patches/gdata/marketplace/__init__.py | 1 + patches/gdata/marketplace/client.py | 160 + patches/gdata/marketplace/data.py | 115 + patches/gdata/media/__init__.py | 355 ++ patches/gdata/media/data.py | 159 + patches/gdata/notebook/__init__.py | 15 + patches/gdata/notebook/data.py | 55 + patches/gdata/oauth/CHANGES.txt | 17 + patches/gdata/oauth/__init__.py | 529 ++ patches/gdata/oauth/rsa.py | 120 + patches/gdata/opensearch/__init__.py | 15 + patches/gdata/opensearch/data.py | 48 + patches/gdata/photos/__init__.py | 1112 ++++ patches/gdata/photos/service.py | 681 ++ patches/gdata/projecthosting/__init__.py | 1 + patches/gdata/projecthosting/__init__.pyc | Bin 0 -> 152 bytes patches/gdata/projecthosting/client.py | 201 + patches/gdata/projecthosting/client.pyc | Bin 0 -> 7240 bytes patches/gdata/projecthosting/data.py | 134 + patches/gdata/projecthosting/data.pyc | Bin 0 -> 5533 bytes patches/gdata/sample_util.py | 269 + patches/gdata/service.py | 1718 +++++ patches/gdata/sites/__init__.py | 0 patches/gdata/sites/client.py | 462 ++ patches/gdata/sites/data.py | 376 ++ patches/gdata/spreadsheet/__init__.py | 474 ++ patches/gdata/spreadsheet/service.py | 484 ++ patches/gdata/spreadsheet/text_db.py | 559 ++ patches/gdata/spreadsheets/__init__.py | 0 patches/gdata/spreadsheets/client.py | 452 ++ patches/gdata/spreadsheets/data.py | 317 + patches/gdata/test_config.py | 421 ++ patches/gdata/test_data.py | 5504 +++++++++++++++++ patches/gdata/tlslite/BaseDB.py | 120 + patches/gdata/tlslite/Checker.py | 146 + patches/gdata/tlslite/FileObject.py | 220 + patches/gdata/tlslite/HandshakeSettings.py | 159 + patches/gdata/tlslite/Session.py | 131 + patches/gdata/tlslite/SessionCache.py | 103 + patches/gdata/tlslite/SharedKeyDB.py | 58 + patches/gdata/tlslite/TLSConnection.py | 1600 +++++ patches/gdata/tlslite/TLSRecordLayer.py | 1123 ++++ patches/gdata/tlslite/VerifierDB.py | 90 + patches/gdata/tlslite/X509.py | 133 + patches/gdata/tlslite/X509CertChain.py | 181 + patches/gdata/tlslite/__init__.py | 39 + patches/gdata/tlslite/api.py | 75 + patches/gdata/tlslite/constants.py | 225 + patches/gdata/tlslite/errors.py | 149 + .../tlslite/integration/AsyncStateMachine.py | 235 + .../gdata/tlslite/integration/ClientHelper.py | 163 + .../tlslite/integration/HTTPTLSConnection.py | 169 + .../gdata/tlslite/integration/IMAP4_TLS.py | 132 + .../tlslite/integration/IntegrationHelper.py | 52 + patches/gdata/tlslite/integration/POP3_TLS.py | 142 + patches/gdata/tlslite/integration/SMTP_TLS.py | 114 + .../integration/TLSAsyncDispatcherMixIn.py | 139 + .../integration/TLSSocketServerMixIn.py | 59 + .../integration/TLSTwistedProtocolWrapper.py | 196 + .../tlslite/integration/XMLRPCTransport.py | 137 + patches/gdata/tlslite/integration/__init__.py | 17 + patches/gdata/tlslite/mathtls.py | 170 + patches/gdata/tlslite/messages.py | 561 ++ patches/gdata/tlslite/utils/AES.py | 31 + patches/gdata/tlslite/utils/ASN1Parser.py | 34 + patches/gdata/tlslite/utils/Cryptlib_AES.py | 34 + patches/gdata/tlslite/utils/Cryptlib_RC4.py | 28 + .../gdata/tlslite/utils/Cryptlib_TripleDES.py | 35 + patches/gdata/tlslite/utils/OpenSSL_AES.py | 49 + patches/gdata/tlslite/utils/OpenSSL_RC4.py | 25 + patches/gdata/tlslite/utils/OpenSSL_RSAKey.py | 148 + .../gdata/tlslite/utils/OpenSSL_TripleDES.py | 44 + patches/gdata/tlslite/utils/PyCrypto_AES.py | 22 + patches/gdata/tlslite/utils/PyCrypto_RC4.py | 22 + .../gdata/tlslite/utils/PyCrypto_RSAKey.py | 61 + .../gdata/tlslite/utils/PyCrypto_TripleDES.py | 22 + patches/gdata/tlslite/utils/Python_AES.py | 68 + patches/gdata/tlslite/utils/Python_RC4.py | 39 + patches/gdata/tlslite/utils/Python_RSAKey.py | 209 + patches/gdata/tlslite/utils/RC4.py | 17 + patches/gdata/tlslite/utils/RSAKey.py | 264 + patches/gdata/tlslite/utils/TripleDES.py | 26 + patches/gdata/tlslite/utils/__init__.py | 31 + patches/gdata/tlslite/utils/cipherfactory.py | 111 + patches/gdata/tlslite/utils/codec.py | 94 + patches/gdata/tlslite/utils/compat.py | 140 + patches/gdata/tlslite/utils/cryptomath.py | 404 ++ patches/gdata/tlslite/utils/dateFuncs.py | 75 + patches/gdata/tlslite/utils/entropy.c | 173 + patches/gdata/tlslite/utils/hmac.py | 104 + patches/gdata/tlslite/utils/jython_compat.py | 195 + patches/gdata/tlslite/utils/keyfactory.py | 243 + patches/gdata/tlslite/utils/rijndael.py | 392 ++ patches/gdata/tlslite/utils/win32prng.c | 63 + patches/gdata/tlslite/utils/xmltools.py | 202 + patches/gdata/urlfetch.py | 247 + patches/gdata/webmastertools/__init__.py | 544 ++ patches/gdata/webmastertools/data.py | 217 + patches/gdata/webmastertools/service.py | 516 ++ patches/gdata/youtube/__init__.py | 684 ++ patches/gdata/youtube/client.py | 264 + patches/gdata/youtube/data.py | 502 ++ patches/gdata/youtube/service.py | 1563 +++++ patches/projecthosting_patches.py | 89 + {auto-compile => patches}/rietveld-json.py | 0 234 files changed, 63472 insertions(+) create mode 100755 patches/atom/__init__.py create mode 100644 patches/atom/__init__.pyc create mode 100644 patches/atom/auth.py create mode 100755 patches/atom/client.py create mode 100644 patches/atom/client.pyc create mode 100644 patches/atom/core.py create mode 100644 patches/atom/core.pyc create mode 100644 patches/atom/data.py create mode 100644 patches/atom/data.pyc create mode 100644 patches/atom/http.py create mode 100644 patches/atom/http_core.py create mode 100644 patches/atom/http_core.pyc create mode 100644 patches/atom/http_interface.py create mode 100644 patches/atom/mock_http.py create mode 100644 patches/atom/mock_http_core.py create mode 100755 patches/atom/mock_service.py create mode 100755 patches/atom/service.py create mode 100644 patches/atom/token_store.py create mode 100644 patches/atom/url.py create mode 100755 patches/gdata/Crypto/Cipher/AES.pyd create mode 100755 patches/gdata/Crypto/Cipher/ARC2.pyd create mode 100755 patches/gdata/Crypto/Cipher/ARC4.pyd create mode 100755 patches/gdata/Crypto/Cipher/Blowfish.pyd create mode 100755 patches/gdata/Crypto/Cipher/CAST.pyd create mode 100755 patches/gdata/Crypto/Cipher/DES.pyd create mode 100755 patches/gdata/Crypto/Cipher/DES3.pyd create mode 100755 patches/gdata/Crypto/Cipher/IDEA.pyd create mode 100755 patches/gdata/Crypto/Cipher/RC5.pyd create mode 100755 patches/gdata/Crypto/Cipher/XOR.pyd create mode 100755 patches/gdata/Crypto/Cipher/__init__.py create mode 100755 patches/gdata/Crypto/Hash/HMAC.py create mode 100755 patches/gdata/Crypto/Hash/MD2.pyd create mode 100755 patches/gdata/Crypto/Hash/MD4.pyd create mode 100755 patches/gdata/Crypto/Hash/MD5.py create mode 100755 patches/gdata/Crypto/Hash/RIPEMD.pyd create mode 100755 patches/gdata/Crypto/Hash/SHA.py create mode 100755 patches/gdata/Crypto/Hash/SHA256.pyd create mode 100755 patches/gdata/Crypto/Hash/__init__.py create mode 100755 patches/gdata/Crypto/Protocol/AllOrNothing.py create mode 100755 patches/gdata/Crypto/Protocol/Chaffing.py create mode 100755 patches/gdata/Crypto/Protocol/__init__.py create mode 100755 patches/gdata/Crypto/PublicKey/DSA.py create mode 100755 patches/gdata/Crypto/PublicKey/ElGamal.py create mode 100755 patches/gdata/Crypto/PublicKey/RSA.py create mode 100755 patches/gdata/Crypto/PublicKey/__init__.py create mode 100755 patches/gdata/Crypto/PublicKey/pubkey.py create mode 100755 patches/gdata/Crypto/PublicKey/qNEW.py create mode 100755 patches/gdata/Crypto/Util/RFC1751.py create mode 100755 patches/gdata/Crypto/Util/__init__.py create mode 100755 patches/gdata/Crypto/Util/number.py create mode 100755 patches/gdata/Crypto/Util/randpool.py create mode 100755 patches/gdata/Crypto/Util/test.py create mode 100755 patches/gdata/Crypto/__init__.py create mode 100755 patches/gdata/Crypto/test.py create mode 100755 patches/gdata/__init__.py create mode 100644 patches/gdata/__init__.pyc create mode 100644 patches/gdata/acl/__init__.py create mode 100644 patches/gdata/acl/data.py create mode 100644 patches/gdata/alt/__init__.py create mode 100644 patches/gdata/alt/app_engine.py create mode 100644 patches/gdata/alt/appengine.py create mode 100644 patches/gdata/analytics/__init__.py create mode 100755 patches/gdata/analytics/client.py create mode 100755 patches/gdata/analytics/data.py create mode 100644 patches/gdata/analytics/service.py create mode 100644 patches/gdata/apps/__init__.py create mode 100644 patches/gdata/apps/adminsettings/__init__.py create mode 100644 patches/gdata/apps/adminsettings/service.py create mode 100644 patches/gdata/apps/audit/__init__.py create mode 100644 patches/gdata/apps/audit/service.py create mode 100644 patches/gdata/apps/emailsettings/__init__.py create mode 100644 patches/gdata/apps/emailsettings/client.py create mode 100644 patches/gdata/apps/emailsettings/data.py create mode 100644 patches/gdata/apps/emailsettings/service.py create mode 100644 patches/gdata/apps/groups/__init__.py create mode 100644 patches/gdata/apps/groups/service.py create mode 100644 patches/gdata/apps/migration/__init__.py create mode 100644 patches/gdata/apps/migration/service.py create mode 100644 patches/gdata/apps/organization/__init__.py create mode 100644 patches/gdata/apps/organization/service.py create mode 100644 patches/gdata/apps/service.py create mode 100644 patches/gdata/apps_property.py create mode 100644 patches/gdata/auth.py create mode 100755 patches/gdata/base/__init__.py create mode 100755 patches/gdata/base/service.py create mode 100644 patches/gdata/blogger/__init__.py create mode 100644 patches/gdata/blogger/client.py create mode 100644 patches/gdata/blogger/data.py create mode 100644 patches/gdata/blogger/service.py create mode 100644 patches/gdata/books/__init__.py create mode 100644 patches/gdata/books/data.py create mode 100644 patches/gdata/books/service.py create mode 100755 patches/gdata/calendar/__init__.py create mode 100755 patches/gdata/calendar/client.py create mode 100644 patches/gdata/calendar/data.py create mode 100755 patches/gdata/calendar/service.py create mode 100644 patches/gdata/calendar_resource/__init__.py create mode 100644 patches/gdata/calendar_resource/client.py create mode 100644 patches/gdata/calendar_resource/data.py create mode 100644 patches/gdata/client.py create mode 100644 patches/gdata/client.pyc create mode 100644 patches/gdata/codesearch/__init__.py create mode 100644 patches/gdata/codesearch/service.py create mode 100644 patches/gdata/contacts/__init__.py create mode 100644 patches/gdata/contacts/client.py create mode 100644 patches/gdata/contacts/data.py create mode 100644 patches/gdata/contacts/service.py create mode 100644 patches/gdata/contentforshopping/__init__.py create mode 100644 patches/gdata/contentforshopping/client.py create mode 100644 patches/gdata/contentforshopping/data.py create mode 100644 patches/gdata/core.py create mode 100644 patches/gdata/data.py create mode 100644 patches/gdata/data.pyc create mode 100644 patches/gdata/docs/__init__.py create mode 100755 patches/gdata/docs/client.py create mode 100755 patches/gdata/docs/data.py create mode 100644 patches/gdata/docs/service.py create mode 100644 patches/gdata/dublincore/__init__.py create mode 100644 patches/gdata/dublincore/data.py create mode 100644 patches/gdata/exif/__init__.py create mode 100644 patches/gdata/finance/__init__.py create mode 100644 patches/gdata/finance/data.py create mode 100644 patches/gdata/finance/service.py create mode 100644 patches/gdata/gauth.py create mode 100644 patches/gdata/gauth.pyc create mode 100644 patches/gdata/geo/__init__.py create mode 100644 patches/gdata/geo/data.py create mode 100644 patches/gdata/health/__init__.py create mode 100644 patches/gdata/health/service.py create mode 100644 patches/gdata/marketplace/__init__.py create mode 100644 patches/gdata/marketplace/client.py create mode 100644 patches/gdata/marketplace/data.py create mode 100644 patches/gdata/media/__init__.py create mode 100644 patches/gdata/media/data.py create mode 100644 patches/gdata/notebook/__init__.py create mode 100644 patches/gdata/notebook/data.py create mode 100755 patches/gdata/oauth/CHANGES.txt create mode 100755 patches/gdata/oauth/__init__.py create mode 100755 patches/gdata/oauth/rsa.py create mode 100644 patches/gdata/opensearch/__init__.py create mode 100644 patches/gdata/opensearch/data.py create mode 100644 patches/gdata/photos/__init__.py create mode 100755 patches/gdata/photos/service.py create mode 100755 patches/gdata/projecthosting/__init__.py create mode 100644 patches/gdata/projecthosting/__init__.pyc create mode 100755 patches/gdata/projecthosting/client.py create mode 100644 patches/gdata/projecthosting/client.pyc create mode 100755 patches/gdata/projecthosting/data.py create mode 100644 patches/gdata/projecthosting/data.pyc create mode 100644 patches/gdata/sample_util.py create mode 100755 patches/gdata/service.py create mode 100755 patches/gdata/sites/__init__.py create mode 100755 patches/gdata/sites/client.py create mode 100644 patches/gdata/sites/data.py create mode 100755 patches/gdata/spreadsheet/__init__.py create mode 100755 patches/gdata/spreadsheet/service.py create mode 100644 patches/gdata/spreadsheet/text_db.py create mode 100644 patches/gdata/spreadsheets/__init__.py create mode 100644 patches/gdata/spreadsheets/client.py create mode 100644 patches/gdata/spreadsheets/data.py create mode 100644 patches/gdata/test_config.py create mode 100755 patches/gdata/test_data.py create mode 100755 patches/gdata/tlslite/BaseDB.py create mode 100755 patches/gdata/tlslite/Checker.py create mode 100755 patches/gdata/tlslite/FileObject.py create mode 100755 patches/gdata/tlslite/HandshakeSettings.py create mode 100755 patches/gdata/tlslite/Session.py create mode 100755 patches/gdata/tlslite/SessionCache.py create mode 100755 patches/gdata/tlslite/SharedKeyDB.py create mode 100755 patches/gdata/tlslite/TLSConnection.py create mode 100755 patches/gdata/tlslite/TLSRecordLayer.py create mode 100755 patches/gdata/tlslite/VerifierDB.py create mode 100755 patches/gdata/tlslite/X509.py create mode 100755 patches/gdata/tlslite/X509CertChain.py create mode 100755 patches/gdata/tlslite/__init__.py create mode 100755 patches/gdata/tlslite/api.py create mode 100755 patches/gdata/tlslite/constants.py create mode 100755 patches/gdata/tlslite/errors.py create mode 100755 patches/gdata/tlslite/integration/AsyncStateMachine.py create mode 100755 patches/gdata/tlslite/integration/ClientHelper.py create mode 100755 patches/gdata/tlslite/integration/HTTPTLSConnection.py create mode 100755 patches/gdata/tlslite/integration/IMAP4_TLS.py create mode 100755 patches/gdata/tlslite/integration/IntegrationHelper.py create mode 100755 patches/gdata/tlslite/integration/POP3_TLS.py create mode 100755 patches/gdata/tlslite/integration/SMTP_TLS.py create mode 100755 patches/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py create mode 100755 patches/gdata/tlslite/integration/TLSSocketServerMixIn.py create mode 100755 patches/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py create mode 100755 patches/gdata/tlslite/integration/XMLRPCTransport.py create mode 100755 patches/gdata/tlslite/integration/__init__.py create mode 100755 patches/gdata/tlslite/mathtls.py create mode 100755 patches/gdata/tlslite/messages.py create mode 100755 patches/gdata/tlslite/utils/AES.py create mode 100755 patches/gdata/tlslite/utils/ASN1Parser.py create mode 100755 patches/gdata/tlslite/utils/Cryptlib_AES.py create mode 100755 patches/gdata/tlslite/utils/Cryptlib_RC4.py create mode 100755 patches/gdata/tlslite/utils/Cryptlib_TripleDES.py create mode 100755 patches/gdata/tlslite/utils/OpenSSL_AES.py create mode 100755 patches/gdata/tlslite/utils/OpenSSL_RC4.py create mode 100755 patches/gdata/tlslite/utils/OpenSSL_RSAKey.py create mode 100755 patches/gdata/tlslite/utils/OpenSSL_TripleDES.py create mode 100755 patches/gdata/tlslite/utils/PyCrypto_AES.py create mode 100755 patches/gdata/tlslite/utils/PyCrypto_RC4.py create mode 100755 patches/gdata/tlslite/utils/PyCrypto_RSAKey.py create mode 100755 patches/gdata/tlslite/utils/PyCrypto_TripleDES.py create mode 100755 patches/gdata/tlslite/utils/Python_AES.py create mode 100755 patches/gdata/tlslite/utils/Python_RC4.py create mode 100755 patches/gdata/tlslite/utils/Python_RSAKey.py create mode 100755 patches/gdata/tlslite/utils/RC4.py create mode 100755 patches/gdata/tlslite/utils/RSAKey.py create mode 100755 patches/gdata/tlslite/utils/TripleDES.py create mode 100755 patches/gdata/tlslite/utils/__init__.py create mode 100755 patches/gdata/tlslite/utils/cipherfactory.py create mode 100755 patches/gdata/tlslite/utils/codec.py create mode 100755 patches/gdata/tlslite/utils/compat.py create mode 100755 patches/gdata/tlslite/utils/cryptomath.py create mode 100755 patches/gdata/tlslite/utils/dateFuncs.py create mode 100755 patches/gdata/tlslite/utils/entropy.c create mode 100755 patches/gdata/tlslite/utils/hmac.py create mode 100755 patches/gdata/tlslite/utils/jython_compat.py create mode 100755 patches/gdata/tlslite/utils/keyfactory.py create mode 100755 patches/gdata/tlslite/utils/rijndael.py create mode 100755 patches/gdata/tlslite/utils/win32prng.c create mode 100755 patches/gdata/tlslite/utils/xmltools.py create mode 100644 patches/gdata/urlfetch.py create mode 100644 patches/gdata/webmastertools/__init__.py create mode 100644 patches/gdata/webmastertools/data.py create mode 100644 patches/gdata/webmastertools/service.py create mode 100755 patches/gdata/youtube/__init__.py create mode 100644 patches/gdata/youtube/client.py create mode 100644 patches/gdata/youtube/data.py create mode 100644 patches/gdata/youtube/service.py create mode 100755 patches/projecthosting_patches.py rename {auto-compile => patches}/rietveld-json.py (100%) diff --git a/patches/atom/__init__.py b/patches/atom/__init__.py new file mode 100755 index 0000000..6aa96c1 --- /dev/null +++ b/patches/atom/__init__.py @@ -0,0 +1,1484 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 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. + + +"""Contains classes representing Atom elements. + + Module objective: provide data classes for Atom constructs. These classes hide + the XML-ness of Atom and provide a set of native Python classes to interact + with. + + Conversions to and from XML should only be necessary when the Atom classes + "touch the wire" and are sent over HTTP. For this reason this module + provides methods and functions to convert Atom classes to and from strings. + + For more information on the Atom data model, see RFC 4287 + (http://www.ietf.org/rfc/rfc4287.txt) + + AtomBase: A foundation class on which Atom classes are built. It + handles the parsing of attributes and children which are common to all + Atom classes. By default, the AtomBase class translates all XML child + nodes into ExtensionElements. + + ExtensionElement: Atom allows Atom objects to contain XML which is not part + of the Atom specification, these are called extension elements. If a + classes parser encounters an unexpected XML construct, it is translated + into an ExtensionElement instance. ExtensionElement is designed to fully + capture the information in the XML. Child nodes in an XML extension are + turned into ExtensionElements as well. +""" + + +__author__ = 'api.jscudder (Jeffrey Scudder)' + +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 warnings + + +# XML namespaces which are often used in Atom entities. +ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom' +ELEMENT_TEMPLATE = '{http://www.w3.org/2005/Atom}%s' +APP_NAMESPACE = 'http://purl.org/atom/app#' +APP_TEMPLATE = '{http://purl.org/atom/app#}%s' + +# This encoding is used for converting strings before translating the XML +# into an object. +XML_STRING_ENCODING = 'utf-8' +# The desired string encoding for object members. set or monkey-patch to +# unicode if you want object members to be Python unicode strings, instead of +# encoded strings +MEMBER_STRING_ENCODING = 'utf-8' +#MEMBER_STRING_ENCODING = unicode + +# If True, all methods which are exclusive to v1 will raise a +# DeprecationWarning +ENABLE_V1_WARNINGS = False + + +def v1_deprecated(warning=None): + """Shows a warning if ENABLE_V1_WARNINGS is True. + + Function decorator used to mark methods used in v1 classes which + may be removed in future versions of the library. + """ + warning = warning or '' + # This closure is what is returned from the deprecated function. + def mark_deprecated(f): + # The deprecated_function wraps the actual call to f. + def optional_warn_function(*args, **kwargs): + if ENABLE_V1_WARNINGS: + warnings.warn(warning, DeprecationWarning, stacklevel=2) + return f(*args, **kwargs) + # Preserve the original name to avoid masking all decorated functions as + # 'deprecated_function' + try: + optional_warn_function.func_name = f.func_name + except TypeError: + pass # In Python2.3 we can't set the func_name + return optional_warn_function + return mark_deprecated + + +def CreateClassFromXMLString(target_class, xml_string, string_encoding=None): + """Creates an instance of the target class from the string contents. + + Args: + target_class: class The class which will be instantiated and populated + with the contents of the XML. This class must have a _tag and a + _namespace class variable. + xml_string: str A string which contains valid XML. The root element + of the XML string should match the tag and namespace of the desired + class. + string_encoding: str The character encoding which the xml_string should + be converted to before it is interpreted and translated into + objects. The default is None in which case the string encoding + is not changed. + + Returns: + An instance of the target class with members assigned according to the + contents of the XML - or None if the root XML tag and namespace did not + match those of the target class. + """ + encoding = string_encoding or XML_STRING_ENCODING + if encoding and isinstance(xml_string, unicode): + xml_string = xml_string.encode(encoding) + tree = ElementTree.fromstring(xml_string) + return _CreateClassFromElementTree(target_class, tree) + + +CreateClassFromXMLString = v1_deprecated( + 'Please use atom.core.parse with atom.data classes instead.')( + CreateClassFromXMLString) + + +def _CreateClassFromElementTree(target_class, tree, namespace=None, tag=None): + """Instantiates the class and populates members according to the tree. + + Note: Only use this function with classes that have _namespace and _tag + class members. + + Args: + target_class: class The class which will be instantiated and populated + with the contents of the XML. + tree: ElementTree An element tree whose contents will be converted into + members of the new target_class instance. + namespace: str (optional) The namespace which the XML tree's root node must + match. If omitted, the namespace defaults to the _namespace of the + target class. + tag: str (optional) The tag which the XML tree's root node must match. If + omitted, the tag defaults to the _tag class member of the target + class. + + Returns: + An instance of the target class - or None if the tag and namespace of + the XML tree's root node did not match the desired namespace and tag. + """ + if namespace is None: + namespace = target_class._namespace + if tag is None: + tag = target_class._tag + if tree.tag == '{%s}%s' % (namespace, tag): + target = target_class() + target._HarvestElementTree(tree) + return target + else: + return None + + +class ExtensionContainer(object): + + def __init__(self, extension_elements=None, extension_attributes=None, + text=None): + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + __init__ = v1_deprecated( + 'Please use data model classes in atom.data instead.')( + __init__) + + # Three methods to create an object from an ElementTree + def _HarvestElementTree(self, tree): + # Fill in the instance members from the contents of the XML tree. + for child in tree: + self._ConvertElementTreeToMember(child) + for attribute, value in tree.attrib.iteritems(): + self._ConvertElementAttributeToMember(attribute, value) + # Encode the text string according to the desired encoding type. (UTF-8) + if tree.text: + if MEMBER_STRING_ENCODING is unicode: + self.text = tree.text + else: + self.text = tree.text.encode(MEMBER_STRING_ENCODING) + + def _ConvertElementTreeToMember(self, child_tree, current_class=None): + self.extension_elements.append(_ExtensionElementFromElementTree( + child_tree)) + + def _ConvertElementAttributeToMember(self, attribute, value): + # Encode the attribute value's string with the desired type Default UTF-8 + if value: + if MEMBER_STRING_ENCODING is unicode: + self.extension_attributes[attribute] = value + else: + self.extension_attributes[attribute] = value.encode( + MEMBER_STRING_ENCODING) + + # One method to create an ElementTree from an object + def _AddMembersToElementTree(self, tree): + for child in self.extension_elements: + child._BecomeChildElement(tree) + for attribute, value in self.extension_attributes.iteritems(): + if value: + if isinstance(value, unicode) or MEMBER_STRING_ENCODING is unicode: + tree.attrib[attribute] = value + else: + # Decode the value from the desired encoding (default UTF-8). + tree.attrib[attribute] = value.decode(MEMBER_STRING_ENCODING) + if self.text: + if isinstance(self.text, unicode) or MEMBER_STRING_ENCODING is unicode: + tree.text = self.text + else: + tree.text = self.text.decode(MEMBER_STRING_ENCODING) + + def FindExtensions(self, tag=None, namespace=None): + """Searches extension elements for child nodes with the desired name. + + Returns a list of extension elements within this object whose tag + and/or namespace match those passed in. To find all extensions in + a particular namespace, specify the namespace but not the tag name. + If you specify only the tag, the result list may contain extension + elements in multiple namespaces. + + Args: + tag: str (optional) The desired tag + namespace: str (optional) The desired namespace + + Returns: + A list of elements whose tag and/or namespace match the parameters + values + """ + + results = [] + + if tag and namespace: + for element in self.extension_elements: + if element.tag == tag and element.namespace == namespace: + results.append(element) + elif tag and not namespace: + for element in self.extension_elements: + if element.tag == tag: + results.append(element) + elif namespace and not tag: + for element in self.extension_elements: + if element.namespace == namespace: + results.append(element) + else: + for element in self.extension_elements: + results.append(element) + + return results + + +class AtomBase(ExtensionContainer): + + _children = {} + _attributes = {} + + def __init__(self, extension_elements=None, extension_attributes=None, + text=None): + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + __init__ = v1_deprecated( + 'Please use data model classes in atom.data instead.')( + __init__) + + def _ConvertElementTreeToMember(self, child_tree): + # Find the element's tag in this class's list of child members + if self.__class__._children.has_key(child_tree.tag): + member_name = self.__class__._children[child_tree.tag][0] + member_class = self.__class__._children[child_tree.tag][1] + # If the class member is supposed to contain a list, make sure the + # matching member is set to a list, then append the new member + # instance to the list. + if isinstance(member_class, list): + if getattr(self, member_name) is None: + setattr(self, member_name, []) + getattr(self, member_name).append(_CreateClassFromElementTree( + member_class[0], child_tree)) + else: + setattr(self, member_name, + _CreateClassFromElementTree(member_class, child_tree)) + else: + ExtensionContainer._ConvertElementTreeToMember(self, child_tree) + + def _ConvertElementAttributeToMember(self, attribute, value): + # Find the attribute in this class's list of attributes. + if self.__class__._attributes.has_key(attribute): + # Find the member of this class which corresponds to the XML attribute + # (lookup in current_class._attributes) and set this member to the + # desired value (using self.__dict__). + if value: + # Encode the string to capture non-ascii characters (default UTF-8) + if MEMBER_STRING_ENCODING is unicode: + setattr(self, self.__class__._attributes[attribute], value) + else: + setattr(self, self.__class__._attributes[attribute], + value.encode(MEMBER_STRING_ENCODING)) + else: + ExtensionContainer._ConvertElementAttributeToMember( + self, attribute, value) + + # Three methods to create an ElementTree from an object + def _AddMembersToElementTree(self, tree): + # Convert the members of this class which are XML child nodes. + # This uses the class's _children dictionary to find the members which + # should become XML child nodes. + member_node_names = [values[0] for tag, values in + self.__class__._children.iteritems()] + for member_name in member_node_names: + member = getattr(self, member_name) + if member is None: + pass + elif isinstance(member, list): + for instance in member: + instance._BecomeChildElement(tree) + else: + member._BecomeChildElement(tree) + # Convert the members of this class which are XML attributes. + for xml_attribute, member_name in self.__class__._attributes.iteritems(): + member = getattr(self, member_name) + if member is not None: + if isinstance(member, unicode) or MEMBER_STRING_ENCODING is unicode: + tree.attrib[xml_attribute] = member + else: + tree.attrib[xml_attribute] = member.decode(MEMBER_STRING_ENCODING) + # Lastly, call the ExtensionContainers's _AddMembersToElementTree to + # convert any extension attributes. + ExtensionContainer._AddMembersToElementTree(self, tree) + + + def _BecomeChildElement(self, tree): + """ + + Note: Only for use with classes that have a _tag and _namespace class + member. It is in AtomBase so that it can be inherited but it should + not be called on instances of AtomBase. + + """ + new_child = ElementTree.Element('') + tree.append(new_child) + new_child.tag = '{%s}%s' % (self.__class__._namespace, + self.__class__._tag) + self._AddMembersToElementTree(new_child) + + def _ToElementTree(self): + """ + + Note, this method is designed to be used only with classes that have a + _tag and _namespace. It is placed in AtomBase for inheritance but should + not be called on this class. + + """ + new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace, + self.__class__._tag)) + self._AddMembersToElementTree(new_tree) + return new_tree + + def ToString(self, string_encoding='UTF-8'): + """Converts the Atom object to a string containing XML.""" + return ElementTree.tostring(self._ToElementTree(), encoding=string_encoding) + + def __str__(self): + return self.ToString() + + +class Name(AtomBase): + """The atom:name element""" + + _tag = 'name' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Name + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def NameFromString(xml_string): + return CreateClassFromXMLString(Name, xml_string) + + +class Email(AtomBase): + """The atom:email element""" + + _tag = 'email' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Email + + Args: + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def EmailFromString(xml_string): + return CreateClassFromXMLString(Email, xml_string) + + +class Uri(AtomBase): + """The atom:uri element""" + + _tag = 'uri' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Uri + + Args: + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def UriFromString(xml_string): + return CreateClassFromXMLString(Uri, xml_string) + + +class Person(AtomBase): + """A foundation class from which atom:author and atom:contributor extend. + + A person contains information like name, email address, and web page URI for + an author or contributor to an Atom feed. + """ + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _children['{%s}name' % (ATOM_NAMESPACE)] = ('name', Name) + _children['{%s}email' % (ATOM_NAMESPACE)] = ('email', Email) + _children['{%s}uri' % (ATOM_NAMESPACE)] = ('uri', Uri) + + def __init__(self, name=None, email=None, uri=None, + extension_elements=None, extension_attributes=None, text=None): + """Foundation from which author and contributor are derived. + + The constructor is provided for illustrative purposes, you should not + need to instantiate a Person. + + Args: + name: Name The person's name + email: Email The person's email address + uri: Uri The URI of the person's webpage + extension_elements: list A list of ExtensionElement instances which are + children of this element. + extension_attributes: dict A dictionary of strings which are the values + for additional XML attributes of this element. + text: String The text contents of the element. This is the contents + of the Entry's XML text node. (Example: This is the text) + """ + + self.name = name + self.email = email + self.uri = uri + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +class Author(Person): + """The atom:author element + + An author is a required element in Feed. + """ + + _tag = 'author' + _namespace = ATOM_NAMESPACE + _children = Person._children.copy() + _attributes = Person._attributes.copy() + #_children = {} + #_attributes = {} + + def __init__(self, name=None, email=None, uri=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for Author + + Args: + name: Name + email: Email + uri: Uri + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.name = name + self.email = email + self.uri = uri + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +def AuthorFromString(xml_string): + return CreateClassFromXMLString(Author, xml_string) + + +class Contributor(Person): + """The atom:contributor element""" + + _tag = 'contributor' + _namespace = ATOM_NAMESPACE + _children = Person._children.copy() + _attributes = Person._attributes.copy() + + def __init__(self, name=None, email=None, uri=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for Contributor + + Args: + name: Name + email: Email + uri: Uri + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.name = name + self.email = email + self.uri = uri + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +def ContributorFromString(xml_string): + return CreateClassFromXMLString(Contributor, xml_string) + + +class Link(AtomBase): + """The atom:link element""" + + _tag = 'link' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['rel'] = 'rel' + _attributes['href'] = 'href' + _attributes['type'] = 'type' + _attributes['title'] = 'title' + _attributes['length'] = 'length' + _attributes['hreflang'] = 'hreflang' + + def __init__(self, href=None, rel=None, link_type=None, hreflang=None, + title=None, length=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Link + + Args: + href: string The href attribute of the link + rel: string + type: string + hreflang: string The language for the href + title: string + length: string The length of the href's destination + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.href = href + self.rel = rel + self.type = link_type + self.hreflang = hreflang + self.title = title + self.length = length + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def LinkFromString(xml_string): + return CreateClassFromXMLString(Link, xml_string) + + +class Generator(AtomBase): + """The atom:generator element""" + + _tag = 'generator' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['uri'] = 'uri' + _attributes['version'] = 'version' + + def __init__(self, uri=None, version=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Generator + + Args: + uri: string + version: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.uri = uri + self.version = version + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +def GeneratorFromString(xml_string): + return CreateClassFromXMLString(Generator, xml_string) + + +class Text(AtomBase): + """A foundation class from which atom:title, summary, etc. extend. + + This class should never be instantiated. + """ + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['type'] = 'type' + + def __init__(self, text_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Text + + Args: + text_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = text_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Title(Text): + """The atom:title element""" + + _tag = 'title' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, title_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Title + + Args: + title_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = title_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def TitleFromString(xml_string): + return CreateClassFromXMLString(Title, xml_string) + + +class Subtitle(Text): + """The atom:subtitle element""" + + _tag = 'subtitle' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, subtitle_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Subtitle + + Args: + subtitle_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = subtitle_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SubtitleFromString(xml_string): + return CreateClassFromXMLString(Subtitle, xml_string) + + +class Rights(Text): + """The atom:rights element""" + + _tag = 'rights' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, rights_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Rights + + Args: + rights_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = rights_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def RightsFromString(xml_string): + return CreateClassFromXMLString(Rights, xml_string) + + +class Summary(Text): + """The atom:summary element""" + + _tag = 'summary' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, summary_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Summary + + Args: + summary_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = summary_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SummaryFromString(xml_string): + return CreateClassFromXMLString(Summary, xml_string) + + +class Content(Text): + """The atom:content element""" + + _tag = 'content' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + _attributes['src'] = 'src' + + def __init__(self, content_type=None, src=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Content + + Args: + content_type: string + src: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = content_type + self.src = src + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +def ContentFromString(xml_string): + return CreateClassFromXMLString(Content, xml_string) + + +class Category(AtomBase): + """The atom:category element""" + + _tag = 'category' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['term'] = 'term' + _attributes['scheme'] = 'scheme' + _attributes['label'] = 'label' + + def __init__(self, term=None, scheme=None, label=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Category + + Args: + term: str + scheme: str + label: str + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.term = term + self.scheme = scheme + self.label = label + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def CategoryFromString(xml_string): + return CreateClassFromXMLString(Category, xml_string) + + +class Id(AtomBase): + """The atom:id element.""" + + _tag = 'id' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Id + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def IdFromString(xml_string): + return CreateClassFromXMLString(Id, xml_string) + + +class Icon(AtomBase): + """The atom:icon element.""" + + _tag = 'icon' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Icon + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def IconFromString(xml_string): + return CreateClassFromXMLString(Icon, xml_string) + + +class Logo(AtomBase): + """The atom:logo element.""" + + _tag = 'logo' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Logo + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def LogoFromString(xml_string): + return CreateClassFromXMLString(Logo, xml_string) + + +class Draft(AtomBase): + """The app:draft element which indicates if this entry should be public.""" + + _tag = 'draft' + _namespace = APP_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for app:draft + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def DraftFromString(xml_string): + return CreateClassFromXMLString(Draft, xml_string) + + +class Control(AtomBase): + """The app:control element indicating restrictions on publication. + + The APP control element may contain a draft element indicating whether or + not this entry should be publicly available. + """ + + _tag = 'control' + _namespace = APP_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _children['{%s}draft' % APP_NAMESPACE] = ('draft', Draft) + + def __init__(self, draft=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for app:control""" + + self.draft = draft + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ControlFromString(xml_string): + return CreateClassFromXMLString(Control, xml_string) + + +class Date(AtomBase): + """A parent class for atom:updated, published, etc.""" + + #TODO Add text to and from time conversion methods to allow users to set + # the contents of a Date to a python DateTime object. + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Updated(Date): + """The atom:updated element.""" + + _tag = 'updated' + _namespace = ATOM_NAMESPACE + _children = Date._children.copy() + _attributes = Date._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Updated + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def UpdatedFromString(xml_string): + return CreateClassFromXMLString(Updated, xml_string) + + +class Published(Date): + """The atom:published element.""" + + _tag = 'published' + _namespace = ATOM_NAMESPACE + _children = Date._children.copy() + _attributes = Date._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Published + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def PublishedFromString(xml_string): + return CreateClassFromXMLString(Published, xml_string) + + +class LinkFinder(object): + """An "interface" providing methods to find link elements + + Entry elements often contain multiple links which differ in the rel + attribute or content type. Often, developers are interested in a specific + type of link so this class provides methods to find specific classes of + links. + + This class is used as a mixin in Atom entries and feeds. + """ + + def GetSelfLink(self): + """Find the first link with rel set to 'self' + + Returns: + An atom.Link or none if none of the links had rel equal to 'self' + """ + + for a_link in self.link: + if a_link.rel == 'self': + return a_link + return None + + def GetEditLink(self): + for a_link in self.link: + if a_link.rel == 'edit': + return a_link + return None + + def GetEditMediaLink(self): + for a_link in self.link: + if a_link.rel == 'edit-media': + return a_link + return None + + def GetNextLink(self): + for a_link in self.link: + if a_link.rel == 'next': + return a_link + return None + + def GetLicenseLink(self): + for a_link in self.link: + if a_link.rel == 'license': + return a_link + return None + + def GetAlternateLink(self): + for a_link in self.link: + if a_link.rel == 'alternate': + return a_link + return None + + +class FeedEntryParent(AtomBase, LinkFinder): + """A super class for atom:feed and entry, contains shared attributes""" + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _children['{%s}author' % ATOM_NAMESPACE] = ('author', [Author]) + _children['{%s}category' % ATOM_NAMESPACE] = ('category', [Category]) + _children['{%s}contributor' % ATOM_NAMESPACE] = ('contributor', [Contributor]) + _children['{%s}id' % ATOM_NAMESPACE] = ('id', Id) + _children['{%s}link' % ATOM_NAMESPACE] = ('link', [Link]) + _children['{%s}rights' % ATOM_NAMESPACE] = ('rights', Rights) + _children['{%s}title' % ATOM_NAMESPACE] = ('title', Title) + _children['{%s}updated' % ATOM_NAMESPACE] = ('updated', Updated) + + def __init__(self, author=None, category=None, contributor=None, + atom_id=None, link=None, rights=None, title=None, updated=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.rights = rights + self.title = title + self.updated = updated + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Source(FeedEntryParent): + """The atom:source element""" + + _tag = 'source' + _namespace = ATOM_NAMESPACE + _children = FeedEntryParent._children.copy() + _attributes = FeedEntryParent._attributes.copy() + _children['{%s}generator' % ATOM_NAMESPACE] = ('generator', Generator) + _children['{%s}icon' % ATOM_NAMESPACE] = ('icon', Icon) + _children['{%s}logo' % ATOM_NAMESPACE] = ('logo', Logo) + _children['{%s}subtitle' % ATOM_NAMESPACE] = ('subtitle', Subtitle) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Source + + Args: + author: list (optional) A list of Author instances which belong to this + class. + category: list (optional) A list of Category instances + contributor: list (optional) A list on Contributor instances + generator: Generator (optional) + icon: Icon (optional) + id: Id (optional) The entry's Id element + link: list (optional) A list of Link instances + logo: Logo (optional) + rights: Rights (optional) The entry's Rights element + subtitle: Subtitle (optional) The entry's subtitle element + title: Title (optional) the entry's title element + updated: Updated (optional) the entry's updated element + text: String (optional) The text contents of the element. This is the + contents of the Entry's XML text node. + (Example: This is the text) + extension_elements: list (optional) A list of ExtensionElement instances + which are children of this element. + extension_attributes: dict (optional) A dictionary of strings which are + the values for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.generator = generator + self.icon = icon + self.id = atom_id + self.link = link or [] + self.logo = logo + self.rights = rights + self.subtitle = subtitle + self.title = title + self.updated = updated + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SourceFromString(xml_string): + return CreateClassFromXMLString(Source, xml_string) + + +class Entry(FeedEntryParent): + """The atom:entry element""" + + _tag = 'entry' + _namespace = ATOM_NAMESPACE + _children = FeedEntryParent._children.copy() + _attributes = FeedEntryParent._attributes.copy() + _children['{%s}content' % ATOM_NAMESPACE] = ('content', Content) + _children['{%s}published' % ATOM_NAMESPACE] = ('published', Published) + _children['{%s}source' % ATOM_NAMESPACE] = ('source', Source) + _children['{%s}summary' % ATOM_NAMESPACE] = ('summary', Summary) + _children['{%s}control' % APP_NAMESPACE] = ('control', Control) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, control=None, title=None, updated=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for atom:entry + + Args: + author: list A list of Author instances which belong to this class. + category: list A list of Category instances + content: Content The entry's Content + contributor: list A list on Contributor instances + id: Id The entry's Id element + link: list A list of Link instances + published: Published The entry's Published element + rights: Rights The entry's Rights element + source: Source the entry's source element + summary: Summary the entry's summary element + title: Title the entry's title element + updated: Updated the entry's updated element + control: The entry's app:control element which can be used to mark an + entry as a draft which should not be publicly viewable. + text: String The text contents of the element. This is the contents + of the Entry's XML text node. (Example: This is the text) + extension_elements: list A list of ExtensionElement instances which are + children of this element. + extension_attributes: dict A dictionary of strings which are the values + for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.title = title + self.updated = updated + self.control = control + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + __init__ = v1_deprecated('Please use atom.data.Entry instead.')(__init__) + + +def EntryFromString(xml_string): + return CreateClassFromXMLString(Entry, xml_string) + + +class Feed(Source): + """The atom:feed element""" + + _tag = 'feed' + _namespace = ATOM_NAMESPACE + _children = Source._children.copy() + _attributes = Source._attributes.copy() + _children['{%s}entry' % ATOM_NAMESPACE] = ('entry', [Entry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, entry=None, + text=None, extension_elements=None, extension_attributes=None): + """Constructor for Source + + Args: + author: list (optional) A list of Author instances which belong to this + class. + category: list (optional) A list of Category instances + contributor: list (optional) A list on Contributor instances + generator: Generator (optional) + icon: Icon (optional) + id: Id (optional) The entry's Id element + link: list (optional) A list of Link instances + logo: Logo (optional) + rights: Rights (optional) The entry's Rights element + subtitle: Subtitle (optional) The entry's subtitle element + title: Title (optional) the entry's title element + updated: Updated (optional) the entry's updated element + entry: list (optional) A list of the Entry instances contained in the + feed. + text: String (optional) The text contents of the element. This is the + contents of the Entry's XML text node. + (Example: This is the text) + extension_elements: list (optional) A list of ExtensionElement instances + which are children of this element. + extension_attributes: dict (optional) A dictionary of strings which are + the values for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.generator = generator + self.icon = icon + self.id = atom_id + self.link = link or [] + self.logo = logo + self.rights = rights + self.subtitle = subtitle + self.title = title + self.updated = updated + self.entry = entry or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + __init__ = v1_deprecated('Please use atom.data.Feed instead.')(__init__) + + +def FeedFromString(xml_string): + return CreateClassFromXMLString(Feed, xml_string) + + +class ExtensionElement(object): + """Represents extra XML elements contained in Atom classes.""" + + def __init__(self, tag, namespace=None, attributes=None, + children=None, text=None): + """Constructor for EtensionElement + + Args: + namespace: string (optional) The XML namespace for this element. + tag: string (optional) The tag (without the namespace qualifier) for + this element. To reconstruct the full qualified name of the element, + combine this tag with the namespace. + attributes: dict (optinal) The attribute value string pairs for the XML + attributes of this element. + children: list (optional) A list of ExtensionElements which represent + the XML child nodes of this element. + """ + + self.namespace = namespace + self.tag = tag + self.attributes = attributes or {} + self.children = children or [] + self.text = text + + def ToString(self): + element_tree = self._TransferToElementTree(ElementTree.Element('')) + return ElementTree.tostring(element_tree, encoding="UTF-8") + + def _TransferToElementTree(self, element_tree): + if self.tag is None: + return None + + if self.namespace is not None: + element_tree.tag = '{%s}%s' % (self.namespace, self.tag) + else: + element_tree.tag = self.tag + + for key, value in self.attributes.iteritems(): + element_tree.attrib[key] = value + + for child in self.children: + child._BecomeChildElement(element_tree) + + element_tree.text = self.text + + return element_tree + + def _BecomeChildElement(self, element_tree): + """Converts this object into an etree element and adds it as a child node. + + Adds self to the ElementTree. This method is required to avoid verbose XML + which constantly redefines the namespace. + + Args: + element_tree: ElementTree._Element The element to which this object's XML + will be added. + """ + new_element = ElementTree.Element('') + element_tree.append(new_element) + self._TransferToElementTree(new_element) + + def FindChildren(self, tag=None, namespace=None): + """Searches child nodes for objects with the desired tag/namespace. + + Returns a list of extension elements within this object whose tag + and/or namespace match those passed in. To find all children in + a particular namespace, specify the namespace but not the tag name. + If you specify only the tag, the result list may contain extension + elements in multiple namespaces. + + Args: + tag: str (optional) The desired tag + namespace: str (optional) The desired namespace + + Returns: + A list of elements whose tag and/or namespace match the parameters + values + """ + + results = [] + + if tag and namespace: + for element in self.children: + if element.tag == tag and element.namespace == namespace: + results.append(element) + elif tag and not namespace: + for element in self.children: + if element.tag == tag: + results.append(element) + elif namespace and not tag: + for element in self.children: + if element.namespace == namespace: + results.append(element) + else: + for element in self.children: + results.append(element) + + return results + + +def ExtensionElementFromString(xml_string): + element_tree = ElementTree.fromstring(xml_string) + return _ExtensionElementFromElementTree(element_tree) + + +def _ExtensionElementFromElementTree(element_tree): + element_tag = element_tree.tag + if '}' in element_tag: + namespace = element_tag[1:element_tag.index('}')] + tag = element_tag[element_tag.index('}')+1:] + else: + namespace = None + tag = element_tag + extension = ExtensionElement(namespace=namespace, tag=tag) + for key, value in element_tree.attrib.iteritems(): + extension.attributes[key] = value + for child in element_tree: + extension.children.append(_ExtensionElementFromElementTree(child)) + extension.text = element_tree.text + return extension + + +def deprecated(warning=None): + """Decorator to raise warning each time the function is called. + + Args: + warning: The warning message to be displayed as a string (optinoal). + """ + warning = warning or '' + # This closure is what is returned from the deprecated function. + def mark_deprecated(f): + # The deprecated_function wraps the actual call to f. + def deprecated_function(*args, **kwargs): + warnings.warn(warning, DeprecationWarning, stacklevel=2) + return f(*args, **kwargs) + # Preserve the original name to avoid masking all decorated functions as + # 'deprecated_function' + try: + deprecated_function.func_name = f.func_name + except TypeError: + # Setting the func_name is not allowed in Python2.3. + pass + return deprecated_function + return mark_deprecated diff --git a/patches/atom/__init__.pyc b/patches/atom/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ad68dc7073017ce3bb6ca8b6ed2dc1b3aa6777e GIT binary patch literal 51858 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 QVm*o9LHy#OfuSw`AMuY1+5i9m literal 0 HcmV?d00001 diff --git a/patches/atom/auth.py b/patches/atom/auth.py new file mode 100644 index 0000000..1d84175 --- /dev/null +++ b/patches/atom/auth.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import base64 + + +class BasicAuth(object): + """Sets the Authorization header as defined in RFC1945""" + + def __init__(self, user_id, password): + self.basic_cookie = base64.encodestring( + '%s:%s' % (user_id, password)).strip() + + def modify_request(self, http_request): + http_request.headers['Authorization'] = 'Basic %s' % self.basic_cookie + + ModifyRequest = modify_request + + +class NoAuth(object): + + def modify_request(self, http_request): + pass diff --git a/patches/atom/client.py b/patches/atom/client.py new file mode 100755 index 0000000..3211bfa --- /dev/null +++ b/patches/atom/client.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +"""AtomPubClient provides CRUD ops. in line with the Atom Publishing Protocol. + +""" + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.http_core + + +class Error(Exception): + pass + + +class MissingHost(Error): + pass + + +class AtomPubClient(object): + host = None + auth_token = None + ssl = False # Whether to force all requests over https + + def __init__(self, http_client=None, host=None, + auth_token=None, source=None, **kwargs): + """Creates a new AtomPubClient instance. + + Args: + source: The name of your application. + http_client: An object capable of performing HTTP requests through a + request method. This object is used to perform the request + when the AtomPubClient's request method is called. Used to + allow HTTP requests to be directed to a mock server, or use + an alternate library instead of the default of httplib to + make HTTP requests. + host: str The default host name to use if a host is not specified in the + requested URI. + auth_token: An object which sets the HTTP Authorization header when its + modify_request method is called. + """ + self.http_client = http_client or atom.http_core.ProxiedHttpClient() + if host is not None: + self.host = host + if auth_token is not None: + self.auth_token = auth_token + self.source = source + + def request(self, method=None, uri=None, auth_token=None, + http_request=None, **kwargs): + """Performs an HTTP request to the server indicated. + + Uses the http_client instance to make the request. + + Args: + method: The HTTP method as a string, usually one of 'GET', 'POST', + 'PUT', or 'DELETE' + uri: The URI desired as a string or atom.http_core.Uri. + http_request: + auth_token: An authorization token object whose modify_request method + sets the HTTP Authorization header. + + Returns: + The results of calling self.http_client.request. With the default + http_client, this is an HTTP response object. + """ + # Modify the request based on the AtomPubClient settings and parameters + # passed in to the request. + http_request = self.modify_request(http_request) + if isinstance(uri, (str, unicode)): + uri = atom.http_core.Uri.parse_uri(uri) + if uri is not None: + uri.modify_request(http_request) + if isinstance(method, (str, unicode)): + http_request.method = method + # Any unrecognized arguments are assumed to be capable of modifying the + # HTTP request. + for name, value in kwargs.iteritems(): + if value is not None: + value.modify_request(http_request) + # Default to an http request if the protocol scheme is not set. + if http_request.uri.scheme is None: + http_request.uri.scheme = 'http' + # Override scheme. Force requests over https. + if self.ssl: + http_request.uri.scheme = 'https' + if http_request.uri.path is None: + http_request.uri.path = '/' + # Add the Authorization header at the very end. The Authorization header + # value may need to be calculated using information in the request. + if auth_token: + auth_token.modify_request(http_request) + elif self.auth_token: + self.auth_token.modify_request(http_request) + # Check to make sure there is a host in the http_request. + if http_request.uri.host is None: + raise MissingHost('No host provided in request %s %s' % ( + http_request.method, str(http_request.uri))) + # Perform the fully specified request using the http_client instance. + # Sends the request to the server and returns the server's response. + return self.http_client.request(http_request) + + Request = request + + def get(self, uri=None, auth_token=None, http_request=None, **kwargs): + """Performs a request using the GET method, returns an HTTP response.""" + return self.request(method='GET', uri=uri, auth_token=auth_token, + http_request=http_request, **kwargs) + + Get = get + + def post(self, uri=None, data=None, auth_token=None, http_request=None, + **kwargs): + """Sends data using the POST method, returns an HTTP response.""" + return self.request(method='POST', uri=uri, auth_token=auth_token, + http_request=http_request, data=data, **kwargs) + + Post = post + + def put(self, uri=None, data=None, auth_token=None, http_request=None, + **kwargs): + """Sends data using the PUT method, returns an HTTP response.""" + return self.request(method='PUT', uri=uri, auth_token=auth_token, + http_request=http_request, data=data, **kwargs) + + Put = put + + def delete(self, uri=None, auth_token=None, http_request=None, **kwargs): + """Performs a request using the DELETE method, returns an HTTP response.""" + return self.request(method='DELETE', uri=uri, auth_token=auth_token, + http_request=http_request, **kwargs) + + Delete = delete + + def modify_request(self, http_request): + """Changes the HTTP request before sending it to the server. + + Sets the User-Agent HTTP header and fills in the HTTP host portion + of the URL if one was not included in the request (for this it uses + the self.host member if one is set). This method is called in + self.request. + + Args: + http_request: An atom.http_core.HttpRequest() (optional) If one is + not provided, a new HttpRequest is instantiated. + + Returns: + An atom.http_core.HttpRequest() with the User-Agent header set and + if this client has a value in its host member, the host in the request + URL is set. + """ + if http_request is None: + http_request = atom.http_core.HttpRequest() + + if self.host is not None and http_request.uri.host is None: + http_request.uri.host = self.host + + # Set the user agent header for logging purposes. + if self.source: + http_request.headers['User-Agent'] = '%s gdata-py/2.0.14' % self.source + else: + http_request.headers['User-Agent'] = 'gdata-py/2.0.14' + + return http_request + + ModifyRequest = modify_request diff --git a/patches/atom/client.pyc b/patches/atom/client.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a63ccc6f790aeedc25e78e1502fca3d71737f064 GIT binary patch literal 6054 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 literal 0 HcmV?d00001 diff --git a/patches/atom/core.py b/patches/atom/core.py new file mode 100644 index 0000000..a19a70d --- /dev/null +++ b/patches/atom/core.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +# This module is used for version 2 of the Google Data APIs. + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import inspect +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 + + +try: + from xml.dom.minidom import parseString as xmlString +except ImportError: + xmlString = None + +STRING_ENCODING = 'utf-8' + + +class XmlElement(object): + """Represents an element node in an XML document. + + The text member is a UTF-8 encoded str or unicode. + """ + _qname = None + _other_elements = None + _other_attributes = None + # The rule set contains mappings for XML qnames to child members and the + # appropriate member classes. + _rule_set = None + _members = None + text = None + + def __init__(self, text=None, *args, **kwargs): + if ('_members' not in self.__class__.__dict__ + or self.__class__._members is None): + self.__class__._members = tuple(self.__class__._list_xml_members()) + for member_name, member_type in self.__class__._members: + if member_name in kwargs: + setattr(self, member_name, kwargs[member_name]) + else: + if isinstance(member_type, list): + setattr(self, member_name, []) + else: + setattr(self, member_name, None) + self._other_elements = [] + self._other_attributes = {} + if text is not None: + self.text = text + + def _list_xml_members(cls): + """Generator listing all members which are XML elements or attributes. + + The following members would be considered XML members: + foo = 'abc' - indicates an XML attribute with the qname abc + foo = SomeElement - indicates an XML child element + foo = [AnElement] - indicates a repeating XML child element, each instance + will be stored in a list in this member + foo = ('att1', '{http://example.com/namespace}att2') - indicates an XML + attribute which has different parsing rules in different versions of + the protocol. Version 1 of the XML parsing rules will look for an + attribute with the qname 'att1' but verion 2 of the parsing rules will + look for a namespaced attribute with the local name of 'att2' and an + XML namespace of 'http://example.com/namespace'. + """ + members = [] + for pair in inspect.getmembers(cls): + if not pair[0].startswith('_') and pair[0] != 'text': + member_type = pair[1] + if (isinstance(member_type, tuple) or isinstance(member_type, list) + or isinstance(member_type, (str, unicode)) + or (inspect.isclass(member_type) + and issubclass(member_type, XmlElement))): + members.append(pair) + return members + + _list_xml_members = classmethod(_list_xml_members) + + def _get_rules(cls, version): + """Initializes the _rule_set for the class which is used when parsing XML. + + This method is used internally for parsing and generating XML for an + XmlElement. It is not recommended that you call this method directly. + + Returns: + A tuple containing the XML parsing rules for the appropriate version. + + The tuple looks like: + (qname, {sub_element_qname: (member_name, member_class, repeating), ..}, + {attribute_qname: member_name}) + + To give a couple of concrete example, the atom.data.Control _get_rules + with version of 2 will return: + ('{http://www.w3.org/2007/app}control', + {'{http://www.w3.org/2007/app}draft': ('draft', + , + False)}, + {}) + Calling _get_rules with version 1 on gdata.data.FeedLink will produce: + ('{http://schemas.google.com/g/2005}feedLink', + {'{http://www.w3.org/2005/Atom}feed': ('feed', + , + False)}, + {'href': 'href', 'readOnly': 'read_only', 'countHint': 'count_hint', + 'rel': 'rel'}) + """ + # Initialize the _rule_set to make sure there is a slot available to store + # the parsing rules for this version of the XML schema. + # Look for rule set in the class __dict__ proxy so that only the + # _rule_set for this class will be found. By using the dict proxy + # we avoid finding rule_sets defined in superclasses. + # The four lines below provide support for any number of versions, but it + # runs a bit slower then hard coding slots for two versions, so I'm using + # the below two lines. + #if '_rule_set' not in cls.__dict__ or cls._rule_set is None: + # cls._rule_set = [] + #while len(cls.__dict__['_rule_set']) < version: + # cls._rule_set.append(None) + # If there is no rule set cache in the class, provide slots for two XML + # versions. If and when there is a version 3, this list will need to be + # expanded. + if '_rule_set' not in cls.__dict__ or cls._rule_set is None: + cls._rule_set = [None, None] + # If a version higher than 2 is requested, fall back to version 2 because + # 2 is currently the highest supported version. + if version > 2: + return cls._get_rules(2) + # Check the dict proxy for the rule set to avoid finding any rule sets + # which belong to the superclass. We only want rule sets for this class. + if cls._rule_set[version-1] is None: + # The rule set for each version consists of the qname for this element + # ('{namespace}tag'), a dictionary (elements) for looking up the + # corresponding class member when given a child element's qname, and a + # dictionary (attributes) for looking up the corresponding class member + # when given an XML attribute's qname. + elements = {} + attributes = {} + if ('_members' not in cls.__dict__ or cls._members is None): + cls._members = tuple(cls._list_xml_members()) + for member_name, target in cls._members: + if isinstance(target, list): + # This member points to a repeating element. + elements[_get_qname(target[0], version)] = (member_name, target[0], + True) + elif isinstance(target, tuple): + # This member points to a versioned XML attribute. + if version <= len(target): + attributes[target[version-1]] = member_name + else: + attributes[target[-1]] = member_name + elif isinstance(target, (str, unicode)): + # This member points to an XML attribute. + attributes[target] = member_name + elif issubclass(target, XmlElement): + # This member points to a single occurance element. + elements[_get_qname(target, version)] = (member_name, target, False) + version_rules = (_get_qname(cls, version), elements, attributes) + cls._rule_set[version-1] = version_rules + return version_rules + else: + return cls._rule_set[version-1] + + _get_rules = classmethod(_get_rules) + + def get_elements(self, tag=None, namespace=None, version=1): + """Find all sub elements which match the tag and namespace. + + To find all elements in this object, call get_elements with the tag and + namespace both set to None (the default). This method searches through + the object's members and the elements stored in _other_elements which + did not match any of the XML parsing rules for this class. + + Args: + tag: str + namespace: str + version: int Specifies the version of the XML rules to be used when + searching for matching elements. + + Returns: + A list of the matching XmlElements. + """ + matches = [] + ignored1, elements, ignored2 = self.__class__._get_rules(version) + if elements: + for qname, element_def in elements.iteritems(): + member = getattr(self, element_def[0]) + if member: + if _qname_matches(tag, namespace, qname): + if element_def[2]: + # If this is a repeating element, copy all instances into the + # result list. + matches.extend(member) + else: + matches.append(member) + for element in self._other_elements: + if _qname_matches(tag, namespace, element._qname): + matches.append(element) + return matches + + GetElements = get_elements + # FindExtensions and FindChildren are provided for backwards compatibility + # to the atom.AtomBase class. + # However, FindExtensions may return more results than the v1 atom.AtomBase + # method does, because get_elements searches both the expected children + # and the unexpected "other elements". The old AtomBase.FindExtensions + # method searched only "other elements" AKA extension_elements. + FindExtensions = get_elements + FindChildren = get_elements + + def get_attributes(self, tag=None, namespace=None, version=1): + """Find all attributes which match the tag and namespace. + + To find all attributes in this object, call get_attributes with the tag + and namespace both set to None (the default). This method searches + through the object's members and the attributes stored in + _other_attributes which did not fit any of the XML parsing rules for this + class. + + Args: + tag: str + namespace: str + version: int Specifies the version of the XML rules to be used when + searching for matching attributes. + + Returns: + A list of XmlAttribute objects for the matching attributes. + """ + matches = [] + ignored1, ignored2, attributes = self.__class__._get_rules(version) + if attributes: + for qname, attribute_def in attributes.iteritems(): + if isinstance(attribute_def, (list, tuple)): + attribute_def = attribute_def[0] + member = getattr(self, attribute_def) + # TODO: ensure this hasn't broken existing behavior. + #member = getattr(self, attribute_def[0]) + if member: + if _qname_matches(tag, namespace, qname): + matches.append(XmlAttribute(qname, member)) + for qname, value in self._other_attributes.iteritems(): + if _qname_matches(tag, namespace, qname): + matches.append(XmlAttribute(qname, value)) + return matches + + GetAttributes = get_attributes + + def _harvest_tree(self, tree, version=1): + """Populates object members from the data in the tree Element.""" + qname, elements, attributes = self.__class__._get_rules(version) + for element in tree: + if elements and element.tag in elements: + definition = elements[element.tag] + # If this is a repeating element, make sure the member is set to a + # list. + if definition[2]: + if getattr(self, definition[0]) is None: + setattr(self, definition[0], []) + getattr(self, definition[0]).append(_xml_element_from_tree(element, + definition[1], version)) + else: + setattr(self, definition[0], _xml_element_from_tree(element, + definition[1], version)) + else: + self._other_elements.append(_xml_element_from_tree(element, XmlElement, + version)) + for attrib, value in tree.attrib.iteritems(): + if attributes and attrib in attributes: + setattr(self, attributes[attrib], value) + else: + self._other_attributes[attrib] = value + if tree.text: + self.text = tree.text + + def _to_tree(self, version=1, encoding=None): + new_tree = ElementTree.Element(_get_qname(self, version)) + self._attach_members(new_tree, version, encoding) + return new_tree + + def _attach_members(self, tree, version=1, encoding=None): + """Convert members to XML elements/attributes and add them to the tree. + + Args: + tree: An ElementTree.Element which will be modified. The members of + this object will be added as child elements or attributes + according to the rules described in _expected_elements and + _expected_attributes. The elements and attributes stored in + other_attributes and other_elements are also added a children + of this tree. + version: int Ingnored in this method but used by VersionedElement. + encoding: str (optional) + """ + qname, elements, attributes = self.__class__._get_rules(version) + encoding = encoding or STRING_ENCODING + # Add the expected elements and attributes to the tree. + if elements: + for tag, element_def in elements.iteritems(): + member = getattr(self, element_def[0]) + # If this is a repeating element and there are members in the list. + if member and element_def[2]: + for instance in member: + instance._become_child(tree, version) + elif member: + member._become_child(tree, version) + if attributes: + for attribute_tag, member_name in attributes.iteritems(): + value = getattr(self, member_name) + if value: + tree.attrib[attribute_tag] = value + # Add the unexpected (other) elements and attributes to the tree. + for element in self._other_elements: + element._become_child(tree, version) + for key, value in self._other_attributes.iteritems(): + # I'm not sure if unicode can be used in the attribute name, so for now + # we assume the encoding is correct for the attribute name. + if not isinstance(value, unicode): + value = value.decode(encoding) + tree.attrib[key] = value + if self.text: + if isinstance(self.text, unicode): + tree.text = self.text + else: + tree.text = self.text.decode(encoding) + + def to_string(self, version=1, encoding=None, pretty_print=None): + """Converts this object to XML.""" + + tree_string = ElementTree.tostring(self._to_tree(version, encoding)) + + if pretty_print and xmlString is not None: + return xmlString(tree_string).toprettyxml() + + return tree_string + + ToString = to_string + + def __str__(self): + return self.to_string() + + def _become_child(self, tree, version=1): + """Adds a child element to tree with the XML data in self.""" + new_child = ElementTree.Element('') + tree.append(new_child) + new_child.tag = _get_qname(self, version) + self._attach_members(new_child, version) + + def __get_extension_elements(self): + return self._other_elements + + def __set_extension_elements(self, elements): + self._other_elements = elements + + extension_elements = property(__get_extension_elements, + __set_extension_elements, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + def __get_extension_attributes(self): + return self._other_attributes + + def __set_extension_attributes(self, attributes): + self._other_attributes = attributes + + extension_attributes = property(__get_extension_attributes, + __set_extension_attributes, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + def _get_tag(self, version=1): + qname = _get_qname(self, version) + if qname: + return qname[qname.find('}')+1:] + return None + + def _get_namespace(self, version=1): + qname = _get_qname(self, version) + if qname.startswith('{'): + return qname[1:qname.find('}')] + else: + return None + + def _set_tag(self, tag): + if isinstance(self._qname, tuple): + self._qname = self._qname.copy() + if self._qname[0].startswith('{'): + self._qname[0] = '{%s}%s' % (self._get_namespace(1), tag) + else: + self._qname[0] = tag + else: + if self._qname is not None and self._qname.startswith('{'): + self._qname = '{%s}%s' % (self._get_namespace(), tag) + else: + self._qname = tag + + def _set_namespace(self, namespace): + tag = self._get_tag(1) + if tag is None: + tag = '' + if isinstance(self._qname, tuple): + self._qname = self._qname.copy() + if namespace: + self._qname[0] = '{%s}%s' % (namespace, tag) + else: + self._qname[0] = tag + else: + if namespace: + self._qname = '{%s}%s' % (namespace, tag) + else: + self._qname = tag + + tag = property(_get_tag, _set_tag, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + namespace = property(_get_namespace, _set_namespace, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + # Provided for backwards compatibility to atom.ExtensionElement + children = extension_elements + attributes = extension_attributes + + +def _get_qname(element, version): + if isinstance(element._qname, tuple): + if version <= len(element._qname): + return element._qname[version-1] + else: + return element._qname[-1] + else: + return element._qname + + +def _qname_matches(tag, namespace, qname): + """Logic determines if a QName matches the desired local tag and namespace. + + This is used in XmlElement.get_elements and XmlElement.get_attributes to + find matches in the element's members (among all expected-and-unexpected + elements-and-attributes). + + Args: + expected_tag: string + expected_namespace: string + qname: string in the form '{xml_namespace}localtag' or 'tag' if there is + no namespace. + + Returns: + boolean True if the member's tag and namespace fit the expected tag and + namespace. + """ + # If there is no expected namespace or tag, then everything will match. + if qname is None: + member_tag = None + member_namespace = None + else: + if qname.startswith('{'): + member_namespace = qname[1:qname.index('}')] + member_tag = qname[qname.index('}') + 1:] + else: + member_namespace = None + member_tag = qname + return ((tag is None and namespace is None) + # If there is a tag, but no namespace, see if the local tag matches. + or (namespace is None and member_tag == tag) + # There was no tag, but there was a namespace so see if the namespaces + # match. + or (tag is None and member_namespace == namespace) + # There was no tag, and the desired elements have no namespace, so check + # to see that the member's namespace is None. + or (tag is None and namespace == '' + and member_namespace is None) + # The tag and the namespace both match. + or (tag == member_tag + and namespace == member_namespace) + # The tag matches, and the expected namespace is the empty namespace, + # check to make sure the member's namespace is None. + or (tag == member_tag and namespace == '' + and member_namespace is None)) + + +def parse(xml_string, target_class=None, version=1, encoding=None): + """Parses the XML string according to the rules for the target_class. + + Args: + xml_string: str or unicode + target_class: XmlElement or a subclass. If None is specified, the + XmlElement class is used. + version: int (optional) The version of the schema which should be used when + converting the XML into an object. The default is 1. + encoding: str (optional) The character encoding of the bytes in the + xml_string. Default is 'UTF-8'. + """ + if target_class is None: + target_class = XmlElement + if isinstance(xml_string, unicode): + if encoding is None: + xml_string = xml_string.encode(STRING_ENCODING) + else: + xml_string = xml_string.encode(encoding) + tree = ElementTree.fromstring(xml_string) + return _xml_element_from_tree(tree, target_class, version) + + +Parse = parse +xml_element_from_string = parse +XmlElementFromString = xml_element_from_string + + +def _xml_element_from_tree(tree, target_class, version=1): + if target_class._qname is None: + instance = target_class() + instance._qname = tree.tag + instance._harvest_tree(tree, version) + return instance + # TODO handle the namespace-only case + # Namespace only will be used with Google Spreadsheets rows and + # Google Base item attributes. + elif tree.tag == _get_qname(target_class, version): + instance = target_class() + instance._harvest_tree(tree, version) + return instance + return None + + +class XmlAttribute(object): + + def __init__(self, qname, value): + self._qname = qname + self.value = value + diff --git a/patches/atom/core.pyc b/patches/atom/core.pyc new file mode 100644 index 0000000000000000000000000000000000000000..306359203c4a94d39a3456e5f954c6b14dd3f5db GIT binary patch literal 16448 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 Mojfi_qdYSHztCc%$^ZZW literal 0 HcmV?d00001 diff --git a/patches/atom/data.py b/patches/atom/data.py new file mode 100644 index 0000000..5a3d257 --- /dev/null +++ b/patches/atom/data.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core + + +XML_TEMPLATE = '{http://www.w3.org/XML/1998/namespace}%s' +ATOM_TEMPLATE = '{http://www.w3.org/2005/Atom}%s' +APP_TEMPLATE_V1 = '{http://purl.org/atom/app#}%s' +APP_TEMPLATE_V2 = '{http://www.w3.org/2007/app}%s' + + +class Name(atom.core.XmlElement): + """The atom:name element.""" + _qname = ATOM_TEMPLATE % 'name' + + +class Email(atom.core.XmlElement): + """The atom:email element.""" + _qname = ATOM_TEMPLATE % 'email' + + +class Uri(atom.core.XmlElement): + """The atom:uri element.""" + _qname = ATOM_TEMPLATE % 'uri' + + +class Person(atom.core.XmlElement): + """A foundation class which atom:author and atom:contributor extend. + + A person contains information like name, email address, and web page URI for + an author or contributor to an Atom feed. + """ + name = Name + email = Email + uri = Uri + + +class Author(Person): + """The atom:author element. + + An author is a required element in Feed unless each Entry contains an Author. + """ + _qname = ATOM_TEMPLATE % 'author' + + +class Contributor(Person): + """The atom:contributor element.""" + _qname = ATOM_TEMPLATE % 'contributor' + + +class Link(atom.core.XmlElement): + """The atom:link element.""" + _qname = ATOM_TEMPLATE % 'link' + href = 'href' + rel = 'rel' + type = 'type' + hreflang = 'hreflang' + title = 'title' + length = 'length' + + +class Generator(atom.core.XmlElement): + """The atom:generator element.""" + _qname = ATOM_TEMPLATE % 'generator' + uri = 'uri' + version = 'version' + + +class Text(atom.core.XmlElement): + """A foundation class from which atom:title, summary, etc. extend. + + This class should never be instantiated. + """ + type = 'type' + + +class Title(Text): + """The atom:title element.""" + _qname = ATOM_TEMPLATE % 'title' + + +class Subtitle(Text): + """The atom:subtitle element.""" + _qname = ATOM_TEMPLATE % 'subtitle' + + +class Rights(Text): + """The atom:rights element.""" + _qname = ATOM_TEMPLATE % 'rights' + + +class Summary(Text): + """The atom:summary element.""" + _qname = ATOM_TEMPLATE % 'summary' + + +class Content(Text): + """The atom:content element.""" + _qname = ATOM_TEMPLATE % 'content' + src = 'src' + + +class Category(atom.core.XmlElement): + """The atom:category element.""" + _qname = ATOM_TEMPLATE % 'category' + term = 'term' + scheme = 'scheme' + label = 'label' + + +class Id(atom.core.XmlElement): + """The atom:id element.""" + _qname = ATOM_TEMPLATE % 'id' + + +class Icon(atom.core.XmlElement): + """The atom:icon element.""" + _qname = ATOM_TEMPLATE % 'icon' + + +class Logo(atom.core.XmlElement): + """The atom:logo element.""" + _qname = ATOM_TEMPLATE % 'logo' + + +class Draft(atom.core.XmlElement): + """The app:draft element which indicates if this entry should be public.""" + _qname = (APP_TEMPLATE_V1 % 'draft', APP_TEMPLATE_V2 % 'draft') + + +class Control(atom.core.XmlElement): + """The app:control element indicating restrictions on publication. + + The APP control element may contain a draft element indicating whether or + not this entry should be publicly available. + """ + _qname = (APP_TEMPLATE_V1 % 'control', APP_TEMPLATE_V2 % 'control') + draft = Draft + + +class Date(atom.core.XmlElement): + """A parent class for atom:updated, published, etc.""" + + +class Updated(Date): + """The atom:updated element.""" + _qname = ATOM_TEMPLATE % 'updated' + + +class Published(Date): + """The atom:published element.""" + _qname = ATOM_TEMPLATE % 'published' + + +class LinkFinder(object): + """An "interface" providing methods to find link elements + + Entry elements often contain multiple links which differ in the rel + attribute or content type. Often, developers are interested in a specific + type of link so this class provides methods to find specific classes of + links. + + This class is used as a mixin in Atom entries and feeds. + """ + + def find_url(self, rel): + """Returns the URL in a link with the desired rel value.""" + for link in self.link: + if link.rel == rel and link.href: + return link.href + return None + + FindUrl = find_url + + def get_link(self, rel): + """Returns a link object which has the desired rel value. + + If you are interested in the URL instead of the link object, + consider using find_url instead. + """ + for link in self.link: + if link.rel == rel and link.href: + return link + return None + + GetLink = get_link + + def find_self_link(self): + """Find the first link with rel set to 'self' + + Returns: + A str containing the link's href or None if none of the links had rel + equal to 'self' + """ + return self.find_url('self') + + FindSelfLink = find_self_link + + def get_self_link(self): + return self.get_link('self') + + GetSelfLink = get_self_link + + def find_edit_link(self): + return self.find_url('edit') + + FindEditLink = find_edit_link + + def get_edit_link(self): + return self.get_link('edit') + + GetEditLink = get_edit_link + + def find_edit_media_link(self): + link = self.find_url('edit-media') + # Search for media-edit as well since Picasa API used media-edit instead. + if link is None: + return self.find_url('media-edit') + return link + + FindEditMediaLink = find_edit_media_link + + def get_edit_media_link(self): + link = self.get_link('edit-media') + if link is None: + return self.get_link('media-edit') + return link + + GetEditMediaLink = get_edit_media_link + + def find_next_link(self): + return self.find_url('next') + + FindNextLink = find_next_link + + def get_next_link(self): + return self.get_link('next') + + GetNextLink = get_next_link + + def find_license_link(self): + return self.find_url('license') + + FindLicenseLink = find_license_link + + def get_license_link(self): + return self.get_link('license') + + GetLicenseLink = get_license_link + + def find_alternate_link(self): + return self.find_url('alternate') + + FindAlternateLink = find_alternate_link + + def get_alternate_link(self): + return self.get_link('alternate') + + GetAlternateLink = get_alternate_link + + +class FeedEntryParent(atom.core.XmlElement, LinkFinder): + """A super class for atom:feed and entry, contains shared attributes""" + author = [Author] + category = [Category] + contributor = [Contributor] + id = Id + link = [Link] + rights = Rights + title = Title + updated = Updated + + def __init__(self, atom_id=None, text=None, *args, **kwargs): + if atom_id is not None: + self.id = atom_id + atom.core.XmlElement.__init__(self, text=text, *args, **kwargs) + + +class Source(FeedEntryParent): + """The atom:source element.""" + _qname = ATOM_TEMPLATE % 'source' + generator = Generator + icon = Icon + logo = Logo + subtitle = Subtitle + + +class Entry(FeedEntryParent): + """The atom:entry element.""" + _qname = ATOM_TEMPLATE % 'entry' + content = Content + published = Published + source = Source + summary = Summary + control = Control + + +class Feed(Source): + """The atom:feed element which contains entries.""" + _qname = ATOM_TEMPLATE % 'feed' + entry = [Entry] + + +class ExtensionElement(atom.core.XmlElement): + """Provided for backwards compatibility to the v1 atom.ExtensionElement.""" + + def __init__(self, tag=None, namespace=None, attributes=None, + children=None, text=None, *args, **kwargs): + if namespace: + self._qname = '{%s}%s' % (namespace, tag) + else: + self._qname = tag + self.children = children or [] + self.attributes = attributes or {} + self.text = text + + _BecomeChildElement = atom.core.XmlElement._become_child + + diff --git a/patches/atom/data.pyc b/patches/atom/data.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16f5291899377e13e2ade324083d7d197251f80d GIT binary patch literal 13479 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 literal 0 HcmV?d00001 diff --git a/patches/atom/http.py b/patches/atom/http.py new file mode 100644 index 0000000..247fed5 --- /dev/null +++ b/patches/atom/http.py @@ -0,0 +1,364 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""HttpClients in this module use httplib to make HTTP requests. + +This module make HTTP requests based on httplib, but there are environments +in which an httplib based approach will not work (if running in Google App +Engine for example). In those cases, higher level classes (like AtomService +and GDataService) can swap out the HttpClient to transparently use a +different mechanism for making HTTP requests. + + HttpClient: Contains a request method which performs an HTTP call to the + server. + + ProxiedHttpClient: Contains a request method which connects to a proxy using + settings stored in operating system environment variables then + performs an HTTP call to the endpoint server. +""" + + +__author__ = 'api.jscudder (Jeff Scudder)' + + +import types +import os +import httplib +import atom.url +import atom.http_interface +import socket +import base64 +import atom.http_core +ssl_imported = False +ssl = None +try: + import ssl + ssl_imported = True +except ImportError: + pass + + + +class ProxyError(atom.http_interface.Error): + pass + + +class TestConfigurationError(Exception): + pass + + +DEFAULT_CONTENT_TYPE = 'application/atom+xml' + + +class HttpClient(atom.http_interface.GenericHttpClient): + # Added to allow old v1 HttpClient objects to use the new + # http_code.HttpClient. Used in unit tests to inject a mock client. + v2_http_client = None + + def __init__(self, headers=None): + self.debug = False + self.headers = headers or {} + + def request(self, operation, url, data=None, headers=None): + """Performs an HTTP call to the server, supports GET, POST, PUT, and + DELETE. + + Usage example, perform and HTTP GET on http://www.google.com/: + import atom.http + client = atom.http.HttpClient() + http_response = client.request('GET', 'http://www.google.com/') + + Args: + operation: str The HTTP operation to be performed. This is usually one + of 'GET', 'POST', 'PUT', or 'DELETE' + data: filestream, list of parts, or other object which can be converted + to a string. Should be set to None when performing a GET or DELETE. + 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. + url: The full URL to which the request should be sent. Can be a string + or atom.url.Url. + headers: dict of strings. HTTP headers which should be sent + in the request. + """ + all_headers = self.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: + if isinstance(data, types.StringTypes): + all_headers['Content-Length'] = str(len(data)) + else: + raise atom.http_interface.ContentLengthRequired('Unable to calculate ' + 'the length of the data parameter. Specify a value for ' + 'Content-Length') + + # Set the content type to the default value if none was set. + if 'Content-Type' not in all_headers: + all_headers['Content-Type'] = DEFAULT_CONTENT_TYPE + + if self.v2_http_client is not None: + http_request = atom.http_core.HttpRequest(method=operation) + atom.http_core.Uri.parse_uri(str(url)).modify_request(http_request) + http_request.headers = all_headers + if data: + http_request._body_parts.append(data) + return self.v2_http_client.request(http_request=http_request) + + if not isinstance(url, atom.url.Url): + if isinstance(url, types.StringTypes): + url = atom.url.parse_url(url) + else: + raise atom.http_interface.UnparsableUrlObject('Unable to parse url ' + 'parameter because it was not a string or atom.url.Url') + + connection = self._prepare_connection(url, all_headers) + + if self.debug: + connection.debuglevel = 1 + + connection.putrequest(operation, self._get_access_url(url), + skip_host=True) + if url.port is not None: + connection.putheader('Host', '%s:%s' % (url.host, url.port)) + else: + connection.putheader('Host', url.host) + + # Overcome a bug in Python 2.4 and 2.5 + # httplib.HTTPConnection.putrequest adding + # HTTP request header 'Host: www.google.com:443' instead of + # 'Host: www.google.com', and thus resulting the error message + # 'Token invalid - AuthSub token has wrong scope' in the HTTP response. + if (url.protocol == 'https' and int(url.port or 443) == 443 and + hasattr(connection, '_buffer') and + isinstance(connection._buffer, list)): + header_line = 'Host: %s:443' % url.host + replacement_header_line = 'Host: %s' % url.host + try: + connection._buffer[connection._buffer.index(header_line)] = ( + replacement_header_line) + except ValueError: # header_line missing from connection._buffer + pass + + # Send the HTTP headers. + for header_name in all_headers: + connection.putheader(header_name, all_headers[header_name]) + connection.endheaders() + + # If there is data, send it in the request. + if data: + if isinstance(data, list): + for data_part in data: + _send_data_part(data_part, connection) + else: + _send_data_part(data, connection) + + # Return the HTTP Response from the server. + return connection.getresponse() + + def _prepare_connection(self, url, headers): + if not isinstance(url, atom.url.Url): + if isinstance(url, types.StringTypes): + url = atom.url.parse_url(url) + else: + raise atom.http_interface.UnparsableUrlObject('Unable to parse url ' + 'parameter because it was not a string or atom.url.Url') + if url.protocol == 'https': + if not url.port: + return httplib.HTTPSConnection(url.host) + return httplib.HTTPSConnection(url.host, int(url.port)) + else: + if not url.port: + return httplib.HTTPConnection(url.host) + return httplib.HTTPConnection(url.host, int(url.port)) + + def _get_access_url(self, url): + return url.to_string() + + +class ProxiedHttpClient(HttpClient): + """Performs an HTTP request through a proxy. + + The proxy settings are obtained from enviroment variables. The URL of the + proxy server is assumed to be stored in the environment variables + 'https_proxy' and 'http_proxy' respectively. If the proxy server requires + a Basic Auth authorization header, the username and password are expected to + be in the 'proxy-username' or 'proxy_username' variable and the + 'proxy-password' or 'proxy_password' variable, or in 'http_proxy' or + 'https_proxy' as "protocol://[username:password@]host:port". + + After connecting to the proxy server, the request is completed as in + HttpClient.request. + """ + def _prepare_connection(self, url, headers): + proxy_settings = os.environ.get('%s_proxy' % url.protocol) + if not proxy_settings: + # The request was HTTP or HTTPS, but there was no appropriate proxy set. + return HttpClient._prepare_connection(self, url, headers) + else: + print '!!!!%s' % proxy_settings + proxy_auth = _get_proxy_auth(proxy_settings) + proxy_netloc = _get_proxy_net_location(proxy_settings) + print '!!!!%s' % proxy_auth + print '!!!!%s' % proxy_netloc + if url.protocol == 'https': + # Set any proxy auth headers + if proxy_auth: + proxy_auth = 'Proxy-authorization: %s' % proxy_auth + + # Construct the proxy connect command. + port = url.port + if not port: + port = '443' + proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (url.host, port) + + # Set the user agent to send to the proxy + if headers and 'User-Agent' in headers: + user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent']) + else: + user_agent = 'User-Agent: python\r\n' + + proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent) + + # Find the proxy host and port. + proxy_url = atom.url.parse_url(proxy_netloc) + if not proxy_url.port: + proxy_url.port = '80' + + # Connect to the proxy server, very simple recv and error checking + p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + p_sock.connect((proxy_url.host, int(proxy_url.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 ProxyError('Error status=%s' % str(p_status)) + + # Trivial setup for ssl socket. + sslobj = None + if ssl_imported: + sslobj = ssl.wrap_socket(p_sock, None, None) + else: + sock_ssl = socket.ssl(p_sock, None, None) + sslobj = httplib.FakeSocket(p_sock, sock_ssl) + + # Initalize httplib and replace with the proxy socket. + connection = httplib.HTTPConnection(proxy_url.host) + connection.sock = sslobj + return connection + else: + # If protocol was not https. + # Find the proxy host and port. + proxy_url = atom.url.parse_url(proxy_netloc) + if not proxy_url.port: + proxy_url.port = '80' + + if proxy_auth: + headers['Proxy-Authorization'] = proxy_auth.strip() + + return httplib.HTTPConnection(proxy_url.host, int(proxy_url.port)) + + def _get_access_url(self, url): + return url.to_string() + + +def _get_proxy_auth(proxy_settings): + """Returns proxy authentication string for header. + + Will check environment variables for proxy authentication info, starting with + proxy(_/-)username and proxy(_/-)password before checking the given + proxy_settings for a [protocol://]username:password@host[:port] string. + + Args: + proxy_settings: String from http_proxy or https_proxy environment variable. + + Returns: + Authentication string for proxy, or empty string if no proxy username was + found. + """ + proxy_username = None + proxy_password = None + + 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 not proxy_username: + if '@' in proxy_settings: + protocol_and_proxy_auth = proxy_settings.split('@')[0].split(':') + if len(protocol_and_proxy_auth) == 3: + # 3 elements means we have [, //, ] + proxy_username = protocol_and_proxy_auth[1].lstrip('/') + proxy_password = protocol_and_proxy_auth[2] + elif len(protocol_and_proxy_auth) == 2: + # 2 elements means we have [, ] + proxy_username = protocol_and_proxy_auth[0] + proxy_password = protocol_and_proxy_auth[1] + if proxy_username: + user_auth = base64.encodestring('%s:%s' % (proxy_username, + proxy_password)) + return 'Basic %s\r\n' % (user_auth.strip()) + else: + return '' + + +def _get_proxy_net_location(proxy_settings): + """Returns proxy host and port. + + Args: + proxy_settings: String from http_proxy or https_proxy environment variable. + Must be in the form of protocol://[username:password@]host:port + + Returns: + String in the form of protocol://host:port + """ + if '@' in proxy_settings: + protocol = proxy_settings.split(':')[0] + netloc = proxy_settings.split('@')[1] + return '%s://%s' % (protocol, netloc) + else: + return proxy_settings + + +def _send_data_part(data, connection): + if isinstance(data, types.StringTypes): + connection.send(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 diff --git a/patches/atom/http_core.py b/patches/atom/http_core.py new file mode 100644 index 0000000..b9d6fb1 --- /dev/null +++ b/patches/atom/http_core.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. +# TODO: add proxy handling. + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import os +import StringIO +import urlparse +import urllib +import httplib +ssl = None +try: + import ssl +except ImportError: + pass + + + +class Error(Exception): + pass + + +class UnknownSize(Error): + pass + + +class ProxyError(Error): + pass + + +MIME_BOUNDARY = 'END_OF_PART' + + +def get_headers(http_response): + """Retrieves all HTTP headers from an HTTP response from the server. + + This method is provided for backwards compatibility for Python2.2 and 2.3. + The httplib.HTTPResponse object in 2.2 and 2.3 does not have a getheaders + method so this function will use getheaders if available, but if not it + will retrieve a few using getheader. + """ + if hasattr(http_response, 'getheaders'): + return http_response.getheaders() + else: + headers = [] + for header in ( + 'location', 'content-type', 'content-length', 'age', 'allow', + 'cache-control', 'content-location', 'content-encoding', 'date', + 'etag', 'expires', 'last-modified', 'pragma', 'server', + 'set-cookie', 'transfer-encoding', 'vary', 'via', 'warning', + 'www-authenticate', 'gdata-version'): + value = http_response.getheader(header, None) + if value is not None: + headers.append((header, value)) + return headers + + +class HttpRequest(object): + """Contains all of the parameters for an HTTP 1.1 request. + + The HTTP headers are represented by a dictionary, and it is the + responsibility of the user to ensure that duplicate field names are combined + into one header value according to the rules in section 4.2 of RFC 2616. + """ + method = None + uri = None + + def __init__(self, uri=None, method=None, headers=None): + """Construct an HTTP request. + + Args: + uri: The full path or partial path as a Uri object or a string. + method: The HTTP method for the request, examples include 'GET', 'POST', + etc. + headers: dict of strings The HTTP headers to include in the request. + """ + self.headers = headers or {} + self._body_parts = [] + if method is not None: + self.method = method + if isinstance(uri, (str, unicode)): + uri = Uri.parse_uri(uri) + self.uri = uri or Uri() + + + def add_body_part(self, data, mime_type, size=None): + """Adds data to the HTTP request body. + + If more than one part is added, this is assumed to be a mime-multipart + request. This method is designed to create MIME 1.0 requests as specified + in RFC 1341. + + Args: + data: str or a file-like object containing a part of the request body. + mime_type: str The MIME type describing the data + size: int Required if the data is a file like object. If the data is a + string, the size is calculated so this parameter is ignored. + """ + if isinstance(data, str): + size = len(data) + if size is None: + # TODO: support chunked transfer if some of the body is of unknown size. + raise UnknownSize('Each part of the body must have a known size.') + if 'Content-Length' in self.headers: + content_length = int(self.headers['Content-Length']) + else: + content_length = 0 + # If this is the first part added to the body, then this is not a multipart + # request. + if len(self._body_parts) == 0: + self.headers['Content-Type'] = mime_type + content_length = size + self._body_parts.append(data) + elif len(self._body_parts) == 1: + # This is the first member in a mime-multipart request, so change the + # _body_parts list to indicate a multipart payload. + self._body_parts.insert(0, 'Media multipart posting') + boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,) + content_length += len(boundary_string) + size + self._body_parts.insert(1, boundary_string) + content_length += len('Media multipart posting') + # Put the content type of the first part of the body into the multipart + # payload. + original_type_string = 'Content-Type: %s\r\n\r\n' % ( + self.headers['Content-Type'],) + self._body_parts.insert(2, original_type_string) + content_length += len(original_type_string) + boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,) + self._body_parts.append(boundary_string) + content_length += len(boundary_string) + # Change the headers to indicate this is now a mime multipart request. + self.headers['Content-Type'] = 'multipart/related; boundary="%s"' % ( + MIME_BOUNDARY,) + self.headers['MIME-version'] = '1.0' + # Include the mime type of this part. + type_string = 'Content-Type: %s\r\n\r\n' % (mime_type) + self._body_parts.append(type_string) + content_length += len(type_string) + self._body_parts.append(data) + ending_boundary_string = '\r\n--%s--' % (MIME_BOUNDARY,) + self._body_parts.append(ending_boundary_string) + content_length += len(ending_boundary_string) + else: + # This is a mime multipart request. + boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,) + self._body_parts.insert(-1, boundary_string) + content_length += len(boundary_string) + size + # Include the mime type of this part. + type_string = 'Content-Type: %s\r\n\r\n' % (mime_type) + self._body_parts.insert(-1, type_string) + content_length += len(type_string) + self._body_parts.insert(-1, data) + self.headers['Content-Length'] = str(content_length) + # I could add an "append_to_body_part" method as well. + + AddBodyPart = add_body_part + + def add_form_inputs(self, form_data, + mime_type='application/x-www-form-urlencoded'): + """Form-encodes and adds data to the request body. + + Args: + form_data: dict or sequnce or two member tuples which contains the + form keys and values. + mime_type: str The MIME type of the form data being sent. Defaults + to 'application/x-www-form-urlencoded'. + """ + body = urllib.urlencode(form_data) + self.add_body_part(body, mime_type) + + AddFormInputs = add_form_inputs + + def _copy(self): + """Creates a deep copy of this request.""" + copied_uri = Uri(self.uri.scheme, self.uri.host, self.uri.port, + self.uri.path, self.uri.query.copy()) + new_request = HttpRequest(uri=copied_uri, method=self.method, + headers=self.headers.copy()) + new_request._body_parts = self._body_parts[:] + return new_request + + def _dump(self): + """Converts to a printable string for debugging purposes. + + In order to preserve the request, it does not read from file-like objects + in the body. + """ + output = 'HTTP Request\n method: %s\n url: %s\n headers:\n' % ( + self.method, str(self.uri)) + for header, value in self.headers.iteritems(): + output += ' %s: %s\n' % (header, value) + output += ' body sections:\n' + i = 0 + for part in self._body_parts: + if isinstance(part, (str, unicode)): + output += ' %s: %s\n' % (i, part) + else: + output += ' %s: \n' % i + i += 1 + return output + + +def _apply_defaults(http_request): + if http_request.uri.scheme is None: + if http_request.uri.port == 443: + http_request.uri.scheme = 'https' + else: + http_request.uri.scheme = 'http' + + +class Uri(object): + """A URI as used in HTTP 1.1""" + scheme = None + host = None + port = None + path = None + + def __init__(self, scheme=None, host=None, port=None, path=None, query=None): + """Constructor for a URI. + + Args: + scheme: str This is usually 'http' or 'https'. + host: str The host name or IP address of the desired server. + post: int The server's port number. + path: str The path of the resource following the host. This begins with + a /, example: '/calendar/feeds/default/allcalendars/full' + query: dict of strings The URL query parameters. The keys and values are + both escaped so this dict should contain the unescaped values. + For example {'my key': 'val', 'second': '!!!'} will become + '?my+key=val&second=%21%21%21' which is appended to the path. + """ + self.query = query or {} + if scheme is not None: + self.scheme = scheme + if host is not None: + self.host = host + if port is not None: + self.port = port + if path: + self.path = path + + def _get_query_string(self): + param_pairs = [] + for key, value in self.query.iteritems(): + param_pairs.append('='.join((urllib.quote_plus(key), + urllib.quote_plus(str(value))))) + return '&'.join(param_pairs) + + def _get_relative_path(self): + """Returns the path with the query parameters escaped and appended.""" + param_string = self._get_query_string() + if self.path is None: + path = '/' + else: + path = self.path + if param_string: + return '?'.join([path, param_string]) + else: + return path + + def _to_string(self): + if self.scheme is None and self.port == 443: + scheme = 'https' + elif self.scheme is None: + scheme = 'http' + else: + scheme = self.scheme + if self.path is None: + path = '/' + else: + path = self.path + if self.port is None: + return '%s://%s%s' % (scheme, self.host, self._get_relative_path()) + else: + return '%s://%s:%s%s' % (scheme, self.host, str(self.port), + self._get_relative_path()) + + def __str__(self): + return self._to_string() + + def modify_request(self, http_request=None): + """Sets HTTP request components based on the URI.""" + if http_request is None: + http_request = HttpRequest() + if http_request.uri is None: + http_request.uri = Uri() + # Determine the correct scheme. + if self.scheme: + http_request.uri.scheme = self.scheme + if self.port: + http_request.uri.port = self.port + if self.host: + http_request.uri.host = self.host + # Set the relative uri path + if self.path: + http_request.uri.path = self.path + if self.query: + http_request.uri.query = self.query.copy() + return http_request + + ModifyRequest = modify_request + + def parse_uri(uri_string): + """Creates a Uri object which corresponds to the URI string. + + This method can accept partial URIs, but it will leave missing + members of the Uri unset. + """ + parts = urlparse.urlparse(uri_string) + uri = Uri() + if parts[0]: + uri.scheme = parts[0] + if parts[1]: + host_parts = parts[1].split(':') + if host_parts[0]: + uri.host = host_parts[0] + if len(host_parts) > 1: + uri.port = int(host_parts[1]) + if parts[2]: + uri.path = parts[2] + if parts[4]: + param_pairs = parts[4].split('&') + for pair in param_pairs: + pair_parts = pair.split('=') + if len(pair_parts) > 1: + uri.query[urllib.unquote_plus(pair_parts[0])] = ( + urllib.unquote_plus(pair_parts[1])) + elif len(pair_parts) == 1: + uri.query[urllib.unquote_plus(pair_parts[0])] = None + return uri + + parse_uri = staticmethod(parse_uri) + + ParseUri = parse_uri + + +parse_uri = Uri.parse_uri + + +ParseUri = Uri.parse_uri + + +class HttpResponse(object): + status = None + reason = None + _body = None + + def __init__(self, status=None, reason=None, headers=None, body=None): + self._headers = headers or {} + if status is not None: + self.status = status + if reason is not None: + self.reason = reason + if body is not None: + if hasattr(body, 'read'): + self._body = body + else: + self._body = StringIO.StringIO(body) + + def getheader(self, name, default=None): + if name in self._headers: + return self._headers[name] + else: + return default + + def getheaders(self): + return self._headers + + def read(self, amt=None): + if self._body is None: + return None + if not amt: + return self._body.read() + else: + return self._body.read(amt) + + +def _dump_response(http_response): + """Converts to a string for printing debug messages. + + Does not read the body since that may consume the content. + """ + output = 'HttpResponse\n status: %s\n reason: %s\n headers:' % ( + http_response.status, http_response.reason) + headers = get_headers(http_response) + if isinstance(headers, dict): + for header, value in headers.iteritems(): + output += ' %s: %s\n' % (header, value) + else: + for pair in headers: + output += ' %s: %s\n' % (pair[0], pair[1]) + return output + + +class HttpClient(object): + """Performs HTTP requests using httplib.""" + debug = None + + def request(self, http_request): + return self._http_request(http_request.method, http_request.uri, + http_request.headers, http_request._body_parts) + + Request = request + + def _get_connection(self, uri, headers=None): + """Opens a socket connection to the server to set up an HTTP request. + + Args: + uri: The full URL for the request as a Uri object. + headers: A dict of string pairs containing the HTTP headers for the + request. + """ + connection = None + if uri.scheme == 'https': + if not uri.port: + connection = httplib.HTTPSConnection(uri.host) + else: + connection = httplib.HTTPSConnection(uri.host, int(uri.port)) + else: + if not uri.port: + connection = httplib.HTTPConnection(uri.host) + else: + connection = httplib.HTTPConnection(uri.host, int(uri.port)) + return connection + + def _http_request(self, method, uri, headers=None, body_parts=None): + """Makes an HTTP request using httplib. + + Args: + method: str example: 'GET', 'POST', 'PUT', 'DELETE', etc. + uri: str or atom.http_core.Uri + headers: dict of strings mapping to strings which will be sent as HTTP + headers in the request. + body_parts: list of strings, objects with a read method, or objects + which can be converted to strings using str. Each of these + will be sent in order as the body of the HTTP request. + """ + if isinstance(uri, (str, unicode)): + uri = Uri.parse_uri(uri) + + connection = self._get_connection(uri, headers=headers) + + if self.debug: + connection.debuglevel = 1 + + if connection.host != uri.host: + connection.putrequest(method, str(uri)) + else: + connection.putrequest(method, uri._get_relative_path()) + + # Overcome a bug in Python 2.4 and 2.5 + # httplib.HTTPConnection.putrequest adding + # HTTP request header 'Host: www.google.com:443' instead of + # 'Host: www.google.com', and thus resulting the error message + # 'Token invalid - AuthSub token has wrong scope' in the HTTP response. + if (uri.scheme == 'https' and int(uri.port or 443) == 443 and + hasattr(connection, '_buffer') and + isinstance(connection._buffer, list)): + header_line = 'Host: %s:443' % uri.host + replacement_header_line = 'Host: %s' % uri.host + try: + connection._buffer[connection._buffer.index(header_line)] = ( + replacement_header_line) + except ValueError: # header_line missing from connection._buffer + pass + + # Send the HTTP headers. + for header_name, value in headers.iteritems(): + connection.putheader(header_name, value) + connection.endheaders() + + # If there is data, send it in the request. + if body_parts: + for part in body_parts: + _send_data_part(part, connection) + + # Return the HTTP Response from the server. + return connection.getresponse() + + +def _send_data_part(data, connection): + if isinstance(data, (str, unicode)): + # I might want to just allow str, not unicode. + connection.send(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 + + +class ProxiedHttpClient(HttpClient): + + def _get_connection(self, uri, headers=None): + # Check to see if there are proxy settings required for this request. + proxy = None + if uri.scheme == 'https': + proxy = os.environ.get('https_proxy') + elif uri.scheme == 'http': + proxy = os.environ.get('http_proxy') + if not proxy: + return HttpClient._get_connection(self, uri, headers=headers) + # Now we have the URL of the appropriate proxy server. + # Get a username and password for the proxy if required. + proxy_auth = _get_proxy_auth() + if uri.scheme == 'https': + import socket + if proxy_auth: + proxy_auth = 'Proxy-authorization: %s' % proxy_auth + # Construct the proxy connect command. + port = uri.port + if not port: + port = 443 + proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (uri.host, port) + # Set the user agent to send to the proxy + user_agent = '' + if headers and 'User-Agent' in headers: + user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent']) + proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent) + # Find the proxy host and port. + proxy_uri = Uri.parse_uri(proxy) + if not proxy_uri.port: + proxy_uri.port = '80' + # Connect to the proxy server, very simple recv and error checking + p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + p_sock.connect((proxy_uri.host, int(proxy_uri.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 ProxyError('Error status=%s' % str(p_status)) + # Trivial setup for ssl socket. + sslobj = None + if ssl is not None: + sslobj = ssl.wrap_socket(p_sock, None, None) + else: + sock_ssl = socket.ssl(p_sock, None, Nonesock_) + sslobj = httplib.FakeSocket(p_sock, sock_ssl) + # Initalize httplib and replace with the proxy socket. + connection = httplib.HTTPConnection(proxy_uri.host) + connection.sock = sslobj + return connection + elif uri.scheme == 'http': + proxy_uri = Uri.parse_uri(proxy) + if not proxy_uri.port: + proxy_uri.port = '80' + if proxy_auth: + headers['Proxy-Authorization'] = proxy_auth.strip() + return httplib.HTTPConnection(proxy_uri.host, int(proxy_uri.port)) + return None + + +def _get_proxy_auth(): + import base64 + 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.b64encode('%s:%s' % (proxy_username, + proxy_password)) + return 'Basic %s\r\n' % (user_auth.strip()) + else: + return '' diff --git a/patches/atom/http_core.pyc b/patches/atom/http_core.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95e6ee67f87f977d4003ad2192a17f1a967ef24d GIT binary patch literal 18835 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 j 1: + url.port = host_parts[1] + if parts[2]: + url.path = parts[2] + if parts[4]: + param_pairs = parts[4].split('&') + for pair in param_pairs: + pair_parts = pair.split('=') + if len(pair_parts) > 1: + url.params[urllib.unquote_plus(pair_parts[0])] = ( + urllib.unquote_plus(pair_parts[1])) + elif len(pair_parts) == 1: + url.params[urllib.unquote_plus(pair_parts[0])] = None + return url + +class Url(object): + """Represents a URL and implements comparison logic. + + URL strings which are not identical can still be equivalent, so this object + provides a better interface for comparing and manipulating URLs than + strings. URL parameters are represented as a dictionary of strings, and + defaults are used for the protocol (http) and port (80) if not provided. + """ + def __init__(self, protocol=None, host=None, port=None, path=None, + params=None): + self.protocol = protocol + self.host = host + self.port = port + self.path = path + self.params = params or {} + + def to_string(self): + url_parts = ['', '', '', '', '', ''] + if self.protocol: + url_parts[0] = self.protocol + if self.host: + if self.port: + url_parts[1] = ':'.join((self.host, str(self.port))) + else: + url_parts[1] = self.host + if self.path: + url_parts[2] = self.path + if self.params: + url_parts[4] = self.get_param_string() + return urlparse.urlunparse(url_parts) + + def get_param_string(self): + param_pairs = [] + for key, value in self.params.iteritems(): + param_pairs.append('='.join((urllib.quote_plus(key), + urllib.quote_plus(str(value))))) + return '&'.join(param_pairs) + + def get_request_uri(self): + """Returns the path with the parameters escaped and appended.""" + param_string = self.get_param_string() + if param_string: + return '?'.join([self.path, param_string]) + else: + return self.path + + def __cmp__(self, other): + if not isinstance(other, Url): + return cmp(self.to_string(), str(other)) + difference = 0 + # Compare the protocol + if self.protocol and other.protocol: + difference = cmp(self.protocol, other.protocol) + elif self.protocol and not other.protocol: + difference = cmp(self.protocol, DEFAULT_PROTOCOL) + elif not self.protocol and other.protocol: + difference = cmp(DEFAULT_PROTOCOL, other.protocol) + if difference != 0: + return difference + # Compare the host + difference = cmp(self.host, other.host) + if difference != 0: + return difference + # Compare the port + if self.port and other.port: + difference = cmp(self.port, other.port) + elif self.port and not other.port: + difference = cmp(self.port, DEFAULT_PORT) + elif not self.port and other.port: + difference = cmp(DEFAULT_PORT, other.port) + if difference != 0: + return difference + # Compare the path + difference = cmp(self.path, other.path) + if difference != 0: + return difference + # Compare the parameters + return cmp(self.params, other.params) + + def __str__(self): + return self.to_string() + diff --git a/patches/gdata/Crypto/Cipher/AES.pyd b/patches/gdata/Crypto/Cipher/AES.pyd new file mode 100755 index 0000000000000000000000000000000000000000..707b36ff2d69f7f0013b6da4dd3374b7ec87d969 GIT binary patch literal 27648 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 Z!yWBC9Xy>q-95cLeLVd=S^WQO{2$QKpBexF literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/ARC2.pyd b/patches/gdata/Crypto/Cipher/ARC2.pyd new file mode 100755 index 0000000000000000000000000000000000000000..a9dfbf691c4fdf7e4ba78894f38e6cc4835b72f8 GIT binary patch literal 15872 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 CDWGrw literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/ARC4.pyd b/patches/gdata/Crypto/Cipher/ARC4.pyd new file mode 100755 index 0000000000000000000000000000000000000000..4cd51f209b382223f06a5104fbd2d5198c9f3561 GIT binary patch literal 8704 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
8>TbC`m>Hg`rf}_b{VO+A$x9i#U zLeVe9pARg~IPlqPZl&kQRlju0HqEb+`TVryUR>wzx%rRh?lmz`aNPNlSV;}{IX&lD z^3CT@)&##~SJ^Td$%YFUsmA0v=$C&XaGw$sVq{C6!QgP&Oma9G5Xdg^tmNppq*yY8 z8Ocsf^No&nH5crh+~GcQ_1KzYB(v$pgx~b44k8(WC%>mO#=9*1d~#kk%lnLoyC?;If=|o zLfp}e{5<@~(F{gZM0n%^{HKq`0EVAPCNMa$tSAJDi;iO?;kZ+RjLC>vEStrQTZ|~g zamdMWoLK2p7?F%91dWVKjAgLV&j*o2DexLCN=u@Taz9Z_CbTQZNcA&a6q5t(aYmvT zc%+FrLA#IWcy%5;6dW~56ocN`gu(cM{b*dz6#=;T7q$VgdZYxmeS#>a4(Rvb{J(b3 zl|EbRJFlMSYyTVMgOZVj`CB`#QG_ZyrBXg1&3?`XE4rfN8G2^8(9ZyaShaW97VA8!q!(o!~@!=6@ zFdmJ-DSm{{V#cS!qGv>N<0T8$HDCt5+CgIEM^l)IT#i&aSo>1h!lCwkY9J}7HJ-tY z;l%#aF-UPz1(T72L7sj@d{_`@5?B!b!WV_dNdy5VJ?89*wiueOR1M^4E;CYM`?x6P zcn+D}6Q?Bs^2fLmr0;cWiiKB7=!x z{t(ahX^Uc>L7A!}igDBx#q1@EVwCknG5?i}()j+*<_&X>V@9&rY{)!)Y;oF96w^6Y z6l1S1ia7;khk+<&@OV*75)@=BNe&>h5*cjCxKG1YG;|c;;YxR#|trU zUpIOv-PJ`>admN(R@|i!$e|L5Zx00g6@z~+0CM-nD+XF`Tv({RWRak7nRrcc@o;qw zwxF8OJxwf3{HJiORb)J)0$Qb0K$6+>tnLotW) zcp`>5!X191FWsz}CZsujq3A;~hGGrH4T>L>o@*}DAh~crU=|h{o|FXV2&iwtLK$o} zlZEs^k`0ew0n}VJIy#=4gzrThWuw^)28OX^6JS4t{)-2kJ7i&N5t*2Z>Zz!nz*+<- z*kV*qVj!|``K3CuV?zDH*-5BRrf)ld3!5_ZZ$RWSU0KW|PB;@ODgpX@@eO7u5>-+KcrCyslMi)Hl1lOLI}DMRKtW~x zXoC&cz9fup;MClxe*)$!ZdndI0lBzhIXhs9GT-KQqIHM;9B1J zu@z&X_7sTx5`ioc-wm>%y+XJ#;<3CwZ6DYLRHJsN5BMKUnHwqb4-`|RWg2~k58cax zY6?z@=xG4_pOl28MUiZdWRxL{`B#<1R5-yislD?>bN;uyRMEP?FjsSL^M&TC%y*gJ zFmEt#Hy=hZrbJNUDeEXZC}$>ro$PHj!z#q;tyPDW*otGb*ha;cZM)c3Xj^1^*tXR6 zm2Io-CtFRLHqDSWj}}Q|(lTkQXlrRZXoqOWXy<63X-n+{b|>tf*frU`wR5m{wfC`S z+b7#Ew=c8@=46QDu;DPHxKO50W>FZF`4ldtkWxxHPq{+*NC~G#Qx{T;sEyQCs)mJ~ zg|mg1#X5`47P~AiTU@ueXVGD?#pa*xs}q zN}Eh`0ZHO%0@?=J30gJn2JI!Sg*L=a%} f}Mq(tDTqKTswvx*Dk|uwcTdBeRjom z<#t!??%O@H6WP7D`)sFXKgOPF?_xj0J_4j&Zokfcm;G`3O8YzZ&+OanWgXNU#yC(N zf*qn9SPnT3g${=uE;w9uXm)6K`0AkUsO@O#XyZ7;alT`s<8sGij;|d%96?v+5PRW9 zG*>fsFb^_knP;1CH$Py0+`P=Z(!9p}zWHd%c(9!{#e?EUnF*Rqq@+-mQTSlT0?IYY zT}mUR1#~JyRibK8b*L2T3@U?~K;=?*QIAtAsduQ)sO?l)3pI-|7E}utiy0OX7Fia+ z&=2w^+;*E6nIAGQHa}@zPPs&>q0~}-r!)Y~CQ2&>gP(3HQH`nAR41w%SPKi2{9CKt lV7o)qV(Lk1DfJvx-@?$s*uvbx(!v^S2_*mL`oE}w{{RNQ^hE#w literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/CAST.pyd b/patches/gdata/Crypto/Cipher/CAST.pyd new file mode 100755 index 0000000000000000000000000000000000000000..c3e1fa6233dee810949052844150f8ba1275e0b1 GIT binary patch literal 26112 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 PatsB=LRdlnpZEMP-9TFF literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/DES.pyd b/patches/gdata/Crypto/Cipher/DES.pyd new file mode 100755 index 0000000000000000000000000000000000000000..b9967dcb1a0b50410c391efd4cea16b38cdeb023 GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/DES3.pyd b/patches/gdata/Crypto/Cipher/DES3.pyd new file mode 100755 index 0000000000000000000000000000000000000000..6768126507cbe50f0f43974a4bfa5b8d262a2808 GIT binary patch literal 20992 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�v zxa^WO^JTJDRbT6_zK<+ad*pkoJylvL&I)HGQ7fwJs+{%o=btp9VtfsGHbLa1xv1TB z5AfWX0`CA`3H+(K?JgaScIDXI5$QI(RQ<=P?XD>FWyljP{Nj2VAr-QRG{Pk45V`)A zcGv28m+OVw_-pn}^bJeI6?#t@*+z(cgMN(+;E;wh>9}zQ!-NSFCr+AVGF^9FT-^27 z-+1HHsR;?wrpfZmnX_iio;`Q&ym|BI-+HUXVznkGfBV}hDQRgL8JU^c*>=0bk)OY0 z$sa-g)QUcbAr~TUTDbVMAr*rcG5W|gxZQPi4GmuJmMx8q z_uaQ`+m0R0%|2gC%L5N6%HRFn?%fYP^zg%b_wL*G{qG+*@Pi*b`sibiwY5F|_>)im z@P~&E{rJcK@-ILA>EXk_{N=BHb>zsgW5V}yss@WYdIQr-BR7u=kmrJW_P#^I&t2*2P=BvcL*Yu1r@GGNq>7n>Ke+eY>vXzhi(KxmB%L{0uyf?Pa{OVw6LF8I zmLpD=t9%0~^Qw&)t|VXUb~U0fIyZVAoO1igzZR-Le)|rZ}hq= z_#RbRUFn|Vk?TYInAkwBEGvXYHzxXT4UlRv63TGoyis+(Z0^#;-qBE1>veJ8RpoVc zPB&w3%xHJbxw+l70q`Q=xoPdL+0)xy-vxZt__V$6>&1+CtKAi~_4U}9!{*@I*ju(H zw!8inaNDeQSHkRe*8_9fT^9f}M`C}JYwMi#`S`5FaB3PD*BqF{M46blTs+Fk9bE_cx~N1gHFp5!KJTvxjmEL)OWV7ITe zSr<4KFR(3Gy>!{q1qH>6S1l-9vb+FfDBf$cmNitB*E=igJoO7Q`E8Vu!EdM9jY*Ix zb1W`fy*Ou;BX2JGS-9|4{A{YISYTV2yzn;a-C9xW-dMfq)@obI?YF|-0%30f3@+qz zTj2EMcxr2wqrBwEN#*F`G=v|F1wI<)&XuQU$mXk!;v4#3GXtUjd&Sy}rvU^Kf+XPc z0cOCu`S?9;w2UwOg>{XUj8^MkT%V-z&Gp|m1I2qrV{6^VSj@wWRfW+yK$Af`5JpSc z&sY&?SGzjEdl}FhCKEt^7$EJ%vpgUkkO-I$NCD&liU6VO9`s#+X23x}JKzMM7cdBr zz6TqC`G9OdF`x>t3-AcwFyIuR4=@CfFpgM2A|M&C7;q<`3J|*fC&sN~IhB=Ed2<;1 z8OyJBYshI9UXd|wR#sN&!Z8*bum=OKWo6|J4R{Vk{{Sm<*4MjhDGzb_@(s11n)Mqu zy1WhioR+2ESnqV=Sy5kuvm*U3K8$VEetk6fe5>g`VCGpi>s(Hr+!me zQF(m>@pR#Mu@?tcde;&r!)ZD0N*tf>t*x(YfX+?e+qDyFnZ3$cu^Dp>nxnqH4A1N} z&R^4A`$mhdI0{$fDMJnb|Bt*(sMQGEZZ0(ct$DZkaq}zY)8_N$$(H$+4HlPWujLWT_pJYD z{gL&k^@Q~e>ly1gYl3a2&1%cB7258xZMNNKd)D@UY`?L6VEfqirEOwzLUKX!^5oKF zSMt{62b2Fk`Ja-{eS1wxY08$Ae@J;cr6c9vQUWP!Q}0RLl-iVfG4=D*hZdb*^!cKR zX-R1d(o)hkrMc4XO*@eGXxgE)chf#d`;Ro19+N&fJt2L5x+Oh5y)iwQJ|V-FQI=7e zQJe8>Mt8=m8Rs)T&CqAg%w#g^91br(vvgTru)J;g(DISxPnMb16l;ccv2}~}ee3TL zeUfdx&1KtXd&bsjd)YQ6`G({f$@$55B(F*KB*&-Rl44ECOdeQTX-d@zV=#Ps&TlD3k z=(HQt7N*(KGSil)`O}W1bzvNXX%o_8({1Sm>4oVV(>JH9>3h=;r2ljJFVnlzUrs-r z{$cu`(!WfflyO7GoD6eDcE*y7?__MqsLE)_Xw2A~@p#5BGP*Ke&p3mz>NBTg&d9W8 z=4P(S+>lwHd4J~K%*Qi-k@;5U+02hK4Oug@7Gz~+Ey=3Rs?Tc5`d-$fSwGD>mi2np zM_EHzm}oQhR5W_C#r!vBkGa*n*Zi#cP4ma*Yb-Ze=2@(kMV4I4GRt=?Kd}7J@)OHZ zOONH0r5BkTvJ6{v)@W;jb-Hz_wZyvKx*0j$Y29tzXMN22bL;EY53PT+{>eH6Ya`dT z%C^B)Z@b^N*Y>#W7q%|j>$WpCi7G`A_FpvX&6VaVv&&p(Zno^Q?6&N&?88bsXn7Jo zJ1i$GFIxuTkKvE84_euJ+P_f0ZLPN5wmq1;1GYzOELlo6B*!G1l4FzOc@2WdH`o7T G2L3NHP{`8& literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/IDEA.pyd b/patches/gdata/Crypto/Cipher/IDEA.pyd new file mode 100755 index 0000000000000000000000000000000000000000..860c12177453abcb1b7807cb624c28af0f242edb GIT binary patch literal 15360 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 literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/RC5.pyd b/patches/gdata/Crypto/Cipher/RC5.pyd new file mode 100755 index 0000000000000000000000000000000000000000..ef39b3ea0f99fe59557eac8420c45d4b92d487d0 GIT binary patch literal 15872 zcmeHO4Rlo1wZ4;#FzCRHnrPH0H!w&Li1UMz%uGU($t0iz14+QY7$=#5$BQgM6@;#o-_E<6~WfArWzI7Nt4zTcoOkv-nY-alS~G% zcD=UmbuI78nS0JY`|Q87&)(mI#e(_Hix@XPaJBxdI z#;ZAd6f0lNscCBV@oiqu2Ct))Z*;ib9zWmU;=KVk-|XhguB+x-Jx*8N%$aBFWYSf3 zj$5gi&iOa}x-7wMh?_O{97S3_M@VLrG!(5|igD*6PSysT9Mg;A zcxfyRAR4&Xt&QVKsnNx8%g>_98LsUdw~hG!jjuevYm*-|ADxRI5=e(Kp#Kd)!9bNzCMKjX1jx`R(GRTGU2nZcn~~Bg%rq-iyTa?mqvq_<)4XfWDEC|8h>q(8ZR| z;W)z~fe{KTX(4bi9bnz@fHpxsP;U5$;g5w}$8Wl&zHYyoqc-rn68MiWN&5UPVL&+U z5fUeM7bK5O!_V;L@pyc@BxGnK7kYj^A$D)da9r!39vtWWvq$pL z|L!xaf9b>MuT+Kaen=tZfRRhk*|_@MXE6BL_N5Pt9jfrxq|SK!xe0QrP#KpN9Ot%> zqlC*IAQN!5f@tR$28b2P?!NZ)VBd7%#eJWrGyZIVU>_7KwJU_k11CNg>``_Po>Tx@d=1tQ`$2A7sqY z-aH1+y>` zULjNt1v`c~%{LxrI4ZGNDcD~pJKRS+gxUe¯hBQakS?J;zvmp!OXw;vH}UtjQ& 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 literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Cipher/XOR.pyd b/patches/gdata/Crypto/Cipher/XOR.pyd new file mode 100755 index 0000000000000000000000000000000000000000..fb53d53e29d9cae5a590f14b016a623949456bc8 GIT binary patch literal 8704 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 aH5@d&fw(avXDmYGtVW0NedBlG blocksize: + key = digestmod.new(key).digest() + + key = key + chr(0) * (blocksize - len(key)) + self.outer.update(_strxor(key, opad)) + self.inner.update(_strxor(key, ipad)) + if (msg): + self.update(msg) + +## def clear(self): +## raise NotImplementedError, "clear() method not available in HMAC." + + def update(self, msg): + """Update this hashing object with the string msg. + """ + self.inner.update(msg) + + def copy(self): + """Return a separate copy of this hashing object. + + An update to this copy won't affect the original object. + """ + other = HMAC("") + other.digestmod = self.digestmod + other.inner = self.inner.copy() + other.outer = self.outer.copy() + return other + + def digest(self): + """Return the hash value of this hashing object. + + This returns a string containing 8-bit data. The object is + not altered in any way by this function; you can continue + updating the object after calling this function. + """ + h = self.outer.copy() + h.update(self.inner.digest()) + return h.digest() + + def hexdigest(self): + """Like digest(), but returns a string of hexadecimal digits instead. + """ + return "".join([string.zfill(hex(ord(x))[2:], 2) + for x in tuple(self.digest())]) + +def new(key, msg = None, digestmod = None): + """Create a new hashing object and return it. + + key: The starting key for the hash. + msg: if available, will immediately be hashed into the object's starting + state. + + You can now feed arbitrary strings into the object using its update() + method, and can ask for the hash value at any time by calling its digest() + method. + """ + return HMAC(key, msg, digestmod) + diff --git a/patches/gdata/Crypto/Hash/MD2.pyd b/patches/gdata/Crypto/Hash/MD2.pyd new file mode 100755 index 0000000000000000000000000000000000000000..d11706263ee485ae287607511773126100dc456e GIT binary patch literal 8704 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 b>JIf&>ci@ont|x7C^XWH{7iQ7&tCM8%=%{- literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Hash/MD4.pyd b/patches/gdata/Crypto/Hash/MD4.pyd new file mode 100755 index 0000000000000000000000000000000000000000..4ba243eccd17d97f91875e27668fb8c6c9ed7c2f GIT binary patch literal 9728 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 GTK)@BR;<a-&EZe8L@97?PG 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 literal 0 HcmV?d00001 diff --git a/patches/gdata/Crypto/Hash/SHA.py b/patches/gdata/Crypto/Hash/SHA.py new file mode 100755 index 0000000..ea3c6a3 --- /dev/null +++ b/patches/gdata/Crypto/Hash/SHA.py @@ -0,0 +1,11 @@ + +# Just use the SHA module from the Python standard library + +__revision__ = "$Id: SHA.py,v 1.4 2002/07/11 14:31:19 akuchling Exp $" + +from sha import * +import sha +if hasattr(sha, 'digestsize'): + digest_size = digestsize + del digestsize +del sha diff --git a/patches/gdata/Crypto/Hash/SHA256.pyd b/patches/gdata/Crypto/Hash/SHA256.pyd new file mode 100755 index 0000000000000000000000000000000000000000..865a16c4d41a80e801a628d32fb40acddb50ee7d GIT binary patch literal 9216 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 pool.entropy: + pool.add_event() + + # we now have enough entropy in the pool to get a key_size'd key + return pool.get_bytes(key_size) + + def __newcipher(self, key): + if self.__mode is None and self.__IV is None: + return self.__ciphermodule.new(key) + elif self.__IV is None: + return self.__ciphermodule.new(key, self.__mode) + else: + return self.__ciphermodule.new(key, self.__mode, self.__IV) + + + +if __name__ == '__main__': + import sys + import getopt + import base64 + + usagemsg = '''\ +Test module usage: %(program)s [-c cipher] [-l] [-h] + +Where: + --cipher module + -c module + Cipher module to use. Default: %(ciphermodule)s + + --aslong + -l + Print the encoded message blocks as long integers instead of base64 + encoded strings + + --help + -h + Print this help message +''' + + ciphermodule = 'AES' + aslong = 0 + + def usage(code, msg=None): + if msg: + print msg + print usagemsg % {'program': sys.argv[0], + 'ciphermodule': ciphermodule} + sys.exit(code) + + try: + opts, args = getopt.getopt(sys.argv[1:], + 'c:l', ['cipher=', 'aslong']) + except getopt.error, msg: + usage(1, msg) + + if args: + usage(1, 'Too many arguments') + + for opt, arg in opts: + if opt in ('-h', '--help'): + usage(0) + elif opt in ('-c', '--cipher'): + ciphermodule = arg + elif opt in ('-l', '--aslong'): + aslong = 1 + + # ugly hack to force __import__ to give us the end-path module + module = __import__('Crypto.Cipher.'+ciphermodule, None, None, ['new']) + + a = AllOrNothing(module) + print 'Original text:\n==========' + print __doc__ + print '==========' + msgblocks = a.digest(__doc__) + print 'message blocks:' + for i, blk in map(None, range(len(msgblocks)), msgblocks): + # base64 adds a trailing newline + print ' %3d' % i, + if aslong: + print bytes_to_long(blk) + else: + print base64.encodestring(blk)[:-1] + # + # get a new undigest-only object so there's no leakage + b = AllOrNothing(module) + text = b.undigest(msgblocks) + if text == __doc__: + print 'They match!' + else: + print 'They differ!' diff --git a/patches/gdata/Crypto/Protocol/Chaffing.py b/patches/gdata/Crypto/Protocol/Chaffing.py new file mode 100755 index 0000000..fdfb82d --- /dev/null +++ b/patches/gdata/Crypto/Protocol/Chaffing.py @@ -0,0 +1,229 @@ +"""This file implements the chaffing algorithm. + +Winnowing and chaffing is a technique for enhancing privacy without requiring +strong encryption. In short, the technique takes a set of authenticated +message blocks (the wheat) and adds a number of chaff blocks which have +randomly chosen data and MAC fields. This means that to an adversary, the +chaff blocks look as valid as the wheat blocks, and so the authentication +would have to be performed on every block. By tailoring the number of chaff +blocks added to the message, the sender can make breaking the message +computationally infeasible. There are many other interesting properties of +the winnow/chaff technique. + +For example, say Alice is sending a message to Bob. She packetizes the +message and performs an all-or-nothing transformation on the packets. Then +she authenticates each packet with a message authentication code (MAC). The +MAC is a hash of the data packet, and there is a secret key which she must +share with Bob (key distribution is an exercise left to the reader). She then +adds a serial number to each packet, and sends the packets to Bob. + +Bob receives the packets, and using the shared secret authentication key, +authenticates the MACs for each packet. Those packets that have bad MACs are +simply discarded. The remainder are sorted by serial number, and passed +through the reverse all-or-nothing transform. The transform means that an +eavesdropper (say Eve) must acquire all the packets before any of the data can +be read. If even one packet is missing, the data is useless. + +There's one twist: by adding chaff packets, Alice and Bob can make Eve's job +much harder, since Eve now has to break the shared secret key, or try every +combination of wheat and chaff packet to read any of the message. The cool +thing is that Bob doesn't need to add any additional code; the chaff packets +are already filtered out because their MACs don't match (in all likelihood -- +since the data and MACs for the chaff packets are randomly chosen it is +possible, but very unlikely that a chaff MAC will match the chaff data). And +Alice need not even be the party adding the chaff! She could be completely +unaware that a third party, say Charles, is adding chaff packets to her +messages as they are transmitted. + +For more information on winnowing and chaffing see this paper: + +Ronald L. Rivest, "Chaffing and Winnowing: Confidentiality without Encryption" +http://theory.lcs.mit.edu/~rivest/chaffing.txt + +""" + +__revision__ = "$Id: Chaffing.py,v 1.7 2003/02/28 15:23:21 akuchling Exp $" + +from Crypto.Util.number import bytes_to_long + +class Chaff: + """Class implementing the chaff adding algorithm. + + Methods for subclasses: + + _randnum(size): + Returns a randomly generated number with a byte-length equal + to size. Subclasses can use this to implement better random + data and MAC generating algorithms. The default algorithm is + probably not very cryptographically secure. It is most + important that the chaff data does not contain any patterns + that can be used to discern it from wheat data without running + the MAC. + + """ + + def __init__(self, factor=1.0, blocksper=1): + """Chaff(factor:float, blocksper:int) + + factor is the number of message blocks to add chaff to, + expressed as a percentage between 0.0 and 1.0. blocksper is + the number of chaff blocks to include for each block being + chaffed. Thus the defaults add one chaff block to every + message block. By changing the defaults, you can adjust how + computationally difficult it could be for an adversary to + brute-force crack the message. The difficulty is expressed + as: + + pow(blocksper, int(factor * number-of-blocks)) + + For ease of implementation, when factor < 1.0, only the first + int(factor*number-of-blocks) message blocks are chaffed. + """ + + if not (0.0<=factor<=1.0): + raise ValueError, "'factor' must be between 0.0 and 1.0" + if blocksper < 0: + raise ValueError, "'blocksper' must be zero or more" + + self.__factor = factor + self.__blocksper = blocksper + + + def chaff(self, blocks): + """chaff( [(serial-number:int, data:string, MAC:string)] ) + : [(int, string, string)] + + Add chaff to message blocks. blocks is a list of 3-tuples of the + form (serial-number, data, MAC). + + Chaff is created by choosing a random number of the same + byte-length as data, and another random number of the same + byte-length as MAC. The message block's serial number is + placed on the chaff block and all the packet's chaff blocks + are randomly interspersed with the single wheat block. This + method then returns a list of 3-tuples of the same form. + Chaffed blocks will contain multiple instances of 3-tuples + with the same serial number, but the only way to figure out + which blocks are wheat and which are chaff is to perform the + MAC hash and compare values. + """ + + chaffedblocks = [] + + # count is the number of blocks to add chaff to. blocksper is the + # number of chaff blocks to add per message block that is being + # chaffed. + count = len(blocks) * self.__factor + blocksper = range(self.__blocksper) + for i, wheat in map(None, range(len(blocks)), blocks): + # it shouldn't matter which of the n blocks we add chaff to, so for + # ease of implementation, we'll just add them to the first count + # blocks + if i < count: + serial, data, mac = wheat + datasize = len(data) + macsize = len(mac) + addwheat = 1 + # add chaff to this block + for j in blocksper: + import sys + chaffdata = self._randnum(datasize) + chaffmac = self._randnum(macsize) + chaff = (serial, chaffdata, chaffmac) + # mix up the order, if the 5th bit is on then put the + # wheat on the list + if addwheat and bytes_to_long(self._randnum(16)) & 0x40: + chaffedblocks.append(wheat) + addwheat = 0 + chaffedblocks.append(chaff) + if addwheat: + chaffedblocks.append(wheat) + else: + # just add the wheat + chaffedblocks.append(wheat) + return chaffedblocks + + def _randnum(self, size): + # TBD: Not a very secure algorithm. + # TBD: size * 2 to work around possible bug in RandomPool + from Crypto.Util import randpool + import time + pool = randpool.RandomPool(size * 2) + while size > pool.entropy: + pass + + # we now have enough entropy in the pool to get size bytes of random + # data... well, probably + return pool.get_bytes(size) + + + +if __name__ == '__main__': + text = """\ +We hold these truths to be self-evident, that all men are created equal, that +they are endowed by their Creator with certain unalienable Rights, that among +these are Life, Liberty, and the pursuit of Happiness. That to secure these +rights, Governments are instituted among Men, deriving their just powers from +the consent of the governed. That whenever any Form of Government becomes +destructive of these ends, it is the Right of the People to alter or to +abolish it, and to institute new Government, laying its foundation on such +principles and organizing its powers in such form, as to them shall seem most +likely to effect their Safety and Happiness. +""" + print 'Original text:\n==========' + print text + print '==========' + + # first transform the text into packets + blocks = [] ; size = 40 + for i in range(0, len(text), size): + blocks.append( text[i:i+size] ) + + # now get MACs for all the text blocks. The key is obvious... + print 'Calculating MACs...' + from Crypto.Hash import HMAC, SHA + key = 'Jefferson' + macs = [HMAC.new(key, block, digestmod=SHA).digest() + for block in blocks] + + assert len(blocks) == len(macs) + + # put these into a form acceptable as input to the chaffing procedure + source = [] + m = map(None, range(len(blocks)), blocks, macs) + print m + for i, data, mac in m: + source.append((i, data, mac)) + + # now chaff these + print 'Adding chaff...' + c = Chaff(factor=0.5, blocksper=2) + chaffed = c.chaff(source) + + from base64 import encodestring + + # print the chaffed message blocks. meanwhile, separate the wheat from + # the chaff + + wheat = [] + print 'chaffed message blocks:' + for i, data, mac in chaffed: + # do the authentication + h = HMAC.new(key, data, digestmod=SHA) + pmac = h.digest() + if pmac == mac: + tag = '-->' + wheat.append(data) + else: + tag = ' ' + # base64 adds a trailing newline + print tag, '%3d' % i, \ + repr(data), encodestring(mac)[:-1] + + # now decode the message packets and check it against the original text + print 'Undigesting wheat...' + newtext = "".join(wheat) + if newtext == text: + print 'They match!' + else: + print 'They differ!' diff --git a/patches/gdata/Crypto/Protocol/__init__.py b/patches/gdata/Crypto/Protocol/__init__.py new file mode 100755 index 0000000..a6d68bc --- /dev/null +++ b/patches/gdata/Crypto/Protocol/__init__.py @@ -0,0 +1,17 @@ + +"""Cryptographic protocols + +Implements various cryptographic protocols. (Don't expect to find +network protocols here.) + +Crypto.Protocol.AllOrNothing Transforms a message into a set of message + blocks, such that the blocks can be + recombined to get the message back. + +Crypto.Protocol.Chaffing Takes a set of authenticated message blocks + (the wheat) and adds a number of + randomly generated blocks (the chaff). +""" + +__all__ = ['AllOrNothing', 'Chaffing'] +__revision__ = "$Id: __init__.py,v 1.4 2003/02/28 15:23:21 akuchling Exp $" diff --git a/patches/gdata/Crypto/PublicKey/DSA.py b/patches/gdata/Crypto/PublicKey/DSA.py new file mode 100755 index 0000000..7947b6f --- /dev/null +++ b/patches/gdata/Crypto/PublicKey/DSA.py @@ -0,0 +1,238 @@ + +# +# DSA.py : Digital Signature Algorithm +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: DSA.py,v 1.16 2004/05/06 12:52:54 akuchling Exp $" + +from Crypto.PublicKey.pubkey import * +from Crypto.Util import number +from Crypto.Util.number import bytes_to_long, long_to_bytes +from Crypto.Hash import SHA + +try: + from Crypto.PublicKey import _fastmath +except ImportError: + _fastmath = None + +class error (Exception): + pass + +def generateQ(randfunc): + S=randfunc(20) + hash1=SHA.new(S).digest() + hash2=SHA.new(long_to_bytes(bytes_to_long(S)+1)).digest() + q = bignum(0) + for i in range(0,20): + c=ord(hash1[i])^ord(hash2[i]) + if i==0: + c=c | 128 + if i==19: + c= c | 1 + q=q*256+c + while (not isPrime(q)): + q=q+2 + if pow(2,159L) < q < pow(2,160L): + return S, q + raise error, 'Bad q value generated' + +def generate(bits, randfunc, progress_func=None): + """generate(bits:int, randfunc:callable, progress_func:callable) + + Generate a DSA key of length 'bits', using 'randfunc' to get + random data and 'progress_func', if present, to display + the progress of the key generation. + """ + + if bits<160: + raise error, 'Key length <160 bits' + obj=DSAobj() + # Generate string S and prime q + if progress_func: + progress_func('p,q\n') + while (1): + S, obj.q = generateQ(randfunc) + n=(bits-1)/160 + C, N, V = 0, 2, {} + b=(obj.q >> 5) & 15 + powb=pow(bignum(2), b) + powL1=pow(bignum(2), bits-1) + while C<4096: + for k in range(0, n+1): + V[k]=bytes_to_long(SHA.new(S+str(N)+str(k)).digest()) + W=V[n] % powb + for k in range(n-1, -1, -1): + W=(W<<160L)+V[k] + X=W+powL1 + p=X-(X%(2*obj.q)-1) + if powL1<=p and isPrime(p): + break + C, N = C+1, N+n+1 + if C<4096: + break + if progress_func: + progress_func('4096 multiples failed\n') + + obj.p = p + power=(p-1)/obj.q + if progress_func: + progress_func('h,g\n') + while (1): + h=bytes_to_long(randfunc(bits)) % (p-1) + g=pow(h, power, p) + if 11: + break + obj.g=g + if progress_func: + progress_func('x,y\n') + while (1): + x=bytes_to_long(randfunc(20)) + if 0 < x < obj.q: + break + obj.x, obj.y = x, pow(g, x, p) + return obj + +def construct(tuple): + """construct(tuple:(long,long,long,long)|(long,long,long,long,long)):DSAobj + Construct a DSA object from a 4- or 5-tuple of numbers. + """ + obj=DSAobj() + if len(tuple) not in [4,5]: + raise error, 'argument for construct() wrong length' + for i in range(len(tuple)): + field = obj.keydata[i] + setattr(obj, field, tuple[i]) + return obj + +class DSAobj(pubkey): + keydata=['y', 'g', 'p', 'q', 'x'] + + def _encrypt(self, s, Kstr): + raise error, 'DSA algorithm cannot encrypt data' + + def _decrypt(self, s): + raise error, 'DSA algorithm cannot decrypt data' + + def _sign(self, M, K): + if (K<2 or self.q<=K): + raise error, 'K is not between 2 and q' + r=pow(self.g, K, self.p) % self.q + s=(inverse(K, self.q)*(M+self.x*r)) % self.q + return (r,s) + + def _verify(self, M, sig): + r, s = sig + if r<=0 or r>=self.q or s<=0 or s>=self.q: + return 0 + w=inverse(s, self.q) + u1, u2 = (M*w) % self.q, (r*w) % self.q + v1 = pow(self.g, u1, self.p) + v2 = pow(self.y, u2, self.p) + v = ((v1*v2) % self.p) + v = v % self.q + if v==r: + return 1 + return 0 + + def size(self): + "Return the maximum number of bits that can be handled by this key." + return number.size(self.p) - 1 + + def has_private(self): + """Return a Boolean denoting whether the object contains + private components.""" + if hasattr(self, 'x'): + return 1 + else: + return 0 + + def can_sign(self): + """Return a Boolean value recording whether this algorithm can generate signatures.""" + return 1 + + def can_encrypt(self): + """Return a Boolean value recording whether this algorithm can encrypt data.""" + return 0 + + def publickey(self): + """Return a new key object containing only the public information.""" + return construct((self.y, self.g, self.p, self.q)) + +object=DSAobj + +generate_py = generate +construct_py = construct + +class DSAobj_c(pubkey): + keydata = ['y', 'g', 'p', 'q', 'x'] + + def __init__(self, key): + self.key = key + + def __getattr__(self, attr): + if attr in self.keydata: + return getattr(self.key, attr) + else: + if self.__dict__.has_key(attr): + self.__dict__[attr] + else: + raise AttributeError, '%s instance has no attribute %s' % (self.__class__, attr) + + def __getstate__(self): + d = {} + for k in self.keydata: + if hasattr(self.key, k): + d[k]=getattr(self.key, k) + return d + + def __setstate__(self, state): + y,g,p,q = state['y'], state['g'], state['p'], state['q'] + if not state.has_key('x'): + self.key = _fastmath.dsa_construct(y,g,p,q) + else: + x = state['x'] + self.key = _fastmath.dsa_construct(y,g,p,q,x) + + def _sign(self, M, K): + return self.key._sign(M, K) + + def _verify(self, M, (r, s)): + return self.key._verify(M, r, s) + + def size(self): + return self.key.size() + + def has_private(self): + return self.key.has_private() + + def publickey(self): + return construct_c((self.key.y, self.key.g, self.key.p, self.key.q)) + + def can_sign(self): + return 1 + + def can_encrypt(self): + return 0 + +def generate_c(bits, randfunc, progress_func=None): + obj = generate_py(bits, randfunc, progress_func) + y,g,p,q,x = obj.y, obj.g, obj.p, obj.q, obj.x + return construct_c((y,g,p,q,x)) + +def construct_c(tuple): + key = apply(_fastmath.dsa_construct, tuple) + return DSAobj_c(key) + +if _fastmath: + #print "using C version of DSA" + generate = generate_c + construct = construct_c + error = _fastmath.error diff --git a/patches/gdata/Crypto/PublicKey/ElGamal.py b/patches/gdata/Crypto/PublicKey/ElGamal.py new file mode 100755 index 0000000..026881c --- /dev/null +++ b/patches/gdata/Crypto/PublicKey/ElGamal.py @@ -0,0 +1,132 @@ +# +# ElGamal.py : ElGamal encryption/decryption and signatures +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: ElGamal.py,v 1.9 2003/04/04 19:44:26 akuchling Exp $" + +from Crypto.PublicKey.pubkey import * +from Crypto.Util import number + +class error (Exception): + pass + +# Generate an ElGamal key with N bits +def generate(bits, randfunc, progress_func=None): + """generate(bits:int, randfunc:callable, progress_func:callable) + + Generate an ElGamal key of length 'bits', using 'randfunc' to get + random data and 'progress_func', if present, to display + the progress of the key generation. + """ + obj=ElGamalobj() + # Generate prime p + if progress_func: + progress_func('p\n') + obj.p=bignum(getPrime(bits, randfunc)) + # Generate random number g + if progress_func: + progress_func('g\n') + size=bits-1-(ord(randfunc(1)) & 63) # g will be from 1--64 bits smaller than p + if size<1: + size=bits-1 + while (1): + obj.g=bignum(getPrime(size, randfunc)) + if obj.g < obj.p: + break + size=(size+1) % bits + if size==0: + size=4 + # Generate random number x + if progress_func: + progress_func('x\n') + while (1): + size=bits-1-ord(randfunc(1)) # x will be from 1 to 256 bits smaller than p + if size>2: + break + while (1): + obj.x=bignum(getPrime(size, randfunc)) + if obj.x < obj.p: + break + size = (size+1) % bits + if size==0: + size=4 + if progress_func: + progress_func('y\n') + obj.y = pow(obj.g, obj.x, obj.p) + return obj + +def construct(tuple): + """construct(tuple:(long,long,long,long)|(long,long,long,long,long))) + : ElGamalobj + Construct an ElGamal key from a 3- or 4-tuple of numbers. + """ + + obj=ElGamalobj() + if len(tuple) not in [3,4]: + raise error, 'argument for construct() wrong length' + for i in range(len(tuple)): + field = obj.keydata[i] + setattr(obj, field, tuple[i]) + return obj + +class ElGamalobj(pubkey): + keydata=['p', 'g', 'y', 'x'] + + def _encrypt(self, M, K): + a=pow(self.g, K, self.p) + b=( M*pow(self.y, K, self.p) ) % self.p + return ( a,b ) + + def _decrypt(self, M): + if (not hasattr(self, 'x')): + raise error, 'Private key not available in this object' + ax=pow(M[0], self.x, self.p) + plaintext=(M[1] * inverse(ax, self.p ) ) % self.p + return plaintext + + def _sign(self, M, K): + if (not hasattr(self, 'x')): + raise error, 'Private key not available in this object' + p1=self.p-1 + if (GCD(K, p1)!=1): + raise error, 'Bad K value: GCD(K,p-1)!=1' + a=pow(self.g, K, self.p) + t=(M-self.x*a) % p1 + while t<0: t=t+p1 + b=(t*inverse(K, p1)) % p1 + return (a, b) + + def _verify(self, M, sig): + v1=pow(self.y, sig[0], self.p) + v1=(v1*pow(sig[0], sig[1], self.p)) % self.p + v2=pow(self.g, M, self.p) + if v1==v2: + return 1 + return 0 + + def size(self): + "Return the maximum number of bits that can be handled by this key." + return number.size(self.p) - 1 + + def has_private(self): + """Return a Boolean denoting whether the object contains + private components.""" + if hasattr(self, 'x'): + return 1 + else: + return 0 + + def publickey(self): + """Return a new key object containing only the public information.""" + return construct((self.p, self.g, self.y)) + + +object=ElGamalobj diff --git a/patches/gdata/Crypto/PublicKey/RSA.py b/patches/gdata/Crypto/PublicKey/RSA.py new file mode 100755 index 0000000..e0e877e --- /dev/null +++ b/patches/gdata/Crypto/PublicKey/RSA.py @@ -0,0 +1,256 @@ +# +# RSA.py : RSA encryption/decryption +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: RSA.py,v 1.20 2004/05/06 12:52:54 akuchling Exp $" + +from Crypto.PublicKey import pubkey +from Crypto.Util import number + +try: + from Crypto.PublicKey import _fastmath +except ImportError: + _fastmath = None + +class error (Exception): + pass + +def generate(bits, randfunc, progress_func=None): + """generate(bits:int, randfunc:callable, progress_func:callable) + + Generate an RSA key of length 'bits', using 'randfunc' to get + random data and 'progress_func', if present, to display + the progress of the key generation. + """ + obj=RSAobj() + + # Generate the prime factors of n + if progress_func: + progress_func('p,q\n') + p = q = 1L + while number.size(p*q) < bits: + p = pubkey.getPrime(bits/2, randfunc) + q = pubkey.getPrime(bits/2, randfunc) + + # p shall be smaller than q (for calc of u) + if p > q: + (p, q)=(q, p) + obj.p = p + obj.q = q + + if progress_func: + progress_func('u\n') + obj.u = pubkey.inverse(obj.p, obj.q) + obj.n = obj.p*obj.q + + obj.e = 65537L + if progress_func: + progress_func('d\n') + obj.d=pubkey.inverse(obj.e, (obj.p-1)*(obj.q-1)) + + assert bits <= 1+obj.size(), "Generated key is too small" + + return obj + +def construct(tuple): + """construct(tuple:(long,) : RSAobj + Construct an RSA object from a 2-, 3-, 5-, or 6-tuple of numbers. + """ + + obj=RSAobj() + if len(tuple) not in [2,3,5,6]: + raise error, 'argument for construct() wrong length' + for i in range(len(tuple)): + field = obj.keydata[i] + setattr(obj, field, tuple[i]) + if len(tuple) >= 5: + # Ensure p is smaller than q + if obj.p>obj.q: + (obj.p, obj.q)=(obj.q, obj.p) + + if len(tuple) == 5: + # u not supplied, so we're going to have to compute it. + obj.u=pubkey.inverse(obj.p, obj.q) + + return obj + +class RSAobj(pubkey.pubkey): + keydata = ['n', 'e', 'd', 'p', 'q', 'u'] + def _encrypt(self, plaintext, K=''): + if self.n<=plaintext: + raise error, 'Plaintext too large' + return (pow(plaintext, self.e, self.n),) + + def _decrypt(self, ciphertext): + if (not hasattr(self, 'd')): + raise error, 'Private key not available in this object' + if self.n<=ciphertext[0]: + raise error, 'Ciphertext too large' + return pow(ciphertext[0], self.d, self.n) + + def _sign(self, M, K=''): + return (self._decrypt((M,)),) + + def _verify(self, M, sig): + m2=self._encrypt(sig[0]) + if m2[0]==M: + return 1 + else: return 0 + + def _blind(self, M, B): + tmp = pow(B, self.e, self.n) + return (M * tmp) % self.n + + def _unblind(self, M, B): + tmp = pubkey.inverse(B, self.n) + return (M * tmp) % self.n + + def can_blind (self): + """can_blind() : bool + Return a Boolean value recording whether this algorithm can + blind data. (This does not imply that this + particular key object has the private information required to + to blind a message.) + """ + return 1 + + def size(self): + """size() : int + Return the maximum number of bits that can be handled by this key. + """ + return number.size(self.n) - 1 + + def has_private(self): + """has_private() : bool + Return a Boolean denoting whether the object contains + private components. + """ + if hasattr(self, 'd'): + return 1 + else: return 0 + + def publickey(self): + """publickey(): RSAobj + Return a new key object containing only the public key information. + """ + return construct((self.n, self.e)) + +class RSAobj_c(pubkey.pubkey): + keydata = ['n', 'e', 'd', 'p', 'q', 'u'] + + def __init__(self, key): + self.key = key + + def __getattr__(self, attr): + if attr in self.keydata: + return getattr(self.key, attr) + else: + if self.__dict__.has_key(attr): + self.__dict__[attr] + else: + raise AttributeError, '%s instance has no attribute %s' % (self.__class__, attr) + + def __getstate__(self): + d = {} + for k in self.keydata: + if hasattr(self.key, k): + d[k]=getattr(self.key, k) + return d + + def __setstate__(self, state): + n,e = state['n'], state['e'] + if not state.has_key('d'): + self.key = _fastmath.rsa_construct(n,e) + else: + d = state['d'] + if not state.has_key('q'): + self.key = _fastmath.rsa_construct(n,e,d) + else: + p, q, u = state['p'], state['q'], state['u'] + self.key = _fastmath.rsa_construct(n,e,d,p,q,u) + + def _encrypt(self, plain, K): + return (self.key._encrypt(plain),) + + def _decrypt(self, cipher): + return self.key._decrypt(cipher[0]) + + def _sign(self, M, K): + return (self.key._sign(M),) + + def _verify(self, M, sig): + return self.key._verify(M, sig[0]) + + def _blind(self, M, B): + return self.key._blind(M, B) + + def _unblind(self, M, B): + return self.key._unblind(M, B) + + def can_blind (self): + return 1 + + def size(self): + return self.key.size() + + def has_private(self): + return self.key.has_private() + + def publickey(self): + return construct_c((self.key.n, self.key.e)) + +def generate_c(bits, randfunc, progress_func = None): + # Generate the prime factors of n + if progress_func: + progress_func('p,q\n') + + p = q = 1L + while number.size(p*q) < bits: + p = pubkey.getPrime(bits/2, randfunc) + q = pubkey.getPrime(bits/2, randfunc) + + # p shall be smaller than q (for calc of u) + if p > q: + (p, q)=(q, p) + if progress_func: + progress_func('u\n') + u=pubkey.inverse(p, q) + n=p*q + + e = 65537L + if progress_func: + progress_func('d\n') + d=pubkey.inverse(e, (p-1)*(q-1)) + key = _fastmath.rsa_construct(n,e,d,p,q,u) + obj = RSAobj_c(key) + +## print p +## print q +## print number.size(p), number.size(q), number.size(q*p), +## print obj.size(), bits + assert bits <= 1+obj.size(), "Generated key is too small" + return obj + + +def construct_c(tuple): + key = apply(_fastmath.rsa_construct, tuple) + return RSAobj_c(key) + +object = RSAobj + +generate_py = generate +construct_py = construct + +if _fastmath: + #print "using C version of RSA" + generate = generate_c + construct = construct_c + error = _fastmath.error diff --git a/patches/gdata/Crypto/PublicKey/__init__.py b/patches/gdata/Crypto/PublicKey/__init__.py new file mode 100755 index 0000000..ad1c80c --- /dev/null +++ b/patches/gdata/Crypto/PublicKey/__init__.py @@ -0,0 +1,17 @@ +"""Public-key encryption and signature algorithms. + +Public-key encryption uses two different keys, one for encryption and +one for decryption. The encryption key can be made public, and the +decryption key is kept private. Many public-key algorithms can also +be used to sign messages, and some can *only* be used for signatures. + +Crypto.PublicKey.DSA Digital Signature Algorithm. (Signature only) +Crypto.PublicKey.ElGamal (Signing and encryption) +Crypto.PublicKey.RSA (Signing, encryption, and blinding) +Crypto.PublicKey.qNEW (Signature only) + +""" + +__all__ = ['RSA', 'DSA', 'ElGamal', 'qNEW'] +__revision__ = "$Id: __init__.py,v 1.4 2003/04/03 20:27:13 akuchling Exp $" + diff --git a/patches/gdata/Crypto/PublicKey/pubkey.py b/patches/gdata/Crypto/PublicKey/pubkey.py new file mode 100755 index 0000000..5c75c3e --- /dev/null +++ b/patches/gdata/Crypto/PublicKey/pubkey.py @@ -0,0 +1,172 @@ +# +# pubkey.py : Internal functions for public key operations +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: pubkey.py,v 1.11 2003/04/03 20:36:14 akuchling Exp $" + +import types, warnings +from Crypto.Util.number import * + +# Basic public key class +class pubkey: + def __init__(self): + pass + + def __getstate__(self): + """To keep key objects platform-independent, the key data is + converted to standard Python long integers before being + written out. It will then be reconverted as necessary on + restoration.""" + d=self.__dict__ + for key in self.keydata: + if d.has_key(key): d[key]=long(d[key]) + return d + + def __setstate__(self, d): + """On unpickling a key object, the key data is converted to the big +number representation being used, whether that is Python long +integers, MPZ objects, or whatever.""" + for key in self.keydata: + if d.has_key(key): self.__dict__[key]=bignum(d[key]) + + def encrypt(self, plaintext, K): + """encrypt(plaintext:string|long, K:string|long) : tuple + Encrypt the string or integer plaintext. K is a random + parameter required by some algorithms. + """ + wasString=0 + if isinstance(plaintext, types.StringType): + plaintext=bytes_to_long(plaintext) ; wasString=1 + if isinstance(K, types.StringType): + K=bytes_to_long(K) + ciphertext=self._encrypt(plaintext, K) + if wasString: return tuple(map(long_to_bytes, ciphertext)) + else: return ciphertext + + def decrypt(self, ciphertext): + """decrypt(ciphertext:tuple|string|long): string + Decrypt 'ciphertext' using this key. + """ + wasString=0 + if not isinstance(ciphertext, types.TupleType): + ciphertext=(ciphertext,) + if isinstance(ciphertext[0], types.StringType): + ciphertext=tuple(map(bytes_to_long, ciphertext)) ; wasString=1 + plaintext=self._decrypt(ciphertext) + if wasString: return long_to_bytes(plaintext) + else: return plaintext + + def sign(self, M, K): + """sign(M : string|long, K:string|long) : tuple + Return a tuple containing the signature for the message M. + K is a random parameter required by some algorithms. + """ + if (not self.has_private()): + raise error, 'Private key not available in this object' + if isinstance(M, types.StringType): M=bytes_to_long(M) + if isinstance(K, types.StringType): K=bytes_to_long(K) + return self._sign(M, K) + + def verify (self, M, signature): + """verify(M:string|long, signature:tuple) : bool + Verify that the signature is valid for the message M; + returns true if the signature checks out. + """ + if isinstance(M, types.StringType): M=bytes_to_long(M) + return self._verify(M, signature) + + # alias to compensate for the old validate() name + def validate (self, M, signature): + warnings.warn("validate() method name is obsolete; use verify()", + DeprecationWarning) + + def blind(self, M, B): + """blind(M : string|long, B : string|long) : string|long + Blind message M using blinding factor B. + """ + wasString=0 + if isinstance(M, types.StringType): + M=bytes_to_long(M) ; wasString=1 + if isinstance(B, types.StringType): B=bytes_to_long(B) + blindedmessage=self._blind(M, B) + if wasString: return long_to_bytes(blindedmessage) + else: return blindedmessage + + def unblind(self, M, B): + """unblind(M : string|long, B : string|long) : string|long + Unblind message M using blinding factor B. + """ + wasString=0 + if isinstance(M, types.StringType): + M=bytes_to_long(M) ; wasString=1 + if isinstance(B, types.StringType): B=bytes_to_long(B) + unblindedmessage=self._unblind(M, B) + if wasString: return long_to_bytes(unblindedmessage) + else: return unblindedmessage + + + # The following methods will usually be left alone, except for + # signature-only algorithms. They both return Boolean values + # recording whether this key's algorithm can sign and encrypt. + def can_sign (self): + """can_sign() : bool + Return a Boolean value recording whether this algorithm can + generate signatures. (This does not imply that this + particular key object has the private information required to + to generate a signature.) + """ + return 1 + + def can_encrypt (self): + """can_encrypt() : bool + Return a Boolean value recording whether this algorithm can + encrypt data. (This does not imply that this + particular key object has the private information required to + to decrypt a message.) + """ + return 1 + + def can_blind (self): + """can_blind() : bool + Return a Boolean value recording whether this algorithm can + blind data. (This does not imply that this + particular key object has the private information required to + to blind a message.) + """ + return 0 + + # The following methods will certainly be overridden by + # subclasses. + + def size (self): + """size() : int + Return the maximum number of bits that can be handled by this key. + """ + return 0 + + def has_private (self): + """has_private() : bool + Return a Boolean denoting whether the object contains + private components. + """ + return 0 + + def publickey (self): + """publickey(): object + Return a new key object containing only the public information. + """ + return self + + def __eq__ (self, other): + """__eq__(other): 0, 1 + Compare us to other for equality. + """ + return self.__getstate__() == other.__getstate__() diff --git a/patches/gdata/Crypto/PublicKey/qNEW.py b/patches/gdata/Crypto/PublicKey/qNEW.py new file mode 100755 index 0000000..65f8ae3 --- /dev/null +++ b/patches/gdata/Crypto/PublicKey/qNEW.py @@ -0,0 +1,170 @@ +# +# qNEW.py : The q-NEW signature algorithm. +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: qNEW.py,v 1.8 2003/04/04 15:13:35 akuchling Exp $" + +from Crypto.PublicKey import pubkey +from Crypto.Util.number import * +from Crypto.Hash import SHA + +class error (Exception): + pass + +HASHBITS = 160 # Size of SHA digests + +def generate(bits, randfunc, progress_func=None): + """generate(bits:int, randfunc:callable, progress_func:callable) + + Generate a qNEW key of length 'bits', using 'randfunc' to get + random data and 'progress_func', if present, to display + the progress of the key generation. + """ + obj=qNEWobj() + + # Generate prime numbers p and q. q is a 160-bit prime + # number. p is another prime number (the modulus) whose bit + # size is chosen by the caller, and is generated so that p-1 + # is a multiple of q. + # + # Note that only a single seed is used to + # generate p and q; if someone generates a key for you, you can + # use the seed to duplicate the key generation. This can + # protect you from someone generating values of p,q that have + # some special form that's easy to break. + if progress_func: + progress_func('p,q\n') + while (1): + obj.q = getPrime(160, randfunc) + # assert pow(2, 159L)1. g is kept; h can be discarded. + if progress_func: + progress_func('h,g\n') + while (1): + h=bytes_to_long(randfunc(bits)) % (p-1) + g=pow(h, power, p) + if 11: + break + obj.g=g + + # x is the private key information, and is + # just a random number between 0 and q. + # y=g**x mod p, and is part of the public information. + if progress_func: + progress_func('x,y\n') + while (1): + x=bytes_to_long(randfunc(20)) + if 0 < x < obj.q: + break + obj.x, obj.y=x, pow(g, x, p) + + return obj + +# Construct a qNEW object +def construct(tuple): + """construct(tuple:(long,long,long,long)|(long,long,long,long,long) + Construct a qNEW object from a 4- or 5-tuple of numbers. + """ + obj=qNEWobj() + if len(tuple) not in [4,5]: + raise error, 'argument for construct() wrong length' + for i in range(len(tuple)): + field = obj.keydata[i] + setattr(obj, field, tuple[i]) + return obj + +class qNEWobj(pubkey.pubkey): + keydata=['p', 'q', 'g', 'y', 'x'] + + def _sign(self, M, K=''): + if (self.q<=K): + raise error, 'K is greater than q' + if M<0: + raise error, 'Illegal value of M (<0)' + if M>=pow(2,161L): + raise error, 'Illegal value of M (too large)' + r=pow(self.g, K, self.p) % self.q + s=(K- (r*M*self.x % self.q)) % self.q + return (r,s) + def _verify(self, M, sig): + r, s = sig + if r<=0 or r>=self.q or s<=0 or s>=self.q: + return 0 + if M<0: + raise error, 'Illegal value of M (<0)' + if M<=0 or M>=pow(2,161L): + return 0 + v1 = pow(self.g, s, self.p) + v2 = pow(self.y, M*r, self.p) + v = ((v1*v2) % self.p) + v = v % self.q + if v==r: + return 1 + return 0 + + def size(self): + "Return the maximum number of bits that can be handled by this key." + return 160 + + def has_private(self): + """Return a Boolean denoting whether the object contains + private components.""" + return hasattr(self, 'x') + + def can_sign(self): + """Return a Boolean value recording whether this algorithm can generate signatures.""" + return 1 + + def can_encrypt(self): + """Return a Boolean value recording whether this algorithm can encrypt data.""" + return 0 + + def publickey(self): + """Return a new key object containing only the public information.""" + return construct((self.p, self.q, self.g, self.y)) + +object = qNEWobj + diff --git a/patches/gdata/Crypto/Util/RFC1751.py b/patches/gdata/Crypto/Util/RFC1751.py new file mode 100755 index 0000000..0a47952 --- /dev/null +++ b/patches/gdata/Crypto/Util/RFC1751.py @@ -0,0 +1,342 @@ +#!/usr/local/bin/python +# rfc1751.py : Converts between 128-bit strings and a human-readable +# sequence of words, as defined in RFC1751: "A Convention for +# Human-Readable 128-bit Keys", by Daniel L. McDonald. + +__revision__ = "$Id: RFC1751.py,v 1.6 2003/04/04 15:15:10 akuchling Exp $" + + +import string, binascii + +binary={0:'0000', 1:'0001', 2:'0010', 3:'0011', 4:'0100', 5:'0101', + 6:'0110', 7:'0111', 8:'1000', 9:'1001', 10:'1010', 11:'1011', + 12:'1100', 13:'1101', 14:'1110', 15:'1111'} + +def _key2bin(s): + "Convert a key into a string of binary digits" + kl=map(lambda x: ord(x), s) + kl=map(lambda x: binary[x/16]+binary[x&15], kl) + return ''.join(kl) + +def _extract(key, start, length): + """Extract a bitstring from a string of binary digits, and return its + numeric value.""" + k=key[start:start+length] + return reduce(lambda x,y: x*2+ord(y)-48, k, 0) + +def key_to_english (key): + """key_to_english(key:string) : string + Transform an arbitrary key into a string containing English words. + The key length must be a multiple of 8. + """ + english='' + for index in range(0, len(key), 8): # Loop over 8-byte subkeys + subkey=key[index:index+8] + # Compute the parity of the key + skbin=_key2bin(subkey) ; p=0 + for i in range(0, 64, 2): p=p+_extract(skbin, i, 2) + # Append parity bits to the subkey + skbin=_key2bin(subkey+chr((p<<6) & 255)) + for i in range(0, 64, 11): + english=english+wordlist[_extract(skbin, i, 11)]+' ' + + return english[:-1] # Remove the trailing space + +def english_to_key (str): + """english_to_key(string):string + Transform a string into a corresponding key. + The string must contain words separated by whitespace; the number + of words must be a multiple of 6. + """ + + L=string.split(string.upper(str)) ; key='' + for index in range(0, len(L), 6): + sublist=L[index:index+6] ; char=9*[0] ; bits=0 + for i in sublist: + index = wordlist.index(i) + shift = (8-(bits+11)%8) %8 + y = index << shift + cl, cc, cr = (y>>16), (y>>8)&0xff, y & 0xff + if (shift>5): + char[bits/8] = char[bits/8] | cl + char[bits/8+1] = char[bits/8+1] | cc + char[bits/8+2] = char[bits/8+2] | cr + elif shift>-3: + char[bits/8] = char[bits/8] | cc + char[bits/8+1] = char[bits/8+1] | cr + else: char[bits/8] = char[bits/8] | cr + bits=bits+11 + subkey=reduce(lambda x,y:x+chr(y), char, '') + + # Check the parity of the resulting key + skbin=_key2bin(subkey) + p=0 + for i in range(0, 64, 2): p=p+_extract(skbin, i, 2) + if (p&3) != _extract(skbin, 64, 2): + raise ValueError, "Parity error in resulting key" + key=key+subkey[0:8] + return key + +wordlist=[ "A", "ABE", "ACE", "ACT", "AD", "ADA", "ADD", + "AGO", "AID", "AIM", "AIR", "ALL", "ALP", "AM", "AMY", "AN", "ANA", + "AND", "ANN", "ANT", "ANY", "APE", "APS", "APT", "ARC", "ARE", "ARK", + "ARM", "ART", "AS", "ASH", "ASK", "AT", "ATE", "AUG", "AUK", "AVE", + "AWE", "AWK", "AWL", "AWN", "AX", "AYE", "BAD", "BAG", "BAH", "BAM", + "BAN", "BAR", "BAT", "BAY", "BE", "BED", "BEE", "BEG", "BEN", "BET", + "BEY", "BIB", "BID", "BIG", "BIN", "BIT", "BOB", "BOG", "BON", "BOO", + "BOP", "BOW", "BOY", "BUB", "BUD", "BUG", "BUM", "BUN", "BUS", "BUT", + "BUY", "BY", "BYE", "CAB", "CAL", "CAM", "CAN", "CAP", "CAR", "CAT", + "CAW", "COD", "COG", "COL", "CON", "COO", "COP", "COT", "COW", "COY", + "CRY", "CUB", "CUE", "CUP", "CUR", "CUT", "DAB", "DAD", "DAM", "DAN", + "DAR", "DAY", "DEE", "DEL", "DEN", "DES", "DEW", "DID", "DIE", "DIG", + "DIN", "DIP", "DO", "DOE", "DOG", "DON", "DOT", "DOW", "DRY", "DUB", + "DUD", "DUE", "DUG", "DUN", "EAR", "EAT", "ED", "EEL", "EGG", "EGO", + "ELI", "ELK", "ELM", "ELY", "EM", "END", "EST", "ETC", "EVA", "EVE", + "EWE", "EYE", "FAD", "FAN", "FAR", "FAT", "FAY", "FED", "FEE", "FEW", + "FIB", "FIG", "FIN", "FIR", "FIT", "FLO", "FLY", "FOE", "FOG", "FOR", + "FRY", "FUM", "FUN", "FUR", "GAB", "GAD", "GAG", "GAL", "GAM", "GAP", + "GAS", "GAY", "GEE", "GEL", "GEM", "GET", "GIG", "GIL", "GIN", "GO", + "GOT", "GUM", "GUN", "GUS", "GUT", "GUY", "GYM", "GYP", "HA", "HAD", + "HAL", "HAM", "HAN", "HAP", "HAS", "HAT", "HAW", "HAY", "HE", "HEM", + "HEN", "HER", "HEW", "HEY", "HI", "HID", "HIM", "HIP", "HIS", "HIT", + "HO", "HOB", "HOC", "HOE", "HOG", "HOP", "HOT", "HOW", "HUB", "HUE", + "HUG", "HUH", "HUM", "HUT", "I", "ICY", "IDA", "IF", "IKE", "ILL", + "INK", "INN", "IO", "ION", "IQ", "IRA", "IRE", "IRK", "IS", "IT", + "ITS", "IVY", "JAB", "JAG", "JAM", "JAN", "JAR", "JAW", "JAY", "JET", + "JIG", "JIM", "JO", "JOB", "JOE", "JOG", "JOT", "JOY", "JUG", "JUT", + "KAY", "KEG", "KEN", "KEY", "KID", "KIM", "KIN", "KIT", "LA", "LAB", + "LAC", "LAD", "LAG", "LAM", "LAP", "LAW", "LAY", "LEA", "LED", "LEE", + "LEG", "LEN", "LEO", "LET", "LEW", "LID", "LIE", "LIN", "LIP", "LIT", + "LO", "LOB", "LOG", "LOP", "LOS", "LOT", "LOU", "LOW", "LOY", "LUG", + "LYE", "MA", "MAC", "MAD", "MAE", "MAN", "MAO", "MAP", "MAT", "MAW", + "MAY", "ME", "MEG", "MEL", "MEN", "MET", "MEW", "MID", "MIN", "MIT", + "MOB", "MOD", "MOE", "MOO", "MOP", "MOS", "MOT", "MOW", "MUD", "MUG", + "MUM", "MY", "NAB", "NAG", "NAN", "NAP", "NAT", "NAY", "NE", "NED", + "NEE", "NET", "NEW", "NIB", "NIL", "NIP", "NIT", "NO", "NOB", "NOD", + "NON", "NOR", "NOT", "NOV", "NOW", "NU", "NUN", "NUT", "O", "OAF", + "OAK", "OAR", "OAT", "ODD", "ODE", "OF", "OFF", "OFT", "OH", "OIL", + "OK", "OLD", "ON", "ONE", "OR", "ORB", "ORE", "ORR", "OS", "OTT", + "OUR", "OUT", "OVA", "OW", "OWE", "OWL", "OWN", "OX", "PA", "PAD", + "PAL", "PAM", "PAN", "PAP", "PAR", "PAT", "PAW", "PAY", "PEA", "PEG", + "PEN", "PEP", "PER", "PET", "PEW", "PHI", "PI", "PIE", "PIN", "PIT", + "PLY", "PO", "POD", "POE", "POP", "POT", "POW", "PRO", "PRY", "PUB", + "PUG", "PUN", "PUP", "PUT", "QUO", "RAG", "RAM", "RAN", "RAP", "RAT", + "RAW", "RAY", "REB", "RED", "REP", "RET", "RIB", "RID", "RIG", "RIM", + "RIO", "RIP", "ROB", "ROD", "ROE", "RON", "ROT", "ROW", "ROY", "RUB", + "RUE", "RUG", "RUM", "RUN", "RYE", "SAC", "SAD", "SAG", "SAL", "SAM", + "SAN", "SAP", "SAT", "SAW", "SAY", "SEA", "SEC", "SEE", "SEN", "SET", + "SEW", "SHE", "SHY", "SIN", "SIP", "SIR", "SIS", "SIT", "SKI", "SKY", + "SLY", "SO", "SOB", "SOD", "SON", "SOP", "SOW", "SOY", "SPA", "SPY", + "SUB", "SUD", "SUE", "SUM", "SUN", "SUP", "TAB", "TAD", "TAG", "TAN", + "TAP", "TAR", "TEA", "TED", "TEE", "TEN", "THE", "THY", "TIC", "TIE", + "TIM", "TIN", "TIP", "TO", "TOE", "TOG", "TOM", "TON", "TOO", "TOP", + "TOW", "TOY", "TRY", "TUB", "TUG", "TUM", "TUN", "TWO", "UN", "UP", + "US", "USE", "VAN", "VAT", "VET", "VIE", "WAD", "WAG", "WAR", "WAS", + "WAY", "WE", "WEB", "WED", "WEE", "WET", "WHO", "WHY", "WIN", "WIT", + "WOK", "WON", "WOO", "WOW", "WRY", "WU", "YAM", "YAP", "YAW", "YE", + "YEA", "YES", "YET", "YOU", "ABED", "ABEL", "ABET", "ABLE", "ABUT", + "ACHE", "ACID", "ACME", "ACRE", "ACTA", "ACTS", "ADAM", "ADDS", + "ADEN", "AFAR", "AFRO", "AGEE", "AHEM", "AHOY", "AIDA", "AIDE", + "AIDS", "AIRY", "AJAR", "AKIN", "ALAN", "ALEC", "ALGA", "ALIA", + "ALLY", "ALMA", "ALOE", "ALSO", "ALTO", "ALUM", "ALVA", "AMEN", + "AMES", "AMID", "AMMO", "AMOK", "AMOS", "AMRA", "ANDY", "ANEW", + "ANNA", "ANNE", "ANTE", "ANTI", "AQUA", "ARAB", "ARCH", "AREA", + "ARGO", "ARID", "ARMY", "ARTS", "ARTY", "ASIA", "ASKS", "ATOM", + "AUNT", "AURA", "AUTO", "AVER", "AVID", "AVIS", "AVON", "AVOW", + "AWAY", "AWRY", "BABE", "BABY", "BACH", "BACK", "BADE", "BAIL", + "BAIT", "BAKE", "BALD", "BALE", "BALI", "BALK", "BALL", "BALM", + "BAND", "BANE", "BANG", "BANK", "BARB", "BARD", "BARE", "BARK", + "BARN", "BARR", "BASE", "BASH", "BASK", "BASS", "BATE", "BATH", + "BAWD", "BAWL", "BEAD", "BEAK", "BEAM", "BEAN", "BEAR", "BEAT", + "BEAU", "BECK", "BEEF", "BEEN", "BEER", + "BEET", "BELA", "BELL", "BELT", "BEND", "BENT", "BERG", "BERN", + "BERT", "BESS", "BEST", "BETA", "BETH", "BHOY", "BIAS", "BIDE", + "BIEN", "BILE", "BILK", "BILL", "BIND", "BING", "BIRD", "BITE", + "BITS", "BLAB", "BLAT", "BLED", "BLEW", "BLOB", "BLOC", "BLOT", + "BLOW", "BLUE", "BLUM", "BLUR", "BOAR", "BOAT", "BOCA", "BOCK", + "BODE", "BODY", "BOGY", "BOHR", "BOIL", "BOLD", "BOLO", "BOLT", + "BOMB", "BONA", "BOND", "BONE", "BONG", "BONN", "BONY", "BOOK", + "BOOM", "BOON", "BOOT", "BORE", "BORG", "BORN", "BOSE", "BOSS", + "BOTH", "BOUT", "BOWL", "BOYD", "BRAD", "BRAE", "BRAG", "BRAN", + "BRAY", "BRED", "BREW", "BRIG", "BRIM", "BROW", "BUCK", "BUDD", + "BUFF", "BULB", "BULK", "BULL", "BUNK", "BUNT", "BUOY", "BURG", + "BURL", "BURN", "BURR", "BURT", "BURY", "BUSH", "BUSS", "BUST", + "BUSY", "BYTE", "CADY", "CAFE", "CAGE", "CAIN", "CAKE", "CALF", + "CALL", "CALM", "CAME", "CANE", "CANT", "CARD", "CARE", "CARL", + "CARR", "CART", "CASE", "CASH", "CASK", "CAST", "CAVE", "CEIL", + "CELL", "CENT", "CERN", "CHAD", "CHAR", "CHAT", "CHAW", "CHEF", + "CHEN", "CHEW", "CHIC", "CHIN", "CHOU", "CHOW", "CHUB", "CHUG", + "CHUM", "CITE", "CITY", "CLAD", "CLAM", "CLAN", "CLAW", "CLAY", + "CLOD", "CLOG", "CLOT", "CLUB", "CLUE", "COAL", "COAT", "COCA", + "COCK", "COCO", "CODA", "CODE", "CODY", "COED", "COIL", "COIN", + "COKE", "COLA", "COLD", "COLT", "COMA", "COMB", "COME", "COOK", + "COOL", "COON", "COOT", "CORD", "CORE", "CORK", "CORN", "COST", + "COVE", "COWL", "CRAB", "CRAG", "CRAM", "CRAY", "CREW", "CRIB", + "CROW", "CRUD", "CUBA", "CUBE", "CUFF", "CULL", "CULT", "CUNY", + "CURB", "CURD", "CURE", "CURL", "CURT", "CUTS", "DADE", "DALE", + "DAME", "DANA", "DANE", "DANG", "DANK", "DARE", "DARK", "DARN", + "DART", "DASH", "DATA", "DATE", "DAVE", "DAVY", "DAWN", "DAYS", + "DEAD", "DEAF", "DEAL", "DEAN", "DEAR", "DEBT", "DECK", "DEED", + "DEEM", "DEER", "DEFT", "DEFY", "DELL", "DENT", "DENY", "DESK", + "DIAL", "DICE", "DIED", "DIET", "DIME", "DINE", "DING", "DINT", + "DIRE", "DIRT", "DISC", "DISH", "DISK", "DIVE", "DOCK", "DOES", + "DOLE", "DOLL", "DOLT", "DOME", "DONE", "DOOM", "DOOR", "DORA", + "DOSE", "DOTE", "DOUG", "DOUR", "DOVE", "DOWN", "DRAB", "DRAG", + "DRAM", "DRAW", "DREW", "DRUB", "DRUG", "DRUM", "DUAL", "DUCK", + "DUCT", "DUEL", "DUET", "DUKE", "DULL", "DUMB", "DUNE", "DUNK", + "DUSK", "DUST", "DUTY", "EACH", "EARL", "EARN", "EASE", "EAST", + "EASY", "EBEN", "ECHO", "EDDY", "EDEN", "EDGE", "EDGY", "EDIT", + "EDNA", "EGAN", "ELAN", "ELBA", "ELLA", "ELSE", "EMIL", "EMIT", + "EMMA", "ENDS", "ERIC", "EROS", "EVEN", "EVER", "EVIL", "EYED", + "FACE", "FACT", "FADE", "FAIL", "FAIN", "FAIR", "FAKE", "FALL", + "FAME", "FANG", "FARM", "FAST", "FATE", "FAWN", "FEAR", "FEAT", + "FEED", "FEEL", "FEET", "FELL", "FELT", "FEND", "FERN", "FEST", + "FEUD", "FIEF", "FIGS", "FILE", "FILL", "FILM", "FIND", "FINE", + "FINK", "FIRE", "FIRM", "FISH", "FISK", "FIST", "FITS", "FIVE", + "FLAG", "FLAK", "FLAM", "FLAT", "FLAW", "FLEA", "FLED", "FLEW", + "FLIT", "FLOC", "FLOG", "FLOW", "FLUB", "FLUE", "FOAL", "FOAM", + "FOGY", "FOIL", "FOLD", "FOLK", "FOND", "FONT", "FOOD", "FOOL", + "FOOT", "FORD", "FORE", "FORK", "FORM", "FORT", "FOSS", "FOUL", + "FOUR", "FOWL", "FRAU", "FRAY", "FRED", "FREE", "FRET", "FREY", + "FROG", "FROM", "FUEL", "FULL", "FUME", "FUND", "FUNK", "FURY", + "FUSE", "FUSS", "GAFF", "GAGE", "GAIL", "GAIN", "GAIT", "GALA", + "GALE", "GALL", "GALT", "GAME", "GANG", "GARB", "GARY", "GASH", + "GATE", "GAUL", "GAUR", "GAVE", "GAWK", "GEAR", "GELD", "GENE", + "GENT", "GERM", "GETS", "GIBE", "GIFT", "GILD", "GILL", "GILT", + "GINA", "GIRD", "GIRL", "GIST", "GIVE", "GLAD", "GLEE", "GLEN", + "GLIB", "GLOB", "GLOM", "GLOW", "GLUE", "GLUM", "GLUT", "GOAD", + "GOAL", "GOAT", "GOER", "GOES", "GOLD", "GOLF", "GONE", "GONG", + "GOOD", "GOOF", "GORE", "GORY", "GOSH", "GOUT", "GOWN", "GRAB", + "GRAD", "GRAY", "GREG", "GREW", "GREY", "GRID", "GRIM", "GRIN", + "GRIT", "GROW", "GRUB", "GULF", "GULL", "GUNK", "GURU", "GUSH", + "GUST", "GWEN", "GWYN", "HAAG", "HAAS", "HACK", "HAIL", "HAIR", + "HALE", "HALF", "HALL", "HALO", "HALT", "HAND", "HANG", "HANK", + "HANS", "HARD", "HARK", "HARM", "HART", "HASH", "HAST", "HATE", + "HATH", "HAUL", "HAVE", "HAWK", "HAYS", "HEAD", "HEAL", "HEAR", + "HEAT", "HEBE", "HECK", "HEED", "HEEL", "HEFT", "HELD", "HELL", + "HELM", "HERB", "HERD", "HERE", "HERO", "HERS", "HESS", "HEWN", + "HICK", "HIDE", "HIGH", "HIKE", "HILL", "HILT", "HIND", "HINT", + "HIRE", "HISS", "HIVE", "HOBO", "HOCK", "HOFF", "HOLD", "HOLE", + "HOLM", "HOLT", "HOME", "HONE", "HONK", "HOOD", "HOOF", "HOOK", + "HOOT", "HORN", "HOSE", "HOST", "HOUR", "HOVE", "HOWE", "HOWL", + "HOYT", "HUCK", "HUED", "HUFF", "HUGE", "HUGH", "HUGO", "HULK", + "HULL", "HUNK", "HUNT", "HURD", "HURL", "HURT", "HUSH", "HYDE", + "HYMN", "IBIS", "ICON", "IDEA", "IDLE", "IFFY", "INCA", "INCH", + "INTO", "IONS", "IOTA", "IOWA", "IRIS", "IRMA", "IRON", "ISLE", + "ITCH", "ITEM", "IVAN", "JACK", "JADE", "JAIL", "JAKE", "JANE", + "JAVA", "JEAN", "JEFF", "JERK", "JESS", "JEST", "JIBE", "JILL", + "JILT", "JIVE", "JOAN", "JOBS", "JOCK", "JOEL", "JOEY", "JOHN", + "JOIN", "JOKE", "JOLT", "JOVE", "JUDD", "JUDE", "JUDO", "JUDY", + "JUJU", "JUKE", "JULY", "JUNE", "JUNK", "JUNO", "JURY", "JUST", + "JUTE", "KAHN", "KALE", "KANE", "KANT", "KARL", "KATE", "KEEL", + "KEEN", "KENO", "KENT", "KERN", "KERR", "KEYS", "KICK", "KILL", + "KIND", "KING", "KIRK", "KISS", "KITE", "KLAN", "KNEE", "KNEW", + "KNIT", "KNOB", "KNOT", "KNOW", "KOCH", "KONG", "KUDO", "KURD", + "KURT", "KYLE", "LACE", "LACK", "LACY", "LADY", "LAID", "LAIN", + "LAIR", "LAKE", "LAMB", "LAME", "LAND", "LANE", "LANG", "LARD", + "LARK", "LASS", "LAST", "LATE", "LAUD", "LAVA", "LAWN", "LAWS", + "LAYS", "LEAD", "LEAF", "LEAK", "LEAN", "LEAR", "LEEK", "LEER", + "LEFT", "LEND", "LENS", "LENT", "LEON", "LESK", "LESS", "LEST", + "LETS", "LIAR", "LICE", "LICK", "LIED", "LIEN", "LIES", "LIEU", + "LIFE", "LIFT", "LIKE", "LILA", "LILT", "LILY", "LIMA", "LIMB", + "LIME", "LIND", "LINE", "LINK", "LINT", "LION", "LISA", "LIST", + "LIVE", "LOAD", "LOAF", "LOAM", "LOAN", "LOCK", "LOFT", "LOGE", + "LOIS", "LOLA", "LONE", "LONG", "LOOK", "LOON", "LOOT", "LORD", + "LORE", "LOSE", "LOSS", "LOST", "LOUD", "LOVE", "LOWE", "LUCK", + "LUCY", "LUGE", "LUKE", "LULU", "LUND", "LUNG", "LURA", "LURE", + "LURK", "LUSH", "LUST", "LYLE", "LYNN", "LYON", "LYRA", "MACE", + "MADE", "MAGI", "MAID", "MAIL", "MAIN", "MAKE", "MALE", "MALI", + "MALL", "MALT", "MANA", "MANN", "MANY", "MARC", "MARE", "MARK", + "MARS", "MART", "MARY", "MASH", "MASK", "MASS", "MAST", "MATE", + "MATH", "MAUL", "MAYO", "MEAD", "MEAL", "MEAN", "MEAT", "MEEK", + "MEET", "MELD", "MELT", "MEMO", "MEND", "MENU", "MERT", "MESH", + "MESS", "MICE", "MIKE", "MILD", "MILE", "MILK", "MILL", "MILT", + "MIMI", "MIND", "MINE", "MINI", "MINK", "MINT", "MIRE", "MISS", + "MIST", "MITE", "MITT", "MOAN", "MOAT", "MOCK", "MODE", "MOLD", + "MOLE", "MOLL", "MOLT", "MONA", "MONK", "MONT", "MOOD", "MOON", + "MOOR", "MOOT", "MORE", "MORN", "MORT", "MOSS", "MOST", "MOTH", + "MOVE", "MUCH", "MUCK", "MUDD", "MUFF", "MULE", "MULL", "MURK", + "MUSH", "MUST", "MUTE", "MUTT", "MYRA", "MYTH", "NAGY", "NAIL", + "NAIR", "NAME", "NARY", "NASH", "NAVE", "NAVY", "NEAL", "NEAR", + "NEAT", "NECK", "NEED", "NEIL", "NELL", "NEON", "NERO", "NESS", + "NEST", "NEWS", "NEWT", "NIBS", "NICE", "NICK", "NILE", "NINA", + "NINE", "NOAH", "NODE", "NOEL", "NOLL", "NONE", "NOOK", "NOON", + "NORM", "NOSE", "NOTE", "NOUN", "NOVA", "NUDE", "NULL", "NUMB", + "OATH", "OBEY", "OBOE", "ODIN", "OHIO", "OILY", "OINT", "OKAY", + "OLAF", "OLDY", "OLGA", "OLIN", "OMAN", "OMEN", "OMIT", "ONCE", + "ONES", "ONLY", "ONTO", "ONUS", "ORAL", "ORGY", "OSLO", "OTIS", + "OTTO", "OUCH", "OUST", "OUTS", "OVAL", "OVEN", "OVER", "OWLY", + "OWNS", "QUAD", "QUIT", "QUOD", "RACE", "RACK", "RACY", "RAFT", + "RAGE", "RAID", "RAIL", "RAIN", "RAKE", "RANK", "RANT", "RARE", + "RASH", "RATE", "RAVE", "RAYS", "READ", "REAL", "REAM", "REAR", + "RECK", "REED", "REEF", "REEK", "REEL", "REID", "REIN", "RENA", + "REND", "RENT", "REST", "RICE", "RICH", "RICK", "RIDE", "RIFT", + "RILL", "RIME", "RING", "RINK", "RISE", "RISK", "RITE", "ROAD", + "ROAM", "ROAR", "ROBE", "ROCK", "RODE", "ROIL", "ROLL", "ROME", + "ROOD", "ROOF", "ROOK", "ROOM", "ROOT", "ROSA", "ROSE", "ROSS", + "ROSY", "ROTH", "ROUT", "ROVE", "ROWE", "ROWS", "RUBE", "RUBY", + "RUDE", "RUDY", "RUIN", "RULE", "RUNG", "RUNS", "RUNT", "RUSE", + "RUSH", "RUSK", "RUSS", "RUST", "RUTH", "SACK", "SAFE", "SAGE", + "SAID", "SAIL", "SALE", "SALK", "SALT", "SAME", "SAND", "SANE", + "SANG", "SANK", "SARA", "SAUL", "SAVE", "SAYS", "SCAN", "SCAR", + "SCAT", "SCOT", "SEAL", "SEAM", "SEAR", "SEAT", "SEED", "SEEK", + "SEEM", "SEEN", "SEES", "SELF", "SELL", "SEND", "SENT", "SETS", + "SEWN", "SHAG", "SHAM", "SHAW", "SHAY", "SHED", "SHIM", "SHIN", + "SHOD", "SHOE", "SHOT", "SHOW", "SHUN", "SHUT", "SICK", "SIDE", + "SIFT", "SIGH", "SIGN", "SILK", "SILL", "SILO", "SILT", "SINE", + "SING", "SINK", "SIRE", "SITE", "SITS", "SITU", "SKAT", "SKEW", + "SKID", "SKIM", "SKIN", "SKIT", "SLAB", "SLAM", "SLAT", "SLAY", + "SLED", "SLEW", "SLID", "SLIM", "SLIT", "SLOB", "SLOG", "SLOT", + "SLOW", "SLUG", "SLUM", "SLUR", "SMOG", "SMUG", "SNAG", "SNOB", + "SNOW", "SNUB", "SNUG", "SOAK", "SOAR", "SOCK", "SODA", "SOFA", + "SOFT", "SOIL", "SOLD", "SOME", "SONG", "SOON", "SOOT", "SORE", + "SORT", "SOUL", "SOUR", "SOWN", "STAB", "STAG", "STAN", "STAR", + "STAY", "STEM", "STEW", "STIR", "STOW", "STUB", "STUN", "SUCH", + "SUDS", "SUIT", "SULK", "SUMS", "SUNG", "SUNK", "SURE", "SURF", + "SWAB", "SWAG", "SWAM", "SWAN", "SWAT", "SWAY", "SWIM", "SWUM", + "TACK", "TACT", "TAIL", "TAKE", "TALE", "TALK", "TALL", "TANK", + "TASK", "TATE", "TAUT", "TEAL", "TEAM", "TEAR", "TECH", "TEEM", + "TEEN", "TEET", "TELL", "TEND", "TENT", "TERM", "TERN", "TESS", + "TEST", "THAN", "THAT", "THEE", "THEM", "THEN", "THEY", "THIN", + "THIS", "THUD", "THUG", "TICK", "TIDE", "TIDY", "TIED", "TIER", + "TILE", "TILL", "TILT", "TIME", "TINA", "TINE", "TINT", "TINY", + "TIRE", "TOAD", "TOGO", "TOIL", "TOLD", "TOLL", "TONE", "TONG", + "TONY", "TOOK", "TOOL", "TOOT", "TORE", "TORN", "TOTE", "TOUR", + "TOUT", "TOWN", "TRAG", "TRAM", "TRAY", "TREE", "TREK", "TRIG", + "TRIM", "TRIO", "TROD", "TROT", "TROY", "TRUE", "TUBA", "TUBE", + "TUCK", "TUFT", "TUNA", "TUNE", "TUNG", "TURF", "TURN", "TUSK", + "TWIG", "TWIN", "TWIT", "ULAN", "UNIT", "URGE", "USED", "USER", + "USES", "UTAH", "VAIL", "VAIN", "VALE", "VARY", "VASE", "VAST", + "VEAL", "VEDA", "VEIL", "VEIN", "VEND", "VENT", "VERB", "VERY", + "VETO", "VICE", "VIEW", "VINE", "VISE", "VOID", "VOLT", "VOTE", + "WACK", "WADE", "WAGE", "WAIL", "WAIT", "WAKE", "WALE", "WALK", + "WALL", "WALT", "WAND", "WANE", "WANG", "WANT", "WARD", "WARM", + "WARN", "WART", "WASH", "WAST", "WATS", "WATT", "WAVE", "WAVY", + "WAYS", "WEAK", "WEAL", "WEAN", "WEAR", "WEED", "WEEK", "WEIR", + "WELD", "WELL", "WELT", "WENT", "WERE", "WERT", "WEST", "WHAM", + "WHAT", "WHEE", "WHEN", "WHET", "WHOA", "WHOM", "WICK", "WIFE", + "WILD", "WILL", "WIND", "WINE", "WING", "WINK", "WINO", "WIRE", + "WISE", "WISH", "WITH", "WOLF", "WONT", "WOOD", "WOOL", "WORD", + "WORE", "WORK", "WORM", "WORN", "WOVE", "WRIT", "WYNN", "YALE", + "YANG", "YANK", "YARD", "YARN", "YAWL", "YAWN", "YEAH", "YEAR", + "YELL", "YOGA", "YOKE" ] + +if __name__=='__main__': + data = [('EB33F77EE73D4053', 'TIDE ITCH SLOW REIN RULE MOT'), + ('CCAC2AED591056BE4F90FD441C534766', + 'RASH BUSH MILK LOOK BAD BRIM AVID GAFF BAIT ROT POD LOVE'), + ('EFF81F9BFBC65350920CDD7416DE8009', + 'TROD MUTE TAIL WARM CHAR KONG HAAG CITY BORE O TEAL AWL') + ] + + for key, words in data: + print 'Trying key', key + key=binascii.a2b_hex(key) + w2=key_to_english(key) + if w2!=words: + print 'key_to_english fails on key', repr(key), ', producing', str(w2) + k2=english_to_key(words) + if k2!=key: + print 'english_to_key fails on key', repr(key), ', producing', repr(k2) + + diff --git a/patches/gdata/Crypto/Util/__init__.py b/patches/gdata/Crypto/Util/__init__.py new file mode 100755 index 0000000..0d14768 --- /dev/null +++ b/patches/gdata/Crypto/Util/__init__.py @@ -0,0 +1,16 @@ +"""Miscellaneous modules + +Contains useful modules that don't belong into any of the +other Crypto.* subpackages. + +Crypto.Util.number Number-theoretic functions (primality testing, etc.) +Crypto.Util.randpool Random number generation +Crypto.Util.RFC1751 Converts between 128-bit keys and human-readable + strings of words. + +""" + +__all__ = ['randpool', 'RFC1751', 'number'] + +__revision__ = "$Id: __init__.py,v 1.4 2003/02/28 15:26:00 akuchling Exp $" + diff --git a/patches/gdata/Crypto/Util/number.py b/patches/gdata/Crypto/Util/number.py new file mode 100755 index 0000000..9d50563 --- /dev/null +++ b/patches/gdata/Crypto/Util/number.py @@ -0,0 +1,201 @@ +# +# number.py : Number-theoretic functions +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: number.py,v 1.13 2003/04/04 18:21:07 akuchling Exp $" + +bignum = long +try: + from Crypto.PublicKey import _fastmath +except ImportError: + _fastmath = None + +# Commented out and replaced with faster versions below +## def long2str(n): +## s='' +## while n>0: +## s=chr(n & 255)+s +## n=n>>8 +## return s + +## import types +## def str2long(s): +## if type(s)!=types.StringType: return s # Integers will be left alone +## return reduce(lambda x,y : x*256+ord(y), s, 0L) + +def size (N): + """size(N:long) : int + Returns the size of the number N in bits. + """ + bits, power = 0,1L + while N >= power: + bits += 1 + power = power << 1 + return bits + +def getRandomNumber(N, randfunc): + """getRandomNumber(N:int, randfunc:callable):long + Return an N-bit random number.""" + + S = randfunc(N/8) + odd_bits = N % 8 + if odd_bits != 0: + char = ord(randfunc(1)) >> (8-odd_bits) + S = chr(char) + S + value = bytes_to_long(S) + value |= 2L ** (N-1) # Ensure high bit is set + assert size(value) >= N + return value + +def GCD(x,y): + """GCD(x:long, y:long): long + Return the GCD of x and y. + """ + x = abs(x) ; y = abs(y) + while x > 0: + x, y = y % x, x + return y + +def inverse(u, v): + """inverse(u:long, u:long):long + Return the inverse of u mod v. + """ + u3, v3 = long(u), long(v) + u1, v1 = 1L, 0L + while v3 > 0: + q=u3 / v3 + u1, v1 = v1, u1 - v1*q + u3, v3 = v3, u3 - v3*q + while u1<0: + u1 = u1 + v + return u1 + +# Given a number of bits to generate and a random generation function, +# find a prime number of the appropriate size. + +def getPrime(N, randfunc): + """getPrime(N:int, randfunc:callable):long + Return a random N-bit prime number. + """ + + number=getRandomNumber(N, randfunc) | 1 + while (not isPrime(number)): + number=number+2 + return number + +def isPrime(N): + """isPrime(N:long):bool + Return true if N is prime. + """ + if N == 1: + return 0 + if N in sieve: + return 1 + for i in sieve: + if (N % i)==0: + return 0 + + # Use the accelerator if available + if _fastmath is not None: + return _fastmath.isPrime(N) + + # Compute the highest bit that's set in N + N1 = N - 1L + n = 1L + while (n> 1L + + # Rabin-Miller test + for c in sieve[:7]: + a=long(c) ; d=1L ; t=n + while (t): # Iterate over the bits in N1 + x=(d*d) % N + if x==1L and d!=1L and d!=N1: + return 0 # Square root of 1 found + if N1 & t: + d=(x*a) % N + else: + d=x + t = t >> 1L + if d!=1L: + return 0 + return 1 + +# Small primes used for checking primality; these are all the primes +# less than 256. This should be enough to eliminate most of the odd +# numbers before needing to do a Rabin-Miller test at all. + +sieve=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, + 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, + 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, + 197, 199, 211, 223, 227, 229, 233, 239, 241, 251] + +# Improved conversion functions contributed by Barry Warsaw, after +# careful benchmarking + +import struct + +def long_to_bytes(n, blocksize=0): + """long_to_bytes(n:long, blocksize:int) : string + Convert a long integer to a byte string. + + If optional blocksize is given and greater than zero, pad the front of the + byte string with binary zeros so that the length is a multiple of + blocksize. + """ + # after much testing, this algorithm was deemed to be the fastest + s = '' + n = long(n) + pack = struct.pack + while n > 0: + s = pack('>I', n & 0xffffffffL) + s + n = n >> 32 + # strip off leading zeros + for i in range(len(s)): + if s[i] != '\000': + break + else: + # only happens when n == 0 + s = '\000' + i = 0 + s = s[i:] + # add back some pad bytes. this could be done more efficiently w.r.t. the + # de-padding being done above, but sigh... + if blocksize > 0 and len(s) % blocksize: + s = (blocksize - len(s) % blocksize) * '\000' + s + return s + +def bytes_to_long(s): + """bytes_to_long(string) : long + Convert a byte string to a long integer. + + This is (essentially) the inverse of long_to_bytes(). + """ + acc = 0L + unpack = struct.unpack + length = len(s) + if length % 4: + extra = (4 - length % 4) + s = '\000' * extra + s + length = length + extra + for i in range(0, length, 4): + acc = (acc << 32) + unpack('>I', s[i:i+4])[0] + return acc + +# For backwards compatibility... +import warnings +def long2str(n, blocksize=0): + warnings.warn("long2str() has been replaced by long_to_bytes()") + return long_to_bytes(n, blocksize) +def str2long(s): + warnings.warn("str2long() has been replaced by bytes_to_long()") + return bytes_to_long(s) diff --git a/patches/gdata/Crypto/Util/randpool.py b/patches/gdata/Crypto/Util/randpool.py new file mode 100755 index 0000000..467501c --- /dev/null +++ b/patches/gdata/Crypto/Util/randpool.py @@ -0,0 +1,421 @@ +# +# randpool.py : Cryptographically strong random number generation +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: randpool.py,v 1.14 2004/05/06 12:56:54 akuchling Exp $" + +import time, array, types, warnings, os.path +from Crypto.Util.number import long_to_bytes +try: + import Crypto.Util.winrandom as winrandom +except: + winrandom = None + +STIRNUM = 3 + +class RandomPool: + """randpool.py : Cryptographically strong random number generation. + + The implementation here is similar to the one in PGP. To be + cryptographically strong, it must be difficult to determine the RNG's + output, whether in the future or the past. This is done by using + a cryptographic hash function to "stir" the random data. + + Entropy is gathered in the same fashion as PGP; the highest-resolution + clock around is read and the data is added to the random number pool. + A conservative estimate of the entropy is then kept. + + If a cryptographically secure random source is available (/dev/urandom + on many Unixes, Windows CryptGenRandom on most Windows), then use + it. + + Instance Attributes: + bits : int + Maximum size of pool in bits + bytes : int + Maximum size of pool in bytes + entropy : int + Number of bits of entropy in this pool. + + Methods: + add_event([s]) : add some entropy to the pool + get_bytes(int) : get N bytes of random data + randomize([N]) : get N bytes of randomness from external source + """ + + + def __init__(self, numbytes = 160, cipher=None, hash=None): + if hash is None: + from Crypto.Hash import SHA as hash + + # The cipher argument is vestigial; it was removed from + # version 1.1 so RandomPool would work even in the limited + # exportable subset of the code + if cipher is not None: + warnings.warn("'cipher' parameter is no longer used") + + if isinstance(hash, types.StringType): + # ugly hack to force __import__ to give us the end-path module + hash = __import__('Crypto.Hash.'+hash, + None, None, ['new']) + warnings.warn("'hash' parameter should now be a hashing module") + + self.bytes = numbytes + self.bits = self.bytes*8 + self.entropy = 0 + self._hash = hash + + # Construct an array to hold the random pool, + # initializing it to 0. + self._randpool = array.array('B', [0]*self.bytes) + + self._event1 = self._event2 = 0 + self._addPos = 0 + self._getPos = hash.digest_size + self._lastcounter=time.time() + self.__counter = 0 + + self._measureTickSize() # Estimate timer resolution + self._randomize() + + def _updateEntropyEstimate(self, nbits): + self.entropy += nbits + if self.entropy < 0: + self.entropy = 0 + elif self.entropy > self.bits: + self.entropy = self.bits + + def _randomize(self, N = 0, devname = '/dev/urandom'): + """_randomize(N, DEVNAME:device-filepath) + collects N bits of randomness from some entropy source (e.g., + /dev/urandom on Unixes that have it, Windows CryptoAPI + CryptGenRandom, etc) + DEVNAME is optional, defaults to /dev/urandom. You can change it + to /dev/random if you want to block till you get enough + entropy. + """ + data = '' + if N <= 0: + nbytes = int((self.bits - self.entropy)/8+0.5) + else: + nbytes = int(N/8+0.5) + if winrandom: + # Windows CryptGenRandom provides random data. + data = winrandom.new().get_bytes(nbytes) + elif os.path.exists(devname): + # Many OSes support a /dev/urandom device + try: + f=open(devname) + data=f.read(nbytes) + f.close() + except IOError, (num, msg): + if num!=2: raise IOError, (num, msg) + # If the file wasn't found, ignore the error + if data: + self._addBytes(data) + # Entropy estimate: The number of bits of + # data obtained from the random source. + self._updateEntropyEstimate(8*len(data)) + self.stir_n() # Wash the random pool + + def randomize(self, N=0): + """randomize(N:int) + use the class entropy source to get some entropy data. + This is overridden by KeyboardRandomize(). + """ + return self._randomize(N) + + def stir_n(self, N = STIRNUM): + """stir_n(N) + stirs the random pool N times + """ + for i in xrange(N): + self.stir() + + def stir (self, s = ''): + """stir(s:string) + Mix up the randomness pool. This will call add_event() twice, + but out of paranoia the entropy attribute will not be + increased. The optional 's' parameter is a string that will + be hashed with the randomness pool. + """ + + entropy=self.entropy # Save inital entropy value + self.add_event() + + # Loop over the randomness pool: hash its contents + # along with a counter, and add the resulting digest + # back into the pool. + for i in range(self.bytes / self._hash.digest_size): + h = self._hash.new(self._randpool) + h.update(str(self.__counter) + str(i) + str(self._addPos) + s) + self._addBytes( h.digest() ) + self.__counter = (self.__counter + 1) & 0xFFFFffffL + + self._addPos, self._getPos = 0, self._hash.digest_size + self.add_event() + + # Restore the old value of the entropy. + self.entropy=entropy + + + def get_bytes (self, N): + """get_bytes(N:int) : string + Return N bytes of random data. + """ + + s='' + i, pool = self._getPos, self._randpool + h=self._hash.new() + dsize = self._hash.digest_size + num = N + while num > 0: + h.update( self._randpool[i:i+dsize] ) + s = s + h.digest() + num = num - dsize + i = (i + dsize) % self.bytes + if i>1, bits+1 + if bits>8: bits=8 + + self._event1, self._event2 = event, self._event1 + + self._updateEntropyEstimate(bits) + return bits + + # Private functions + def _noise(self): + # Adds a bit of noise to the random pool, by adding in the + # current time and CPU usage of this process. + # The difference from the previous call to _noise() is taken + # in an effort to estimate the entropy. + t=time.time() + delta = (t - self._lastcounter)/self._ticksize*1e6 + self._lastcounter = t + self._addBytes(long_to_bytes(long(1000*time.time()))) + self._addBytes(long_to_bytes(long(1000*time.clock()))) + self._addBytes(long_to_bytes(long(1000*time.time()))) + self._addBytes(long_to_bytes(long(delta))) + + # Reduce delta to a maximum of 8 bits so we don't add too much + # entropy as a result of this call. + delta=delta % 0xff + return int(delta) + + + def _measureTickSize(self): + # _measureTickSize() tries to estimate a rough average of the + # resolution of time that you can see from Python. It does + # this by measuring the time 100 times, computing the delay + # between measurements, and taking the median of the resulting + # list. (We also hash all the times and add them to the pool) + interval = [None] * 100 + h = self._hash.new(`(id(self),id(interval))`) + + # Compute 100 differences + t=time.time() + h.update(`t`) + i = 0 + j = 0 + while i < 100: + t2=time.time() + h.update(`(i,j,t2)`) + j += 1 + delta=int((t2-t)*1e6) + if delta: + interval[i] = delta + i += 1 + t=t2 + + # Take the median of the array of intervals + interval.sort() + self._ticksize=interval[len(interval)/2] + h.update(`(interval,self._ticksize)`) + # mix in the measurement times and wash the random pool + self.stir(h.digest()) + + def _addBytes(self, s): + "XOR the contents of the string S into the random pool" + i, pool = self._addPos, self._randpool + for j in range(0, len(s)): + pool[i]=pool[i] ^ ord(s[j]) + i=(i+1) % self.bytes + self._addPos = i + + # Deprecated method names: remove in PCT 2.1 or later. + def getBytes(self, N): + warnings.warn("getBytes() method replaced by get_bytes()", + DeprecationWarning) + return self.get_bytes(N) + + def addEvent (self, event, s=""): + warnings.warn("addEvent() method replaced by add_event()", + DeprecationWarning) + return self.add_event(s + str(event)) + +class PersistentRandomPool (RandomPool): + def __init__ (self, filename=None, *args, **kwargs): + RandomPool.__init__(self, *args, **kwargs) + self.filename = filename + if filename: + try: + # the time taken to open and read the file might have + # a little disk variability, modulo disk/kernel caching... + f=open(filename, 'rb') + self.add_event() + data = f.read() + self.add_event() + # mix in the data from the file and wash the random pool + self.stir(data) + f.close() + except IOError: + # Oh, well; the file doesn't exist or is unreadable, so + # we'll just ignore it. + pass + + def save(self): + if self.filename == "": + raise ValueError, "No filename set for this object" + # wash the random pool before save, provides some forward secrecy for + # old values of the pool. + self.stir_n() + f=open(self.filename, 'wb') + self.add_event() + f.write(self._randpool.tostring()) + f.close() + self.add_event() + # wash the pool again, provide some protection for future values + self.stir() + +# non-echoing Windows keyboard entry +_kb = 0 +if not _kb: + try: + import msvcrt + class KeyboardEntry: + def getch(self): + c = msvcrt.getch() + if c in ('\000', '\xe0'): + # function key + c += msvcrt.getch() + return c + def close(self, delay = 0): + if delay: + time.sleep(delay) + while msvcrt.kbhit(): + msvcrt.getch() + _kb = 1 + except: + pass + +# non-echoing Posix keyboard entry +if not _kb: + try: + import termios + class KeyboardEntry: + def __init__(self, fd = 0): + self._fd = fd + self._old = termios.tcgetattr(fd) + new = termios.tcgetattr(fd) + new[3]=new[3] & ~termios.ICANON & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, new) + def getch(self): + termios.tcflush(0, termios.TCIFLUSH) # XXX Leave this in? + return os.read(self._fd, 1) + def close(self, delay = 0): + if delay: + time.sleep(delay) + termios.tcflush(self._fd, termios.TCIFLUSH) + termios.tcsetattr(self._fd, termios.TCSAFLUSH, self._old) + _kb = 1 + except: + pass + +class KeyboardRandomPool (PersistentRandomPool): + def __init__(self, *args, **kwargs): + PersistentRandomPool.__init__(self, *args, **kwargs) + + def randomize(self, N = 0): + "Adds N bits of entropy to random pool. If N is 0, fill up pool." + import os, string, time + if N <= 0: + bits = self.bits - self.entropy + else: + bits = N*8 + if bits == 0: + return + print bits,'bits of entropy are now required. Please type on the keyboard' + print 'until enough randomness has been accumulated.' + kb = KeyboardEntry() + s='' # We'll save the characters typed and add them to the pool. + hash = self._hash + e = 0 + try: + while e < bits: + temp=str(bits-e).rjust(6) + os.write(1, temp) + s=s+kb.getch() + e += self.add_event(s) + os.write(1, 6*chr(8)) + self.add_event(s+hash.new(s).digest() ) + finally: + kb.close() + print '\n\007 Enough. Please wait a moment.\n' + self.stir_n() # wash the random pool. + kb.close(4) + +if __name__ == '__main__': + pool = RandomPool() + print 'random pool entropy', pool.entropy, 'bits' + pool.add_event('something') + print `pool.get_bytes(100)` + import tempfile, os + fname = tempfile.mktemp() + pool = KeyboardRandomPool(filename=fname) + print 'keyboard random pool entropy', pool.entropy, 'bits' + pool.randomize() + print 'keyboard random pool entropy', pool.entropy, 'bits' + pool.randomize(128) + pool.save() + saved = open(fname, 'rb').read() + print 'saved', `saved` + print 'pool ', `pool._randpool.tostring()` + newpool = PersistentRandomPool(fname) + print 'persistent random pool entropy', pool.entropy, 'bits' + os.remove(fname) diff --git a/patches/gdata/Crypto/Util/test.py b/patches/gdata/Crypto/Util/test.py new file mode 100755 index 0000000..7b23e9f --- /dev/null +++ b/patches/gdata/Crypto/Util/test.py @@ -0,0 +1,453 @@ +# +# test.py : Functions used for testing the modules +# +# Part of the Python Cryptography Toolkit +# +# Distribute and use freely; there are no restrictions on further +# dissemination and usage except those imposed by the laws of your +# country of residence. This software is provided "as is" without +# warranty of fitness for use or suitability for any purpose, express +# or implied. Use at your own risk or not at all. +# + +__revision__ = "$Id: test.py,v 1.16 2004/08/13 22:24:18 akuchling Exp $" + +import binascii +import string +import testdata + +from Crypto.Cipher import * + +def die(string): + import sys + print '***ERROR: ', string +# sys.exit(0) # Will default to continuing onward... + +def print_timing (size, delta, verbose): + if verbose: + if delta == 0: + print 'Unable to measure time -- elapsed time too small' + else: + print '%.2f K/sec' % (size/delta) + +def exerciseBlockCipher(cipher, verbose): + import string, time + try: + ciph = eval(cipher) + except NameError: + print cipher, 'module not available' + return None + print cipher+ ':' + str='1' # Build 128K of test data + for i in xrange(0, 17): + str=str+str + if ciph.key_size==0: ciph.key_size=16 + password = 'password12345678Extra text for password'[0:ciph.key_size] + IV = 'Test IV Test IV Test IV Test'[0:ciph.block_size] + + if verbose: print ' ECB mode:', + obj=ciph.new(password, ciph.MODE_ECB) + if obj.block_size != ciph.block_size: + die("Module and cipher object block_size don't match") + + text='1234567812345678'[0:ciph.block_size] + c=obj.encrypt(text) + if (obj.decrypt(c)!=text): die('Error encrypting "'+text+'"') + text='KuchlingKuchling'[0:ciph.block_size] + c=obj.encrypt(text) + if (obj.decrypt(c)!=text): die('Error encrypting "'+text+'"') + text='NotTodayNotEver!'[0:ciph.block_size] + c=obj.encrypt(text) + if (obj.decrypt(c)!=text): die('Error encrypting "'+text+'"') + + start=time.time() + s=obj.encrypt(str) + s2=obj.decrypt(s) + end=time.time() + if (str!=s2): + die('Error in resulting plaintext from ECB mode') + print_timing(256, end-start, verbose) + del obj + + if verbose: print ' CFB mode:', + obj1=ciph.new(password, ciph.MODE_CFB, IV) + obj2=ciph.new(password, ciph.MODE_CFB, IV) + start=time.time() + ciphertext=obj1.encrypt(str[0:65536]) + plaintext=obj2.decrypt(ciphertext) + end=time.time() + if (plaintext!=str[0:65536]): + die('Error in resulting plaintext from CFB mode') + print_timing(64, end-start, verbose) + del obj1, obj2 + + if verbose: print ' CBC mode:', + obj1=ciph.new(password, ciph.MODE_CBC, IV) + obj2=ciph.new(password, ciph.MODE_CBC, IV) + start=time.time() + ciphertext=obj1.encrypt(str) + plaintext=obj2.decrypt(ciphertext) + end=time.time() + if (plaintext!=str): + die('Error in resulting plaintext from CBC mode') + print_timing(256, end-start, verbose) + del obj1, obj2 + + if verbose: print ' PGP mode:', + obj1=ciph.new(password, ciph.MODE_PGP, IV) + obj2=ciph.new(password, ciph.MODE_PGP, IV) + start=time.time() + ciphertext=obj1.encrypt(str) + plaintext=obj2.decrypt(ciphertext) + end=time.time() + if (plaintext!=str): + die('Error in resulting plaintext from PGP mode') + print_timing(256, end-start, verbose) + del obj1, obj2 + + if verbose: print ' OFB mode:', + obj1=ciph.new(password, ciph.MODE_OFB, IV) + obj2=ciph.new(password, ciph.MODE_OFB, IV) + start=time.time() + ciphertext=obj1.encrypt(str) + plaintext=obj2.decrypt(ciphertext) + end=time.time() + if (plaintext!=str): + die('Error in resulting plaintext from OFB mode') + print_timing(256, end-start, verbose) + del obj1, obj2 + + def counter(length=ciph.block_size): + return length * 'a' + + if verbose: print ' CTR mode:', + obj1=ciph.new(password, ciph.MODE_CTR, counter=counter) + obj2=ciph.new(password, ciph.MODE_CTR, counter=counter) + start=time.time() + ciphertext=obj1.encrypt(str) + plaintext=obj2.decrypt(ciphertext) + end=time.time() + if (plaintext!=str): + die('Error in resulting plaintext from CTR mode') + print_timing(256, end-start, verbose) + del obj1, obj2 + + # Test the IV handling + if verbose: print ' Testing IV handling' + obj1=ciph.new(password, ciph.MODE_CBC, IV) + plaintext='Test'*(ciph.block_size/4)*3 + ciphertext1=obj1.encrypt(plaintext) + obj1.IV=IV + ciphertext2=obj1.encrypt(plaintext) + if ciphertext1!=ciphertext2: + die('Error in setting IV') + + # Test keyword arguments + obj1=ciph.new(key=password) + obj1=ciph.new(password, mode=ciph.MODE_CBC) + obj1=ciph.new(mode=ciph.MODE_CBC, key=password) + obj1=ciph.new(IV=IV, mode=ciph.MODE_CBC, key=password) + + return ciph + +def exerciseStreamCipher(cipher, verbose): + import string, time + try: + ciph = eval(cipher) + except (NameError): + print cipher, 'module not available' + return None + print cipher + ':', + str='1' # Build 128K of test data + for i in xrange(0, 17): + str=str+str + key_size = ciph.key_size or 16 + password = 'password12345678Extra text for password'[0:key_size] + + obj1=ciph.new(password) + obj2=ciph.new(password) + if obj1.block_size != ciph.block_size: + die("Module and cipher object block_size don't match") + if obj1.key_size != ciph.key_size: + die("Module and cipher object key_size don't match") + + text='1234567812345678Python' + c=obj1.encrypt(text) + if (obj2.decrypt(c)!=text): die('Error encrypting "'+text+'"') + text='B1FF I2 A R3A11Y |<00L D00D!!!!!' + c=obj1.encrypt(text) + if (obj2.decrypt(c)!=text): die('Error encrypting "'+text+'"') + text='SpamSpamSpamSpamSpamSpamSpamSpamSpam' + c=obj1.encrypt(text) + if (obj2.decrypt(c)!=text): die('Error encrypting "'+text+'"') + + start=time.time() + s=obj1.encrypt(str) + str=obj2.decrypt(s) + end=time.time() + print_timing(256, end-start, verbose) + del obj1, obj2 + + return ciph + +def TestStreamModules(args=['arc4', 'XOR'], verbose=1): + import sys, string + args=map(string.lower, args) + + if 'arc4' in args: + # Test ARC4 stream cipher + arc4=exerciseStreamCipher('ARC4', verbose) + if (arc4!=None): + for entry in testdata.arc4: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=arc4.new(key) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('ARC4 failed on entry '+`entry`) + + if 'xor' in args: + # Test XOR stream cipher + XOR=exerciseStreamCipher('XOR', verbose) + if (XOR!=None): + for entry in testdata.xor: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=XOR.new(key) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('XOR failed on entry '+`entry`) + + +def TestBlockModules(args=['aes', 'arc2', 'des', 'blowfish', 'cast', 'des3', + 'idea', 'rc5'], + verbose=1): + import string + args=map(string.lower, args) + if 'aes' in args: + ciph=exerciseBlockCipher('AES', verbose) # AES + if (ciph!=None): + if verbose: print ' Verifying against test suite...' + for entry in testdata.aes: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key, ciph.MODE_ECB) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('AES failed on entry '+`entry`) + for i in ciphertext: + if verbose: print hex(ord(i)), + if verbose: print + + for entry in testdata.aes_modes: + mode, key, plain, cipher, kw = entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key, mode, **kw) + obj2=ciph.new(key, mode, **kw) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('AES encrypt failed on entry '+`entry`) + for i in ciphertext: + if verbose: print hex(ord(i)), + if verbose: print + + plain2=obj2.decrypt(ciphertext) + if plain2!=plain: + die('AES decrypt failed on entry '+`entry`) + for i in plain2: + if verbose: print hex(ord(i)), + if verbose: print + + + if 'arc2' in args: + ciph=exerciseBlockCipher('ARC2', verbose) # Alleged RC2 + if (ciph!=None): + if verbose: print ' Verifying against test suite...' + for entry in testdata.arc2: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key, ciph.MODE_ECB) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('ARC2 failed on entry '+`entry`) + for i in ciphertext: + if verbose: print hex(ord(i)), + print + + if 'blowfish' in args: + ciph=exerciseBlockCipher('Blowfish',verbose)# Bruce Schneier's Blowfish cipher + if (ciph!=None): + if verbose: print ' Verifying against test suite...' + for entry in testdata.blowfish: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key, ciph.MODE_ECB) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('Blowfish failed on entry '+`entry`) + for i in ciphertext: + if verbose: print hex(ord(i)), + if verbose: print + + if 'cast' in args: + ciph=exerciseBlockCipher('CAST', verbose) # CAST-128 + if (ciph!=None): + if verbose: print ' Verifying against test suite...' + for entry in testdata.cast: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key, ciph.MODE_ECB) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('CAST failed on entry '+`entry`) + for i in ciphertext: + if verbose: print hex(ord(i)), + if verbose: print + + if 0: + # The full-maintenance test; it requires 4 million encryptions, + # and correspondingly is quite time-consuming. I've disabled + # it; it's faster to compile block/cast.c with -DTEST and run + # the resulting program. + a = b = '\x01\x23\x45\x67\x12\x34\x56\x78\x23\x45\x67\x89\x34\x56\x78\x9A' + + for i in range(0, 1000000): + obj = cast.new(b, cast.MODE_ECB) + a = obj.encrypt(a[:8]) + obj.encrypt(a[-8:]) + obj = cast.new(a, cast.MODE_ECB) + b = obj.encrypt(b[:8]) + obj.encrypt(b[-8:]) + + if a!="\xEE\xA9\xD0\xA2\x49\xFD\x3B\xA6\xB3\x43\x6F\xB8\x9D\x6D\xCA\x92": + if verbose: print 'CAST test failed: value of "a" doesn\'t match' + if b!="\xB2\xC9\x5E\xB0\x0C\x31\xAD\x71\x80\xAC\x05\xB8\xE8\x3D\x69\x6E": + if verbose: print 'CAST test failed: value of "b" doesn\'t match' + + if 'des' in args: + # Test/benchmark DES block cipher + des=exerciseBlockCipher('DES', verbose) + if (des!=None): + # Various tests taken from the DES library packaged with Kerberos V4 + obj=des.new(binascii.a2b_hex('0123456789abcdef'), des.MODE_ECB) + s=obj.encrypt('Now is t') + if (s!=binascii.a2b_hex('3fa40e8a984d4815')): + die('DES fails test 1') + obj=des.new(binascii.a2b_hex('08192a3b4c5d6e7f'), des.MODE_ECB) + s=obj.encrypt('\000\000\000\000\000\000\000\000') + if (s!=binascii.a2b_hex('25ddac3e96176467')): + die('DES fails test 2') + obj=des.new(binascii.a2b_hex('0123456789abcdef'), des.MODE_CBC, + binascii.a2b_hex('1234567890abcdef')) + s=obj.encrypt("Now is the time for all ") + if (s!=binascii.a2b_hex('e5c7cdde872bf27c43e934008c389c0f683788499a7c05f6')): + die('DES fails test 3') + obj=des.new(binascii.a2b_hex('0123456789abcdef'), des.MODE_CBC, + binascii.a2b_hex('fedcba9876543210')) + s=obj.encrypt("7654321 Now is the time for \000\000\000\000") + if (s!=binascii.a2b_hex("ccd173ffab2039f4acd8aefddfd8a1eb468e91157888ba681d269397f7fe62b4")): + die('DES fails test 4') + del obj,s + + # R. Rivest's test: see http://theory.lcs.mit.edu/~rivest/destest.txt + x=binascii.a2b_hex('9474B8E8C73BCA7D') + for i in range(0, 16): + obj=des.new(x, des.MODE_ECB) + if (i & 1): x=obj.decrypt(x) + else: x=obj.encrypt(x) + if x!=binascii.a2b_hex('1B1A2DDB4C642438'): + die("DES fails Rivest's test") + + if verbose: print ' Verifying against test suite...' + for entry in testdata.des: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=des.new(key, des.MODE_ECB) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('DES failed on entry '+`entry`) + for entry in testdata.des_cbc: + key, iv, plain, cipher=entry + key, iv, cipher=binascii.a2b_hex(key),binascii.a2b_hex(iv),binascii.a2b_hex(cipher) + obj1=des.new(key, des.MODE_CBC, iv) + obj2=des.new(key, des.MODE_CBC, iv) + ciphertext=obj1.encrypt(plain) + if (ciphertext!=cipher): + die('DES CBC mode failed on entry '+`entry`) + + if 'des3' in args: + ciph=exerciseBlockCipher('DES3', verbose) # Triple DES + if (ciph!=None): + if verbose: print ' Verifying against test suite...' + for entry in testdata.des3: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key, ciph.MODE_ECB) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('DES3 failed on entry '+`entry`) + for i in ciphertext: + if verbose: print hex(ord(i)), + if verbose: print + for entry in testdata.des3_cbc: + key, iv, plain, cipher=entry + key, iv, cipher=binascii.a2b_hex(key),binascii.a2b_hex(iv),binascii.a2b_hex(cipher) + obj1=ciph.new(key, ciph.MODE_CBC, iv) + obj2=ciph.new(key, ciph.MODE_CBC, iv) + ciphertext=obj1.encrypt(plain) + if (ciphertext!=cipher): + die('DES3 CBC mode failed on entry '+`entry`) + + if 'idea' in args: + ciph=exerciseBlockCipher('IDEA', verbose) # IDEA block cipher + if (ciph!=None): + if verbose: print ' Verifying against test suite...' + for entry in testdata.idea: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key, ciph.MODE_ECB) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('IDEA failed on entry '+`entry`) + + if 'rc5' in args: + # Ronald Rivest's RC5 algorithm + ciph=exerciseBlockCipher('RC5', verbose) + if (ciph!=None): + if verbose: print ' Verifying against test suite...' + for entry in testdata.rc5: + key,plain,cipher=entry + key=binascii.a2b_hex(key) + plain=binascii.a2b_hex(plain) + cipher=binascii.a2b_hex(cipher) + obj=ciph.new(key[4:], ciph.MODE_ECB, + version =ord(key[0]), + word_size=ord(key[1]), + rounds =ord(key[2]) ) + ciphertext=obj.encrypt(plain) + if (ciphertext!=cipher): + die('RC5 failed on entry '+`entry`) + for i in ciphertext: + if verbose: print hex(ord(i)), + if verbose: print + + + diff --git a/patches/gdata/Crypto/__init__.py b/patches/gdata/Crypto/__init__.py new file mode 100755 index 0000000..2324ae8 --- /dev/null +++ b/patches/gdata/Crypto/__init__.py @@ -0,0 +1,25 @@ + +"""Python Cryptography Toolkit + +A collection of cryptographic modules implementing various algorithms +and protocols. + +Subpackages: +Crypto.Cipher Secret-key encryption algorithms (AES, DES, ARC4) +Crypto.Hash Hashing algorithms (MD5, SHA, HMAC) +Crypto.Protocol Cryptographic protocols (Chaffing, all-or-nothing + transform). This package does not contain any + network protocols. +Crypto.PublicKey Public-key encryption and signature algorithms + (RSA, DSA) +Crypto.Util Various useful modules and functions (long-to-string + conversion, random number generation, number + theoretic functions) +""" + +__all__ = ['Cipher', 'Hash', 'Protocol', 'PublicKey', 'Util'] + +__version__ = '2.0.1' +__revision__ = "$Id: __init__.py,v 1.12 2005/06/14 01:20:22 akuchling Exp $" + + diff --git a/patches/gdata/Crypto/test.py b/patches/gdata/Crypto/test.py new file mode 100755 index 0000000..c5ed061 --- /dev/null +++ b/patches/gdata/Crypto/test.py @@ -0,0 +1,38 @@ +# +# Test script for the Python Cryptography Toolkit. +# + +__revision__ = "$Id: test.py,v 1.7 2002/07/11 14:31:19 akuchling Exp $" + +import os, sys + + +# Add the build directory to the front of sys.path +from distutils.util import get_platform +s = "build/lib.%s-%.3s" % (get_platform(), sys.version) +s = os.path.join(os.getcwd(), s) +sys.path.insert(0, s) +s = os.path.join(os.getcwd(), 'test') +sys.path.insert(0, s) + +from Crypto.Util import test + +args = sys.argv[1:] +quiet = "--quiet" in args +if quiet: args.remove('--quiet') + +if not quiet: + print '\nStream Ciphers:' + print '===============' + +if args: test.TestStreamModules(args, verbose= not quiet) +else: test.TestStreamModules(verbose= not quiet) + +if not quiet: + print '\nBlock Ciphers:' + print '==============' + +if args: test.TestBlockModules(args, verbose= not quiet) +else: test.TestBlockModules(verbose= not quiet) + + diff --git a/patches/gdata/__init__.py b/patches/gdata/__init__.py new file mode 100755 index 0000000..634889b --- /dev/null +++ b/patches/gdata/__init__.py @@ -0,0 +1,835 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 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. + + +"""Contains classes representing Google Data elements. + + Extends Atom classes to add Google Data specific elements. +""" + + +__author__ = 'j.s@google.com (Jeffrey Scudder)' + +import os +import atom +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 + + +# XML namespaces which are often used in GData entities. +GDATA_NAMESPACE = 'http://schemas.google.com/g/2005' +GDATA_TEMPLATE = '{http://schemas.google.com/g/2005}%s' +OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/' +OPENSEARCH_TEMPLATE = '{http://a9.com/-/spec/opensearchrss/1.0/}%s' +BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch' +GACL_NAMESPACE = 'http://schemas.google.com/acl/2007' +GACL_TEMPLATE = '{http://schemas.google.com/acl/2007}%s' + + +# Labels used in batch request entries to specify the desired CRUD operation. +BATCH_INSERT = 'insert' +BATCH_UPDATE = 'update' +BATCH_DELETE = 'delete' +BATCH_QUERY = 'query' + +class Error(Exception): + pass + + +class MissingRequiredParameters(Error): + pass + + +class MediaSource(object): + """GData Entries can refer to media sources, so this class provides a + place to store references to these objects along with some metadata. + """ + + def __init__(self, file_handle=None, content_type=None, content_length=None, + file_path=None, file_name=None): + """Creates an object of type MediaSource. + + Args: + file_handle: A file handle pointing to the file to be encapsulated in the + MediaSource + content_type: string The MIME type of the file. Required if a file_handle + is given. + content_length: int The size of the file. Required if a file_handle is + given. + file_path: string (optional) A full path name to the file. Used in + place of a file_handle. + file_name: string The name of the file without any path information. + Required if a file_handle is given. + """ + self.file_handle = file_handle + self.content_type = content_type + self.content_length = content_length + self.file_name = file_name + + if (file_handle is None and content_type is not None and + file_path is not None): + self.setFile(file_path, content_type) + + def setFile(self, file_name, content_type): + """A helper function which can create a file handle from a given filename + and set the content type and length all at once. + + Args: + file_name: string The path and file name to the file containing the media + content_type: string A MIME type representing the type of the media + """ + + self.file_handle = open(file_name, 'rb') + self.content_type = content_type + self.content_length = os.path.getsize(file_name) + self.file_name = os.path.basename(file_name) + + +class LinkFinder(atom.LinkFinder): + """An "interface" providing methods to find link elements + + GData Entry elements often contain multiple links which differ in the rel + attribute or content type. Often, developers are interested in a specific + type of link so this class provides methods to find specific classes of + links. + + This class is used as a mixin in GData entries. + """ + + def GetSelfLink(self): + """Find the first link with rel set to 'self' + + Returns: + An atom.Link or none if none of the links had rel equal to 'self' + """ + + for a_link in self.link: + if a_link.rel == 'self': + return a_link + return None + + def GetEditLink(self): + for a_link in self.link: + if a_link.rel == 'edit': + return a_link + return None + + def GetEditMediaLink(self): + """The Picasa API mistakenly returns media-edit rather than edit-media, but + this may change soon. + """ + for a_link in self.link: + if a_link.rel == 'edit-media': + return a_link + if a_link.rel == 'media-edit': + return a_link + return None + + def GetHtmlLink(self): + """Find the first link with rel of alternate and type of text/html + + Returns: + An atom.Link or None if no links matched + """ + for a_link in self.link: + if a_link.rel == 'alternate' and a_link.type == 'text/html': + return a_link + return None + + def GetPostLink(self): + """Get a link containing the POST target URL. + + The POST target URL is used to insert new entries. + + Returns: + A link object with a rel matching the POST type. + """ + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/g/2005#post': + return a_link + return None + + def GetAclLink(self): + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/acl/2007#accessControlList': + return a_link + return None + + def GetFeedLink(self): + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/g/2005#feed': + return a_link + return None + + def GetNextLink(self): + for a_link in self.link: + if a_link.rel == 'next': + return a_link + return None + + def GetPrevLink(self): + for a_link in self.link: + if a_link.rel == 'previous': + return a_link + return None + + +class TotalResults(atom.AtomBase): + """opensearch:TotalResults for a GData feed""" + + _tag = 'totalResults' + _namespace = OPENSEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def TotalResultsFromString(xml_string): + return atom.CreateClassFromXMLString(TotalResults, xml_string) + + +class StartIndex(atom.AtomBase): + """The opensearch:startIndex element in GData feed""" + + _tag = 'startIndex' + _namespace = OPENSEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def StartIndexFromString(xml_string): + return atom.CreateClassFromXMLString(StartIndex, xml_string) + + +class ItemsPerPage(atom.AtomBase): + """The opensearch:itemsPerPage element in GData feed""" + + _tag = 'itemsPerPage' + _namespace = OPENSEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ItemsPerPageFromString(xml_string): + return atom.CreateClassFromXMLString(ItemsPerPage, xml_string) + + +class ExtendedProperty(atom.AtomBase): + """The Google Data extendedProperty element. + + Used to store arbitrary key-value information specific to your + application. The value can either be a text string stored as an XML + attribute (.value), or an XML node (XmlBlob) as a child element. + + This element is used in the Google Calendar data API and the Google + Contacts data API. + """ + + _tag = 'extendedProperty' + _namespace = GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + _attributes['value'] = 'value' + + def __init__(self, name=None, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + def GetXmlBlobExtensionElement(self): + """Returns the XML blob as an atom.ExtensionElement. + + Returns: + An atom.ExtensionElement representing the blob's XML, or None if no + blob was set. + """ + if len(self.extension_elements) < 1: + return None + else: + return self.extension_elements[0] + + def GetXmlBlobString(self): + """Returns the XML blob as a string. + + Returns: + A string containing the blob's XML, or None if no blob was set. + """ + blob = self.GetXmlBlobExtensionElement() + if blob: + return blob.ToString() + return None + + def SetXmlBlob(self, blob): + """Sets the contents of the extendedProperty to XML as a child node. + + Since the extendedProperty is only allowed one child element as an XML + blob, setting the XML blob will erase any preexisting extension elements + in this object. + + Args: + blob: str, ElementTree Element or atom.ExtensionElement representing + the XML blob stored in the extendedProperty. + """ + # Erase any existing extension_elements, clears the child nodes from the + # extendedProperty. + self.extension_elements = [] + if isinstance(blob, atom.ExtensionElement): + self.extension_elements.append(blob) + elif ElementTree.iselement(blob): + self.extension_elements.append(atom._ExtensionElementFromElementTree( + blob)) + else: + self.extension_elements.append(atom.ExtensionElementFromString(blob)) + + +def ExtendedPropertyFromString(xml_string): + return atom.CreateClassFromXMLString(ExtendedProperty, xml_string) + + +class GDataEntry(atom.Entry, LinkFinder): + """Extends Atom Entry to provide data processing""" + + _tag = atom.Entry._tag + _namespace = atom.Entry._namespace + _children = atom.Entry._children.copy() + _attributes = atom.Entry._attributes.copy() + + def __GetId(self): + return self.__id + + # This method was created to strip the unwanted whitespace from the id's + # text node. + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def IsMedia(self): + """Determines whether or not an entry is a GData Media entry. + """ + if (self.GetEditMediaLink()): + return True + else: + return False + + def GetMediaURL(self): + """Returns the URL to the media content, if the entry is a media entry. + Otherwise returns None. + """ + if not self.IsMedia(): + return None + else: + return self.content.src + + +def GDataEntryFromString(xml_string): + """Creates a new GDataEntry instance given a string of XML.""" + return atom.CreateClassFromXMLString(GDataEntry, xml_string) + + +class GDataFeed(atom.Feed, LinkFinder): + """A Feed from a GData service""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = atom.Feed._children.copy() + _attributes = atom.Feed._attributes.copy() + _children['{%s}totalResults' % OPENSEARCH_NAMESPACE] = ('total_results', + TotalResults) + _children['{%s}startIndex' % OPENSEARCH_NAMESPACE] = ('start_index', + StartIndex) + _children['{%s}itemsPerPage' % OPENSEARCH_NAMESPACE] = ('items_per_page', + ItemsPerPage) + # Add a conversion rule for atom:entry to make it into a GData + # Entry. + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GDataEntry]) + + def __GetId(self): + return self.__id + + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def __GetGenerator(self): + return self.__generator + + def __SetGenerator(self, generator): + self.__generator = generator + if generator is not None: + self.__generator.text = generator.text.strip() + + generator = property(__GetGenerator, __SetGenerator) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, entry=None, + total_results=None, start_index=None, items_per_page=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for Source + + Args: + author: list (optional) A list of Author instances which belong to this + class. + category: list (optional) A list of Category instances + contributor: list (optional) A list on Contributor instances + generator: Generator (optional) + icon: Icon (optional) + id: Id (optional) The entry's Id element + link: list (optional) A list of Link instances + logo: Logo (optional) + rights: Rights (optional) The entry's Rights element + subtitle: Subtitle (optional) The entry's subtitle element + title: Title (optional) the entry's title element + updated: Updated (optional) the entry's updated element + entry: list (optional) A list of the Entry instances contained in the + feed. + text: String (optional) The text contents of the element. This is the + contents of the Entry's XML text node. + (Example: This is the text) + extension_elements: list (optional) A list of ExtensionElement instances + which are children of this element. + extension_attributes: dict (optional) A dictionary of strings which are + the values for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.generator = generator + self.icon = icon + self.id = atom_id + self.link = link or [] + self.logo = logo + self.rights = rights + self.subtitle = subtitle + self.title = title + self.updated = updated + self.entry = entry or [] + self.total_results = total_results + self.start_index = start_index + self.items_per_page = items_per_page + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def GDataFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GDataFeed, xml_string) + + +class BatchId(atom.AtomBase): + _tag = 'id' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + +def BatchIdFromString(xml_string): + return atom.CreateClassFromXMLString(BatchId, xml_string) + + +class BatchOperation(atom.AtomBase): + _tag = 'operation' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['type'] = 'type' + + def __init__(self, op_type=None, extension_elements=None, + extension_attributes=None, + text=None): + self.type = op_type + atom.AtomBase.__init__(self, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def BatchOperationFromString(xml_string): + return atom.CreateClassFromXMLString(BatchOperation, xml_string) + + +class BatchStatus(atom.AtomBase): + """The batch:status element present in a batch response entry. + + A status element contains the code (HTTP response code) and + reason as elements. In a single request these fields would + be part of the HTTP response, but in a batch request each + Entry operation has a corresponding Entry in the response + feed which includes status information. + + See http://code.google.com/apis/gdata/batch.html#Handling_Errors + """ + + _tag = 'status' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['code'] = 'code' + _attributes['reason'] = 'reason' + _attributes['content-type'] = 'content_type' + + def __init__(self, code=None, reason=None, content_type=None, + extension_elements=None, extension_attributes=None, text=None): + self.code = code + self.reason = reason + self.content_type = content_type + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def BatchStatusFromString(xml_string): + return atom.CreateClassFromXMLString(BatchStatus, xml_string) + + +class BatchEntry(GDataEntry): + """An atom:entry for use in batch requests. + + The BatchEntry contains additional members to specify the operation to be + performed on this entry and a batch ID so that the server can reference + individual operations in the response feed. For more information, see: + http://code.google.com/apis/gdata/batch.html + """ + + _tag = GDataEntry._tag + _namespace = GDataEntry._namespace + _children = GDataEntry._children.copy() + _children['{%s}operation' % BATCH_NAMESPACE] = ('batch_operation', BatchOperation) + _children['{%s}id' % BATCH_NAMESPACE] = ('batch_id', BatchId) + _children['{%s}status' % BATCH_NAMESPACE] = ('batch_status', BatchStatus) + _attributes = GDataEntry._attributes.copy() + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, control=None, title=None, updated=None, + batch_operation=None, batch_id=None, batch_status=None, + extension_elements=None, extension_attributes=None, text=None): + self.batch_operation = batch_operation + self.batch_id = batch_id + self.batch_status = batch_status + GDataEntry.__init__(self, author=author, category=category, + content=content, contributor=contributor, atom_id=atom_id, link=link, + published=published, rights=rights, source=source, summary=summary, + control=control, title=title, updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +def BatchEntryFromString(xml_string): + return atom.CreateClassFromXMLString(BatchEntry, xml_string) + + +class BatchInterrupted(atom.AtomBase): + """The batch:interrupted element sent if batch request was interrupted. + + Only appears in a feed if some of the batch entries could not be processed. + See: http://code.google.com/apis/gdata/batch.html#Handling_Errors + """ + + _tag = 'interrupted' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['reason'] = 'reason' + _attributes['success'] = 'success' + _attributes['failures'] = 'failures' + _attributes['parsed'] = 'parsed' + + def __init__(self, reason=None, success=None, failures=None, parsed=None, + extension_elements=None, extension_attributes=None, text=None): + self.reason = reason + self.success = success + self.failures = failures + self.parsed = parsed + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def BatchInterruptedFromString(xml_string): + return atom.CreateClassFromXMLString(BatchInterrupted, xml_string) + + +class BatchFeed(GDataFeed): + """A feed containing a list of batch request entries.""" + + _tag = GDataFeed._tag + _namespace = GDataFeed._namespace + _children = GDataFeed._children.copy() + _attributes = GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BatchEntry]) + _children['{%s}interrupted' % BATCH_NAMESPACE] = ('interrupted', BatchInterrupted) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, entry=None, + total_results=None, start_index=None, items_per_page=None, + interrupted=None, + extension_elements=None, extension_attributes=None, text=None): + self.interrupted = interrupted + GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + def AddBatchEntry(self, entry=None, id_url_string=None, + batch_id_string=None, operation_string=None): + """Logic for populating members of a BatchEntry and adding to the feed. + + + If the entry is not a BatchEntry, it is converted to a BatchEntry so + that the batch specific members will be present. + + The id_url_string can be used in place of an entry if the batch operation + applies to a URL. For example query and delete operations require just + the URL of an entry, no body is sent in the HTTP request. If an + id_url_string is sent instead of an entry, a BatchEntry is created and + added to the feed. + + This method also assigns the desired batch id to the entry so that it + can be referenced in the server's response. If the batch_id_string is + None, this method will assign a batch_id to be the index at which this + entry will be in the feed's entry list. + + Args: + entry: BatchEntry, atom.Entry, or another Entry flavor (optional) The + entry which will be sent to the server as part of the batch request. + The item must have a valid atom id so that the server knows which + entry this request references. + id_url_string: str (optional) The URL of the entry to be acted on. You + can find this URL in the text member of the atom id for an entry. + If an entry is not sent, this id will be used to construct a new + BatchEntry which will be added to the request feed. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. Note that batch_ids should either always be specified or + never, mixing could potentially result in duplicate batch ids. + operation_string: str (optional) The desired batch operation which will + set the batch_operation.type member of the entry. Options are + 'insert', 'update', 'delete', and 'query' + + Raises: + MissingRequiredParameters: Raised if neither an id_ url_string nor an + entry are provided in the request. + + Returns: + The added entry. + """ + if entry is None and id_url_string is None: + raise MissingRequiredParameters('supply either an entry or URL string') + if entry is None and id_url_string is not None: + entry = BatchEntry(atom_id=atom.Id(text=id_url_string)) + # TODO: handle cases in which the entry lacks batch_... members. + #if not isinstance(entry, BatchEntry): + # Convert the entry to a batch entry. + if batch_id_string is not None: + entry.batch_id = BatchId(text=batch_id_string) + elif entry.batch_id is None or entry.batch_id.text is None: + entry.batch_id = BatchId(text=str(len(self.entry))) + if operation_string is not None: + entry.batch_operation = BatchOperation(op_type=operation_string) + self.entry.append(entry) + return entry + + def AddInsert(self, entry, batch_id_string=None): + """Add an insert request to the operations in this batch request feed. + + If the entry doesn't yet have an operation or a batch id, these will + be set to the insert operation and a batch_id specified as a parameter. + + Args: + entry: BatchEntry The entry which will be sent in the batch feed as an + insert request. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. Note that batch_ids should either always be specified or + never, mixing could potentially result in duplicate batch ids. + """ + entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string, + operation_string=BATCH_INSERT) + + def AddUpdate(self, entry, batch_id_string=None): + """Add an update request to the list of batch operations in this feed. + + Sets the operation type of the entry to insert if it is not already set + and assigns the desired batch id to the entry so that it can be + referenced in the server's response. + + Args: + entry: BatchEntry The entry which will be sent to the server as an + update (HTTP PUT) request. The item must have a valid atom id + so that the server knows which entry to replace. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. See also comments for AddInsert. + """ + entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string, + operation_string=BATCH_UPDATE) + + def AddDelete(self, url_string=None, entry=None, batch_id_string=None): + """Adds a delete request to the batch request feed. + + This method takes either the url_string which is the atom id of the item + to be deleted, or the entry itself. The atom id of the entry must be + present so that the server knows which entry should be deleted. + + Args: + url_string: str (optional) The URL of the entry to be deleted. You can + find this URL in the text member of the atom id for an entry. + entry: BatchEntry (optional) The entry to be deleted. + batch_id_string: str (optional) + + Raises: + MissingRequiredParameters: Raised if neither a url_string nor an entry + are provided in the request. + """ + entry = self.AddBatchEntry(entry=entry, id_url_string=url_string, + batch_id_string=batch_id_string, + operation_string=BATCH_DELETE) + + def AddQuery(self, url_string=None, entry=None, batch_id_string=None): + """Adds a query request to the batch request feed. + + This method takes either the url_string which is the query URL + whose results will be added to the result feed. The query URL will + be encapsulated in a BatchEntry, and you may pass in the BatchEntry + with a query URL instead of sending a url_string. + + Args: + url_string: str (optional) + entry: BatchEntry (optional) + batch_id_string: str (optional) + + Raises: + MissingRequiredParameters + """ + entry = self.AddBatchEntry(entry=entry, id_url_string=url_string, + batch_id_string=batch_id_string, + operation_string=BATCH_QUERY) + + def GetBatchLink(self): + for link in self.link: + if link.rel == 'http://schemas.google.com/g/2005#batch': + return link + return None + + +def BatchFeedFromString(xml_string): + return atom.CreateClassFromXMLString(BatchFeed, xml_string) + + +class EntryLink(atom.AtomBase): + """The gd:entryLink element""" + + _tag = 'entryLink' + _namespace = GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + # The entry used to be an atom.Entry, now it is a GDataEntry. + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', GDataEntry) + _attributes['rel'] = 'rel' + _attributes['readOnly'] = 'read_only' + _attributes['href'] = 'href' + + def __init__(self, href=None, read_only=None, rel=None, + entry=None, extension_elements=None, + extension_attributes=None, text=None): + self.href = href + self.read_only = read_only + self.rel = rel + self.entry = entry + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def EntryLinkFromString(xml_string): + return atom.CreateClassFromXMLString(EntryLink, xml_string) + + +class FeedLink(atom.AtomBase): + """The gd:feedLink element""" + + _tag = 'feedLink' + _namespace = GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}feed' % atom.ATOM_NAMESPACE] = ('feed', GDataFeed) + _attributes['rel'] = 'rel' + _attributes['readOnly'] = 'read_only' + _attributes['countHint'] = 'count_hint' + _attributes['href'] = 'href' + + def __init__(self, count_hint=None, href=None, read_only=None, rel=None, + feed=None, extension_elements=None, extension_attributes=None, + text=None): + self.count_hint = count_hint + self.href = href + self.read_only = read_only + self.rel = rel + self.feed = feed + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def FeedLinkFromString(xml_string): + return atom.CreateClassFromXMLString(FeedLink, xml_string) diff --git a/patches/gdata/__init__.pyc b/patches/gdata/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..472cd2b29e36c9214957aea03285291585794275 GIT binary patch literal 32899 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 element to retrieve. + + Returns: + A property object corresponding to the matching element. + if no property is found, None is returned. + """ + + for prop in self.property: + if prop.name == name: + return prop + + return None + + GetProperty = get_property + + +class GetMetric(object): + """Utility class to simplify retrieving Metric objects.""" + + def get_metric(self, name): + """Helper method to return a propery value by its name attribute + + Args: + name: string The name of the element to retrieve. + + Returns: + A property object corresponding to the matching element. + if no property is found, None is returned. + """ + + for met in self.metric: + if met.name == name: + return met + + return None + + GetMetric = get_metric + + +class GetDimension(object): + """Utility class to simplify retrieving Dimension objects.""" + + def get_dimension(self, name): + """Helper method to return a dimention object by its name attribute + + Args: + name: string The name of the element to retrieve. + + Returns: + A dimension object corresponding to the matching element. + if no dimension is found, None is returned. + """ + + for dim in self.dimension: + if dim.name == name: + return dim + + return None + + GetDimension = get_dimension + + +class GaLinkFinder(object): + """Utility class to return specific links in Google Analytics feeds.""" + + def get_parent_links(self): + """Returns a list of all the parent links in an entry.""" + + links = [] + for link in self.link: + if link.rel == link.parent(): + links.append(link) + + return links + + GetParentLinks = get_parent_links + + def get_child_links(self): + """Returns a list of all the child links in an entry.""" + + links = [] + for link in self.link: + if link.rel == link.child(): + links.append(link) + + return links + + GetChildLinks = get_child_links + + def get_child_link(self, target_kind): + """Utility method to return one child link. + + Returns: + A child link with the given target_kind. None if the target_kind was + not found. + """ + + for link in self.link: + if link.rel == link.child() and link.target_kind == target_kind: + return link + + return None + + GetChildLink = get_child_link + + +class StartDate(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'startDate' + + +class EndDate(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'endDate' + + +class Metric(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'metric' + name = 'name' + type = 'type' + value = 'value' + confidence_interval = 'confidenceInterval' + + +class Aggregates(atom.core.XmlElement, GetMetric): + """Analytics Data Feed """ + _qname = DXP_NS % 'aggregates' + metric = [Metric] + + +class ContainsSampledData(atom.core.XmlElement): + """Analytics Data Feed """ + _qname = DXP_NS % 'containsSampledData' + + +class TableId(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'tableId' + + +class TableName(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'tableName' + + +class Property(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'property' + name = 'name' + value = 'value' + + +class Definition(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'definition' + + +class Segment(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'segment' + id = 'id' + name = 'name' + definition = Definition + + +class Engagement(atom.core.XmlElement): + """Analytics Feed """ + _qname = GA_NS % 'engagement' + type = 'type' + comparison = 'comparison' + threshold_value = 'thresholdValue' + + +class Step(atom.core.XmlElement): + """Analytics Feed """ + _qname = GA_NS % 'step' + number = 'number' + name = 'name' + path = 'path' + + +class Destination(atom.core.XmlElement): + """Analytics Feed """ + _qname = GA_NS % 'destination' + step = [Step] + expression = 'expression' + case_sensitive = 'caseSensitive' + match_type = 'matchType' + step1_required = 'step1Required' + + +class Goal(atom.core.XmlElement): + """Analytics Feed """ + _qname = GA_NS % 'goal' + destination = Destination + engagement = Engagement + number = 'number' + name = 'name' + value = 'value' + active = 'active' + + +class CustomVariable(atom.core.XmlElement): + """Analytics Data Feed """ + _qname = GA_NS % 'customVariable' + index = 'index' + name = 'name' + scope = 'scope' + + +class DataSource(atom.core.XmlElement, GetProperty): + """Analytics Data Feed """ + _qname = DXP_NS % 'dataSource' + table_id = TableId + table_name = TableName + property = [Property] + + +class Dimension(atom.core.XmlElement): + """Analytics Feed """ + _qname = DXP_NS % 'dimension' + name = 'name' + value = 'value' + + +class AnalyticsLink(atom.data.Link): + """Subclass of link """ + target_kind = GD_NS % 'targetKind' + + @classmethod + def parent(cls): + """Parent target_kind""" + return '%s#parent' % GA_NS[1:-3] + + @classmethod + def child(cls): + """Child target_kind""" + return '%s#child' % GA_NS[1:-3] + + +# Account Feed. +class AccountEntry(gdata.data.GDEntry, GetProperty): + """Analytics Account Feed """ + _qname = atom.data.ATOM_TEMPLATE % 'entry' + table_id = TableId + property = [Property] + goal = [Goal] + custom_variable = [CustomVariable] + + +class AccountFeed(gdata.data.GDFeed): + """Analytics Account Feed """ + _qname = atom.data.ATOM_TEMPLATE % 'feed' + segment = [Segment] + entry = [AccountEntry] + + +# Data Feed. +class DataEntry(gdata.data.GDEntry, GetMetric, GetDimension): + """Analytics Data Feed """ + _qname = atom.data.ATOM_TEMPLATE % 'entry' + dimension = [Dimension] + metric = [Metric] + + def get_object(self, name): + """Returns either a Dimension or Metric object with the same name as the + name parameter. + + Args: + name: string The name of the object to retrieve. + + Returns: + Either a Dimension or Object that has the same as the name parameter. + """ + + output = self.GetDimension(name) + if not output: + output = self.GetMetric(name) + + return output + + GetObject = get_object + + +class DataFeed(gdata.data.GDFeed): + """Analytics Data Feed . + + Although there is only one datasource, it is stored in an array to replicate + the design of the Java client library and ensure backwards compatibility if + new data sources are added in the future. + """ + + _qname = atom.data.ATOM_TEMPLATE % 'feed' + start_date = StartDate + end_date = EndDate + aggregates = Aggregates + contains_sampled_data = ContainsSampledData + data_source = [DataSource] + entry = [DataEntry] + segment = Segment + + def has_sampled_data(self): + """Returns whether this feed has sampled data.""" + if (self.contains_sampled_data.text == 'true'): + return True + return False + + HasSampledData = has_sampled_data + + +# Management Feed. +class ManagementEntry(gdata.data.GDEntry, GetProperty, GaLinkFinder): + """Analytics Managememt Entry .""" + + _qname = atom.data.ATOM_TEMPLATE % 'entry' + kind = GD_NS % 'kind' + property = [Property] + goal = Goal + segment = Segment + link = [AnalyticsLink] + + +class ManagementFeed(gdata.data.GDFeed): + """Analytics Management Feed . + + This class holds the data for all 5 Management API feeds: Account, + Web Property, Profile, Goal, and Advanced Segment Feeds. + """ + + _qname = atom.data.ATOM_TEMPLATE % 'feed' + entry = [ManagementEntry] + kind = GD_NS % 'kind' diff --git a/patches/gdata/analytics/service.py b/patches/gdata/analytics/service.py new file mode 100644 index 0000000..0638b48 --- /dev/null +++ b/patches/gdata/analytics/service.py @@ -0,0 +1,331 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 Google Inc. +# Refactored in 2009 to work for Google Analytics by Sal Uryasev at Juice 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. + +""" + AccountsService extends the GDataService to streamline Google Analytics + account information operations. + + AnalyticsDataService: Provides methods to query google analytics data feeds. + Extends GDataService. + + DataQuery: Queries a Google Analytics Data list feed. + + AccountQuery: Queries a Google Analytics Account list feed. +""" + + +__author__ = 'api.suryasev (Sal Uryasev)' + + +import urllib +import atom +import gdata.service +import gdata.analytics + + +class AccountsService(gdata.service.GDataService): + + """Client extension for the Google Analytics Account List feed.""" + + def __init__(self, email="", password=None, source=None, + server='www.google.com/analytics', additional_headers=None, + **kwargs): + """Creates a client for the Google Analytics service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + + gdata.service.GDataService.__init__( + self, email=email, password=password, service='analytics', + source=source, server=server, additional_headers=additional_headers, + **kwargs) + + def QueryAccountListFeed(self, uri): + """Retrieves an AccountListFeed by retrieving a URI based off the Document + List feed, including any query parameters. An AccountListFeed object + can be used to construct these parameters. + + Args: + uri: string The URI of the feed being retrieved possibly with query + parameters. + + Returns: + An AccountListFeed object representing the feed returned by the server. + """ + return self.Get(uri, converter=gdata.analytics.AccountListFeedFromString) + + def GetAccountListEntry(self, uri): + """Retrieves a particular AccountListEntry by its unique URI. + + Args: + uri: string The unique URI of an entry in an Account List feed. + + Returns: + An AccountLisFeed object representing the retrieved entry. + """ + return self.Get(uri, converter=gdata.analytics.AccountListEntryFromString) + + def GetAccountList(self, max_results=1000, text_query=None, + params=None, categories=None): + """Retrieves a feed containing all of a user's accounts and profiles.""" + q = gdata.analytics.service.AccountQuery(max_results=max_results, + text_query=text_query, + params=params, + categories=categories); + return self.QueryAccountListFeed(q.ToUri()) + + + + +class AnalyticsDataService(gdata.service.GDataService): + + """Client extension for the Google Analytics service Data List feed.""" + + def __init__(self, email=None, password=None, source=None, + server='www.google.com/analytics', additional_headers=None, + **kwargs): + """Creates a client for the Google Analytics service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'docs.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + + gdata.service.GDataService.__init__(self, + email=email, password=password, service='analytics', source=source, + server=server, additional_headers=additional_headers, **kwargs) + + def GetData(self, ids='', dimensions='', metrics='', + sort='', filters='', start_date='', + end_date='', start_index='', + max_results=''): + """Retrieves a feed containing a user's data + + ids: comma-separated string of analytics accounts. + dimensions: comma-separated string of dimensions. + metrics: comma-separated string of metrics. + sort: comma-separated string of dimensions and metrics for sorting. + This may be previxed with a minus to sort in reverse order. + (e.g. '-ga:keyword') + If ommited, the first dimension passed in will be used. + filters: comma-separated string of filter parameters. + (e.g. 'ga:keyword==google') + start_date: start date for data pull. + end_date: end date for data pull. + start_index: used in combination with max_results to pull more than 1000 + entries. This defaults to 1. + max_results: maximum results that the pull will return. This defaults + to, and maxes out at 1000. + """ + q = gdata.analytics.service.DataQuery(ids=ids, + dimensions=dimensions, + metrics=metrics, + filters=filters, + sort=sort, + start_date=start_date, + end_date=end_date, + start_index=start_index, + max_results=max_results); + return self.AnalyticsDataFeed(q.ToUri()) + + def AnalyticsDataFeed(self, uri): + """Retrieves an AnalyticsListFeed by retrieving a URI based off the + Document List feed, including any query parameters. An + AnalyticsListFeed object can be used to construct these parameters. + + Args: + uri: string The URI of the feed being retrieved possibly with query + parameters. + + Returns: + An AnalyticsListFeed object representing the feed returned by the + server. + """ + return self.Get(uri, + converter=gdata.analytics.AnalyticsDataFeedFromString) + + """ + Account Fetching + """ + + def QueryAccountListFeed(self, uri): + """Retrieves an Account ListFeed by retrieving a URI based off the Account + List feed, including any query parameters. A AccountQuery object can + be used to construct these parameters. + + Args: + uri: string The URI of the feed being retrieved possibly with query + parameters. + + Returns: + An AccountListFeed object representing the feed returned by the server. + """ + return self.Get(uri, converter=gdata.analytics.AccountListFeedFromString) + + def GetAccountListEntry(self, uri): + """Retrieves a particular AccountListEntry by its unique URI. + + Args: + uri: string The unique URI of an entry in an Account List feed. + + Returns: + An AccountListEntry object representing the retrieved entry. + """ + return self.Get(uri, converter=gdata.analytics.AccountListEntryFromString) + + def GetAccountList(self, username="default", max_results=1000, + start_index=1): + """Retrieves a feed containing all of a user's accounts and profiles. + The username parameter is soon to be deprecated, with 'default' + becoming the only allowed parameter. + """ + if not username: + raise Exception("username is a required parameter") + q = gdata.analytics.service.AccountQuery(username=username, + max_results=max_results, + start_index=start_index); + return self.QueryAccountListFeed(q.ToUri()) + +class DataQuery(gdata.service.Query): + """Object used to construct a URI to a data feed""" + def __init__(self, feed='/feeds/data', text_query=None, + params=None, categories=None, ids="", + dimensions="", metrics="", sort="", filters="", + start_date="", end_date="", start_index="", + max_results=""): + """Constructor for Analytics List Query + + Args: + feed: string (optional) The path for the feed. (e.g. '/feeds/data') + + text_query: string (optional) The contents of the q query parameter. + This string is URL escaped upon conversion to a URI. + params: dict (optional) Parameter value string pairs which become URL + params when translated to a URI. These parameters are added to + the query's items. + categories: list (optional) List of category strings which should be + included as query categories. See gdata.service.Query for + additional documentation. + ids: comma-separated string of analytics accounts. + dimensions: comma-separated string of dimensions. + metrics: comma-separated string of metrics. + sort: comma-separated string of dimensions and metrics. + This may be previxed with a minus to sort in reverse order + (e.g. '-ga:keyword'). + If ommited, the first dimension passed in will be used. + filters: comma-separated string of filter parameters + (e.g. 'ga:keyword==google'). + start_date: start date for data pull. + end_date: end date for data pull. + start_index: used in combination with max_results to pull more than 1000 + entries. This defaults to 1. + max_results: maximum results that the pull will return. This defaults + to, and maxes out at 1000. + + Yields: + A DocumentQuery object used to construct a URI based on the Document + List feed. + """ + self.elements = {'ids': ids, + 'dimensions': dimensions, + 'metrics': metrics, + 'sort': sort, + 'filters': filters, + 'start-date': start_date, + 'end-date': end_date, + 'start-index': start_index, + 'max-results': max_results} + + gdata.service.Query.__init__(self, feed, text_query, params, categories) + + def ToUri(self): + """Generates a URI from the query parameters set in the object. + + Returns: + A string containing the URI used to retrieve entries from the Analytics + List feed. + """ + old_feed = self.feed + self.feed = '/'.join([old_feed]) + '?' + \ + urllib.urlencode(dict([(key, value) for key, value in \ + self.elements.iteritems() if value])) + new_feed = gdata.service.Query.ToUri(self) + self.feed = old_feed + return new_feed + + +class AccountQuery(gdata.service.Query): + """Object used to construct a URI to query the Google Account List feed""" + def __init__(self, feed='/feeds/accounts', start_index=1, + max_results=1000, username='default', text_query=None, + params=None, categories=None): + """Constructor for Account List Query + + Args: + feed: string (optional) The path for the feed. (e.g. '/feeds/documents') + visibility: string (optional) The visibility chosen for the current + feed. + projection: string (optional) The projection chosen for the current + feed. + text_query: string (optional) The contents of the q query parameter. + This string is URL escaped upon conversion to a URI. + params: dict (optional) Parameter value string pairs which become URL + params when translated to a URI. These parameters are added to + the query's items. + categories: list (optional) List of category strings which should be + included as query categories. See gdata.service.Query for + additional documentation. + username: string (deprecated) This value should now always be passed as + 'default'. + + Yields: + A DocumentQuery object used to construct a URI based on the Document + List feed. + """ + self.max_results = max_results + self.start_index = start_index + self.username = username + gdata.service.Query.__init__(self, feed, text_query, params, categories) + + def ToUri(self): + """Generates a URI from the query parameters set in the object. + + Returns: + A string containing the URI used to retrieve entries from the Account + List feed. + """ + old_feed = self.feed + self.feed = '/'.join([old_feed, self.username]) + '?' + \ + '&'.join(['max-results=' + str(self.max_results), + 'start-index=' + str(self.start_index)]) + new_feed = self.feed + self.feed = old_feed + return new_feed diff --git a/patches/gdata/apps/__init__.py b/patches/gdata/apps/__init__.py new file mode 100644 index 0000000..ebdf98e --- /dev/null +++ b/patches/gdata/apps/__init__.py @@ -0,0 +1,526 @@ +#!/usr/bin/python +# +# Copyright (C) 2007 SIOS Technology, 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. + +"""Contains objects used with Google Apps.""" + +__author__ = 'tmatsuo@sios.com (Takashi MATSUO)' + + +import atom +import gdata + + +# XML namespaces which are often used in Google Apps entity. +APPS_NAMESPACE = 'http://schemas.google.com/apps/2006' +APPS_TEMPLATE = '{http://schemas.google.com/apps/2006}%s' + + +class EmailList(atom.AtomBase): + """The Google Apps EmailList element""" + + _tag = 'emailList' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + + def __init__(self, name=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +def EmailListFromString(xml_string): + return atom.CreateClassFromXMLString(EmailList, xml_string) + + +class Who(atom.AtomBase): + """The Google Apps Who element""" + + _tag = 'who' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['rel'] = 'rel' + _attributes['email'] = 'email' + + def __init__(self, rel=None, email=None, extension_elements=None, + extension_attributes=None, text=None): + self.rel = rel + self.email = email + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +def WhoFromString(xml_string): + return atom.CreateClassFromXMLString(Who, xml_string) + + +class Login(atom.AtomBase): + """The Google Apps Login element""" + + _tag = 'login' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['userName'] = 'user_name' + _attributes['password'] = 'password' + _attributes['suspended'] = 'suspended' + _attributes['admin'] = 'admin' + _attributes['changePasswordAtNextLogin'] = 'change_password' + _attributes['agreedToTerms'] = 'agreed_to_terms' + _attributes['ipWhitelisted'] = 'ip_whitelisted' + _attributes['hashFunctionName'] = 'hash_function_name' + + def __init__(self, user_name=None, password=None, suspended=None, + ip_whitelisted=None, hash_function_name=None, + admin=None, change_password=None, agreed_to_terms=None, + extension_elements=None, extension_attributes=None, + text=None): + self.user_name = user_name + self.password = password + self.suspended = suspended + self.admin = admin + self.change_password = change_password + self.agreed_to_terms = agreed_to_terms + self.ip_whitelisted = ip_whitelisted + self.hash_function_name = hash_function_name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def LoginFromString(xml_string): + return atom.CreateClassFromXMLString(Login, xml_string) + + +class Quota(atom.AtomBase): + """The Google Apps Quota element""" + + _tag = 'quota' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['limit'] = 'limit' + + def __init__(self, limit=None, extension_elements=None, + extension_attributes=None, text=None): + self.limit = limit + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def QuotaFromString(xml_string): + return atom.CreateClassFromXMLString(Quota, xml_string) + + +class Name(atom.AtomBase): + """The Google Apps Name element""" + + _tag = 'name' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['familyName'] = 'family_name' + _attributes['givenName'] = 'given_name' + + def __init__(self, family_name=None, given_name=None, + extension_elements=None, extension_attributes=None, text=None): + self.family_name = family_name + self.given_name = given_name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def NameFromString(xml_string): + return atom.CreateClassFromXMLString(Name, xml_string) + + +class Nickname(atom.AtomBase): + """The Google Apps Nickname element""" + + _tag = 'nickname' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + + def __init__(self, name=None, + extension_elements=None, extension_attributes=None, text=None): + self.name = name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def NicknameFromString(xml_string): + return atom.CreateClassFromXMLString(Nickname, xml_string) + + +class NicknameEntry(gdata.GDataEntry): + """A Google Apps flavor of an Atom Entry for Nickname""" + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}login' % APPS_NAMESPACE] = ('login', Login) + _children['{%s}nickname' % APPS_NAMESPACE] = ('nickname', Nickname) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + login=None, nickname=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated) + self.login = login + self.nickname = nickname + self.extended_property = extended_property or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def NicknameEntryFromString(xml_string): + return atom.CreateClassFromXMLString(NicknameEntry, xml_string) + + +class NicknameFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Apps Nickname feed flavor of an Atom Feed""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [NicknameEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def NicknameFeedFromString(xml_string): + return atom.CreateClassFromXMLString(NicknameFeed, xml_string) + + +class UserEntry(gdata.GDataEntry): + """A Google Apps flavor of an Atom Entry""" + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}login' % APPS_NAMESPACE] = ('login', Login) + _children['{%s}name' % APPS_NAMESPACE] = ('name', Name) + _children['{%s}quota' % APPS_NAMESPACE] = ('quota', Quota) + # This child may already be defined in GDataEntry, confirm before removing. + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + [gdata.FeedLink]) + _children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + login=None, name=None, quota=None, who=None, feed_link=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated) + self.login = login + self.name = name + self.quota = quota + self.who = who + self.feed_link = feed_link or [] + self.extended_property = extended_property or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def UserEntryFromString(xml_string): + return atom.CreateClassFromXMLString(UserEntry, xml_string) + + +class UserFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Apps User feed flavor of an Atom Feed""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [UserEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def UserFeedFromString(xml_string): + return atom.CreateClassFromXMLString(UserFeed, xml_string) + + +class EmailListEntry(gdata.GDataEntry): + """A Google Apps EmailList flavor of an Atom Entry""" + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}emailList' % APPS_NAMESPACE] = ('email_list', EmailList) + # Might be able to remove this _children entry. + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + [gdata.FeedLink]) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + email_list=None, feed_link=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated) + self.email_list = email_list + self.feed_link = feed_link or [] + self.extended_property = extended_property or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def EmailListEntryFromString(xml_string): + return atom.CreateClassFromXMLString(EmailListEntry, xml_string) + + +class EmailListFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Apps EmailList feed flavor of an Atom Feed""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [EmailListEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def EmailListFeedFromString(xml_string): + return atom.CreateClassFromXMLString(EmailListFeed, xml_string) + + +class EmailListRecipientEntry(gdata.GDataEntry): + """A Google Apps EmailListRecipient flavor of an Atom Entry""" + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + who=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated) + self.who = who + self.extended_property = extended_property or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def EmailListRecipientEntryFromString(xml_string): + return atom.CreateClassFromXMLString(EmailListRecipientEntry, xml_string) + + +class EmailListRecipientFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Apps EmailListRecipient feed flavor of an Atom Feed""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [EmailListRecipientEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def EmailListRecipientFeedFromString(xml_string): + return atom.CreateClassFromXMLString(EmailListRecipientFeed, xml_string) + + +class Property(atom.AtomBase): + """The Google Apps Property element""" + + _tag = 'property' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + _attributes['value'] = 'value' + + def __init__(self, name=None, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def PropertyFromString(xml_string): + return atom.CreateClassFromXMLString(Property, xml_string) + + +class PropertyEntry(gdata.GDataEntry): + """A Google Apps Property flavor of an Atom Entry""" + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}property' % APPS_NAMESPACE] = ('property', [Property]) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + property=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated) + self.property = property + self.extended_property = extended_property or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def PropertyEntryFromString(xml_string): + return atom.CreateClassFromXMLString(PropertyEntry, xml_string) + +class PropertyFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Apps Property feed flavor of an Atom Feed""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [PropertyEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + +def PropertyFeedFromString(xml_string): + return atom.CreateClassFromXMLString(PropertyFeed, xml_string) diff --git a/patches/gdata/apps/adminsettings/__init__.py b/patches/gdata/apps/adminsettings/__init__.py new file mode 100644 index 0000000..d284c7c --- /dev/null +++ b/patches/gdata/apps/adminsettings/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Google +# +# 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. + diff --git a/patches/gdata/apps/adminsettings/service.py b/patches/gdata/apps/adminsettings/service.py new file mode 100644 index 0000000..c69fa36 --- /dev/null +++ b/patches/gdata/apps/adminsettings/service.py @@ -0,0 +1,471 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Allow Google Apps domain administrators to set domain admin settings. + + AdminSettingsService: Set admin settings.""" + +__author__ = 'jlee@pbu.edu' + + +import gdata.apps +import gdata.apps.service +import gdata.service + + +API_VER='2.0' + +class AdminSettingsService(gdata.apps.service.PropertyService): + """Client for the Google Apps Admin Settings service.""" + + def _serviceUrl(self, setting_id, domain=None): + if domain is None: + domain = self.domain + return '/a/feeds/domain/%s/%s/%s' % (API_VER, domain, setting_id) + + def genericGet(self, location): + """Generic HTTP Get Wrapper + + Args: + location: relative uri to Get + + Returns: + A dict containing the result of the get operation.""" + + uri = self._serviceUrl(location) + try: + return self._GetProperties(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def GetDefaultLanguage(self): + """Gets Domain Default Language + + Args: + None + + Returns: + Default Language as a string. All possible values are listed at: + http://code.google.com/apis/apps/email_settings/developers_guide_protocol.html#GA_email_language_tags""" + + result = self.genericGet('general/defaultLanguage') + return result['defaultLanguage'] + + def UpdateDefaultLanguage(self, defaultLanguage): + """Updates Domain Default Language + + Args: + defaultLanguage: Domain Language to set + possible values are at: + http://code.google.com/apis/apps/email_settings/developers_guide_protocol.html#GA_email_language_tags + + Returns: + A dict containing the result of the put operation""" + + uri = self._serviceUrl('general/defaultLanguage') + properties = {'defaultLanguage': defaultLanguage} + return self._PutProperties(uri, properties) + + def GetOrganizationName(self): + """Gets Domain Default Language + + Args: + None + + Returns: + Organization Name as a string.""" + + result = self.genericGet('general/organizationName') + return result['organizationName'] + + + def UpdateOrganizationName(self, organizationName): + """Updates Organization Name + + Args: + organizationName: Name of organization + + Returns: + A dict containing the result of the put operation""" + + uri = self._serviceUrl('general/organizationName') + properties = {'organizationName': organizationName} + return self._PutProperties(uri, properties) + + def GetMaximumNumberOfUsers(self): + """Gets Maximum Number of Users Allowed + + Args: + None + + Returns: An integer, the maximum number of users""" + + result = self.genericGet('general/maximumNumberOfUsers') + return int(result['maximumNumberOfUsers']) + + def GetCurrentNumberOfUsers(self): + """Gets Current Number of Users + + Args: + None + + Returns: An integer, the current number of users""" + + result = self.genericGet('general/currentNumberOfUsers') + return int(result['currentNumberOfUsers']) + + def IsDomainVerified(self): + """Is the domain verified + + Args: + None + + Returns: Boolean, is domain verified""" + + result = self.genericGet('accountInformation/isVerified') + if result['isVerified'] == 'true': + return True + else: + return False + + def GetSupportPIN(self): + """Gets Support PIN + + Args: + None + + Returns: A string, the Support PIN""" + + result = self.genericGet('accountInformation/supportPIN') + return result['supportPIN'] + + def GetEdition(self): + """Gets Google Apps Domain Edition + + Args: + None + + Returns: A string, the domain's edition (premier, education, partner)""" + + result = self.genericGet('accountInformation/edition') + return result['edition'] + + def GetCustomerPIN(self): + """Gets Customer PIN + + Args: + None + + Returns: A string, the customer PIN""" + + result = self.genericGet('accountInformation/customerPIN') + return result['customerPIN'] + + def GetCreationTime(self): + """Gets Domain Creation Time + + Args: + None + + Returns: A string, the domain's creation time""" + + result = self.genericGet('accountInformation/creationTime') + return result['creationTime'] + + def GetCountryCode(self): + """Gets Domain Country Code + + Args: + None + + Returns: A string, the domain's country code. Possible values at: + http://www.iso.org/iso/country_codes/iso_3166_code_lists/english_country_names_and_code_elements.htm""" + + result = self.genericGet('accountInformation/countryCode') + return result['countryCode'] + + def GetAdminSecondaryEmail(self): + """Gets Domain Admin Secondary Email Address + + Args: + None + + Returns: A string, the secondary email address for domain admin""" + + result = self.genericGet('accountInformation/adminSecondaryEmail') + return result['adminSecondaryEmail'] + + def UpdateAdminSecondaryEmail(self, adminSecondaryEmail): + """Gets Domain Creation Time + + Args: + adminSecondaryEmail: string, secondary email address of admin + + Returns: A dict containing the result of the put operation""" + + uri = self._serviceUrl('accountInformation/adminSecondaryEmail') + properties = {'adminSecondaryEmail': adminSecondaryEmail} + return self._PutProperties(uri, properties) + + def GetDomainLogo(self): + """Gets Domain Logo + + This function does not make use of the Google Apps Admin Settings API, + it does an HTTP Get of a url specific to the Google Apps domain. It is + included for completeness sake. + + Args: + None + + Returns: binary image file""" + + import urllib + url = 'http://www.google.com/a/cpanel/'+self.domain+'/images/logo.gif' + response = urllib.urlopen(url) + return response.read() + + def UpdateDomainLogo(self, logoImage): + """Update Domain's Custom Logo + + Args: + logoImage: binary image data + + Returns: A dict containing the result of the put operation""" + + from base64 import base64encode + uri = self._serviceUrl('appearance/customLogo') + properties = {'logoImage': base64encode(logoImage)} + return self._PutProperties(uri, properties) + + def GetCNAMEVerificationStatus(self): + """Gets Domain CNAME Verification Status + + Args: + None + + Returns: A dict {recordName, verified, verifiedMethod}""" + + return self.genericGet('verification/cname') + + def UpdateCNAMEVerificationStatus(self, verified): + """Updates CNAME Verification Status + + Args: + verified: boolean, True will retry verification process + + Returns: A dict containing the result of the put operation""" + + uri = self._serviceUrl('verification/cname') + properties = self.GetCNAMEVerificationStatus() + properties['verified'] = verified + return self._PutProperties(uri, properties) + + def GetMXVerificationStatus(self): + """Gets Domain MX Verification Status + + Args: + None + + Returns: A dict {verified, verifiedMethod}""" + + return self.genericGet('verification/mx') + + def UpdateMXVerificationStatus(self, verified): + """Updates MX Verification Status + + Args: + verified: boolean, True will retry verification process + + Returns: A dict containing the result of the put operation""" + + uri = self._serviceUrl('verification/mx') + properties = self.GetMXVerificationStatus() + properties['verified'] = verified + return self._PutProperties(uri, properties) + + def GetSSOSettings(self): + """Gets Domain Single Sign-On Settings + + Args: + None + + Returns: A dict {samlSignonUri, samlLogoutUri, changePasswordUri, enableSSO, ssoWhitelist, useDomainSpecificIssuer}""" + + return self.genericGet('sso/general') + + def UpdateSSOSettings(self, enableSSO=None, samlSignonUri=None, + samlLogoutUri=None, changePasswordUri=None, + ssoWhitelist=None, useDomainSpecificIssuer=None): + """Update SSO Settings. + + Args: + enableSSO: boolean, SSO Master on/off switch + samlSignonUri: string, SSO Login Page + samlLogoutUri: string, SSO Logout Page + samlPasswordUri: string, SSO Password Change Page + ssoWhitelist: string, Range of IP Addresses which will see SSO + useDomainSpecificIssuer: boolean, Include Google Apps Domain in Issuer + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('sso/general') + + #Get current settings, replace Nones with '' + properties = self.GetSSOSettings() + if properties['samlSignonUri'] == None: + properties['samlSignonUri'] = '' + if properties['samlLogoutUri'] == None: + properties['samlLogoutUri'] = '' + if properties['changePasswordUri'] == None: + properties['changePasswordUri'] = '' + if properties['ssoWhitelist'] == None: + properties['ssoWhitelist'] = '' + + #update only the values we were passed + if enableSSO != None: + properties['enableSSO'] = gdata.apps.service._bool2str(enableSSO) + if samlSignonUri != None: + properties['samlSignonUri'] = samlSignonUri + if samlLogoutUri != None: + properties['samlLogoutUri'] = samlLogoutUri + if changePasswordUri != None: + properties['changePasswordUri'] = changePasswordUri + if ssoWhitelist != None: + properties['ssoWhitelist'] = ssoWhitelist + if useDomainSpecificIssuer != None: + properties['useDomainSpecificIssuer'] = gdata.apps.service._bool2str(useDomainSpecificIssuer) + + return self._PutProperties(uri, properties) + + def GetSSOKey(self): + """Gets Domain Single Sign-On Signing Key + + Args: + None + + Returns: A dict {modulus, exponent, algorithm, format}""" + + return self.genericGet('sso/signingkey') + + def UpdateSSOKey(self, signingKey): + """Update SSO Settings. + + Args: + signingKey: string, public key to be uploaded + + Returns: + A dict containing the result of the update operation.""" + + uri = self._serviceUrl('sso/signingkey') + properties = {'signingKey': signingKey} + return self._PutProperties(uri, properties) + + def IsUserMigrationEnabled(self): + """Is User Migration Enabled + + Args: + None + + Returns: + boolean, is user migration enabled""" + + result = self.genericGet('email/migration') + if result['enableUserMigration'] == 'true': + return True + else: + return False + + def UpdateUserMigrationStatus(self, enableUserMigration): + """Update User Migration Status + + Args: + enableUserMigration: boolean, user migration enable/disable + + Returns: + A dict containing the result of the update operation.""" + + uri = self._serviceUrl('email/migration') + properties = {'enableUserMigration': enableUserMigration} + return self._PutProperties(uri, properties) + + def GetOutboundGatewaySettings(self): + """Get Outbound Gateway Settings + + Args: + None + + Returns: + A dict {smartHost, smtpMode}""" + + uri = self._serviceUrl('email/gateway') + try: + return self._GetProperties(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + except TypeError: + #if no outbound gateway is set, we get a TypeError, + #catch it and return nothing... + return {'smartHost': None, 'smtpMode': None} + + def UpdateOutboundGatewaySettings(self, smartHost=None, smtpMode=None): + """Update Outbound Gateway Settings + + Args: + smartHost: string, ip address or hostname of outbound gateway + smtpMode: string, SMTP or SMTP_TLS + + Returns: + A dict containing the result of the update operation.""" + + uri = self._serviceUrl('email/gateway') + + #Get current settings, replace Nones with '' + properties = GetOutboundGatewaySettings() + if properties['smartHost'] == None: + properties['smartHost'] = '' + if properties['smtpMode'] == None: + properties['smtpMode'] = '' + + #If we were passed new values for smartHost or smtpMode, update them + if smartHost != None: + properties['smartHost'] = smartHost + if smtpMode != None: + properties['smtpMode'] = smtpMode + + return self._PutProperties(uri, properties) + + def AddEmailRoute(self, routeDestination, routeRewriteTo, routeEnabled, bounceNotifications, accountHandling): + """Adds Domain Email Route + + Args: + routeDestination: string, destination ip address or hostname + routeRewriteTo: boolean, rewrite smtp envelop To: + routeEnabled: boolean, enable disable email routing + bounceNotifications: boolean, send bound notificiations to sender + accountHandling: string, which to route, "allAccounts", "provisionedAccounts", "unknownAccounts" + + Returns: + A dict containing the result of the update operation.""" + + uri = self._serviceUrl('emailrouting') + properties = {} + properties['routeDestination'] = routeDestination + properties['routeRewriteTo'] = gdata.apps.service._bool2str(routeRewriteTo) + properties['routeEnabled'] = gdata.apps.service._bool2str(routeEnabled) + properties['bounceNotifications'] = gdata.apps.service._bool2str(bounceNotifications) + properties['accountHandling'] = accountHandling + return self._PostProperties(uri, properties) diff --git a/patches/gdata/apps/audit/__init__.py b/patches/gdata/apps/audit/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/patches/gdata/apps/audit/__init__.py @@ -0,0 +1 @@ + diff --git a/patches/gdata/apps/audit/service.py b/patches/gdata/apps/audit/service.py new file mode 100644 index 0000000..d8cf72c --- /dev/null +++ b/patches/gdata/apps/audit/service.py @@ -0,0 +1,277 @@ +# Copyright (C) 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. + +"""Allow Google Apps domain administrators to audit user data. + + AuditService: Set auditing.""" + +__author__ = 'jlee@pbu.edu' + +from base64 import b64encode + +import gdata.apps +import gdata.apps.service +import gdata.service + +class AuditService(gdata.apps.service.PropertyService): + """Client for the Google Apps Audit service.""" + + def _serviceUrl(self, setting_id, domain=None, user=None): + if domain is None: + domain = self.domain + if user is None: + return '/a/feeds/compliance/audit/%s/%s' % (setting_id, domain) + else: + return '/a/feeds/compliance/audit/%s/%s/%s' % (setting_id, domain, user) + + def updatePGPKey(self, pgpkey): + """Updates Public PGP Key Google uses to encrypt audit data + + Args: + pgpkey: string, ASCII text of PGP Public Key to be used + + Returns: + A dict containing the result of the POST operation.""" + + uri = self._serviceUrl('publickey') + b64pgpkey = b64encode(pgpkey) + properties = {} + properties['publicKey'] = b64pgpkey + return self._PostProperties(uri, properties) + + def createEmailMonitor(self, source_user, destination_user, end_date, + begin_date=None, incoming_headers_only=False, + outgoing_headers_only=False, drafts=False, + drafts_headers_only=False, chats=False, + chats_headers_only=False): + """Creates a email monitor, forwarding the source_users emails/chats + + Args: + source_user: string, the user whose email will be audited + destination_user: string, the user to receive the audited email + end_date: string, the date the audit will end in + "yyyy-MM-dd HH:mm" format, required + begin_date: string, the date the audit will start in + "yyyy-MM-dd HH:mm" format, leave blank to use current time + incoming_headers_only: boolean, whether to audit only the headers of + mail delivered to source user + outgoing_headers_only: boolean, whether to audit only the headers of + mail sent from the source user + drafts: boolean, whether to audit draft messages of the source user + drafts_headers_only: boolean, whether to audit only the headers of + mail drafts saved by the user + chats: boolean, whether to audit archived chats of the source user + chats_headers_only: boolean, whether to audit only the headers of + archived chats of the source user + + Returns: + A dict containing the result of the POST operation.""" + + uri = self._serviceUrl('mail/monitor', user=source_user) + properties = {} + properties['destUserName'] = destination_user + if begin_date is not None: + properties['beginDate'] = begin_date + properties['endDate'] = end_date + if incoming_headers_only: + properties['incomingEmailMonitorLevel'] = 'HEADER_ONLY' + else: + properties['incomingEmailMonitorLevel'] = 'FULL_MESSAGE' + if outgoing_headers_only: + properties['outgoingEmailMonitorLevel'] = 'HEADER_ONLY' + else: + properties['outgoingEmailMonitorLevel'] = 'FULL_MESSAGE' + if drafts: + if drafts_headers_only: + properties['draftMonitorLevel'] = 'HEADER_ONLY' + else: + properties['draftMonitorLevel'] = 'FULL_MESSAGE' + if chats: + if chats_headers_only: + properties['chatMonitorLevel'] = 'HEADER_ONLY' + else: + properties['chatMonitorLevel'] = 'FULL_MESSAGE' + return self._PostProperties(uri, properties) + + def getEmailMonitors(self, user): + """"Gets the email monitors for the given user + + Args: + user: string, the user to retrieve email monitors for + + Returns: + list results of the POST operation + + """ + uri = self._serviceUrl('mail/monitor', user=user) + return self._GetPropertiesList(uri) + + def deleteEmailMonitor(self, source_user, destination_user): + """Deletes the email monitor for the given user + + Args: + source_user: string, the user who is being monitored + destination_user: string, theuser who recieves the monitored emails + + Returns: + Nothing + """ + + uri = self._serviceUrl('mail/monitor', user=source_user+'/'+destination_user) + try: + return self._DeleteProperties(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def createAccountInformationRequest(self, user): + """Creates a request for account auditing details + + Args: + user: string, the user to request account information for + + Returns: + A dict containing the result of the post operation.""" + + uri = self._serviceUrl('account', user=user) + properties = {} + #XML Body is left empty + try: + return self._PostProperties(uri, properties) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def getAccountInformationRequestStatus(self, user, request_id): + """Gets the status of an account auditing request + + Args: + user: string, the user whose account auditing details were requested + request_id: string, the request_id + + Returns: + A dict containing the result of the get operation.""" + + uri = self._serviceUrl('account', user=user+'/'+request_id) + try: + return self._GetProperties(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def getAllAccountInformationRequestsStatus(self): + """Gets the status of all account auditing requests for the domain + + Args: + None + + Returns: + list results of the POST operation + """ + + uri = self._serviceUrl('account') + return self._GetPropertiesList(uri) + + + def deleteAccountInformationRequest(self, user, request_id): + """Deletes the request for account auditing information + + Args: + user: string, the user whose account auditing details were requested + request_id: string, the request_id + + Returns: + Nothing + """ + + uri = self._serviceUrl('account', user=user+'/'+request_id) + try: + return self._DeleteProperties(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def createMailboxExportRequest(self, user, begin_date=None, end_date=None, include_deleted=False, search_query=None, headers_only=False): + """Creates a mailbox export request + + Args: + user: string, the user whose mailbox export is being requested + begin_date: string, date of earliest emails to export, optional, defaults to date of account creation + format is 'yyyy-MM-dd HH:mm' + end_date: string, date of latest emails to export, optional, defaults to current date + format is 'yyyy-MM-dd HH:mm' + include_deleted: boolean, whether to include deleted emails in export, mutually exclusive with search_query + search_query: string, gmail style search query, matched emails will be exported, mutually exclusive with include_deleted + + Returns: + A dict containing the result of the post operation.""" + + uri = self._serviceUrl('mail/export', user=user) + properties = {} + if begin_date is not None: + properties['beginDate'] = begin_date + if end_date is not None: + properties['endDate'] = end_date + if include_deleted is not None: + properties['includeDeleted'] = gdata.apps.service._bool2str(include_deleted) + if search_query is not None: + properties['searchQuery'] = search_query + if headers_only is True: + properties['packageContent'] = 'HEADER_ONLY' + else: + properties['packageContent'] = 'FULL_MESSAGE' + return self._PostProperties(uri, properties) + + def getMailboxExportRequestStatus(self, user, request_id): + """Gets the status of an mailbox export request + + Args: + user: string, the user whose mailbox were requested + request_id: string, the request_id + + Returns: + A dict containing the result of the get operation.""" + + uri = self._serviceUrl('mail/export', user=user+'/'+request_id) + try: + return self._GetProperties(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def getAllMailboxExportRequestsStatus(self): + """Gets the status of all mailbox export requests for the domain + + Args: + None + + Returns: + list results of the POST operation + """ + + uri = self._serviceUrl('mail/export') + return self._GetPropertiesList(uri) + + + def deleteMailboxExportRequest(self, user, request_id): + """Deletes the request for mailbox export + + Args: + user: string, the user whose mailbox were requested + request_id: string, the request_id + + Returns: + Nothing + """ + + uri = self._serviceUrl('mail/export', user=user+'/'+request_id) + try: + return self._DeleteProperties(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) diff --git a/patches/gdata/apps/emailsettings/__init__.py b/patches/gdata/apps/emailsettings/__init__.py new file mode 100644 index 0000000..275c6a0 --- /dev/null +++ b/patches/gdata/apps/emailsettings/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Google +# +# 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. diff --git a/patches/gdata/apps/emailsettings/client.py b/patches/gdata/apps/emailsettings/client.py new file mode 100644 index 0000000..ffab889 --- /dev/null +++ b/patches/gdata/apps/emailsettings/client.py @@ -0,0 +1,400 @@ +#!/usr/bin/python2.4 +# +# Copyright 2010 Google Inc. All Rights Reserved. +# +# 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. + +"""EmailSettingsClient simplifies Email Settings API calls. + +EmailSettingsClient extends gdata.client.GDClient to ease interaction with +the Google Apps Email Settings API. These interactions include the ability +to create labels, filters, aliases, and update web-clip, forwarding, POP, +IMAP, vacation-responder, signature, language, and general settings. +""" + + +__author__ = 'Claudio Cherubino ' + + +import gdata.apps.emailsettings.data +import gdata.client + + +# Email Settings URI template +# The strings in this template are eventually replaced with the API version, +# Google Apps domain name, username, and settingID, respectively. +EMAIL_SETTINGS_URI_TEMPLATE = '/a/feeds/emailsettings/%s/%s/%s/%s' + + +# The settingID value for the label requests +SETTING_ID_LABEL = 'label' +# The settingID value for the filter requests +SETTING_ID_FILTER = 'filter' +# The settingID value for the send-as requests +SETTING_ID_SENDAS = 'sendas' +# The settingID value for the webclip requests +SETTING_ID_WEBCLIP = 'webclip' +# The settingID value for the forwarding requests +SETTING_ID_FORWARDING = 'forwarding' +# The settingID value for the POP requests +SETTING_ID_POP = 'pop' +# The settingID value for the IMAP requests +SETTING_ID_IMAP = 'imap' +# The settingID value for the vacation responder requests +SETTING_ID_VACATION_RESPONDER = 'vacation' +# The settingID value for the signature requests +SETTING_ID_SIGNATURE = 'signature' +# The settingID value for the language requests +SETTING_ID_LANGUAGE = 'language' +# The settingID value for the general requests +SETTING_ID_GENERAL = 'general' + +# The KEEP action for the email settings +ACTION_KEEP = 'KEEP' +# The ARCHIVE action for the email settings +ACTION_ARCHIVE = 'ARCHIVE' +# The DELETE action for the email settings +ACTION_DELETE = 'DELETE' + +# The ALL_MAIL setting for POP enable_for property +POP_ENABLE_FOR_ALL_MAIL = 'ALL_MAIL' +# The MAIL_FROM_NOW_ON setting for POP enable_for property +POP_ENABLE_FOR_MAIL_FROM_NOW_ON = 'MAIL_FROM_NOW_ON' + + +class EmailSettingsClient(gdata.client.GDClient): + """Client extension for the Google Email Settings API service. + + Attributes: + host: string The hostname for the Email Settings API service. + api_version: string The version of the Email Settings API. + """ + + host = 'apps-apis.google.com' + api_version = '2.0' + auth_service = 'apps' + auth_scopes = gdata.gauth.AUTH_SCOPES['apps'] + ssl = True + + def __init__(self, domain, auth_token=None, **kwargs): + """Constructs a new client for the Email Settings API. + + Args: + domain: string The Google Apps domain with Email Settings. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the email settings. + kwargs: The other parameters to pass to the gdata.client.GDClient + constructor. + """ + gdata.client.GDClient.__init__(self, auth_token=auth_token, **kwargs) + self.domain = domain + + def make_email_settings_uri(self, username, setting_id): + """Creates the URI for the Email Settings API call. + + Using this client's Google Apps domain, create the URI to setup + email settings for the given user in that domain. If params are provided, + append them as GET params. + + Args: + username: string The name of the user affected by this setting. + setting_id: string The key of the setting to be configured. + + Returns: + A string giving the URI for Email Settings API calls for this client's + Google Apps domain. + """ + uri = EMAIL_SETTINGS_URI_TEMPLATE % (self.api_version, self.domain, + username, setting_id) + return uri + + MakeEmailSettingsUri = make_email_settings_uri + + def create_label(self, username, name, **kwargs): + """Creates a label with the given properties. + + Args: + username: string The name of the user. + name: string The name of the label. + kwargs: The other parameters to pass to gdata.client.GDClient.post(). + + Returns: + gdata.apps.emailsettings.data.EmailSettingsLabel of the new resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_LABEL) + new_label = gdata.apps.emailsettings.data.EmailSettingsLabel( + uri=uri, name=name) + return self.post(new_label, uri, **kwargs) + + CreateLabel = create_label + + def create_filter(self, username, from_address=None, + to_address=None, subject=None, has_the_word=None, + does_not_have_the_word=None, has_attachments=None, + label=None, mark_as_read=None, archive=None, **kwargs): + """Creates a filter with the given properties. + + Args: + username: string The name of the user. + from_address: string The source email address for the filter. + to_address: string (optional) The destination email address for + the filter. + subject: string (optional) The value the email must have in its + subject to be filtered. + has_the_word: string (optional) The value the email must have + in its subject or body to be filtered. + does_not_have_the_word: string (optional) The value the email + cannot have in its subject or body to be filtered. + has_attachments: string (optional) A boolean string representing + whether the email must have an attachment to be filtered. + label: string (optional) The name of the label to apply to + messages matching the filter criteria. + mark_as_read: Boolean (optional) Whether or not to mark + messages matching the filter criteria as read. + archive: Boolean (optional) Whether or not to move messages + matching to Archived state. + kwargs: The other parameters to pass to gdata.client.GDClient.post(). + + Returns: + gdata.apps.emailsettings.data.EmailSettingsFilter of the new resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_FILTER) + new_filter = gdata.apps.emailsettings.data.EmailSettingsFilter( + uri=uri, from_address=from_address, + to_address=to_address, subject=subject, + has_the_word=has_the_word, + does_not_have_the_word=does_not_have_the_word, + has_attachments=has_attachments, label=label, + mark_as_read=mark_as_read, archive=archive) + return self.post(new_filter, uri, **kwargs) + + CreateFilter = create_filter + + def create_send_as(self, username, name, address, reply_to=None, + make_default=None, **kwargs): + """Creates a send-as alias with the given properties. + + Args: + username: string The name of the user. + name: string The name that will appear in the "From" field. + address: string The email address that appears as the + origination address for emails sent by this user. + reply_to: string (optional) The address to be used as the reply-to + address in email sent using the alias. + make_default: Boolean (optional) Whether or not this alias should + become the default alias for this user. + kwargs: The other parameters to pass to gdata.client.GDClient.post(). + + Returns: + gdata.apps.emailsettings.data.EmailSettingsSendAsAlias of the + new resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_SENDAS) + new_alias = gdata.apps.emailsettings.data.EmailSettingsSendAsAlias( + uri=uri, name=name, address=address, + reply_to=reply_to, make_default=make_default) + return self.post(new_alias, uri, **kwargs) + + CreateSendAs = create_send_as + + def update_webclip(self, username, enable, **kwargs): + """Enable/Disable Google Mail web clip. + + Args: + username: string The name of the user. + enable: Boolean Whether to enable showing Web clips. + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsWebClip of the + updated resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_WEBCLIP) + new_webclip = gdata.apps.emailsettings.data.EmailSettingsWebClip( + uri=uri, enable=enable) + return self.update(new_webclip, **kwargs) + + UpdateWebclip = update_webclip + + def update_forwarding(self, username, enable, forward_to=None, + action=None, **kwargs): + """Update Google Mail Forwarding settings. + + Args: + username: string The name of the user. + enable: Boolean Whether to enable incoming email forwarding. + forward_to: (optional) string The address email will be forwarded to. + action: string (optional) The action to perform after forwarding + an email (ACTION_KEEP, ACTION_ARCHIVE, ACTION_DELETE). + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsForwarding of the + updated resource + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_FORWARDING) + new_forwarding = gdata.apps.emailsettings.data.EmailSettingsForwarding( + uri=uri, enable=enable, forward_to=forward_to, action=action) + return self.update(new_forwarding, **kwargs) + + UpdateForwarding = update_forwarding + + def update_pop(self, username, enable, enable_for=None, action=None, + **kwargs): + """Update Google Mail POP settings. + + Args: + username: string The name of the user. + enable: Boolean Whether to enable incoming POP3 access. + enable_for: string (optional) Whether to enable POP3 for all mail + (POP_ENABLE_FOR_ALL_MAIL), or mail from now on + (POP_ENABLE_FOR_MAIL_FROM_NOW_ON). + action: string (optional) What Google Mail should do with its copy + of the email after it is retrieved using POP (ACTION_KEEP, + ACTION_ARCHIVE, ACTION_DELETE). + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsPop of the updated resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_POP) + new_pop = gdata.apps.emailsettings.data.EmailSettingsPop( + uri=uri, enable=enable, + enable_for=enable_for, action=action) + return self.update(new_pop, **kwargs) + + UpdatePop = update_pop + + def update_imap(self, username, enable, **kwargs): + """Update Google Mail IMAP settings. + + Args: + username: string The name of the user. + enable: Boolean Whether to enable IMAP access.language + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsImap of the updated resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_IMAP) + new_imap = gdata.apps.emailsettings.data.EmailSettingsImap( + uri=uri, enable=enable) + return self.update(new_imap, **kwargs) + + UpdateImap = update_imap + + def update_vacation(self, username, enable, subject=None, message=None, + contacts_only=None, **kwargs): + """Update Google Mail vacation-responder settings. + + Args: + username: string The name of the user. + enable: Boolean Whether to enable the vacation responder. + subject: string (optional) The subject line of the vacation responder + autoresponse. + message: string (optional) The message body of the vacation responder + autoresponse. + contacts_only: Boolean (optional) Whether to only send autoresponses + to known contacts. + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsVacationResponder of the + updated resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_VACATION_RESPONDER) + new_vacation = gdata.apps.emailsettings.data.EmailSettingsVacationResponder( + uri=uri, enable=enable, subject=subject, + message=message, contacts_only=contacts_only) + return self.update(new_vacation, **kwargs) + + UpdateVacation = update_vacation + + def update_signature(self, username, signature, **kwargs): + """Update Google Mail signature. + + Args: + username: string The name of the user. + signature: string The signature to be appended to outgoing messages. + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsSignature of the + updated resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_SIGNATURE) + new_signature = gdata.apps.emailsettings.data.EmailSettingsSignature( + uri=uri, signature=signature) + return self.update(new_signature, **kwargs) + + UpdateSignature = update_signature + + def update_language(self, username, language, **kwargs): + """Update Google Mail language settings. + + Args: + username: string The name of the user. + language: string The language tag for Google Mail's display language. + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsLanguage of the + updated resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_LANGUAGE) + new_language = gdata.apps.emailsettings.data.EmailSettingsLanguage( + uri=uri, language=language) + return self.update(new_language, **kwargs) + + UpdateLanguage = update_language + + def update_general_settings(self, username, page_size=None, shortcuts=None, + arrows=None, snippets=None, use_unicode=None, + **kwargs): + """Update Google Mail general settings. + + Args: + username: string The name of the user. + page_size: int (optional) The number of conversations to be shown per + page. + shortcuts: Boolean (optional) Whether to enable keyboard shortcuts. + arrows: Boolean (optional) Whether to display arrow-shaped personal + indicators next to email sent specifically to the user. + snippets: Boolean (optional) Whether to display snippets of the messages + in the inbox and when searching. + use_unicode: Boolean (optional) Whether to use UTF-8 (unicode) encoding + for all outgoing messages. + kwargs: The other parameters to pass to the update method. + + Returns: + gdata.apps.emailsettings.data.EmailSettingsGeneral of the + updated resource. + """ + uri = self.MakeEmailSettingsUri(username=username, + setting_id=SETTING_ID_GENERAL) + new_general = gdata.apps.emailsettings.data.EmailSettingsGeneral( + uri=uri, page_size=page_size, shortcuts=shortcuts, + arrows=arrows, snippets=snippets, use_unicode=use_unicode) + return self.update(new_general, **kwargs) + + UpdateGeneralSettings = update_general_settings diff --git a/patches/gdata/apps/emailsettings/data.py b/patches/gdata/apps/emailsettings/data.py new file mode 100644 index 0000000..fa8a53c --- /dev/null +++ b/patches/gdata/apps/emailsettings/data.py @@ -0,0 +1,1130 @@ +#!/usr/bin/python +# +# Copyright 2010 Google Inc. All Rights Reserved. +# +# 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. + +"""Data model classes for the Email Settings API.""" + + +__author__ = 'Claudio Cherubino ' + + +import atom.data +import gdata.apps +import gdata.apps_property +import gdata.data + + +# This is required to work around a naming conflict between the Google +# Spreadsheets API and Python's built-in property function +pyproperty = property + + +# The apps:property label of the label property +LABEL_NAME = 'label' + +# The apps:property from of the filter property +FILTER_FROM_NAME = 'from' +# The apps:property to of the filter property +FILTER_TO_NAME = 'to' +# The apps:property subject of the filter property +FILTER_SUBJECT_NAME = 'subject' +# The apps:property hasTheWord of the filter property +FILTER_HAS_THE_WORD_NAME = 'hasTheWord' +# The apps:property doesNotHaveTheWord of the filter property +FILTER_DOES_NOT_HAVE_THE_WORD_NAME = 'doesNotHaveTheWord' +# The apps:property hasAttachment of the filter property +FILTER_HAS_ATTACHMENTS_NAME = 'hasAttachment' +# The apps:property label of the filter action property +FILTER_LABEL = 'label' +# The apps:property shouldMarkAsRead of the filter action property +FILTER_MARK_AS_READ = 'shouldMarkAsRead' +# The apps:property shouldArchive of the filter action propertylabel +FILTER_ARCHIVE = 'shouldArchive' + +# The apps:property name of the send-as alias property +SENDAS_ALIAS_NAME = 'name' +# The apps:property address of theAPPS_TEMPLATE send-as alias property +SENDAS_ALIAS_ADDRESS = 'address' +# The apps:property replyTo of the send-as alias property +SENDAS_ALIAS_REPLY_TO = 'replyTo' +# The apps:property makeDefault of the send-as alias property +SENDAS_ALIAS_MAKE_DEFAULT = 'makeDefault' + +# The apps:property enable of the webclip property +WEBCLIP_ENABLE = 'enable' + +# The apps:property enable of the forwarding property +FORWARDING_ENABLE = 'enable' +# The apps:property forwardTo of the forwarding property +FORWARDING_TO = 'forwardTo' +# The apps:property action of the forwarding property +FORWARDING_ACTION = 'action' + +# The apps:property enable of the POP property +POP_ENABLE = 'enable' +# The apps:property enableFor of the POP propertyACTION +POP_ENABLE_FOR = 'enableFor' +# The apps:property action of the POP property +POP_ACTION = 'action' + +# The apps:property enable of the IMAP property +IMAP_ENABLE = 'enable' + +# The apps:property enable of the vacation responder property +VACATION_RESPONDER_ENABLE = 'enable' +# The apps:property subject of the vacation responder property +VACATION_RESPONDER_SUBJECT = 'subject' +# The apps:property message of the vacation responder property +VACATION_RESPONDER_MESSAGE = 'message' +# The apps:property contactsOnly of the vacation responder property +VACATION_RESPONDER_CONTACTS_ONLY = 'contactsOnly' + +# The apps:property signature of the signature property +SIGNATURE_VALUE = 'signature' + +# The apps:property language of the language property +LANGUAGE_TAG = 'language' + +# The apps:property pageSize of the general settings property +GENERAL_PAGE_SIZE = 'pageSize' +# The apps:property shortcuts of the general settings property +GENERAL_SHORTCUTS = 'shortcuts' +# The apps:property arrows of the general settings property +GENERAL_ARROWS = 'arrows' +# The apps:prgdata.appsoperty snippets of the general settings property +GENERAL_SNIPPETS = 'snippets' +# The apps:property uniAppsProcode of the general settings property +GENERAL_UNICODE = 'unicode' + + +class EmailSettingsEntry(gdata.data.GDEntry): + """Represents an Email Settings entry in object form.""" + + property = [gdata.apps_property.AppsProperty] + + def _GetProperty(self, name): + """Get the apps:property value with the given name. + + Args: + name: string Name of the apps:property value to get. + + Returns: + The apps:property value with the given name, or None if the name was + invalid. + """ + + value = None + for p in self.property: + if p.name == name: + value = p.value + break + return value + + def _SetProperty(self, name, value): + """Set the apps:property value with the given name to the given value. + + Args: + name: string Name of the apps:property value to set. + value: string Value to give the apps:property value with the given name. + """ + found = False + for i in range(len(self.property)): + if self.property[i].name == name: + self.property[i].value = value + found = True + break + if not found: + self.property.append(gdata.apps_property.AppsProperty(name=name, value=value)) + + def find_edit_link(self): + return self.uri + + +class EmailSettingsLabel(EmailSettingsEntry): + """Represents a Label in object form.""" + + def GetName(self): + """Get the name of the Label object. + + Returns: + The name of this Label object as a string or None. + """ + + return self._GetProperty(LABEL_NAME) + + def SetName(self, value): + """Set the name of this Label object. + + Args: + value: string The new label name to give this object. + """ + + self._SetProperty(LABEL_NAME, value) + + name = pyproperty(GetName, SetName) + + def __init__(self, uri=None, name=None, *args, **kwargs): + """Constructs a new EmailSettingsLabel object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + name: string (optional) The name to give this new object. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsLabel, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if name: + self.name = name + + +class EmailSettingsFilter(EmailSettingsEntry): + """Represents an Email Settings Filter in object form.""" + + def GetFrom(self): + """Get the From value of the Filter object. + + Returns: + The From value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_FROM_NAME) + + def SetFrom(self, value): + """Set the From value of this Filter object. + + Args: + value: string The new From value to give this object. + """ + + self._SetProperty(FILTER_FROM_NAME, value) + + from_address = pyproperty(GetFrom, SetFrom) + + def GetTo(self): + """Get the To value of the Filter object. + + Returns: + The To value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_TO_NAME) + + def SetTo(self, value): + """Set the To value of this Filter object. + + Args: + value: string The new To value to give this object. + """ + + self._SetProperty(FILTER_TO_NAME, value) + + to_address = pyproperty(GetTo, SetTo) + + def GetSubject(self): + """Get the Subject value of the Filter object. + + Returns: + The Subject value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_SUBJECT_NAME) + + def SetSubject(self, value): + """Set the Subject value of this Filter object. + + Args: + value: string The new Subject value to give this object. + """ + + self._SetProperty(FILTER_SUBJECT_NAME, value) + + subject = pyproperty(GetSubject, SetSubject) + + def GetHasTheWord(self): + """Get the HasTheWord value of the Filter object. + + Returns: + The HasTheWord value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_HAS_THE_WORD_NAME) + + def SetHasTheWord(self, value): + """Set the HasTheWord value of this Filter object. + + Args: + value: string The new HasTheWord value to give this object. + """ + + self._SetProperty(FILTER_HAS_THE_WORD_NAME, value) + + has_the_word = pyproperty(GetHasTheWord, SetHasTheWord) + + def GetDoesNotHaveTheWord(self): + """Get the DoesNotHaveTheWord value of the Filter object. + + Returns: + The DoesNotHaveTheWord value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_DOES_NOT_HAVE_THE_WORD_NAME) + + def SetDoesNotHaveTheWord(self, value): + """Set the DoesNotHaveTheWord value of this Filter object. + + Args: + value: string The new DoesNotHaveTheWord value to give this object. + """ + + self._SetProperty(FILTER_DOES_NOT_HAVE_THE_WORD_NAME, value) + + does_not_have_the_word = pyproperty(GetDoesNotHaveTheWord, + SetDoesNotHaveTheWord) + + def GetHasAttachments(self): + """Get the HasAttachments value of the Filter object. + + Returns: + The HasAttachments value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_HAS_ATTACHMENTS_NAME) + + def SetHasAttachments(self, value): + """Set the HasAttachments value of this Filter object. + + Args: + value: string The new HasAttachments value to give this object. + """ + + self._SetProperty(FILTER_HAS_ATTACHMENTS_NAME, value) + + has_attachments = pyproperty(GetHasAttachments, + SetHasAttachments) + + def GetLabel(self): + """Get the Label value of the Filter object. + + Returns: + The Label value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_LABEL) + + def SetLabel(self, value): + """Set the Label value of this Filter object. + + Args: + value: string The new Label value to give this object. + """ + + self._SetProperty(FILTER_LABEL, value) + + label = pyproperty(GetLabel, SetLabel) + + def GetMarkAsRead(self): + """Get the MarkAsRead value of the Filter object. + + Returns: + The MarkAsRead value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_MARK_AS_READ) + + def SetMarkAsRead(self, value): + """Set the MarkAsRead value of this Filter object. + + Args: + value: string The new MarkAsRead value to give this object. + """ + + self._SetProperty(FILTER_MARK_AS_READ, value) + + mark_as_read = pyproperty(GetMarkAsRead, SetMarkAsRead) + + def GetArchive(self): + """Get the Archive value of the Filter object. + + Returns: + The Archive value of this Filter object as a string or None. + """ + + return self._GetProperty(FILTER_ARCHIVE) + + def SetArchive(self, value): + """Set the Archive value of this Filter object. + + Args: + value: string The new Archive value to give this object. + """ + + self._SetProperty(FILTER_ARCHIVE, value) + + archive = pyproperty(GetArchive, SetArchive) + + def __init__(self, uri=None, from_address=None, to_address=None, + subject=None, has_the_word=None, does_not_have_the_word=None, + has_attachments=None, label=None, mark_as_read=None, + archive=None, *args, **kwargs): + """Constructs a new EmailSettingsFilter object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + from_address: string (optional) The source email address for the filter. + to_address: string (optional) The destination email address for + the filter. + subject: string (optional) The value the email must have in its + subject to be filtered. + has_the_word: string (optional) The value the email must have in its + subject or body to be filtered. + does_not_have_the_word: string (optional) The value the email cannot + have in its subject or body to be filtered. + has_attachments: Boolean (optional) Whether or not the email must + have an attachment to be filtered. + label: string (optional) The name of the label to apply to + messages matching the filter criteria. + mark_as_read: Boolean (optional) Whether or not to mark messages + matching the filter criteria as read. + archive: Boolean (optional) Whether or not to move messages + matching to Archived state. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsFilter, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if from_address: + self.from_address = from_address + if to_address: + self.to_address = to_address + if subject: + self.subject = subject + if has_the_word: + self.has_the_word = has_the_word + if does_not_have_the_word: + self.does_not_have_the_word = does_not_have_the_word + if has_attachments is not None: + self.has_attachments = str(has_attachments) + if label: + self.label = label + if mark_as_read is not None: + self.mark_as_read = str(mark_as_read) + if archive is not None: + self.archive = str(archive) + + +class EmailSettingsSendAsAlias(EmailSettingsEntry): + """Represents an Email Settings send-as Alias in object form.""" + + def GetName(self): + """Get the Name of the send-as Alias object. + + Returns: + The Name of this send-as Alias object as a string or None. + """ + + return self._GetProperty(SENDAS_ALIAS_NAME) + + def SetName(self, value): + """Set the Name of this send-as Alias object. + + Args: + value: string The new Name to give this object. + """ + + self._SetProperty(SENDAS_ALIAS_NAME, value) + + name = pyproperty(GetName, SetName) + + def GetAddress(self): + """Get the Address of the send-as Alias object. + + Returns: + The Address of this send-as Alias object as a string or None. + """ + + return self._GetProperty(SENDAS_ALIAS_ADDRESS) + + def SetAddress(self, value): + """Set the Address of this send-as Alias object. + + Args: + value: string The new Address to give this object. + """ + + self._SetProperty(SENDAS_ALIAS_ADDRESS, value) + + address = pyproperty(GetAddress, SetAddress) + + def GetReplyTo(self): + """Get the ReplyTo address of the send-as Alias object. + + Returns: + The ReplyTo address of this send-as Alias object as a string or None. + """ + + return self._GetProperty(SENDAS_ALIAS_REPLY_TO) + + def SetReplyTo(self, value): + """Set the ReplyTo address of this send-as Alias object. + + Args: + value: string The new ReplyTo address to give this object. + """ + + self._SetProperty(SENDAS_ALIAS_REPLY_TO, value) + + reply_to = pyproperty(GetReplyTo, SetReplyTo) + + def GetMakeDefault(self): + """Get the MakeDefault value of the send-as Alias object. + + Returns: + The MakeDefault value of this send-as Alias object as a string or None. + """ + + return self._GetProperty(SENDAS_ALIAS_MAKE_DEFAULT) + + def SetMakeDefault(self, value): + """Set the MakeDefault value of this send-as Alias object. + + Args: + value: string The new MakeDefault valueto give this object.WebClip + """ + + self._SetProperty(SENDAS_ALIAS_MAKE_DEFAULT, value) + + make_default = pyproperty(GetMakeDefault, SetMakeDefault) + + def __init__(self, uri=None, name=None, address=None, reply_to=None, + make_default=None, *args, **kwargs): + """Constructs a new EmailSettingsSendAsAlias object with the given + arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + name: string (optional) The name that will appear in the "From" field + for this user. + address: string (optional) The email address that appears as the + origination address for emails sent by this user. + reply_to: string (optional) The address to be used as the reply-to + address in email sent using the alias. + make_default: Boolean (optional) Whether or not this alias should + become the default alias for this user. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsSendAsAlias, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if name: + self.name = name + if address: + self.address = address + if reply_to: + self.reply_to = reply_to + if make_default is not None: + self.make_default = str(make_default) + + +class EmailSettingsWebClip(EmailSettingsEntry): + """Represents a WebClip in object form.""" + + def GetEnable(self): + """Get the Enable value of the WebClip object. + + Returns: + The Enable value of this WebClip object as a string or None. + """ + + return self._GetProperty(WEBCLIP_ENABLE) + + def SetEnable(self, value): + """Set the Enable value of this WebClip object. + + Args: + value: string The new Enable value to give this object. + """ + + self._SetProperty(WEBCLIP_ENABLE, value) + + enable = pyproperty(GetEnable, SetEnable) + + def __init__(self, uri=None, enable=None, *args, **kwargs): + """Constructs a new EmailSettingsWebClip object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + enable: Boolean (optional) Whether to enable showing Web clips. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsWebClip, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if enable is not None: + self.enable = str(enable) + + +class EmailSettingsForwarding(EmailSettingsEntry): + """Represents Forwarding settings in object form.""" + + def GetEnable(self): + """Get the Enable value of the Forwarding object. + + Returns: + The Enable value of this Forwarding object as a string or None. + """ + + return self._GetProperty(FORWARDING_ENABLE) + + def SetEnable(self, value): + """Set the Enable value of this Forwarding object. + + Args: + value: string The new Enable value to give this object. + """ + + self._SetProperty(FORWARDING_ENABLE, value) + + enable = pyproperty(GetEnable, SetEnable) + + def GetForwardTo(self): + """Get the ForwardTo value of the Forwarding object. + + Returns: + The ForwardTo value of this Forwarding object as a string or None. + """ + + return self._GetProperty(FORWARDING_TO) + + def SetForwardTo(self, value): + """Set the ForwardTo value of this Forwarding object. + + Args: + value: string The new ForwardTo value to give this object. + """ + + self._SetProperty(FORWARDING_TO, value) + + forward_to = pyproperty(GetForwardTo, SetForwardTo) + + def GetAction(self): + """Get the Action value of the Forwarding object. + + Returns: + The Action value of this Forwarding object as a string or None. + """ + + return self._GetProperty(FORWARDING_ACTION) + + def SetAction(self, value): + """Set the Action value of this Forwarding object. + + Args: + value: string The new Action value to give this object. + """ + + self._SetProperty(FORWARDING_ACTION, value) + + action = pyproperty(GetAction, SetAction) + + def __init__(self, uri=None, enable=None, forward_to=None, action=None, + *args, **kwargs): + """Constructs a new EmailSettingsForwarding object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + enable: Boolean (optional) Whether to enable incoming email forwarding. + forward_to: string (optional) The address email will be forwarded to. + action: string (optional) The action to perform after forwarding an + email ("KEEP", "ARCHIVE", "DELETE"). + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsForwarding, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if enable is not None: + self.enable = str(enable) + if forward_to: + self.forward_to = forward_to + if action: + self.action = action + + +class EmailSettingsPop(EmailSettingsEntry): + """Represents POP settings in object form.""" + + def GetEnable(self): + """Get the Enable value of the POP object. + + Returns: + The Enable value of this POP object as a string or None. + """ + + return self._GetProperty(POP_ENABLE) + + def SetEnable(self, value): + """Set the Enable value of this POP object. + + Args: + value: string The new Enable value to give this object. + """ + + self._SetProperty(POP_ENABLE, value) + + enable = pyproperty(GetEnable, SetEnable) + + def GetEnableFor(self): + """Get the EnableFor value of the POP object. + + Returns: + The EnableFor value of this POP object as a string or None. + """ + + return self._GetProperty(POP_ENABLE_FOR) + + def SetEnableFor(self, value): + """Set the EnableFor value of this POP object. + + Args: + value: string The new EnableFor value to give this object. + """ + + self._SetProperty(POP_ENABLE_FOR, value) + + enable_for = pyproperty(GetEnableFor, SetEnableFor) + + def GetPopAction(self): + """Get the Action value of the POP object. + + Returns: + The Action value of this POP object as a string or None. + """ + + return self._GetProperty(POP_ACTION) + + def SetPopAction(self, value): + """Set the Action value of this POP object. + + Args: + value: string The new Action value to give this object. + """ + + self._SetProperty(POP_ACTION, value) + + action = pyproperty(GetPopAction, SetPopAction) + + def __init__(self, uri=None, enable=None, enable_for=None, + action=None, *args, **kwargs): + """Constructs a new EmailSettingsPOP object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + enable: Boolean (optional) Whether to enable incoming POP3 access. + enable_for: string (optional) Whether to enable POP3 for all mail + ("ALL_MAIL"), or mail from now on ("MAIL_FROM_NOW_ON"). + action: string (optional) What Google Mail should do with its copy + of the email after it is retrieved using POP + ("KEEP", "ARCHIVE", or "DELETE"). + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsPop, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if enable is not None: + self.enable = str(enable) + if enable_for: + self.enable_for = enable_for + if action: + self.action = action + + +class EmailSettingsImap(EmailSettingsEntry): + """Represents IMAP settings in object form.""" + + def GetEnable(self): + """Get the Enable value of the IMAP object. + + Returns: + The Enable value of this IMAP object as a string or None. + """ + + return self._GetProperty(IMAP_ENABLE) + + def SetEnable(self, value): + """Set the Enable value of this IMAP object. + + Args: + value: string The new Enable value to give this object. + """ + + self._SetProperty(IMAP_ENABLE, value) + + enable = pyproperty(GetEnable, SetEnable) + + def __init__(self, uri=None, enable=None, *args, **kwargs): + """Constructs a new EmailSettingsImap object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + enable: Boolean (optional) Whether to enable IMAP access. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsImap, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if enable is not None: + self.enable = str(enable) + + +class EmailSettingsVacationResponder(EmailSettingsEntry): + """Represents Vacation Responder settings in object form.""" + + def GetEnable(self): + """Get the Enable value of the Vacation Responder object. + + Returns: + The Enable value of this Vacation Responder object as a string or None. + """ + + return self._GetProperty(VACATION_RESPONDER_ENABLE) + + def SetEnable(self, value): + """Set the Enable value of this Vacation Responder object. + + Args: + value: string The new Enable value to give this object. + """ + + self._SetProperty(VACATION_RESPONDER_ENABLE, value) + + enable = pyproperty(GetEnable, SetEnable) + + def GetSubject(self): + """Get the Subject value of the Vacation Responder object. + + Returns: + The Subject value of this Vacation Responder object as a string or None. + """ + + return self._GetProperty(VACATION_RESPONDER_SUBJECT) + + def SetSubject(self, value): + """Set the Subject value of this Vacation Responder object. + + Args: + value: string The new Subject value to give this object. + """ + + self._SetProperty(VACATION_RESPONDER_SUBJECT, value) + + subject = pyproperty(GetSubject, SetSubject) + + def GetMessage(self): + """Get the Message value of the Vacation Responder object. + + Returns: + The Message value of this Vacation Responder object as a string or None. + """ + + return self._GetProperty(VACATION_RESPONDER_MESSAGE) + + def SetMessage(self, value): + """Set the Message value of this Vacation Responder object. + + Args: + value: string The new Message value to give this object. + """ + + self._SetProperty(VACATION_RESPONDER_MESSAGE, value) + + message = pyproperty(GetMessage, SetMessage) + + def GetContactsOnly(self): + """Get the ContactsOnly value of the Vacation Responder object. + + Returns: + The ContactsOnly value of this Vacation Responder object as a + string or None. + """ + + return self._GetProperty(VACATION_RESPONDER_CONTACTS_ONLY) + + def SetContactsOnly(self, value): + """Set the ContactsOnly value of this Vacation Responder object. + + Args: + value: string The new ContactsOnly value to give this object. + """ + + self._SetProperty(VACATION_RESPONDER_CONTACTS_ONLY, value) + + contacts_only = pyproperty(GetContactsOnly, SetContactsOnly) + + def __init__(self, uri=None, enable=None, subject=None, + message=None, contacts_only=None, *args, **kwargs): + """Constructs a new EmailSettingsVacationResponder object with the + given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + enable: Boolean (optional) Whether to enable the vacation responder. + subject: string (optional) The subject line of the vacation responder + autoresponse. + message: string (optional) The message body of the vacation responder + autoresponse. + contacts_only: Boolean (optional) Whether to only send autoresponses + to known contacts. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsVacationResponder, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if enable is not None: + self.enable = str(enable) + if subject: + self.subject = subject + if message: + self.message = message + if contacts_only is not None: + self.contacts_only = str(contacts_only) + + +class EmailSettingsSignature(EmailSettingsEntry): + """Represents a Signature in object form.""" + + def GetValue(self): + """Get the value of the Signature object. + + Returns: + The value of this Signature object as a string or None. + """ + + value = self._GetProperty(SIGNATURE_VALUE) + if value == ' ': # hack to support empty signature + return '' + else: + return value + + def SetValue(self, value): + """Set the name of this Signature object. + + Args: + value: string The new signature value to give this object. + """ + + if value == '': # hack to support empty signature + value = ' ' + self._SetProperty(SIGNATURE_VALUE, value) + + signature_value = pyproperty(GetValue, SetValue) + + def __init__(self, uri=None, signature=None, *args, **kwargs): + """Constructs a new EmailSettingsSignature object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + signature: string (optional) The signature to be appended to outgoing + messages. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsSignature, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if signature is not None: + self.signature_value = signature + + +class EmailSettingsLanguage(EmailSettingsEntry): + """Represents Language Settings in object form.""" + + def GetLanguage(self): + """Get the tag of the Language object. + + Returns: + The tag of this Language object as a string or None. + """ + + return self._GetProperty(LANGUAGE_TAG) + + def SetLanguage(self, value): + """Set the tag of this Language object. + + Args: + value: string The new tag value to give this object. + """ + + self._SetProperty(LANGUAGE_TAG, value) + + language_tag = pyproperty(GetLanguage, SetLanguage) + + def __init__(self, uri=None, language=None, *args, **kwargs): + """Constructs a new EmailSettingsLanguage object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + language: string (optional) The language tag for Google Mail's display + language. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsLanguage, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if language: + self.language_tag = language + + +class EmailSettingsGeneral(EmailSettingsEntry): + """Represents General Settings in object form.""" + + def GetPageSize(self): + """Get the Page Size value of the General Settings object. + + Returns: + The Page Size value of this General Settings object as a string or None. + """ + + return self._GetProperty(GENERAL_PAGE_SIZE) + + def SetPageSize(self, value): + """Set the Page Size value of this General Settings object. + + Args: + value: string The new Page Size value to give this object. + """ + + self._SetProperty(GENERAL_PAGE_SIZE, value) + + page_size = pyproperty(GetPageSize, SetPageSize) + + def GetShortcuts(self): + """Get the Shortcuts value of the General Settings object. + + Returns: + The Shortcuts value of this General Settings object as a string or None. + """ + + return self._GetProperty(GENERAL_SHORTCUTS) + + def SetShortcuts(self, value): + """Set the Shortcuts value of this General Settings object. + + Args: + value: string The new Shortcuts value to give this object. + """ + + self._SetProperty(GENERAL_SHORTCUTS, value) + + shortcuts = pyproperty(GetShortcuts, SetShortcuts) + + def GetArrows(self): + """Get the Arrows value of the General Settings object. + + Returns: + The Arrows value of this General Settings object as a string or None. + """ + + return self._GetProperty(GENERAL_ARROWS) + + def SetArrows(self, value): + """Set the Arrows value of this General Settings object. + + Args: + value: string The new Arrows value to give this object. + """ + + self._SetProperty(GENERAL_ARROWS, value) + + arrows = pyproperty(GetArrows, SetArrows) + + def GetSnippets(self): + """Get the Snippets value of the General Settings object. + + Returns: + The Snippets value of this General Settings object as a string or None. + """ + + return self._GetProperty(GENERAL_SNIPPETS) + + def SetSnippets(self, value): + """Set the Snippets value of this General Settings object. + + Args: + value: string The new Snippets value to give this object. + """ + + self._SetProperty(GENERAL_SNIPPETS, value) + + snippets = pyproperty(GetSnippets, SetSnippets) + + def GetUnicode(self): + """Get the Unicode value of the General Settings object. + + Returns: + The Unicode value of this General Settings object as a string or None. + """ + + return self._GetProperty(GENERAL_UNICODE) + + def SetUnicode(self, value): + """Set the Unicode value of this General Settings object. + + Args: + value: string The new Unicode value to give this object. + """ + + self._SetProperty(GENERAL_UNICODE, value) + + use_unicode = pyproperty(GetUnicode, SetUnicode) + + def __init__(self, uri=None, page_size=None, shortcuts=None, + arrows=None, snippets=None, use_unicode=None, *args, **kwargs): + """Constructs a new EmailSettingsGeneral object with the given arguments. + + Args: + uri: string (optional) The uri of of this object for HTTP requests. + page_size: int (optional) The number of conversations to be shown per page. + shortcuts: Boolean (optional) Whether to enable keyboard shortcuts. + arrows: Boolean (optional) Whether to display arrow-shaped personal + indicators next to email sent specifically to the user. + snippets: Boolean (optional) Whether to display snippets of the messages + in the inbox and when searching. + use_unicode: Boolean (optional) Whether to use UTF-8 (unicode) encoding + for all outgoing messages. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(EmailSettingsGeneral, self).__init__(*args, **kwargs) + if uri: + self.uri = uri + if page_size is not None: + self.page_size = str(page_size) + if shortcuts is not None: + self.shortcuts = str(shortcuts) + if arrows is not None: + self.arrows = str(arrows) + if snippets is not None: + self.snippets = str(snippets) + if use_unicode is not None: + self.use_unicode = str(use_unicode) diff --git a/patches/gdata/apps/emailsettings/service.py b/patches/gdata/apps/emailsettings/service.py new file mode 100644 index 0000000..cab61ea --- /dev/null +++ b/patches/gdata/apps/emailsettings/service.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Allow Google Apps domain administrators to set users' email settings. + + EmailSettingsService: Set various email settings. +""" + +__author__ = 'google-apps-apis@googlegroups.com' + + +import gdata.apps +import gdata.apps.service +import gdata.service + + +API_VER='2.0' +# Forwarding and POP3 options +KEEP='KEEP' +ARCHIVE='ARCHIVE' +DELETE='DELETE' +ALL_MAIL='ALL_MAIL' +MAIL_FROM_NOW_ON='MAIL_FROM_NOW_ON' + + +class EmailSettingsService(gdata.apps.service.PropertyService): + """Client for the Google Apps Email Settings service.""" + + def _serviceUrl(self, setting_id, username, domain=None): + if domain is None: + domain = self.domain + return '/a/feeds/emailsettings/%s/%s/%s/%s' % (API_VER, domain, username, + setting_id) + + def CreateLabel(self, username, label): + """Create a label. + + Args: + username: User to create label for. + label: Label to create. + + Returns: + A dict containing the result of the create operation. + """ + uri = self._serviceUrl('label', username) + properties = {'label': label} + return self._PostProperties(uri, properties) + + def CreateFilter(self, username, from_=None, to=None, subject=None, + has_the_word=None, does_not_have_the_word=None, + has_attachment=None, label=None, should_mark_as_read=None, + should_archive=None): + """Create a filter. + + Args: + username: User to create filter for. + from_: Filter from string. + to: Filter to string. + subject: Filter subject. + has_the_word: Words to filter in. + does_not_have_the_word: Words to filter out. + has_attachment: Boolean for message having attachment. + label: Label to apply. + should_mark_as_read: Boolean for marking message as read. + should_archive: Boolean for archiving message. + + Returns: + A dict containing the result of the create operation. + """ + uri = self._serviceUrl('filter', username) + properties = {} + properties['from'] = from_ + properties['to'] = to + properties['subject'] = subject + properties['hasTheWord'] = has_the_word + properties['doesNotHaveTheWord'] = does_not_have_the_word + properties['hasAttachment'] = gdata.apps.service._bool2str(has_attachment) + properties['label'] = label + properties['shouldMarkAsRead'] = gdata.apps.service._bool2str(should_mark_as_read) + properties['shouldArchive'] = gdata.apps.service._bool2str(should_archive) + return self._PostProperties(uri, properties) + + def CreateSendAsAlias(self, username, name, address, reply_to=None, + make_default=None): + """Create alias to send mail as. + + Args: + username: User to create alias for. + name: Name of alias. + address: Email address to send from. + reply_to: Email address to reply to. + make_default: Boolean for whether this is the new default sending alias. + + Returns: + A dict containing the result of the create operation. + """ + uri = self._serviceUrl('sendas', username) + properties = {} + properties['name'] = name + properties['address'] = address + properties['replyTo'] = reply_to + properties['makeDefault'] = gdata.apps.service._bool2str(make_default) + return self._PostProperties(uri, properties) + + def UpdateWebClipSettings(self, username, enable): + """Update WebClip Settings + + Args: + username: User to update forwarding for. + enable: Boolean whether to enable Web Clip. + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('webclip', username) + properties = {} + properties['enable'] = gdata.apps.service._bool2str(enable) + return self._PutProperties(uri, properties) + + def UpdateForwarding(self, username, enable, forward_to=None, action=None): + """Update forwarding settings. + + Args: + username: User to update forwarding for. + enable: Boolean whether to enable this forwarding rule. + forward_to: Email address to forward to. + action: Action to take after forwarding. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('forwarding', username) + properties = {} + properties['enable'] = gdata.apps.service._bool2str(enable) + if enable is True: + properties['forwardTo'] = forward_to + properties['action'] = action + return self._PutProperties(uri, properties) + + def UpdatePop(self, username, enable, enable_for=None, action=None): + """Update POP3 settings. + + Args: + username: User to update POP3 settings for. + enable: Boolean whether to enable POP3. + enable_for: Which messages to make available via POP3. + action: Action to take after user retrieves email via POP3. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('pop', username) + properties = {} + properties['enable'] = gdata.apps.service._bool2str(enable) + if enable is True: + properties['enableFor'] = enable_for + properties['action'] = action + return self._PutProperties(uri, properties) + + def UpdateImap(self, username, enable): + """Update IMAP settings. + + Args: + username: User to update IMAP settings for. + enable: Boolean whether to enable IMAP. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('imap', username) + properties = {'enable': gdata.apps.service._bool2str(enable)} + return self._PutProperties(uri, properties) + + def UpdateVacation(self, username, enable, subject=None, message=None, + contacts_only=None): + """Update vacation settings. + + Args: + username: User to update vacation settings for. + enable: Boolean whether to enable vacation responses. + subject: Vacation message subject. + message: Vacation message body. + contacts_only: Boolean whether to send message only to contacts. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('vacation', username) + properties = {} + properties['enable'] = gdata.apps.service._bool2str(enable) + if enable is True: + properties['subject'] = subject + properties['message'] = message + properties['contactsOnly'] = gdata.apps.service._bool2str(contacts_only) + return self._PutProperties(uri, properties) + + def UpdateSignature(self, username, signature): + """Update signature. + + Args: + username: User to update signature for. + signature: Signature string. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('signature', username) + properties = {'signature': signature} + return self._PutProperties(uri, properties) + + def UpdateLanguage(self, username, language): + """Update user interface language. + + Args: + username: User to update language for. + language: Language code. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('language', username) + properties = {'language': language} + return self._PutProperties(uri, properties) + + def UpdateGeneral(self, username, page_size=None, shortcuts=None, arrows=None, + snippets=None, unicode=None): + """Update general settings. + + Args: + username: User to update general settings for. + page_size: Number of messages to show. + shortcuts: Boolean whether shortcuts are enabled. + arrows: Boolean whether arrows are enabled. + snippets: Boolean whether snippets are enabled. + unicode: Wheter unicode is enabled. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._serviceUrl('general', username) + properties = {} + if page_size != None: + properties['pageSize'] = str(page_size) + if shortcuts != None: + properties['shortcuts'] = gdata.apps.service._bool2str(shortcuts) + if arrows != None: + properties['arrows'] = gdata.apps.service._bool2str(arrows) + if snippets != None: + properties['snippets'] = gdata.apps.service._bool2str(snippets) + if unicode != None: + properties['unicode'] = gdata.apps.service._bool2str(unicode) + return self._PutProperties(uri, properties) diff --git a/patches/gdata/apps/groups/__init__.py b/patches/gdata/apps/groups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patches/gdata/apps/groups/service.py b/patches/gdata/apps/groups/service.py new file mode 100644 index 0000000..80df417 --- /dev/null +++ b/patches/gdata/apps/groups/service.py @@ -0,0 +1,387 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Allow Google Apps domain administrators to manage groups, group members and group owners. + + GroupsService: Provides methods to manage groups, members and owners. +""" + +__author__ = 'google-apps-apis@googlegroups.com' + + +import urllib +import gdata.apps +import gdata.apps.service +import gdata.service + + +API_VER = '2.0' +BASE_URL = '/a/feeds/group/' + API_VER + '/%s' +GROUP_MEMBER_URL = BASE_URL + '?member=%s' +GROUP_MEMBER_DIRECT_URL = GROUP_MEMBER_URL + '&directOnly=%s' +GROUP_ID_URL = BASE_URL + '/%s' +MEMBER_URL = BASE_URL + '/%s/member' +MEMBER_WITH_SUSPENDED_URL = MEMBER_URL + '?includeSuspendedUsers=%s' +MEMBER_ID_URL = MEMBER_URL + '/%s' +OWNER_URL = BASE_URL + '/%s/owner' +OWNER_WITH_SUSPENDED_URL = OWNER_URL + '?includeSuspendedUsers=%s' +OWNER_ID_URL = OWNER_URL + '/%s' + +PERMISSION_OWNER = 'Owner' +PERMISSION_MEMBER = 'Member' +PERMISSION_DOMAIN = 'Domain' +PERMISSION_ANYONE = 'Anyone' + + +class GroupsService(gdata.apps.service.PropertyService): + """Client for the Google Apps Groups service.""" + + def _ServiceUrl(self, service_type, is_existed, group_id, member_id, owner_email, + direct_only=False, domain=None, suspended_users=False): + if domain is None: + domain = self.domain + + if service_type == 'group': + if group_id != '' and is_existed: + return GROUP_ID_URL % (domain, group_id) + elif member_id != '': + if direct_only: + return GROUP_MEMBER_DIRECT_URL % (domain, urllib.quote_plus(member_id), + self._Bool2Str(direct_only)) + else: + return GROUP_MEMBER_URL % (domain, urllib.quote_plus(member_id)) + else: + return BASE_URL % (domain) + + if service_type == 'member': + if member_id != '' and is_existed: + return MEMBER_ID_URL % (domain, group_id, urllib.quote_plus(member_id)) + elif suspended_users: + return MEMBER_WITH_SUSPENDED_URL % (domain, group_id, + self._Bool2Str(suspended_users)) + else: + return MEMBER_URL % (domain, group_id) + + if service_type == 'owner': + if owner_email != '' and is_existed: + return OWNER_ID_URL % (domain, group_id, urllib.quote_plus(owner_email)) + elif suspended_users: + return OWNER_WITH_SUSPENDED_URL % (domain, group_id, + self._Bool2Str(suspended_users)) + else: + return OWNER_URL % (domain, group_id) + + def _Bool2Str(self, b): + if b is None: + return None + return str(b is True).lower() + + def _IsExisted(self, uri): + try: + self._GetProperties(uri) + return True + except gdata.apps.service.AppsForYourDomainException, e: + if e.error_code == gdata.apps.service.ENTITY_DOES_NOT_EXIST: + return False + else: + raise e + + def CreateGroup(self, group_id, group_name, description, email_permission): + """Create a group. + + Args: + group_id: The ID of the group (e.g. us-sales). + group_name: The name of the group. + description: A description of the group + email_permission: The subscription permission of the group. + + Returns: + A dict containing the result of the create operation. + """ + uri = self._ServiceUrl('group', False, group_id, '', '') + properties = {} + properties['groupId'] = group_id + properties['groupName'] = group_name + properties['description'] = description + properties['emailPermission'] = email_permission + return self._PostProperties(uri, properties) + + def UpdateGroup(self, group_id, group_name, description, email_permission): + """Update a group's name, description and/or permission. + + Args: + group_id: The ID of the group (e.g. us-sales). + group_name: The name of the group. + description: A description of the group + email_permission: The subscription permission of the group. + + Returns: + A dict containing the result of the update operation. + """ + uri = self._ServiceUrl('group', True, group_id, '', '') + properties = {} + properties['groupId'] = group_id + properties['groupName'] = group_name + properties['description'] = description + properties['emailPermission'] = email_permission + return self._PutProperties(uri, properties) + + def RetrieveGroup(self, group_id): + """Retrieve a group based on its ID. + + Args: + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('group', True, group_id, '', '') + return self._GetProperties(uri) + + def RetrieveAllGroups(self): + """Retrieve all groups in the domain. + + Args: + None + + Returns: + A list containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('group', True, '', '', '') + return self._GetPropertiesList(uri) + + def RetrievePageOfGroups(self, start_group=None): + """Retrieve one page of groups in the domain. + + Args: + start_group: The key to continue for pagination through all groups. + + Returns: + A feed object containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('group', True, '', '', '') + if start_group is not None: + uri += "?start="+start_group + property_feed = self._GetPropertyFeed(uri) + return property_feed + + def RetrieveGroups(self, member_id, direct_only=False): + """Retrieve all groups that belong to the given member_id. + + Args: + member_id: The member's email address (e.g. member@example.com). + direct_only: Boolean whether only return groups that this member directly belongs to. + + Returns: + A list containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('group', True, '', member_id, '', direct_only=direct_only) + return self._GetPropertiesList(uri) + + def DeleteGroup(self, group_id): + """Delete a group based on its ID. + + Args: + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the delete operation. + """ + uri = self._ServiceUrl('group', True, group_id, '', '') + return self._DeleteProperties(uri) + + def AddMemberToGroup(self, member_id, group_id): + """Add a member to a group. + + Args: + member_id: The member's email address (e.g. member@example.com). + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the add operation. + """ + uri = self._ServiceUrl('member', False, group_id, member_id, '') + properties = {} + properties['memberId'] = member_id + return self._PostProperties(uri, properties) + + def IsMember(self, member_id, group_id): + """Check whether the given member already exists in the given group. + + Args: + member_id: The member's email address (e.g. member@example.com). + group_id: The ID of the group (e.g. us-sales). + + Returns: + True if the member exists in the group. False otherwise. + """ + uri = self._ServiceUrl('member', True, group_id, member_id, '') + return self._IsExisted(uri) + + def RetrieveMember(self, member_id, group_id): + """Retrieve the given member in the given group. + + Args: + member_id: The member's email address (e.g. member@example.com). + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('member', True, group_id, member_id, '') + return self._GetProperties(uri) + + def RetrieveAllMembers(self, group_id, suspended_users=False): + """Retrieve all members in the given group. + + Args: + group_id: The ID of the group (e.g. us-sales). + suspended_users: A boolean; should we include any suspended users in + the membership list returned? + + Returns: + A list containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('member', True, group_id, '', '', + suspended_users=suspended_users) + return self._GetPropertiesList(uri) + + def RetrievePageOfMembers(self, group_id, suspended_users=False, start=None): + """Retrieve one page of members of a given group. + + Args: + group_id: The ID of the group (e.g. us-sales). + suspended_users: A boolean; should we include any suspended users in + the membership list returned? + start: The key to continue for pagination through all members. + + Returns: + A feed object containing the result of the retrieve operation. + """ + + uri = self._ServiceUrl('member', True, group_id, '', '', + suspended_users=suspended_users) + if start is not None: + if suspended_users: + uri += "&start="+start + else: + uri += "?start="+start + property_feed = self._GetPropertyFeed(uri) + return property_feed + + def RemoveMemberFromGroup(self, member_id, group_id): + """Remove the given member from the given group. + + Args: + member_id: The member's email address (e.g. member@example.com). + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the remove operation. + """ + uri = self._ServiceUrl('member', True, group_id, member_id, '') + return self._DeleteProperties(uri) + + def AddOwnerToGroup(self, owner_email, group_id): + """Add an owner to a group. + + Args: + owner_email: The email address of a group owner. + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the add operation. + """ + uri = self._ServiceUrl('owner', False, group_id, '', owner_email) + properties = {} + properties['email'] = owner_email + return self._PostProperties(uri, properties) + + def IsOwner(self, owner_email, group_id): + """Check whether the given member an owner of the given group. + + Args: + owner_email: The email address of a group owner. + group_id: The ID of the group (e.g. us-sales). + + Returns: + True if the member is an owner of the given group. False otherwise. + """ + uri = self._ServiceUrl('owner', True, group_id, '', owner_email) + return self._IsExisted(uri) + + def RetrieveOwner(self, owner_email, group_id): + """Retrieve the given owner in the given group. + + Args: + owner_email: The email address of a group owner. + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('owner', True, group_id, '', owner_email) + return self._GetProperties(uri) + + def RetrieveAllOwners(self, group_id, suspended_users=False): + """Retrieve all owners of the given group. + + Args: + group_id: The ID of the group (e.g. us-sales). + suspended_users: A boolean; should we include any suspended users in + the ownership list returned? + + Returns: + A list containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('owner', True, group_id, '', '', + suspended_users=suspended_users) + return self._GetPropertiesList(uri) + + def RetrievePageOfOwners(self, group_id, suspended_users=False, start=None): + """Retrieve one page of owners of the given group. + + Args: + group_id: The ID of the group (e.g. us-sales). + suspended_users: A boolean; should we include any suspended users in + the ownership list returned? + start: The key to continue for pagination through all owners. + + Returns: + A feed object containing the result of the retrieve operation. + """ + uri = self._ServiceUrl('owner', True, group_id, '', '', + suspended_users=suspended_users) + if start is not None: + if suspended_users: + uri += "&start="+start + else: + uri += "?start="+start + property_feed = self._GetPropertyFeed(uri) + return property_feed + + def RemoveOwnerFromGroup(self, owner_email, group_id): + """Remove the given owner from the given group. + + Args: + owner_email: The email address of a group owner. + group_id: The ID of the group (e.g. us-sales). + + Returns: + A dict containing the result of the remove operation. + """ + uri = self._ServiceUrl('owner', True, group_id, '', owner_email) + return self._DeleteProperties(uri) diff --git a/patches/gdata/apps/migration/__init__.py b/patches/gdata/apps/migration/__init__.py new file mode 100644 index 0000000..9892671 --- /dev/null +++ b/patches/gdata/apps/migration/__init__.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Google +# +# 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. + +"""Contains objects used with Google Apps.""" + +__author__ = 'google-apps-apis@googlegroups.com' + + +import atom +import gdata + + +# XML namespaces which are often used in Google Apps entity. +APPS_NAMESPACE = 'http://schemas.google.com/apps/2006' +APPS_TEMPLATE = '{http://schemas.google.com/apps/2006}%s' + + +class Rfc822Msg(atom.AtomBase): + """The Migration rfc822Msg element.""" + + _tag = 'rfc822Msg' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['encoding'] = 'encoding' + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.encoding = 'base64' + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def Rfc822MsgFromString(xml_string): + """Parse in the Rrc822 message from the XML definition.""" + + return atom.CreateClassFromXMLString(Rfc822Msg, xml_string) + + +class MailItemProperty(atom.AtomBase): + """The Migration mailItemProperty element.""" + + _tag = 'mailItemProperty' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def MailItemPropertyFromString(xml_string): + """Parse in the MailItemProperiy from the XML definition.""" + + return atom.CreateClassFromXMLString(MailItemProperty, xml_string) + + +class Label(atom.AtomBase): + """The Migration label element.""" + + _tag = 'label' + _namespace = APPS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['labelName'] = 'label_name' + + def __init__(self, label_name=None, + extension_elements=None, extension_attributes=None, + text=None): + self.label_name = label_name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def LabelFromString(xml_string): + """Parse in the mailItemProperty from the XML definition.""" + + return atom.CreateClassFromXMLString(Label, xml_string) + + +class MailEntry(gdata.GDataEntry): + """A Google Migration flavor of an Atom Entry.""" + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}rfc822Msg' % APPS_NAMESPACE] = ('rfc822_msg', Rfc822Msg) + _children['{%s}mailItemProperty' % APPS_NAMESPACE] = ('mail_item_property', + [MailItemProperty]) + _children['{%s}label' % APPS_NAMESPACE] = ('label', [Label]) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + rfc822_msg=None, mail_item_property=None, label=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated) + self.rfc822_msg = rfc822_msg + self.mail_item_property = mail_item_property + self.label = label + self.extended_property = extended_property or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def MailEntryFromString(xml_string): + """Parse in the MailEntry from the XML definition.""" + + return atom.CreateClassFromXMLString(MailEntry, xml_string) + + +class BatchMailEntry(gdata.BatchEntry): + """A Google Migration flavor of an Atom Entry.""" + + _tag = gdata.BatchEntry._tag + _namespace = gdata.BatchEntry._namespace + _children = gdata.BatchEntry._children.copy() + _attributes = gdata.BatchEntry._attributes.copy() + _children['{%s}rfc822Msg' % APPS_NAMESPACE] = ('rfc822_msg', Rfc822Msg) + _children['{%s}mailItemProperty' % APPS_NAMESPACE] = ('mail_item_property', + [MailItemProperty]) + _children['{%s}label' % APPS_NAMESPACE] = ('label', [Label]) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + rfc822_msg=None, mail_item_property=None, label=None, + batch_operation=None, batch_id=None, batch_status=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.BatchEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + batch_operation=batch_operation, + batch_id=batch_id, batch_status=batch_status, + title=title, updated=updated) + self.rfc822_msg = rfc822_msg or None + self.mail_item_property = mail_item_property or [] + self.label = label or [] + self.extended_property = extended_property or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def BatchMailEntryFromString(xml_string): + """Parse in the BatchMailEntry from the XML definition.""" + + return atom.CreateClassFromXMLString(BatchMailEntry, xml_string) + + +class BatchMailEventFeed(gdata.BatchFeed): + """A Migration event feed flavor of an Atom Feed.""" + + _tag = gdata.BatchFeed._tag + _namespace = gdata.BatchFeed._namespace + _children = gdata.BatchFeed._children.copy() + _attributes = gdata.BatchFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BatchMailEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, interrupted=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.BatchFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + interrupted=interrupted, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def BatchMailEventFeedFromString(xml_string): + """Parse in the BatchMailEventFeed from the XML definition.""" + + return atom.CreateClassFromXMLString(BatchMailEventFeed, xml_string) diff --git a/patches/gdata/apps/migration/service.py b/patches/gdata/apps/migration/service.py new file mode 100644 index 0000000..6319995 --- /dev/null +++ b/patches/gdata/apps/migration/service.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Google. +# +# 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. + +"""Contains the methods to import mail via Google Apps Email Migration API. + + MigrationService: Provides methids to import mail. +""" + +__author__ = 'google-apps-apis@googlegroups.com' + + +import base64 +import gdata +import gdata.apps.service +import gdata.service +from gdata.apps import migration + + +API_VER = '2.0' + + +class MigrationService(gdata.apps.service.AppsService): + """Client for the EMAPI migration service. Use either ImportMail to import + one message at a time, or AddBatchEntry and SubmitBatch to import a batch of + messages at a time. + """ + def __init__(self, email=None, password=None, domain=None, source=None, + server='apps-apis.google.com', additional_headers=None): + gdata.apps.service.AppsService.__init__( + self, email=email, password=password, domain=domain, source=source, + server=server, additional_headers=additional_headers) + self.mail_batch = migration.BatchMailEventFeed() + + def _BaseURL(self): + return '/a/feeds/migration/%s/%s' % (API_VER, self.domain) + + def ImportMail(self, user_name, mail_message, mail_item_properties, + mail_labels): + """Import a single mail message. + + Args: + user_name: The username to import messages to. + mail_message: An RFC822 format email message. + mail_item_properties: A list of Gmail properties to apply to the message. + mail_labels: A list of labels to apply to the message. + + Returns: + A MailEntry representing the successfully imported message. + + Raises: + AppsForYourDomainException: An error occurred importing the message. + """ + uri = '%s/%s/mail' % (self._BaseURL(), user_name) + + mail_entry = migration.MailEntry() + mail_entry.rfc822_msg = migration.Rfc822Msg(text=(base64.b64encode( + mail_message))) + mail_entry.rfc822_msg.encoding = 'base64' + mail_entry.mail_item_property = map( + lambda x: migration.MailItemProperty(value=x), mail_item_properties) + mail_entry.label = map(lambda x: migration.Label(label_name=x), + mail_labels) + + try: + return migration.MailEntryFromString(str(self.Post(mail_entry, uri))) + except gdata.service.RequestError, e: + raise gdata.apps.service.AppsForYourDomainException(e.args[0]) + + def AddBatchEntry(self, mail_message, mail_item_properties, + mail_labels): + """Add a message to the current batch that you later will submit. + + Args: + mail_message: An RFC822 format email message. + mail_item_properties: A list of Gmail properties to apply to the message. + mail_labels: A list of labels to apply to the message. + + Returns: + The length of the MailEntry representing the message. + """ + mail_entry = migration.BatchMailEntry() + mail_entry.rfc822_msg = migration.Rfc822Msg(text=(base64.b64encode( + mail_message))) + mail_entry.rfc822_msg.encoding = 'base64' + mail_entry.mail_item_property = map( + lambda x: migration.MailItemProperty(value=x), mail_item_properties) + mail_entry.label = map(lambda x: migration.Label(label_name=x), + mail_labels) + + self.mail_batch.AddBatchEntry(mail_entry) + + return len(str(mail_entry)) + + def SubmitBatch(self, user_name): + """Send a all the mail items you have added to the batch to the server. + + Args: + user_name: The username to import messages to. + + Returns: + A HTTPResponse from the web service call. + + Raises: + AppsForYourDomainException: An error occurred importing the batch. + """ + uri = '%s/%s/mail/batch' % (self._BaseURL(), user_name) + + try: + self.result = self.Post(self.mail_batch, uri, + converter=migration.BatchMailEventFeedFromString) + except gdata.service.RequestError, e: + raise gdata.apps.service.AppsForYourDomainException(e.args[0]) + + self.mail_batch = migration.BatchMailEventFeed() + + return self.result diff --git a/patches/gdata/apps/organization/__init__.py b/patches/gdata/apps/organization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patches/gdata/apps/organization/service.py b/patches/gdata/apps/organization/service.py new file mode 100644 index 0000000..763a6bc --- /dev/null +++ b/patches/gdata/apps/organization/service.py @@ -0,0 +1,297 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Allow Google Apps domain administrators to manage organization unit and organization user. + + OrganizationService: Provides methods to manage organization unit and organization user. +""" + +__author__ = 'Alexandre Vivien (alex@simplecode.fr)' + + +import gdata.apps +import gdata.apps.service +import gdata.service + + +API_VER = '2.0' +CUSTOMER_BASE_URL = '/a/feeds/customer/2.0/customerId' +BASE_UNIT_URL = '/a/feeds/orgunit/' + API_VER + '/%s' +UNIT_URL = BASE_UNIT_URL + '/%s' +UNIT_ALL_URL = BASE_UNIT_URL + '?get=all' +UNIT_CHILD_URL = BASE_UNIT_URL + '?get=children&orgUnitPath=%s' +BASE_USER_URL = '/a/feeds/orguser/' + API_VER + '/%s' +USER_URL = BASE_USER_URL + '/%s' +USER_ALL_URL = BASE_USER_URL + '?get=all' +USER_CHILD_URL = BASE_USER_URL + '?get=children&orgUnitPath=%s' + + +class OrganizationService(gdata.apps.service.PropertyService): + """Client for the Google Apps Organizations service.""" + + def _Bool2Str(self, b): + if b is None: + return None + return str(b is True).lower() + + def RetrieveCustomerId(self): + """Retrieve the Customer ID for the account of the authenticated administrator making this request. + + Args: + None. + + Returns: + A dict containing the result of the retrieve operation. + """ + + uri = CUSTOMER_BASE_URL + return self._GetProperties(uri) + + def CreateOrgUnit(self, customer_id, name, parent_org_unit_path='/', description='', block_inheritance=False): + """Create a Organization Unit. + + Args: + customer_id: The ID of the Google Apps customer. + name: The simple organization unit text name, not the full path name. + parent_org_unit_path: The full path of the parental tree to this organization unit (default: '/'). + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + description: The human readable text description of the organization unit (optional). + block_inheritance: This parameter blocks policy setting inheritance + from organization units higher in the organization tree (default: False). + + Returns: + A dict containing the result of the create operation. + """ + + uri = BASE_UNIT_URL % (customer_id) + properties = {} + properties['name'] = name + properties['parentOrgUnitPath'] = parent_org_unit_path + properties['description'] = description + properties['blockInheritance'] = self._Bool2Str(block_inheritance) + return self._PostProperties(uri, properties) + + def UpdateOrgUnit(self, customer_id, org_unit_path, name=None, parent_org_unit_path=None, + description=None, block_inheritance=None): + """Update a Organization Unit. + + Args: + customer_id: The ID of the Google Apps customer. + org_unit_path: The organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + name: The simple organization unit text name, not the full path name. + parent_org_unit_path: The full path of the parental tree to this organization unit. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + description: The human readable text description of the organization unit. + block_inheritance: This parameter blocks policy setting inheritance + from organization units higher in the organization tree. + + Returns: + A dict containing the result of the update operation. + """ + + uri = UNIT_URL % (customer_id, org_unit_path) + properties = {} + if name: + properties['name'] = name + if parent_org_unit_path: + properties['parentOrgUnitPath'] = parent_org_unit_path + if description: + properties['description'] = description + if block_inheritance: + properties['blockInheritance'] = self._Bool2Str(block_inheritance) + return self._PutProperties(uri, properties) + + def MoveUserToOrgUnit(self, customer_id, org_unit_path, users_to_move): + """Move a user to an Organization Unit. + + Args: + customer_id: The ID of the Google Apps customer. + org_unit_path: The organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + users_to_move: Email addresses list of users to move. Note: You can move a maximum of 25 users at one time. + + Returns: + A dict containing the result of the update operation. + """ + + uri = UNIT_URL % (customer_id, org_unit_path) + properties = {} + if users_to_move and isinstance(users_to_move, list): + properties['usersToMove'] = ', '.join(users_to_move) + return self._PutProperties(uri, properties) + + def RetrieveOrgUnit(self, customer_id, org_unit_path): + """Retrieve a Orgunit based on its path. + + Args: + customer_id: The ID of the Google Apps customer. + org_unit_path: The organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + + Returns: + A dict containing the result of the retrieve operation. + """ + uri = UNIT_URL % (customer_id, org_unit_path) + return self._GetProperties(uri) + + def DeleteOrgUnit(self, customer_id, org_unit_path): + """Delete a Orgunit based on its path. + + Args: + customer_id: The ID of the Google Apps customer. + org_unit_path: The organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + + Returns: + A dict containing the result of the delete operation. + """ + uri = UNIT_URL % (customer_id, org_unit_path) + return self._DeleteProperties(uri) + + def RetrieveAllOrgUnits(self, customer_id): + """Retrieve all OrgUnits in the customer's domain. + + Args: + customer_id: The ID of the Google Apps customer. + + Returns: + A list containing the result of the retrieve operation. + """ + uri = UNIT_ALL_URL % (customer_id) + return self._GetPropertiesList(uri) + + def RetrievePageOfOrgUnits(self, customer_id, startKey=None): + """Retrieve one page of OrgUnits in the customer's domain. + + Args: + customer_id: The ID of the Google Apps customer. + startKey: The key to continue for pagination through all OrgUnits. + + Returns: + A feed object containing the result of the retrieve operation. + """ + uri = UNIT_ALL_URL % (customer_id) + if startKey is not None: + uri += "&startKey=" + startKey + property_feed = self._GetPropertyFeed(uri) + return property_feed + + def RetrieveSubOrgUnits(self, customer_id, org_unit_path): + """Retrieve all Sub-OrgUnits of the provided OrgUnit. + + Args: + customer_id: The ID of the Google Apps customer. + org_unit_path: The organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + + Returns: + A list containing the result of the retrieve operation. + """ + uri = UNIT_CHILD_URL % (customer_id, org_unit_path) + return self._GetPropertiesList(uri) + + def RetrieveOrgUser(self, customer_id, user_email): + """Retrieve the OrgUnit of the user. + + Args: + customer_id: The ID of the Google Apps customer. + user_email: The email address of the user. + + Returns: + A dict containing the result of the retrieve operation. + """ + uri = USER_URL % (customer_id, user_email) + return self._GetProperties(uri) + + def UpdateOrgUser(self, customer_id, user_email, org_unit_path): + """Update the OrgUnit of a OrgUser. + + Args: + customer_id: The ID of the Google Apps customer. + user_email: The email address of the user. + org_unit_path: The new organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + + Returns: + A dict containing the result of the update operation. + """ + + uri = USER_URL % (customer_id, user_email) + properties = {} + if org_unit_path: + properties['orgUnitPath'] = org_unit_path + return self._PutProperties(uri, properties) + + def RetrieveAllOrgUsers(self, customer_id): + """Retrieve all OrgUsers in the customer's domain. + + Args: + customer_id: The ID of the Google Apps customer. + + Returns: + A list containing the result of the retrieve operation. + """ + uri = USER_ALL_URL % (customer_id) + return self._GetPropertiesList(uri) + + def RetrievePageOfOrgUsers(self, customer_id, startKey=None): + """Retrieve one page of OrgUsers in the customer's domain. + + Args: + customer_id: The ID of the Google Apps customer. + startKey: The key to continue for pagination through all OrgUnits. + + Returns: + A feed object containing the result of the retrieve operation. + """ + uri = USER_ALL_URL % (customer_id) + if startKey is not None: + uri += "&startKey=" + startKey + property_feed = self._GetPropertyFeed(uri) + return property_feed + + def RetrieveOrgUnitUsers(self, customer_id, org_unit_path): + """Retrieve all OrgUsers of the provided OrgUnit. + + Args: + customer_id: The ID of the Google Apps customer. + org_unit_path: The organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + + Returns: + A list containing the result of the retrieve operation. + """ + uri = USER_CHILD_URL % (customer_id, org_unit_path) + return self._GetPropertiesList(uri) + + def RetrieveOrgUnitPageOfUsers(self, customer_id, org_unit_path, startKey=None): + """Retrieve one page of OrgUsers of the provided OrgUnit. + + Args: + customer_id: The ID of the Google Apps customer. + org_unit_path: The organization's full path name. + Note: Each element of the path MUST be URL encoded (example: finance%2Forganization/suborganization) + startKey: The key to continue for pagination through all OrgUsers. + + Returns: + A feed object containing the result of the retrieve operation. + """ + uri = USER_CHILD_URL % (customer_id, org_unit_path) + if startKey is not None: + uri += "&startKey=" + startKey + property_feed = self._GetPropertyFeed(uri) + return property_feed diff --git a/patches/gdata/apps/service.py b/patches/gdata/apps/service.py new file mode 100644 index 0000000..bc97484 --- /dev/null +++ b/patches/gdata/apps/service.py @@ -0,0 +1,552 @@ +#!/usr/bin/python +# +# Copyright (C) 2007 SIOS Technology, 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. + +__author__ = 'tmatsuo@sios.com (Takashi MATSUO)' + +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 urllib +import gdata +import atom.service +import gdata.service +import gdata.apps +import atom + +API_VER="2.0" +HTTP_OK=200 + +UNKOWN_ERROR=1000 +USER_DELETED_RECENTLY=1100 +USER_SUSPENDED=1101 +DOMAIN_USER_LIMIT_EXCEEDED=1200 +DOMAIN_ALIAS_LIMIT_EXCEEDED=1201 +DOMAIN_SUSPENDED=1202 +DOMAIN_FEATURE_UNAVAILABLE=1203 +ENTITY_EXISTS=1300 +ENTITY_DOES_NOT_EXIST=1301 +ENTITY_NAME_IS_RESERVED=1302 +ENTITY_NAME_NOT_VALID=1303 +INVALID_GIVEN_NAME=1400 +INVALID_FAMILY_NAME=1401 +INVALID_PASSWORD=1402 +INVALID_USERNAME=1403 +INVALID_HASH_FUNCTION_NAME=1404 +INVALID_HASH_DIGGEST_LENGTH=1405 +INVALID_EMAIL_ADDRESS=1406 +INVALID_QUERY_PARAMETER_VALUE=1407 +TOO_MANY_RECIPIENTS_ON_EMAIL_LIST=1500 + +DEFAULT_QUOTA_LIMIT='2048' + + +class Error(Exception): + pass + + +class AppsForYourDomainException(Error): + + def __init__(self, response): + + Error.__init__(self, response) + try: + self.element_tree = ElementTree.fromstring(response['body']) + self.error_code = int(self.element_tree[0].attrib['errorCode']) + self.reason = self.element_tree[0].attrib['reason'] + self.invalidInput = self.element_tree[0].attrib['invalidInput'] + except: + self.error_code = UNKOWN_ERROR + + +class AppsService(gdata.service.GDataService): + """Client for the Google Apps Provisioning service.""" + + def __init__(self, email=None, password=None, domain=None, source=None, + server='apps-apis.google.com', additional_headers=None, + **kwargs): + """Creates a client for the Google Apps Provisioning service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + domain: string (optional) The Google Apps domain name. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'apps-apis.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='apps', source=source, + server=server, additional_headers=additional_headers, **kwargs) + self.ssl = True + self.port = 443 + self.domain = domain + + def _baseURL(self): + return "/a/feeds/%s" % self.domain + + def AddAllElementsFromAllPages(self, link_finder, func): + """retrieve all pages and add all elements""" + next = link_finder.GetNextLink() + while next is not None: + next_feed = self.Get(next.href, converter=func) + for a_entry in next_feed.entry: + link_finder.entry.append(a_entry) + next = next_feed.GetNextLink() + return link_finder + + def RetrievePageOfEmailLists(self, start_email_list_name=None, + num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, + backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve one page of email list""" + uri = "%s/emailList/%s" % (self._baseURL(), API_VER) + if start_email_list_name is not None: + uri += "?startEmailListName=%s" % start_email_list_name + try: + return gdata.apps.EmailListFeedFromString(str(self.GetWithRetries( + uri, num_retries=num_retries, delay=delay, backoff=backoff))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def GetGeneratorForAllEmailLists( + self, num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve a generator for all emaillists in this domain.""" + first_page = self.RetrievePageOfEmailLists(num_retries=num_retries, + delay=delay, + backoff=backoff) + return self.GetGeneratorFromLinkFinder( + first_page, gdata.apps.EmailListRecipientFeedFromString, + num_retries=num_retries, delay=delay, backoff=backoff) + + def RetrieveAllEmailLists(self): + """Retrieve all email list of a domain.""" + + ret = self.RetrievePageOfEmailLists() + # pagination + return self.AddAllElementsFromAllPages( + ret, gdata.apps.EmailListFeedFromString) + + def RetrieveEmailList(self, list_name): + """Retreive a single email list by the list's name.""" + + uri = "%s/emailList/%s/%s" % ( + self._baseURL(), API_VER, list_name) + try: + return self.Get(uri, converter=gdata.apps.EmailListEntryFromString) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def RetrieveEmailLists(self, recipient): + """Retrieve All Email List Subscriptions for an Email Address.""" + + uri = "%s/emailList/%s?recipient=%s" % ( + self._baseURL(), API_VER, recipient) + try: + ret = gdata.apps.EmailListFeedFromString(str(self.Get(uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + # pagination + return self.AddAllElementsFromAllPages( + ret, gdata.apps.EmailListFeedFromString) + + def RemoveRecipientFromEmailList(self, recipient, list_name): + """Remove recipient from email list.""" + + uri = "%s/emailList/%s/%s/recipient/%s" % ( + self._baseURL(), API_VER, list_name, recipient) + try: + self.Delete(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def RetrievePageOfRecipients(self, list_name, start_recipient=None, + num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, + backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve one page of recipient of an email list. """ + + uri = "%s/emailList/%s/%s/recipient" % ( + self._baseURL(), API_VER, list_name) + + if start_recipient is not None: + uri += "?startRecipient=%s" % start_recipient + try: + return gdata.apps.EmailListRecipientFeedFromString(str( + self.GetWithRetries( + uri, num_retries=num_retries, delay=delay, backoff=backoff))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def GetGeneratorForAllRecipients( + self, list_name, num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve a generator for all recipients of a particular emaillist.""" + first_page = self.RetrievePageOfRecipients(list_name, + num_retries=num_retries, + delay=delay, + backoff=backoff) + return self.GetGeneratorFromLinkFinder( + first_page, gdata.apps.EmailListRecipientFeedFromString, + num_retries=num_retries, delay=delay, backoff=backoff) + + def RetrieveAllRecipients(self, list_name): + """Retrieve all recipient of an email list.""" + + ret = self.RetrievePageOfRecipients(list_name) + # pagination + return self.AddAllElementsFromAllPages( + ret, gdata.apps.EmailListRecipientFeedFromString) + + def AddRecipientToEmailList(self, recipient, list_name): + """Add a recipient to a email list.""" + + uri = "%s/emailList/%s/%s/recipient" % ( + self._baseURL(), API_VER, list_name) + recipient_entry = gdata.apps.EmailListRecipientEntry() + recipient_entry.who = gdata.apps.Who(email=recipient) + + try: + return gdata.apps.EmailListRecipientEntryFromString( + str(self.Post(recipient_entry, uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def DeleteEmailList(self, list_name): + """Delete a email list""" + + uri = "%s/emailList/%s/%s" % (self._baseURL(), API_VER, list_name) + try: + self.Delete(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def CreateEmailList(self, list_name): + """Create a email list. """ + + uri = "%s/emailList/%s" % (self._baseURL(), API_VER) + email_list_entry = gdata.apps.EmailListEntry() + email_list_entry.email_list = gdata.apps.EmailList(name=list_name) + try: + return gdata.apps.EmailListEntryFromString( + str(self.Post(email_list_entry, uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def DeleteNickname(self, nickname): + """Delete a nickname""" + + uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname) + try: + self.Delete(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def RetrievePageOfNicknames(self, start_nickname=None, + num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, + backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve one page of nicknames in the domain""" + + uri = "%s/nickname/%s" % (self._baseURL(), API_VER) + if start_nickname is not None: + uri += "?startNickname=%s" % start_nickname + try: + return gdata.apps.NicknameFeedFromString(str(self.GetWithRetries( + uri, num_retries=num_retries, delay=delay, backoff=backoff))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def GetGeneratorForAllNicknames( + self, num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve a generator for all nicknames in this domain.""" + first_page = self.RetrievePageOfNicknames(num_retries=num_retries, + delay=delay, + backoff=backoff) + return self.GetGeneratorFromLinkFinder( + first_page, gdata.apps.NicknameFeedFromString, num_retries=num_retries, + delay=delay, backoff=backoff) + + def RetrieveAllNicknames(self): + """Retrieve all nicknames in the domain""" + + ret = self.RetrievePageOfNicknames() + # pagination + return self.AddAllElementsFromAllPages( + ret, gdata.apps.NicknameFeedFromString) + + def GetGeneratorForAllNicknamesOfAUser( + self, user_name, num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve a generator for all nicknames of a particular user.""" + uri = "%s/nickname/%s?username=%s" % (self._baseURL(), API_VER, user_name) + try: + first_page = gdata.apps.NicknameFeedFromString(str(self.GetWithRetries( + uri, num_retries=num_retries, delay=delay, backoff=backoff))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + return self.GetGeneratorFromLinkFinder( + first_page, gdata.apps.NicknameFeedFromString, num_retries=num_retries, + delay=delay, backoff=backoff) + + def RetrieveNicknames(self, user_name): + """Retrieve nicknames of the user""" + + uri = "%s/nickname/%s?username=%s" % (self._baseURL(), API_VER, user_name) + try: + ret = gdata.apps.NicknameFeedFromString(str(self.Get(uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + # pagination + return self.AddAllElementsFromAllPages( + ret, gdata.apps.NicknameFeedFromString) + + def RetrieveNickname(self, nickname): + """Retrieve a nickname. + + Args: + nickname: string The nickname to retrieve + + Returns: + gdata.apps.NicknameEntry + """ + + uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname) + try: + return gdata.apps.NicknameEntryFromString(str(self.Get(uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def CreateNickname(self, user_name, nickname): + """Create a nickname""" + + uri = "%s/nickname/%s" % (self._baseURL(), API_VER) + nickname_entry = gdata.apps.NicknameEntry() + nickname_entry.login = gdata.apps.Login(user_name=user_name) + nickname_entry.nickname = gdata.apps.Nickname(name=nickname) + + try: + return gdata.apps.NicknameEntryFromString( + str(self.Post(nickname_entry, uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def DeleteUser(self, user_name): + """Delete a user account""" + + uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name) + try: + return self.Delete(uri) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def UpdateUser(self, user_name, user_entry): + """Update a user account.""" + + uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name) + try: + return gdata.apps.UserEntryFromString(str(self.Put(user_entry, uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def CreateUser(self, user_name, family_name, given_name, password, + suspended='false', quota_limit=None, + password_hash_function=None, + change_password=None): + """Create a user account. """ + + uri = "%s/user/%s" % (self._baseURL(), API_VER) + user_entry = gdata.apps.UserEntry() + user_entry.login = gdata.apps.Login( + user_name=user_name, password=password, suspended=suspended, + hash_function_name=password_hash_function, + change_password=change_password) + user_entry.name = gdata.apps.Name(family_name=family_name, + given_name=given_name) + if quota_limit is not None: + user_entry.quota = gdata.apps.Quota(limit=str(quota_limit)) + + try: + return gdata.apps.UserEntryFromString(str(self.Post(user_entry, uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def SuspendUser(self, user_name): + user_entry = self.RetrieveUser(user_name) + if user_entry.login.suspended != 'true': + user_entry.login.suspended = 'true' + user_entry = self.UpdateUser(user_name, user_entry) + return user_entry + + def RestoreUser(self, user_name): + user_entry = self.RetrieveUser(user_name) + if user_entry.login.suspended != 'false': + user_entry.login.suspended = 'false' + user_entry = self.UpdateUser(user_name, user_entry) + return user_entry + + def RetrieveUser(self, user_name): + """Retrieve an user account. + + Args: + user_name: string The user name to retrieve + + Returns: + gdata.apps.UserEntry + """ + + uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name) + try: + return gdata.apps.UserEntryFromString(str(self.Get(uri))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def RetrievePageOfUsers(self, start_username=None, + num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, + backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve one page of users in this domain.""" + + uri = "%s/user/%s" % (self._baseURL(), API_VER) + if start_username is not None: + uri += "?startUsername=%s" % start_username + try: + return gdata.apps.UserFeedFromString(str(self.GetWithRetries( + uri, num_retries=num_retries, delay=delay, backoff=backoff))) + except gdata.service.RequestError, e: + raise AppsForYourDomainException(e.args[0]) + + def GetGeneratorForAllUsers(self, + num_retries=gdata.service.DEFAULT_NUM_RETRIES, + delay=gdata.service.DEFAULT_DELAY, + backoff=gdata.service.DEFAULT_BACKOFF): + """Retrieve a generator for all users in this domain.""" + first_page = self.RetrievePageOfUsers(num_retries=num_retries, delay=delay, + backoff=backoff) + return self.GetGeneratorFromLinkFinder( + first_page, gdata.apps.UserFeedFromString, num_retries=num_retries, + delay=delay, backoff=backoff) + + def RetrieveAllUsers(self): + """Retrieve all users in this domain. OBSOLETE""" + + ret = self.RetrievePageOfUsers() + # pagination + return self.AddAllElementsFromAllPages( + ret, gdata.apps.UserFeedFromString) + + +class PropertyService(gdata.service.GDataService): + """Client for the Google Apps Property service.""" + + def __init__(self, email=None, password=None, domain=None, source=None, + server='apps-apis.google.com', additional_headers=None): + gdata.service.GDataService.__init__(self, email=email, password=password, + service='apps', source=source, + server=server, + additional_headers=additional_headers) + self.ssl = True + self.port = 443 + self.domain = domain + + def AddAllElementsFromAllPages(self, link_finder, func): + """retrieve all pages and add all elements""" + next = link_finder.GetNextLink() + while next is not None: + next_feed = self.Get(next.href, converter=func) + for a_entry in next_feed.entry: + link_finder.entry.append(a_entry) + next = next_feed.GetNextLink() + return link_finder + + def _GetPropertyEntry(self, properties): + property_entry = gdata.apps.PropertyEntry() + property = [] + for name, value in properties.iteritems(): + if name is not None and value is not None: + property.append(gdata.apps.Property(name=name, value=value)) + property_entry.property = property + return property_entry + + def _PropertyEntry2Dict(self, property_entry): + properties = {} + for i, property in enumerate(property_entry.property): + properties[property.name] = property.value + return properties + + def _GetPropertyFeed(self, uri): + try: + return gdata.apps.PropertyFeedFromString(str(self.Get(uri))) + except gdata.service.RequestError, e: + raise gdata.apps.service.AppsForYourDomainException(e.args[0]) + + def _GetPropertiesList(self, uri): + property_feed = self._GetPropertyFeed(uri) + # pagination + property_feed = self.AddAllElementsFromAllPages( + property_feed, gdata.apps.PropertyFeedFromString) + properties_list = [] + for property_entry in property_feed.entry: + properties_list.append(self._PropertyEntry2Dict(property_entry)) + return properties_list + + def _GetProperties(self, uri): + try: + return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString( + str(self.Get(uri)))) + except gdata.service.RequestError, e: + raise gdata.apps.service.AppsForYourDomainException(e.args[0]) + + def _PostProperties(self, uri, properties): + property_entry = self._GetPropertyEntry(properties) + try: + return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString( + str(self.Post(property_entry, uri)))) + except gdata.service.RequestError, e: + raise gdata.apps.service.AppsForYourDomainException(e.args[0]) + + def _PutProperties(self, uri, properties): + property_entry = self._GetPropertyEntry(properties) + try: + return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString( + str(self.Put(property_entry, uri)))) + except gdata.service.RequestError, e: + raise gdata.apps.service.AppsForYourDomainException(e.args[0]) + + def _DeleteProperties(self, uri): + try: + self.Delete(uri) + except gdata.service.RequestError, e: + raise gdata.apps.service.AppsForYourDomainException(e.args[0]) + + +def _bool2str(b): + if b is None: + return None + return str(b is True).lower() diff --git a/patches/gdata/apps_property.py b/patches/gdata/apps_property.py new file mode 100644 index 0000000..5afa1f3 --- /dev/null +++ b/patches/gdata/apps_property.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# +# Copyright (C) 2010 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 base class to represent property elements in feeds. + +This module is used for version 2 of the Google Data APIs. The primary class +in this module is AppsProperty. +""" + + +__author__ = 'Vic Fryzel ' + + +import atom.core +import gdata.apps + + +class AppsProperty(atom.core.XmlElement): + """Represents an element in a feed.""" + _qname = gdata.apps.APPS_TEMPLATE % 'property' + name = 'name' + value = 'value' diff --git a/patches/gdata/auth.py b/patches/gdata/auth.py new file mode 100644 index 0000000..139c6cd --- /dev/null +++ b/patches/gdata/auth.py @@ -0,0 +1,952 @@ +#!/usr/bin/python +# +# Copyright (C) 2007 - 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. + + +import cgi +import math +import random +import re +import time +import types +import urllib +import atom.http_interface +import atom.token_store +import atom.url +import gdata.oauth as oauth +import gdata.oauth.rsa as oauth_rsa +import gdata.tlslite.utils.keyfactory as keyfactory +import gdata.tlslite.utils.cryptomath as cryptomath + +import gdata.gauth + +__author__ = 'api.jscudder (Jeff Scudder)' + + +PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth=' +AUTHSUB_AUTH_LABEL = 'AuthSub token=' + + +"""This module provides functions and objects used with Google authentication. + +Details on Google authorization mechanisms used with the Google Data APIs can +be found here: +http://code.google.com/apis/gdata/auth.html +http://code.google.com/apis/accounts/ + +The essential functions are the following. +Related to ClientLogin: + generate_client_login_request_body: Constructs the body of an HTTP request to + obtain a ClientLogin token for a specific + service. + extract_client_login_token: Creates a ClientLoginToken with the token from a + success response to a ClientLogin request. + get_captcha_challenge: If the server responded to the ClientLogin request + with a CAPTCHA challenge, this method extracts the + CAPTCHA URL and identifying CAPTCHA token. + +Related to AuthSub: + generate_auth_sub_url: Constructs a full URL for a AuthSub request. The + user's browser must be sent to this Google Accounts + URL and redirected back to the app to obtain the + AuthSub token. + extract_auth_sub_token_from_url: Once the user's browser has been + redirected back to the web app, use this + function to create an AuthSubToken with + the correct authorization token and scope. + token_from_http_body: Extracts the AuthSubToken value string from the + server's response to an AuthSub session token upgrade + request. +""" + +def generate_client_login_request_body(email, password, service, source, + account_type='HOSTED_OR_GOOGLE', captcha_token=None, + captcha_response=None): + """Creates the body of the autentication request + + See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request + for more details. + + Args: + email: str + password: str + service: str + source: str + account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid + values are 'GOOGLE' and 'HOSTED' + captcha_token: str (optional) + captcha_response: str (optional) + + Returns: + The HTTP body to send in a request for a client login token. + """ + return gdata.gauth.generate_client_login_request_body(email, password, + service, source, account_type, captcha_token, captcha_response) + + +GenerateClientLoginRequestBody = generate_client_login_request_body + + +def GenerateClientLoginAuthToken(http_body): + """Returns the token value to use in Authorization headers. + + Reads the token from the server's response to a Client Login request and + creates header value to use in requests. + + Args: + http_body: str The body of the server's HTTP response to a Client Login + request + + Returns: + The value half of an Authorization header. + """ + token = get_client_login_token(http_body) + if token: + return 'GoogleLogin auth=%s' % token + return None + + +def get_client_login_token(http_body): + """Returns the token value for a ClientLoginToken. + + Reads the token from the server's response to a Client Login request and + creates the token value string to use in requests. + + Args: + http_body: str The body of the server's HTTP response to a Client Login + request + + Returns: + The token value string for a ClientLoginToken. + """ + return gdata.gauth.get_client_login_token_string(http_body) + + +def extract_client_login_token(http_body, scopes): + """Parses the server's response and returns a ClientLoginToken. + + Args: + http_body: str The body of the server's HTTP response to a Client Login + request. It is assumed that the login request was successful. + scopes: list containing atom.url.Urls or strs. The scopes list contains + all of the partial URLs under which the client login token is + valid. For example, if scopes contains ['http://example.com/foo'] + then the client login token would be valid for + http://example.com/foo/bar/baz + + Returns: + A ClientLoginToken which is valid for the specified scopes. + """ + token_string = get_client_login_token(http_body) + token = ClientLoginToken(scopes=scopes) + token.set_token_string(token_string) + return token + + +def get_captcha_challenge(http_body, + captcha_base_url='http://www.google.com/accounts/'): + """Returns the URL and token for a CAPTCHA challenge issued by the server. + + Args: + http_body: str The body of the HTTP response from the server which + contains the CAPTCHA challenge. + captcha_base_url: str This function returns a full URL for viewing the + challenge image which is built from the server's response. This + base_url is used as the beginning of the URL because the server + only provides the end of the URL. For example the server provides + 'Captcha?ctoken=Hi...N' and the URL for the image is + 'http://www.google.com/accounts/Captcha?ctoken=Hi...N' + + Returns: + A dictionary containing the information needed to repond to the CAPTCHA + challenge, the image URL and the ID token of the challenge. The + dictionary is in the form: + {'token': string identifying the CAPTCHA image, + 'url': string containing the URL of the image} + Returns None if there was no CAPTCHA challenge in the response. + """ + return gdata.gauth.get_captcha_challenge(http_body, captcha_base_url) + + +GetCaptchaChallenge = get_captcha_challenge + + +def GenerateOAuthRequestTokenUrl( + oauth_input_params, scopes, + request_token_url='https://www.google.com/accounts/OAuthGetRequestToken', + extra_parameters=None): + """Generate a URL at which a request for OAuth request token is to be sent. + + Args: + oauth_input_params: OAuthInputParams OAuth input parameters. + scopes: list of strings The URLs of the services to be accessed. + request_token_url: string The beginning of the request token URL. This is + normally 'https://www.google.com/accounts/OAuthGetRequestToken' or + '/accounts/OAuthGetRequestToken' + extra_parameters: dict (optional) key-value pairs as any additional + parameters to be included in the URL and signature while making a + request for fetching an OAuth request token. All the OAuth parameters + are added by default. But if provided through this argument, any + default parameters will be overwritten. For e.g. a default parameter + oauth_version 1.0 can be overwritten if + extra_parameters = {'oauth_version': '2.0'} + + Returns: + atom.url.Url OAuth request token URL. + """ + scopes_string = ' '.join([str(scope) for scope in scopes]) + parameters = {'scope': scopes_string} + if extra_parameters: + parameters.update(extra_parameters) + oauth_request = oauth.OAuthRequest.from_consumer_and_token( + oauth_input_params.GetConsumer(), http_url=request_token_url, + parameters=parameters) + oauth_request.sign_request(oauth_input_params.GetSignatureMethod(), + oauth_input_params.GetConsumer(), None) + return atom.url.parse_url(oauth_request.to_url()) + + +def GenerateOAuthAuthorizationUrl( + request_token, + authorization_url='https://www.google.com/accounts/OAuthAuthorizeToken', + callback_url=None, extra_params=None, + include_scopes_in_callback=False, scopes_param_prefix='oauth_token_scope'): + """Generates URL at which user will login to authorize the request token. + + Args: + request_token: gdata.auth.OAuthToken OAuth request token. + authorization_url: string The beginning of the authorization URL. This is + normally 'https://www.google.com/accounts/OAuthAuthorizeToken' or + '/accounts/OAuthAuthorizeToken' + callback_url: string (optional) The URL user will be sent to after + logging in and granting access. + extra_params: dict (optional) Additional parameters to be sent. + include_scopes_in_callback: Boolean (default=False) if set to True, and + if 'callback_url' is present, the 'callback_url' will be modified to + include the scope(s) from the request token as a URL parameter. The + key for the 'callback' URL's scope parameter will be + OAUTH_SCOPE_URL_PARAM_NAME. The benefit of including the scope URL as + a parameter to the 'callback' URL, is that the page which receives + the OAuth token will be able to tell which URLs the token grants + access to. + scopes_param_prefix: string (default='oauth_token_scope') The URL + parameter key which maps to the list of valid scopes for the token. + This URL parameter will be included in the callback URL along with + the scopes of the token as value if include_scopes_in_callback=True. + + Returns: + atom.url.Url OAuth authorization URL. + """ + scopes = request_token.scopes + if isinstance(scopes, list): + scopes = ' '.join(scopes) + if include_scopes_in_callback and callback_url: + if callback_url.find('?') > -1: + callback_url += '&' + else: + callback_url += '?' + callback_url += urllib.urlencode({scopes_param_prefix:scopes}) + oauth_token = oauth.OAuthToken(request_token.key, request_token.secret) + oauth_request = oauth.OAuthRequest.from_token_and_callback( + token=oauth_token, callback=callback_url, + http_url=authorization_url, parameters=extra_params) + return atom.url.parse_url(oauth_request.to_url()) + + +def GenerateOAuthAccessTokenUrl( + authorized_request_token, + oauth_input_params, + access_token_url='https://www.google.com/accounts/OAuthGetAccessToken', + oauth_version='1.0', + oauth_verifier=None): + """Generates URL at which user will login to authorize the request token. + + Args: + authorized_request_token: gdata.auth.OAuthToken OAuth authorized request + token. + oauth_input_params: OAuthInputParams OAuth input parameters. + access_token_url: string The beginning of the authorization URL. This is + normally 'https://www.google.com/accounts/OAuthGetAccessToken' or + '/accounts/OAuthGetAccessToken' + oauth_version: str (default='1.0') oauth_version parameter. + oauth_verifier: str (optional) If present, it is assumed that the client + will use the OAuth v1.0a protocol which includes passing the + oauth_verifier (as returned by the SP) in the access token step. + + Returns: + atom.url.Url OAuth access token URL. + """ + oauth_token = oauth.OAuthToken(authorized_request_token.key, + authorized_request_token.secret) + parameters = {'oauth_version': oauth_version} + if oauth_verifier is not None: + parameters['oauth_verifier'] = oauth_verifier + oauth_request = oauth.OAuthRequest.from_consumer_and_token( + oauth_input_params.GetConsumer(), token=oauth_token, + http_url=access_token_url, parameters=parameters) + oauth_request.sign_request(oauth_input_params.GetSignatureMethod(), + oauth_input_params.GetConsumer(), oauth_token) + return atom.url.parse_url(oauth_request.to_url()) + + +def GenerateAuthSubUrl(next, scope, secure=False, session=True, + request_url='https://www.google.com/accounts/AuthSubRequest', + domain='default'): + """Generate a URL at which the user will login and be redirected back. + + Users enter their credentials on a Google login page and a token is sent + to the URL specified in next. See documentation for AuthSub login at: + http://code.google.com/apis/accounts/AuthForWebApps.html + + Args: + request_url: str The beginning of the request URL. This is normally + 'http://www.google.com/accounts/AuthSubRequest' or + '/accounts/AuthSubRequest' + next: string The URL user will be sent to after logging in. + scope: string The URL of the service to be accessed. + secure: boolean (optional) Determines whether or not the issued token + is a secure token. + session: boolean (optional) Determines whether or not the issued token + can be upgraded to a session token. + domain: str (optional) The Google Apps domain for this account. If this + is not a Google Apps account, use 'default' which is the default + value. + """ + # Translate True/False values for parameters into numeric values acceoted + # by the AuthSub service. + if secure: + secure = 1 + else: + secure = 0 + + if session: + session = 1 + else: + session = 0 + + request_params = urllib.urlencode({'next': next, 'scope': scope, + 'secure': secure, 'session': session, + 'hd': domain}) + if request_url.find('?') == -1: + return '%s?%s' % (request_url, request_params) + else: + # The request URL already contained url parameters so we should add + # the parameters using the & seperator + return '%s&%s' % (request_url, request_params) + + +def generate_auth_sub_url(next, scopes, secure=False, session=True, + request_url='https://www.google.com/accounts/AuthSubRequest', + domain='default', scopes_param_prefix='auth_sub_scopes'): + """Constructs a URL string for requesting a multiscope AuthSub token. + + The generated token will contain a URL parameter to pass along the + requested scopes to the next URL. When the Google Accounts page + redirects the broswser to the 'next' URL, it appends the single use + AuthSub token value to the URL as a URL parameter with the key 'token'. + However, the information about which scopes were requested is not + included by Google Accounts. This method adds the scopes to the next + URL before making the request so that the redirect will be sent to + a page, and both the token value and the list of scopes can be + extracted from the request URL. + + Args: + next: atom.url.URL or string The URL user will be sent to after + authorizing this web application to access their data. + scopes: list containint strings The URLs of the services to be accessed. + secure: boolean (optional) Determines whether or not the issued token + is a secure token. + session: boolean (optional) Determines whether or not the issued token + can be upgraded to a session token. + request_url: atom.url.Url or str The beginning of the request URL. This + is normally 'http://www.google.com/accounts/AuthSubRequest' or + '/accounts/AuthSubRequest' + domain: The domain which the account is part of. This is used for Google + Apps accounts, the default value is 'default' which means that the + requested account is a Google Account (@gmail.com for example) + scopes_param_prefix: str (optional) The requested scopes are added as a + URL parameter to the next URL so that the page at the 'next' URL can + extract the token value and the valid scopes from the URL. The key + for the URL parameter defaults to 'auth_sub_scopes' + + Returns: + An atom.url.Url which the user's browser should be directed to in order + to authorize this application to access their information. + """ + if isinstance(next, (str, unicode)): + next = atom.url.parse_url(next) + scopes_string = ' '.join([str(scope) for scope in scopes]) + next.params[scopes_param_prefix] = scopes_string + + if isinstance(request_url, (str, unicode)): + request_url = atom.url.parse_url(request_url) + request_url.params['next'] = str(next) + request_url.params['scope'] = scopes_string + if session: + request_url.params['session'] = 1 + else: + request_url.params['session'] = 0 + if secure: + request_url.params['secure'] = 1 + else: + request_url.params['secure'] = 0 + request_url.params['hd'] = domain + return request_url + + +def AuthSubTokenFromUrl(url): + """Extracts the AuthSub token from the URL. + + Used after the AuthSub redirect has sent the user to the 'next' page and + appended the token to the URL. This function returns the value to be used + in the Authorization header. + + Args: + url: str The URL of the current page which contains the AuthSub token as + a URL parameter. + """ + token = TokenFromUrl(url) + if token: + return 'AuthSub token=%s' % token + return None + + +def TokenFromUrl(url): + """Extracts the AuthSub token from the URL. + + Returns the raw token value. + + Args: + url: str The URL or the query portion of the URL string (after the ?) of + the current page which contains the AuthSub token as a URL parameter. + """ + if url.find('?') > -1: + query_params = url.split('?')[1] + else: + query_params = url + for pair in query_params.split('&'): + if pair.startswith('token='): + return pair[6:] + return None + + +def extract_auth_sub_token_from_url(url, + scopes_param_prefix='auth_sub_scopes', rsa_key=None): + """Creates an AuthSubToken and sets the token value and scopes from the URL. + + After the Google Accounts AuthSub pages redirect the user's broswer back to + the web application (using the 'next' URL from the request) the web app must + extract the token from the current page's URL. The token is provided as a + URL parameter named 'token' and if generate_auth_sub_url was used to create + the request, the token's valid scopes are included in a URL parameter whose + name is specified in scopes_param_prefix. + + Args: + url: atom.url.Url or str representing the current URL. The token value + and valid scopes should be included as URL parameters. + scopes_param_prefix: str (optional) The URL parameter key which maps to + the list of valid scopes for the token. + + Returns: + An AuthSubToken with the token value from the URL and set to be valid for + the scopes passed in on the URL. If no scopes were included in the URL, + the AuthSubToken defaults to being valid for no scopes. If there was no + 'token' parameter in the URL, this function returns None. + """ + if isinstance(url, (str, unicode)): + url = atom.url.parse_url(url) + if 'token' not in url.params: + return None + scopes = [] + if scopes_param_prefix in url.params: + scopes = url.params[scopes_param_prefix].split(' ') + token_value = url.params['token'] + if rsa_key: + token = SecureAuthSubToken(rsa_key, scopes=scopes) + else: + token = AuthSubToken(scopes=scopes) + token.set_token_string(token_value) + return token + + +def AuthSubTokenFromHttpBody(http_body): + """Extracts the AuthSub token from an HTTP body string. + + Used to find the new session token after making a request to upgrade a + single use AuthSub token. + + Args: + http_body: str The repsonse from the server which contains the AuthSub + key. For example, this function would find the new session token + from the server's response to an upgrade token request. + + Returns: + The header value to use for Authorization which contains the AuthSub + token. + """ + token_value = token_from_http_body(http_body) + if token_value: + return '%s%s' % (AUTHSUB_AUTH_LABEL, token_value) + return None + + +def token_from_http_body(http_body): + """Extracts the AuthSub token from an HTTP body string. + + Used to find the new session token after making a request to upgrade a + single use AuthSub token. + + Args: + http_body: str The repsonse from the server which contains the AuthSub + key. For example, this function would find the new session token + from the server's response to an upgrade token request. + + Returns: + The raw token value to use in an AuthSubToken object. + """ + for response_line in http_body.splitlines(): + if response_line.startswith('Token='): + # Strip off Token= and return the token value string. + return response_line[6:] + return None + + +TokenFromHttpBody = token_from_http_body + + +def OAuthTokenFromUrl(url, scopes_param_prefix='oauth_token_scope'): + """Creates an OAuthToken and sets token key and scopes (if present) from URL. + + After the Google Accounts OAuth pages redirect the user's broswer back to + the web application (using the 'callback' URL from the request) the web app + can extract the token from the current page's URL. The token is same as the + request token, but it is either authorized (if user grants access) or + unauthorized (if user denies access). The token is provided as a + URL parameter named 'oauth_token' and if it was chosen to use + GenerateOAuthAuthorizationUrl with include_scopes_in_param=True, the token's + valid scopes are included in a URL parameter whose name is specified in + scopes_param_prefix. + + Args: + url: atom.url.Url or str representing the current URL. The token value + and valid scopes should be included as URL parameters. + scopes_param_prefix: str (optional) The URL parameter key which maps to + the list of valid scopes for the token. + + Returns: + An OAuthToken with the token key from the URL and set to be valid for + the scopes passed in on the URL. If no scopes were included in the URL, + the OAuthToken defaults to being valid for no scopes. If there was no + 'oauth_token' parameter in the URL, this function returns None. + """ + if isinstance(url, (str, unicode)): + url = atom.url.parse_url(url) + if 'oauth_token' not in url.params: + return None + scopes = [] + if scopes_param_prefix in url.params: + scopes = url.params[scopes_param_prefix].split(' ') + token_key = url.params['oauth_token'] + token = OAuthToken(key=token_key, scopes=scopes) + return token + + +def OAuthTokenFromHttpBody(http_body): + """Parses the HTTP response body and returns an OAuth token. + + The returned OAuth token will just have key and secret parameters set. + It won't have any knowledge about the scopes or oauth_input_params. It is + your responsibility to make it aware of the remaining parameters. + + Returns: + OAuthToken OAuth token. + """ + token = oauth.OAuthToken.from_string(http_body) + oauth_token = OAuthToken(key=token.key, secret=token.secret) + return oauth_token + + +class OAuthSignatureMethod(object): + """Holds valid OAuth signature methods. + + RSA_SHA1: Class to build signature according to RSA-SHA1 algorithm. + HMAC_SHA1: Class to build signature according to HMAC-SHA1 algorithm. + """ + + HMAC_SHA1 = oauth.OAuthSignatureMethod_HMAC_SHA1 + + class RSA_SHA1(oauth_rsa.OAuthSignatureMethod_RSA_SHA1): + """Provides implementation for abstract methods to return RSA certs.""" + + def __init__(self, private_key, public_cert): + self.private_key = private_key + self.public_cert = public_cert + + def _fetch_public_cert(self, unused_oauth_request): + return self.public_cert + + def _fetch_private_cert(self, unused_oauth_request): + return self.private_key + + +class OAuthInputParams(object): + """Stores OAuth input parameters. + + This class is a store for OAuth input parameters viz. consumer key and secret, + signature method and RSA key. + """ + + def __init__(self, signature_method, consumer_key, consumer_secret=None, + rsa_key=None, requestor_id=None): + """Initializes object with parameters required for using OAuth mechanism. + + NOTE: Though consumer_secret and rsa_key are optional, either of the two + is required depending on the value of the signature_method. + + Args: + signature_method: class which provides implementation for strategy class + oauth.oauth.OAuthSignatureMethod. Signature method to be used for + signing each request. Valid implementations are provided as the + constants defined by gdata.auth.OAuthSignatureMethod. Currently + they are gdata.auth.OAuthSignatureMethod.RSA_SHA1 and + gdata.auth.OAuthSignatureMethod.HMAC_SHA1. Instead of passing in + the strategy class, you may pass in a string for 'RSA_SHA1' or + 'HMAC_SHA1'. If you plan to use OAuth on App Engine (or another + WSGI environment) I recommend specifying signature method using a + string (the only options are 'RSA_SHA1' and 'HMAC_SHA1'). In these + environments there are sometimes issues with pickling an object in + which a member references a class or function. Storing a string to + refer to the signature method mitigates complications when + pickling. + consumer_key: string Domain identifying third_party web application. + consumer_secret: string (optional) Secret generated during registration. + Required only for HMAC_SHA1 signature method. + rsa_key: string (optional) Private key required for RSA_SHA1 signature + method. + requestor_id: string (optional) User email adress to make requests on + their behalf. This parameter should only be set when performing + 2 legged OAuth requests. + """ + if (signature_method == OAuthSignatureMethod.RSA_SHA1 + or signature_method == 'RSA_SHA1'): + self.__signature_strategy = 'RSA_SHA1' + elif (signature_method == OAuthSignatureMethod.HMAC_SHA1 + or signature_method == 'HMAC_SHA1'): + self.__signature_strategy = 'HMAC_SHA1' + else: + self.__signature_strategy = signature_method + self.rsa_key = rsa_key + self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) + self.requestor_id = requestor_id + + def __get_signature_method(self): + if self.__signature_strategy == 'RSA_SHA1': + return OAuthSignatureMethod.RSA_SHA1(self.rsa_key, None) + elif self.__signature_strategy == 'HMAC_SHA1': + return OAuthSignatureMethod.HMAC_SHA1() + else: + return self.__signature_strategy() + + def __set_signature_method(self, signature_method): + if (signature_method == OAuthSignatureMethod.RSA_SHA1 + or signature_method == 'RSA_SHA1'): + self.__signature_strategy = 'RSA_SHA1' + elif (signature_method == OAuthSignatureMethod.HMAC_SHA1 + or signature_method == 'HMAC_SHA1'): + self.__signature_strategy = 'HMAC_SHA1' + else: + self.__signature_strategy = signature_method + + _signature_method = property(__get_signature_method, __set_signature_method, + doc="""Returns object capable of signing the request using RSA of HMAC. + + Replaces the _signature_method member to avoid pickle errors.""") + + def GetSignatureMethod(self): + """Gets the OAuth signature method. + + Returns: + object of supertype + """ + return self._signature_method + + def GetConsumer(self): + """Gets the OAuth consumer. + + Returns: + object of type + """ + return self._consumer + + +class ClientLoginToken(atom.http_interface.GenericToken): + """Stores the Authorization header in auth_header and adds to requests. + + This token will add it's Authorization header to an HTTP request + as it is made. Ths token class is simple but + some Token classes must calculate portions of the Authorization header + based on the request being made, which is why the token is responsible + for making requests via an http_client parameter. + + 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' + """ + def __init__(self, auth_header=None, scopes=None): + self.auth_header = auth_header + self.scopes = scopes or [] + + def __str__(self): + return self.auth_header + + def perform_request(self, http_client, operation, url, data=None, + headers=None): + """Sets the Authorization header and makes the HTTP request.""" + 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 get_token_string(self): + """Removes PROGRAMMATIC_AUTH_LABEL to give just the token value.""" + return self.auth_header[len(PROGRAMMATIC_AUTH_LABEL):] + + def set_token_string(self, token_string): + self.auth_header = '%s%s' % (PROGRAMMATIC_AUTH_LABEL, token_string) + + 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 + + +class AuthSubToken(ClientLoginToken): + def get_token_string(self): + """Removes AUTHSUB_AUTH_LABEL to give just the token value.""" + return self.auth_header[len(AUTHSUB_AUTH_LABEL):] + + def set_token_string(self, token_string): + self.auth_header = '%s%s' % (AUTHSUB_AUTH_LABEL, token_string) + + +class OAuthToken(atom.http_interface.GenericToken): + """Stores the token key, token secret and scopes for which token is valid. + + This token adds the authorization header to each request made. It + re-calculates authorization header for every request since the OAuth + signature to be added to the authorization header is dependent on the + request parameters. + + Attributes: + key: str The value for the OAuth token i.e. token key. + secret: str The value for the OAuth token secret. + 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' + oauth_input_params: OAuthInputParams OAuth input parameters. + """ + + def __init__(self, key=None, secret=None, scopes=None, + oauth_input_params=None): + self.key = key + self.secret = secret + self.scopes = scopes or [] + self.oauth_input_params = oauth_input_params + + def __str__(self): + return self.get_token_string() + + def get_token_string(self): + """Returns the token string. + + The token string returned is of format + oauth_token=[0]&oauth_token_secret=[1], where [0] and [1] are some strings. + + Returns: + A token string of format oauth_token=[0]&oauth_token_secret=[1], + where [0] and [1] are some strings. If self.secret is absent, it just + returns oauth_token=[0]. If self.key is absent, it just returns + oauth_token_secret=[1]. If both are absent, it returns None. + """ + if self.key and self.secret: + return urllib.urlencode({'oauth_token': self.key, + 'oauth_token_secret': self.secret}) + elif self.key: + return 'oauth_token=%s' % self.key + elif self.secret: + return 'oauth_token_secret=%s' % self.secret + else: + return None + + def set_token_string(self, token_string): + """Sets the token key and secret from the token string. + + Args: + token_string: str Token string of form + oauth_token=[0]&oauth_token_secret=[1]. If oauth_token is not present, + self.key will be None. If oauth_token_secret is not present, + self.secret will be None. + """ + token_params = cgi.parse_qs(token_string, keep_blank_values=False) + if 'oauth_token' in token_params: + self.key = token_params['oauth_token'][0] + if 'oauth_token_secret' in token_params: + self.secret = token_params['oauth_token_secret'][0] + + def GetAuthHeader(self, http_method, http_url, realm=''): + """Get the authentication header. + + Args: + http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc. + http_url: string or atom.url.Url HTTP URL to which request is made. + realm: string (default='') realm parameter to be included in the + authorization header. + + Returns: + dict Header to be sent with every subsequent request after + authentication. + """ + if isinstance(http_url, types.StringTypes): + http_url = atom.url.parse_url(http_url) + header = None + token = None + if self.key or self.secret: + token = oauth.OAuthToken(self.key, self.secret) + oauth_request = oauth.OAuthRequest.from_consumer_and_token( + self.oauth_input_params.GetConsumer(), token=token, + http_url=str(http_url), http_method=http_method, + parameters=http_url.params) + oauth_request.sign_request(self.oauth_input_params.GetSignatureMethod(), + self.oauth_input_params.GetConsumer(), token) + header = oauth_request.to_header(realm=realm) + header['Authorization'] = header['Authorization'].replace('+', '%2B') + return header + + def perform_request(self, http_client, operation, url, data=None, + headers=None): + """Sets the Authorization header and makes the HTTP request.""" + if not headers: + headers = {} + if self.oauth_input_params.requestor_id: + url.params['xoauth_requestor_id'] = self.oauth_input_params.requestor_id + headers.update(self.GetAuthHeader(operation, url)) + return http_client.request(operation, url, data=data, headers=headers) + + def valid_for_scope(self, 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 + + +class SecureAuthSubToken(AuthSubToken): + """Stores the rsa private key, token, and scopes for the secure AuthSub token. + + This token adds the authorization header to each request made. It + re-calculates authorization header for every request since the secure AuthSub + signature to be added to the authorization header is dependent on the + request parameters. + + Attributes: + rsa_key: string The RSA private key in PEM format that the token will + use to sign requests + token_string: string (optional) The value for the AuthSub token. + 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' + """ + + def __init__(self, rsa_key, token_string=None, scopes=None): + self.rsa_key = keyfactory.parsePEMKey(rsa_key) + self.token_string = token_string or '' + self.scopes = scopes or [] + + def __str__(self): + return self.get_token_string() + + def get_token_string(self): + return str(self.token_string) + + def set_token_string(self, token_string): + self.token_string = token_string + + def GetAuthHeader(self, http_method, http_url): + """Generates the Authorization header. + + The form of the secure AuthSub Authorization header is + Authorization: AuthSub token="token" sigalg="sigalg" data="data" sig="sig" + and data represents a string in the form + data = http_method http_url timestamp nonce + + Args: + http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc. + http_url: string or atom.url.Url HTTP URL to which request is made. + + Returns: + dict Header to be sent with every subsequent request after authentication. + """ + timestamp = int(math.floor(time.time())) + nonce = '%lu' % random.randrange(1, 2**64) + data = '%s %s %d %s' % (http_method, str(http_url), timestamp, nonce) + sig = cryptomath.bytesToBase64(self.rsa_key.hashAndSign(data)) + header = {'Authorization': '%s"%s" data="%s" sig="%s" sigalg="rsa-sha1"' % + (AUTHSUB_AUTH_LABEL, self.token_string, data, sig)} + return header + + def perform_request(self, http_client, operation, url, data=None, + headers=None): + """Sets the Authorization header and makes the HTTP request.""" + if not headers: + headers = {} + headers.update(self.GetAuthHeader(operation, url)) + return http_client.request(operation, url, data=data, headers=headers) diff --git a/patches/gdata/base/__init__.py b/patches/gdata/base/__init__.py new file mode 100755 index 0000000..3a4756f --- /dev/null +++ b/patches/gdata/base/__init__.py @@ -0,0 +1,746 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 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. + +"""Contains extensions to Atom objects used with Google Base.""" + + +__author__ = 'api.jscudder (Jeffrey Scudder)' + + +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 +import gdata + + +# XML namespaces which are often used in Google Base entities. +GBASE_NAMESPACE = 'http://base.google.com/ns/1.0' +GBASE_TEMPLATE = '{http://base.google.com/ns/1.0}%s' +GMETA_NAMESPACE = 'http://base.google.com/ns-metadata/1.0' +GMETA_TEMPLATE = '{http://base.google.com/ns-metadata/1.0}%s' + + +class ItemAttributeContainer(atom.AtomBase): + """Provides methods for finding Google Base Item attributes. + + Google Base item attributes are child nodes in the gbase namespace. Google + Base allows you to define your own item attributes and this class provides + methods to interact with the custom attributes. + """ + + def GetItemAttributes(self, name): + """Returns a list of all item attributes which have the desired name. + + Args: + name: str The tag of the desired base attributes. For example, calling + this method with 'rating' would return a list of ItemAttributes + represented by a 'g:rating' tag. + + Returns: + A list of matching ItemAttribute objects. + """ + result = [] + for attrib in self.item_attributes: + if attrib.name == name: + result.append(attrib) + return result + + def FindItemAttribute(self, name): + """Get the contents of the first Base item attribute which matches name. + + This method is deprecated, please use GetItemAttributes instead. + + Args: + name: str The tag of the desired base attribute. For example, calling + this method with name = 'rating' would search for a tag rating + in the GBase namespace in the item attributes. + + Returns: + The text contents of the item attribute, or none if the attribute was + not found. + """ + + for attrib in self.item_attributes: + if attrib.name == name: + return attrib.text + return None + + def AddItemAttribute(self, name, value, value_type=None, access=None): + """Adds a new item attribute tag containing the value. + + Creates a new extension element in the GBase namespace to represent a + Google Base item attribute. + + Args: + name: str The tag name for the new attribute. This must be a valid xml + tag name. The tag will be placed in the GBase namespace. + value: str Contents for the item attribute + value_type: str (optional) The type of data in the vlaue, Examples: text + float + access: str (optional) Used to hide attributes. The attribute is not + exposed in the snippets feed if access is set to 'private'. + """ + + new_attribute = ItemAttribute(name, text=value, + text_type=value_type, access=access) + self.item_attributes.append(new_attribute) + return new_attribute + + def SetItemAttribute(self, name, value): + """Changes an existing item attribute's value.""" + + for attrib in self.item_attributes: + if attrib.name == name: + attrib.text = value + return + + def RemoveItemAttribute(self, name): + """Deletes the first extension element which matches name. + + Deletes the first extension element which matches name. + """ + + for i in xrange(len(self.item_attributes)): + if self.item_attributes[i].name == name: + del self.item_attributes[i] + return + + # We need to overwrite _ConvertElementTreeToMember to add special logic to + # convert custom attributes to members + def _ConvertElementTreeToMember(self, child_tree): + # Find the element's tag in this class's list of child members + if self.__class__._children.has_key(child_tree.tag): + member_name = self.__class__._children[child_tree.tag][0] + member_class = self.__class__._children[child_tree.tag][1] + # If the class member is supposed to contain a list, make sure the + # matching member is set to a list, then append the new member + # instance to the list. + if isinstance(member_class, list): + if getattr(self, member_name) is None: + setattr(self, member_name, []) + getattr(self, member_name).append(atom._CreateClassFromElementTree( + member_class[0], child_tree)) + else: + setattr(self, member_name, + atom._CreateClassFromElementTree(member_class, child_tree)) + elif child_tree.tag.find('{%s}' % GBASE_NAMESPACE) == 0: + # If this is in the gbase namespace, make it into an extension element. + name = child_tree.tag[child_tree.tag.index('}')+1:] + value = child_tree.text + if child_tree.attrib.has_key('type'): + value_type = child_tree.attrib['type'] + else: + value_type = None + attrib=self.AddItemAttribute(name, value, value_type) + for sub in child_tree.getchildren(): + sub_name = sub.tag[sub.tag.index('}')+1:] + sub_value=sub.text + if sub.attrib.has_key('type'): + sub_type = sub.attrib['type'] + else: + sub_type=None + attrib.AddItemAttribute(sub_name, sub_value, sub_type) + else: + atom.ExtensionContainer._ConvertElementTreeToMember(self, child_tree) + + # We need to overwtite _AddMembersToElementTree to add special logic to + # convert custom members to XML nodes. + def _AddMembersToElementTree(self, tree): + # Convert the members of this class which are XML child nodes. + # This uses the class's _children dictionary to find the members which + # should become XML child nodes. + member_node_names = [values[0] for tag, values in + self.__class__._children.iteritems()] + for member_name in member_node_names: + member = getattr(self, member_name) + if member is None: + pass + elif isinstance(member, list): + for instance in member: + instance._BecomeChildElement(tree) + else: + member._BecomeChildElement(tree) + # Convert the members of this class which are XML attributes. + for xml_attribute, member_name in self.__class__._attributes.iteritems(): + member = getattr(self, member_name) + if member is not None: + tree.attrib[xml_attribute] = member + # Convert all special custom item attributes to nodes + for attribute in self.item_attributes: + attribute._BecomeChildElement(tree) + # Lastly, call the ExtensionContainers's _AddMembersToElementTree to + # convert any extension attributes. + atom.ExtensionContainer._AddMembersToElementTree(self, tree) + + +class ItemAttribute(ItemAttributeContainer): + """An optional or user defined attribute for a GBase item. + + Google Base allows items to have custom attribute child nodes. These nodes + have contents and a type attribute which tells Google Base whether the + contents are text, a float value with units, etc. The Atom text class has + the same structure, so this class inherits from Text. + """ + + _namespace = GBASE_NAMESPACE + _children = atom.Text._children.copy() + _attributes = atom.Text._attributes.copy() + _attributes['access'] = 'access' + + def __init__(self, name, text_type=None, access=None, text=None, + extension_elements=None, extension_attributes=None, item_attributes=None): + """Constructor for a GBase item attribute + + Args: + name: str The name of the attribute. Examples include + price, color, make, model, pages, salary, etc. + text_type: str (optional) The type associated with the text contents + access: str (optional) If the access attribute is set to 'private', the + attribute will not be included in the item's description in the + snippets feed + text: str (optional) The text data in the this element + extension_elements: list (optional) A list of ExtensionElement + instances + extension_attributes: dict (optional) A dictionary of attribute + value string pairs + """ + + self.name = name + self.type = text_type + self.access = access + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.item_attributes = item_attributes or [] + + def _BecomeChildElement(self, tree): + new_child = ElementTree.Element('') + tree.append(new_child) + new_child.tag = '{%s}%s' % (self.__class__._namespace, + self.name) + self._AddMembersToElementTree(new_child) + + def _ToElementTree(self): + new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace, + self.name)) + self._AddMembersToElementTree(new_tree) + return new_tree + + +def ItemAttributeFromString(xml_string): + element_tree = ElementTree.fromstring(xml_string) + return _ItemAttributeFromElementTree(element_tree) + + +def _ItemAttributeFromElementTree(element_tree): + if element_tree.tag.find(GBASE_TEMPLATE % '') == 0: + to_return = ItemAttribute('') + to_return._HarvestElementTree(element_tree) + to_return.name = element_tree.tag[element_tree.tag.index('}')+1:] + if to_return.name and to_return.name != '': + return to_return + return None + + +class Label(atom.AtomBase): + """The Google Base label element""" + + _tag = 'label' + _namespace = GBASE_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def LabelFromString(xml_string): + return atom.CreateClassFromXMLString(Label, xml_string) + + +class Thumbnail(atom.AtomBase): + """The Google Base thumbnail element""" + + _tag = 'thumbnail' + _namespace = GMETA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['width'] = 'width' + _attributes['height'] = 'height' + + def __init__(self, width=None, height=None, text=None, extension_elements=None, + extension_attributes=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.width = width + self.height = height + + +def ThumbnailFromString(xml_string): + return atom.CreateClassFromXMLString(Thumbnail, xml_string) + + +class ImageLink(atom.Text): + """The Google Base image_link element""" + + _tag = 'image_link' + _namespace = GBASE_NAMESPACE + _children = atom.Text._children.copy() + _attributes = atom.Text._attributes.copy() + _children['{%s}thumbnail' % GMETA_NAMESPACE] = ('thumbnail', [Thumbnail]) + + def __init__(self, thumbnail=None, text=None, extension_elements=None, + text_type=None, extension_attributes=None): + self.thumbnail = thumbnail or [] + self.text = text + self.type = text_type + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ImageLinkFromString(xml_string): + return atom.CreateClassFromXMLString(ImageLink, xml_string) + + +class ItemType(atom.Text): + """The Google Base item_type element""" + + _tag = 'item_type' + _namespace = GBASE_NAMESPACE + _children = atom.Text._children.copy() + _attributes = atom.Text._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + text_type=None, extension_attributes=None): + self.text = text + self.type = text_type + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ItemTypeFromString(xml_string): + return atom.CreateClassFromXMLString(ItemType, xml_string) + + +class MetaItemType(ItemType): + """The Google Base item_type element""" + + _tag = 'item_type' + _namespace = GMETA_NAMESPACE + _children = ItemType._children.copy() + _attributes = ItemType._attributes.copy() + + +def MetaItemTypeFromString(xml_string): + return atom.CreateClassFromXMLString(MetaItemType, xml_string) + + +class Value(atom.AtomBase): + """Metadata about common values for a given attribute + + A value is a child of an attribute which comes from the attributes feed. + The value's text is a commonly used value paired with an attribute name + and the value's count tells how often this value appears for the given + attribute in the search results. + """ + + _tag = 'value' + _namespace = GMETA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['count'] = 'count' + + def __init__(self, count=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Attribute metadata element + + Args: + count: str (optional) The number of times the value in text is given + for the parent attribute. + text: str (optional) The value which appears in the search results. + extension_elements: list (optional) A list of ExtensionElement + instances + extension_attributes: dict (optional) A dictionary of attribute value + string pairs + """ + + self.count = count + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ValueFromString(xml_string): + return atom.CreateClassFromXMLString(Value, xml_string) + + +class Bucket(atom.AtomBase): + """Metadata about common values for a given attribute + + A bucket is a child of an attribute which comes from the attributes feed. + The bucket's text is a commonly used value paired with an attribute name + and the bucket's count tells how often this attribute's value falls within + the bucket's range in the search results. + """ + + _tag = 'bucket' + _namespace = GMETA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['count'] = 'count' + _attributes['low'] = 'low' + _attributes['high'] = 'high' + + def __init__(self, count=None, text=None, low=None, high=None, + extension_elements=None, extension_attributes=None): + """Constructor for Bucket metadata element + + Args: + count: str (optional) The number of times the value in text is given + for the parent attribute. + text: str (optional) The bucket's string representation. + low: str (optional) The low end of the bucket. + high: str (optional) The high end of the bucket. + extension_elements: list (optional) A list of ExtensionElement + instances + extension_attributes: dict (optional) A dictionary of attribute value + string pairs + """ + + self.count = count + self.text = text + self.low = low + self.high = high + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def BucketFromString(xml_string): + return atom.CreateClassFromXMLString(Bucket, xml_string) + + +class Attribute(atom.Text): + """Metadata about an attribute from the attributes feed + + An entry from the attributes feed contains a list of attributes. Each + attribute describes the attribute's type and count of the items which + use the attribute. + """ + + _tag = 'attribute' + _namespace = GMETA_NAMESPACE + _children = atom.Text._children.copy() + _attributes = atom.Text._attributes.copy() + _children['{%s}value' % GMETA_NAMESPACE] = ('value', [Value]) + _children['{%s}bucket' % GMETA_NAMESPACE] = ('bucket', [Bucket]) + _attributes['count'] = 'count' + _attributes['name'] = 'name' + + def __init__(self, name=None, attribute_type=None, count=None, value=None, + bucket=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Attribute metadata element + + Args: + name: str (optional) The name of the attribute + attribute_type: str (optional) The type for the attribute. Examples: + test, float, etc. + count: str (optional) The number of times this attribute appears in + the query results. + value: list (optional) The values which are often used for this + attirbute. + bucket: list (optional) The buckets for this attribute. + text: str (optional) The text contents of the XML for this attribute. + extension_elements: list (optional) A list of ExtensionElement + instances + extension_attributes: dict (optional) A dictionary of attribute value + string pairs + """ + + self.name = name + self.type = attribute_type + self.count = count + self.value = value or [] + self.bucket = bucket or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def AttributeFromString(xml_string): + return atom.CreateClassFromXMLString(Attribute, xml_string) + + +class Attributes(atom.AtomBase): + """A collection of Google Base metadata attributes""" + + _tag = 'attributes' + _namespace = GMETA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}attribute' % GMETA_NAMESPACE] = ('attribute', [Attribute]) + + def __init__(self, attribute=None, extension_elements=None, + extension_attributes=None, text=None): + self.attribute = attribute or [] + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +class GBaseItem(ItemAttributeContainer, gdata.BatchEntry): + """An Google Base flavor of an Atom Entry. + + Google Base items have required attributes, recommended attributes, and user + defined attributes. The required attributes are stored in this class as + members, and other attributes are stored as extension elements. You can + access the recommended and user defined attributes by using + AddItemAttribute, SetItemAttribute, FindItemAttribute, and + RemoveItemAttribute. + + The Base Item + """ + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.BatchEntry._children.copy() + _attributes = gdata.BatchEntry._attributes.copy() + _children['{%s}label' % GBASE_NAMESPACE] = ('label', [Label]) + _children['{%s}item_type' % GBASE_NAMESPACE] = ('item_type', ItemType) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, title=None, updated=None, control=None, + label=None, item_type=None, item_attributes=None, + batch_operation=None, batch_id=None, batch_status=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.title = title + self.updated = updated + self.control = control + self.label = label or [] + self.item_type = item_type + self.item_attributes = item_attributes or [] + self.batch_operation = batch_operation + self.batch_id = batch_id + self.batch_status = batch_status + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def GBaseItemFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseItem, xml_string) + + +class GBaseSnippet(GBaseItem): + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = GBaseItem._children.copy() + _attributes = GBaseItem._attributes.copy() + + +def GBaseSnippetFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseSnippet, xml_string) + + +class GBaseAttributeEntry(gdata.GDataEntry): + """An Atom Entry from the attributes feed""" + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}attribute' % GMETA_NAMESPACE] = ('attribute', [Attribute]) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, title=None, updated=None, label=None, + attribute=None, control=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.control = control + self.title = title + self.updated = updated + self.label = label or [] + self.attribute = attribute or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def GBaseAttributeEntryFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseAttributeEntry, xml_string) + + +class GBaseItemTypeEntry(gdata.GDataEntry): + """An Atom entry from the item types feed + + These entries contain a list of attributes which are stored in one + XML node called attributes. This class simplifies the data structure + by treating attributes as a list of attribute instances. + + Note that the item_type for an item type entry is in the Google Base meta + namespace as opposed to item_types encountered in other feeds. + """ + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}attributes' % GMETA_NAMESPACE] = ('attributes', Attributes) + _children['{%s}attribute' % GMETA_NAMESPACE] = ('attribute', [Attribute]) + _children['{%s}item_type' % GMETA_NAMESPACE] = ('item_type', MetaItemType) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, title=None, updated=None, label=None, + item_type=None, control=None, attribute=None, attributes=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.title = title + self.updated = updated + self.control = control + self.label = label or [] + self.item_type = item_type + self.attributes = attributes + self.attribute = attribute or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def GBaseItemTypeEntryFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseItemTypeEntry, xml_string) + + +class GBaseItemFeed(gdata.BatchFeed): + """A feed containing Google Base Items""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.BatchFeed._children.copy() + _attributes = gdata.BatchFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GBaseItem]) + + +def GBaseItemFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseItemFeed, xml_string) + + +class GBaseSnippetFeed(gdata.GDataFeed): + """A feed containing Google Base Snippets""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GBaseSnippet]) + + +def GBaseSnippetFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseSnippetFeed, xml_string) + + +class GBaseAttributesFeed(gdata.GDataFeed): + """A feed containing Google Base Attributes + + A query sent to the attributes feed will return a feed of + attributes which are present in the items that match the + query. + """ + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [GBaseAttributeEntry]) + + +def GBaseAttributesFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseAttributesFeed, xml_string) + + +class GBaseLocalesFeed(gdata.GDataFeed): + """The locales feed from Google Base. + + This read-only feed defines the permitted locales for Google Base. The + locale value identifies the language, currency, and date formats used in a + feed. + """ + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + + +def GBaseLocalesFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseLocalesFeed, xml_string) + + +class GBaseItemTypesFeed(gdata.GDataFeed): + """A feed from the Google Base item types feed""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GBaseItemTypeEntry]) + + +def GBaseItemTypesFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GBaseItemTypesFeed, xml_string) diff --git a/patches/gdata/base/service.py b/patches/gdata/base/service.py new file mode 100755 index 0000000..f6fbfa5 --- /dev/null +++ b/patches/gdata/base/service.py @@ -0,0 +1,256 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 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. + +"""GBaseService extends the GDataService to streamline Google Base operations. + + GBaseService: Provides methods to query feeds and manipulate items. Extends + GDataService. + + DictionaryToParamList: Function which converts a dictionary into a list of + URL arguments (represented as strings). This is a + utility function used in CRUD operations. +""" + +__author__ = 'api.jscudder (Jeffrey Scudder)' + +import urllib +import gdata +import atom.service +import gdata.service +import gdata.base +import atom + + +# URL to which all batch requests are sent. +BASE_BATCH_URL = 'http://www.google.com/base/feeds/items/batch' + + +class Error(Exception): + pass + + +class RequestError(Error): + pass + + +class GBaseService(gdata.service.GDataService): + """Client for the Google Base service.""" + + def __init__(self, email=None, password=None, source=None, + server='base.google.com', api_key=None, additional_headers=None, + handler=None, **kwargs): + """Creates a client for the Google Base service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'base.google.com'. + api_key: string (optional) The Google Base API key to use. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='gbase', source=source, + server=server, additional_headers=additional_headers, handler=handler, + **kwargs) + self.api_key = api_key + + def _SetAPIKey(self, api_key): + if not isinstance(self.additional_headers, dict): + self.additional_headers = {} + self.additional_headers['X-Google-Key'] = api_key + + def __SetAPIKey(self, api_key): + self._SetAPIKey(api_key) + + def _GetAPIKey(self): + if 'X-Google-Key' not in self.additional_headers: + return None + else: + return self.additional_headers['X-Google-Key'] + + def __GetAPIKey(self): + return self._GetAPIKey() + + api_key = property(__GetAPIKey, __SetAPIKey, + doc="""Get or set the API key to be included in all requests.""") + + def Query(self, uri, converter=None): + """Performs a style query and returns a resulting feed or entry. + + Args: + uri: string The full URI which be queried. Examples include + '/base/feeds/snippets?bq=digital+camera', + 'http://www.google.com/base/feeds/snippets?bq=digital+camera' + '/base/feeds/items' + I recommend creating a URI using a query class. + converter: func (optional) A function which will be executed on the + server's response. Examples include GBaseItemFromString, etc. + + Returns: + If converter was specified, returns the results of calling converter on + the server's response. If converter was not specified, and the result + was an Atom Entry, returns a GBaseItem, by default, the method returns + the result of calling gdata.service's Get method. + """ + + result = self.Get(uri, converter=converter) + if converter: + return result + elif isinstance(result, atom.Entry): + return gdata.base.GBaseItemFromString(result.ToString()) + return result + + def QuerySnippetsFeed(self, uri): + return self.Get(uri, converter=gdata.base.GBaseSnippetFeedFromString) + + def QueryItemsFeed(self, uri): + return self.Get(uri, converter=gdata.base.GBaseItemFeedFromString) + + def QueryAttributesFeed(self, uri): + return self.Get(uri, converter=gdata.base.GBaseAttributesFeedFromString) + + def QueryItemTypesFeed(self, uri): + return self.Get(uri, converter=gdata.base.GBaseItemTypesFeedFromString) + + def QueryLocalesFeed(self, uri): + return self.Get(uri, converter=gdata.base.GBaseLocalesFeedFromString) + + def GetItem(self, uri): + return self.Get(uri, converter=gdata.base.GBaseItemFromString) + + def GetSnippet(self, uri): + return self.Get(uri, converter=gdata.base.GBaseSnippetFromString) + + def GetAttribute(self, uri): + return self.Get(uri, converter=gdata.base.GBaseAttributeEntryFromString) + + def GetItemType(self, uri): + return self.Get(uri, converter=gdata.base.GBaseItemTypeEntryFromString) + + def GetLocale(self, uri): + return self.Get(uri, converter=gdata.base.GDataEntryFromString) + + def InsertItem(self, new_item, url_params=None, escape_params=True, + converter=None): + """Adds an item to Google Base. + + Args: + new_item: atom.Entry or subclass A new item which is to be added to + Google Base. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + GBaseItemFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a GBaseItem. + """ + + response = self.Post(new_item, '/base/feeds/items', url_params=url_params, + escape_params=escape_params, converter=converter) + + if not converter and isinstance(response, atom.Entry): + return gdata.base.GBaseItemFromString(response.ToString()) + return response + + def DeleteItem(self, item_id, url_params=None, escape_params=True): + """Removes an item with the specified ID from Google Base. + + Args: + item_id: string The ID of the item to be deleted. Example: + 'http://www.google.com/base/feeds/items/13185446517496042648' + url_params: dict (optional) Additional URL parameters to be included + in the deletion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + True if the delete succeeded. + """ + + return self.Delete('%s' % (item_id[len('http://www.google.com'):],), + url_params=url_params, escape_params=escape_params) + + def UpdateItem(self, item_id, updated_item, url_params=None, + escape_params=True, + converter=gdata.base.GBaseItemFromString): + """Updates an existing item. + + Args: + item_id: string The ID of the item to be updated. Example: + 'http://www.google.com/base/feeds/items/13185446517496042648' + updated_item: atom.Entry, subclass, or string, containing + the Atom Entry which will replace the base item which is + stored at the item_id. + url_params: dict (optional) Additional URL parameters to be included + in the update request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + GBaseItemFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a GBaseItem. + """ + + response = self.Put(updated_item, + item_id, url_params=url_params, escape_params=escape_params, + converter=converter) + if not converter and isinstance(response, atom.Entry): + return gdata.base.GBaseItemFromString(response.ToString()) + return response + + def ExecuteBatch(self, batch_feed, + converter=gdata.base.GBaseItemFeedFromString): + """Sends a batch request feed to the server. + + Args: + batch_feed: gdata.BatchFeed A feed containing BatchEntry elements which + contain the desired CRUD operation and any necessary entry data. + converter: Function (optional) Function to be executed on the server's + response. This function should take one string as a parameter. The + default value is GBaseItemFeedFromString which will turn the result + into a gdata.base.GBaseItem object. + + Returns: + A gdata.BatchFeed containing the results. + """ + + return self.Post(batch_feed, BASE_BATCH_URL, converter=converter) + + +class BaseQuery(gdata.service.Query): + + def _GetBaseQuery(self): + return self['bq'] + + def _SetBaseQuery(self, base_query): + self['bq'] = base_query + + bq = property(_GetBaseQuery, _SetBaseQuery, + doc="""The bq query parameter""") diff --git a/patches/gdata/blogger/__init__.py b/patches/gdata/blogger/__init__.py new file mode 100644 index 0000000..156f25c --- /dev/null +++ b/patches/gdata/blogger/__init__.py @@ -0,0 +1,202 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""Contains extensions to Atom objects used with Blogger.""" + + +__author__ = 'api.jscudder (Jeffrey Scudder)' + + +import atom +import gdata +import re + + +LABEL_SCHEME = 'http://www.blogger.com/atom/ns#' +THR_NAMESPACE = 'http://purl.org/syndication/thread/1.0' + + +class BloggerEntry(gdata.GDataEntry): + """Adds convenience methods inherited by all Blogger entries.""" + + blog_name_pattern = re.compile('(http://)(\w*)') + blog_id_pattern = re.compile('(tag:blogger.com,1999:blog-)(\w*)') + blog_id2_pattern = re.compile('tag:blogger.com,1999:user-(\d+)\.blog-(\d+)') + + def GetBlogId(self): + """Extracts the Blogger id of this blog. + This method is useful when contructing URLs by hand. The blog id is + often used in blogger operation URLs. This should not be confused with + the id member of a BloggerBlog. The id element is the Atom id XML element. + The blog id which this method returns is a part of the Atom id. + + Returns: + The blog's unique id as a string. + """ + if self.id.text: + match = self.blog_id_pattern.match(self.id.text) + if match: + return match.group(2) + else: + return self.blog_id2_pattern.match(self.id.text).group(2) + return None + + def GetBlogName(self): + """Finds the name of this blog as used in the 'alternate' URL. + An alternate URL is in the form 'http://blogName.blogspot.com/'. For an + entry representing the above example, this method would return 'blogName'. + + Returns: + The blog's URL name component as a string. + """ + for link in self.link: + if link.rel == 'alternate': + return self.blog_name_pattern.match(link.href).group(2) + return None + + +class BlogEntry(BloggerEntry): + """Describes a blog entry in the feed listing a user's blogs.""" + + +def BlogEntryFromString(xml_string): + return atom.CreateClassFromXMLString(BlogEntry, xml_string) + + +class BlogFeed(gdata.GDataFeed): + """Describes a feed of a user's blogs.""" + + _children = gdata.GDataFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BlogEntry]) + + +def BlogFeedFromString(xml_string): + return atom.CreateClassFromXMLString(BlogFeed, xml_string) + + +class BlogPostEntry(BloggerEntry): + """Describes a blog post entry in the feed of a blog's posts.""" + + post_id_pattern = re.compile('(tag:blogger.com,1999:blog-)(\w*)(.post-)(\w*)') + + def AddLabel(self, label): + """Adds a label to the blog post. + + The label is represented by an Atom category element, so this method + is shorthand for appending a new atom.Category object. + + Args: + label: str + """ + self.category.append(atom.Category(scheme=LABEL_SCHEME, term=label)) + + def GetPostId(self): + """Extracts the postID string from the entry's Atom id. + + Returns: A string of digits which identify this post within the blog. + """ + if self.id.text: + return self.post_id_pattern.match(self.id.text).group(4) + return None + + +def BlogPostEntryFromString(xml_string): + return atom.CreateClassFromXMLString(BlogPostEntry, xml_string) + + +class BlogPostFeed(gdata.GDataFeed): + """Describes a feed of a blog's posts.""" + + _children = gdata.GDataFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BlogPostEntry]) + + +def BlogPostFeedFromString(xml_string): + return atom.CreateClassFromXMLString(BlogPostFeed, xml_string) + + +class InReplyTo(atom.AtomBase): + _tag = 'in-reply-to' + _namespace = THR_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['href'] = 'href' + _attributes['ref'] = 'ref' + _attributes['source'] = 'source' + _attributes['type'] = 'type' + + def __init__(self, href=None, ref=None, source=None, type=None, + extension_elements=None, extension_attributes=None, text=None): + self.href = href + self.ref = ref + self.source = source + self.type = type + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +def InReplyToFromString(xml_string): + return atom.CreateClassFromXMLString(InReplyTo, xml_string) + + +class CommentEntry(BloggerEntry): + """Describes a blog post comment entry in the feed of a blog post's + comments.""" + + _children = BloggerEntry._children.copy() + _children['{%s}in-reply-to' % THR_NAMESPACE] = ('in_reply_to', InReplyTo) + + comment_id_pattern = re.compile('.*-(\w*)$') + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, control=None, title=None, updated=None, + in_reply_to=None, extension_elements=None, extension_attributes=None, + text=None): + BloggerEntry.__init__(self, author=author, category=category, + content=content, contributor=contributor, atom_id=atom_id, link=link, + published=published, rights=rights, source=source, summary=summary, + control=control, title=title, updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + self.in_reply_to = in_reply_to + + def GetCommentId(self): + """Extracts the commentID string from the entry's Atom id. + + Returns: A string of digits which identify this post within the blog. + """ + if self.id.text: + return self.comment_id_pattern.match(self.id.text).group(1) + return None + + +def CommentEntryFromString(xml_string): + return atom.CreateClassFromXMLString(CommentEntry, xml_string) + + +class CommentFeed(gdata.GDataFeed): + """Describes a feed of a blog post's comments.""" + + _children = gdata.GDataFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [CommentEntry]) + + +def CommentFeedFromString(xml_string): + return atom.CreateClassFromXMLString(CommentFeed, xml_string) + + diff --git a/patches/gdata/blogger/client.py b/patches/gdata/blogger/client.py new file mode 100644 index 0000000..a0bad63 --- /dev/null +++ b/patches/gdata/blogger/client.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +"""Contains a client to communicate with the Blogger servers. + +For documentation on the Blogger API, see: +http://code.google.com/apis/blogger/ +""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import gdata.client +import gdata.gauth +import gdata.blogger.data +import atom.data +import atom.http_core + + +# List user's blogs, takes a user ID, or 'default'. +BLOGS_URL = 'http://www.blogger.com/feeds/%s/blogs' +# Takes a blog ID. +BLOG_POST_URL = 'http://www.blogger.com/feeds/%s/posts/default' +# Takes a blog ID. +BLOG_PAGE_URL = 'http://www.blogger.com/feeds/%s/pages/default' +# Takes a blog ID and post ID. +BLOG_POST_COMMENTS_URL = 'http://www.blogger.com/feeds/%s/%s/comments/default' +# Takes a blog ID. +BLOG_COMMENTS_URL = 'http://www.blogger.com/feeds/%s/comments/default' +# Takes a blog ID. +BLOG_ARCHIVE_URL = 'http://www.blogger.com/feeds/%s/archive/full' + + +class BloggerClient(gdata.client.GDClient): + api_version = '2' + auth_service = 'blogger' + auth_scopes = gdata.gauth.AUTH_SCOPES['blogger'] + + def get_blogs(self, user_id='default', auth_token=None, + desired_class=gdata.blogger.data.BlogFeed, **kwargs): + return self.get_feed(BLOGS_URL % user_id, auth_token=auth_token, + desired_class=desired_class, **kwargs) + + GetBlogs = get_blogs + + def get_posts(self, blog_id, auth_token=None, + desired_class=gdata.blogger.data.BlogPostFeed, query=None, + **kwargs): + return self.get_feed(BLOG_POST_URL % blog_id, auth_token=auth_token, + desired_class=desired_class, query=query, **kwargs) + + GetPosts = get_posts + + def get_pages(self, blog_id, auth_token=None, + desired_class=gdata.blogger.data.BlogPageFeed, query=None, + **kwargs): + return self.get_feed(BLOG_PAGE_URL % blog_id, auth_token=auth_token, + desired_class=desired_class, query=query, **kwargs) + + GetPages = get_pages + + def get_post_comments(self, blog_id, post_id, auth_token=None, + desired_class=gdata.blogger.data.CommentFeed, + query=None, **kwargs): + return self.get_feed(BLOG_POST_COMMENTS_URL % (blog_id, post_id), + auth_token=auth_token, desired_class=desired_class, + query=query, **kwargs) + + GetPostComments = get_post_comments + + def get_blog_comments(self, blog_id, auth_token=None, + desired_class=gdata.blogger.data.CommentFeed, + query=None, **kwargs): + return self.get_feed(BLOG_COMMENTS_URL % blog_id, auth_token=auth_token, + desired_class=desired_class, query=query, **kwargs) + + GetBlogComments = get_blog_comments + + def get_blog_archive(self, blog_id, auth_token=None, **kwargs): + return self.get_feed(BLOG_ARCHIVE_URL % blog_id, auth_token=auth_token, + **kwargs) + + GetBlogArchive = get_blog_archive + + def add_post(self, blog_id, title, body, labels=None, draft=False, + auth_token=None, title_type='text', body_type='html', **kwargs): + # Construct an atom Entry for the blog post to be sent to the server. + new_entry = gdata.blogger.data.BlogPost( + title=atom.data.Title(text=title, type=title_type), + content=atom.data.Content(text=body, type=body_type)) + if labels: + for label in labels: + new_entry.add_label(label) + if draft: + new_entry.control = atom.data.Control(draft=atom.data.Draft(text='yes')) + return self.post(new_entry, BLOG_POST_URL % blog_id, auth_token=auth_token, **kwargs) + + AddPost = add_post + + def add_page(self, blog_id, title, body, draft=False, auth_token=None, + title_type='text', body_type='html', **kwargs): + new_entry = gdata.blogger.data.BlogPage( + title=atom.data.Title(text=title, type=title_type), + content=atom.data.Content(text=body, type=body_type)) + if draft: + new_entry.control = atom.data.Control(draft=atom.data.Draft(text='yes')) + return self.post(new_entry, BLOG_PAGE_URL % blog_id, auth_token=auth_token, **kwargs) + + AddPage = add_page + + def add_comment(self, blog_id, post_id, body, auth_token=None, + title_type='text', body_type='html', **kwargs): + new_entry = gdata.blogger.data.Comment( + content=atom.data.Content(text=body, type=body_type)) + return self.post(new_entry, BLOG_POST_COMMENTS_URL % (blog_id, post_id), + auth_token=auth_token, **kwargs) + + AddComment = add_comment + + def update(self, entry, auth_token=None, **kwargs): + # The Blogger API does not currently support ETags, so for now remove + # the ETag before performing an update. + old_etag = entry.etag + entry.etag = None + response = gdata.client.GDClient.update(self, entry, + auth_token=auth_token, **kwargs) + entry.etag = old_etag + return response + + Update = update + + def delete(self, entry_or_uri, auth_token=None, **kwargs): + if isinstance(entry_or_uri, (str, unicode, atom.http_core.Uri)): + return gdata.client.GDClient.delete(self, entry_or_uri, + auth_token=auth_token, **kwargs) + # The Blogger API does not currently support ETags, so for now remove + # the ETag before performing a delete. + old_etag = entry_or_uri.etag + entry_or_uri.etag = None + response = gdata.client.GDClient.delete(self, entry_or_uri, + auth_token=auth_token, **kwargs) + # TODO: if GDClient.delete raises and exception, the entry's etag may be + # left as None. Should revisit this logic. + entry_or_uri.etag = old_etag + return response + + Delete = delete + + +class Query(gdata.client.Query): + + def __init__(self, order_by=None, **kwargs): + gdata.client.Query.__init__(self, **kwargs) + self.order_by = order_by + + def modify_request(self, http_request): + gdata.client._add_query_param('orderby', self.order_by, http_request) + gdata.client.Query.modify_request(self, http_request) + + ModifyRequest = modify_request diff --git a/patches/gdata/blogger/data.py b/patches/gdata/blogger/data.py new file mode 100644 index 0000000..3cdaa73 --- /dev/null +++ b/patches/gdata/blogger/data.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +"""Data model classes for parsing and generating XML for the Blogger API.""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import re +import urlparse +import atom.core +import gdata.data + + +LABEL_SCHEME = 'http://www.blogger.com/atom/ns#' +THR_TEMPLATE = '{http://purl.org/syndication/thread/1.0}%s' + +BLOG_NAME_PATTERN = re.compile('(http://)(\w*)') +BLOG_ID_PATTERN = re.compile('(tag:blogger.com,1999:blog-)(\w*)') +BLOG_ID2_PATTERN = re.compile('tag:blogger.com,1999:user-(\d+)\.blog-(\d+)') +POST_ID_PATTERN = re.compile( + '(tag:blogger.com,1999:blog-)(\w*)(.post-)(\w*)') +PAGE_ID_PATTERN = re.compile( + '(tag:blogger.com,1999:blog-)(\w*)(.page-)(\w*)') +COMMENT_ID_PATTERN = re.compile('.*-(\w*)$') + + +class BloggerEntry(gdata.data.GDEntry): + """Adds convenience methods inherited by all Blogger entries.""" + + def get_blog_id(self): + """Extracts the Blogger id of this blog. + + This method is useful when contructing URLs by hand. The blog id is + often used in blogger operation URLs. This should not be confused with + the id member of a BloggerBlog. The id element is the Atom id XML element. + The blog id which this method returns is a part of the Atom id. + + Returns: + The blog's unique id as a string. + """ + if self.id.text: + match = BLOG_ID_PATTERN.match(self.id.text) + if match: + return match.group(2) + else: + return BLOG_ID2_PATTERN.match(self.id.text).group(2) + return None + + GetBlogId = get_blog_id + + def get_blog_name(self): + """Finds the name of this blog as used in the 'alternate' URL. + + An alternate URL is in the form 'http://blogName.blogspot.com/'. For an + entry representing the above example, this method would return 'blogName'. + + Returns: + The blog's URL name component as a string. + """ + for link in self.link: + if link.rel == 'alternate': + return urlparse.urlparse(link.href)[1].split(".", 1)[0] + return None + + GetBlogName = get_blog_name + + +class Blog(BloggerEntry): + """Represents a blog which belongs to the user.""" + + +class BlogFeed(gdata.data.GDFeed): + entry = [Blog] + + +class BlogPost(BloggerEntry): + """Represents a single post on a blog.""" + + def add_label(self, label): + """Adds a label to the blog post. + + The label is represented by an Atom category element, so this method + is shorthand for appending a new atom.Category object. + + Args: + label: str + """ + self.category.append(atom.data.Category(scheme=LABEL_SCHEME, term=label)) + + AddLabel = add_label + + def get_post_id(self): + """Extracts the postID string from the entry's Atom id. + + Returns: A string of digits which identify this post within the blog. + """ + if self.id.text: + return POST_ID_PATTERN.match(self.id.text).group(4) + return None + + GetPostId = get_post_id + + +class BlogPostFeed(gdata.data.GDFeed): + entry = [BlogPost] + + +class BlogPage(BloggerEntry): + """Represents a single page on a blog.""" + + def get_page_id(self): + """Extracts the pageID string from entry's Atom id. + + Returns: A string of digits which identify this post within the blog. + """ + if self.id.text: + return PAGE_ID_PATTERN.match(self.id.text).group(4) + return None + + GetPageId = get_page_id + + +class BlogPageFeed(gdata.data.GDFeed): + entry = [BlogPage] + + +class InReplyTo(atom.core.XmlElement): + _qname = THR_TEMPLATE % 'in-reply-to' + href = 'href' + ref = 'ref' + source = 'source' + type = 'type' + + +class Comment(BloggerEntry): + """Blog post comment entry in a feed listing comments on a post or blog.""" + in_reply_to = InReplyTo + + def get_comment_id(self): + """Extracts the commentID string from the entry's Atom id. + + Returns: A string of digits which identify this post within the blog. + """ + if self.id.text: + return COMMENT_ID_PATTERN.match(self.id.text).group(1) + return None + + GetCommentId = get_comment_id + + +class CommentFeed(gdata.data.GDFeed): + entry = [Comment] diff --git a/patches/gdata/blogger/service.py b/patches/gdata/blogger/service.py new file mode 100644 index 0000000..ad74d63 --- /dev/null +++ b/patches/gdata/blogger/service.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# +# Copyright (C) 2007 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. + +"""Classes to interact with the Blogger server.""" + +__author__ = 'api.jscudder (Jeffrey Scudder)' + +import gdata.service +import gdata.blogger + + +class BloggerService(gdata.service.GDataService): + + def __init__(self, email=None, password=None, source=None, + server='www.blogger.com', **kwargs): + """Creates a client for the Blogger service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'www.blogger.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='blogger', source=source, + server=server, **kwargs) + + def GetBlogFeed(self, uri=None): + """Retrieve a list of the blogs to which the current user may manage.""" + if not uri: + uri = '/feeds/default/blogs' + return self.Get(uri, converter=gdata.blogger.BlogFeedFromString) + + def GetBlogCommentFeed(self, blog_id=None, uri=None): + """Retrieve a list of the comments for this blog.""" + if blog_id: + uri = '/feeds/%s/comments/default' % blog_id + return self.Get(uri, converter=gdata.blogger.CommentFeedFromString) + + def GetBlogPostFeed(self, blog_id=None, uri=None): + if blog_id: + uri = '/feeds/%s/posts/default' % blog_id + return self.Get(uri, converter=gdata.blogger.BlogPostFeedFromString) + + def GetPostCommentFeed(self, blog_id=None, post_id=None, uri=None): + """Retrieve a list of the comments for this particular blog post.""" + if blog_id and post_id: + uri = '/feeds/%s/%s/comments/default' % (blog_id, post_id) + return self.Get(uri, converter=gdata.blogger.CommentFeedFromString) + + def AddPost(self, entry, blog_id=None, uri=None): + if blog_id: + uri = '/feeds/%s/posts/default' % blog_id + return self.Post(entry, uri, + converter=gdata.blogger.BlogPostEntryFromString) + + def UpdatePost(self, entry, uri=None): + if not uri: + uri = entry.GetEditLink().href + return self.Put(entry, uri, + converter=gdata.blogger.BlogPostEntryFromString) + + def DeletePost(self, entry=None, uri=None): + if not uri: + uri = entry.GetEditLink().href + return self.Delete(uri) + + def AddComment(self, comment_entry, blog_id=None, post_id=None, uri=None): + """Adds a new comment to the specified blog post.""" + if blog_id and post_id: + uri = '/feeds/%s/%s/comments/default' % (blog_id, post_id) + return self.Post(comment_entry, uri, + converter=gdata.blogger.CommentEntryFromString) + + def DeleteComment(self, entry=None, uri=None): + if not uri: + uri = entry.GetEditLink().href + return self.Delete(uri) + + +class BlogQuery(gdata.service.Query): + + def __init__(self, feed=None, params=None, categories=None, blog_id=None): + """Constructs a query object for the list of a user's Blogger blogs. + + Args: + feed: str (optional) The beginning of the URL to be queried. If the + feed is not set, and there is no blog_id passed in, the default + value is used ('/feeds/default/blogs'). + params: dict (optional) + categories: list (optional) + blog_id: str (optional) + """ + if not feed and blog_id: + feed = '/feeds/default/blogs/%s' % blog_id + elif not feed: + feed = '/feeds/default/blogs' + gdata.service.Query.__init__(self, feed=feed, params=params, + categories=categories) + + +class BlogPostQuery(gdata.service.Query): + + def __init__(self, feed=None, params=None, categories=None, blog_id=None, + post_id=None): + if not feed and blog_id and post_id: + feed = '/feeds/%s/posts/default/%s' % (blog_id, post_id) + elif not feed and blog_id: + feed = '/feeds/%s/posts/default' % blog_id + gdata.service.Query.__init__(self, feed=feed, params=params, + categories=categories) + + +class BlogCommentQuery(gdata.service.Query): + + def __init__(self, feed=None, params=None, categories=None, blog_id=None, + post_id=None, comment_id=None): + if not feed and blog_id and comment_id: + feed = '/feeds/%s/comments/default/%s' % (blog_id, comment_id) + elif not feed and blog_id and post_id: + feed = '/feeds/%s/%s/comments/default' % (blog_id, post_id) + elif not feed and blog_id: + feed = '/feeds/%s/comments/default' % blog_id + gdata.service.Query.__init__(self, feed=feed, params=params, + categories=categories) diff --git a/patches/gdata/books/__init__.py b/patches/gdata/books/__init__.py new file mode 100644 index 0000000..1a961ab --- /dev/null +++ b/patches/gdata/books/__init__.py @@ -0,0 +1,473 @@ +#!/usr/bin/python + +""" + Data Models for books.service + + All classes can be instantiated from an xml string using their FromString + class method. + + Notes: + * Book.title displays the first dc:title because the returned XML + repeats that datum as atom:title. + There is an undocumented gbs:openAccess element that is not parsed. +""" + +__author__ = "James Sams " +__copyright__ = "Apache License v2.0" + +import atom +import gdata + + +BOOK_SEARCH_NAMESPACE = 'http://schemas.google.com/books/2008' +DC_NAMESPACE = 'http://purl.org/dc/terms' +ANNOTATION_REL = "http://schemas.google.com/books/2008/annotation" +INFO_REL = "http://schemas.google.com/books/2008/info" +LABEL_SCHEME = "http://schemas.google.com/books/2008/labels" +PREVIEW_REL = "http://schemas.google.com/books/2008/preview" +THUMBNAIL_REL = "http://schemas.google.com/books/2008/thumbnail" +FULL_VIEW = "http://schemas.google.com/books/2008#view_all_pages" +PARTIAL_VIEW = "http://schemas.google.com/books/2008#view_partial" +NO_VIEW = "http://schemas.google.com/books/2008#view_no_pages" +UNKNOWN_VIEW = "http://schemas.google.com/books/2008#view_unknown" +EMBEDDABLE = "http://schemas.google.com/books/2008#embeddable" +NOT_EMBEDDABLE = "http://schemas.google.com/books/2008#not_embeddable" + + + +class _AtomFromString(atom.AtomBase): + + #@classmethod + def FromString(cls, s): + return atom.CreateClassFromXMLString(cls, s) + + FromString = classmethod(FromString) + + +class Creator(_AtomFromString): + """ + The element identifies an author-or more generally, an entity + responsible for creating the volume in question. Examples of a creator + include a person, an organization, or a service. In the case of + anthologies, proceedings, or other edited works, this field may be used to + indicate editors or other entities responsible for collecting the volume's + contents. + + This element appears as a child of . If there are multiple authors or + contributors to the book, there may be multiple elements in the + volume entry (one for each creator or contributor). + """ + + _tag = 'creator' + _namespace = DC_NAMESPACE + + +class Date(_AtomFromString): #iso 8601 / W3CDTF profile + """ + The element indicates the publication date of the specific volume + in question. If the book is a reprint, this is the reprint date, not the + original publication date. The date is encoded according to the ISO-8601 + standard (and more specifically, the W3CDTF profile). + + The element can appear only as a child of . + + Usually only the year or the year and the month are given. + + YYYY-MM-DDThh:mm:ssTZD TZD = -hh:mm or +hh:mm + """ + + _tag = 'date' + _namespace = DC_NAMESPACE + + +class Description(_AtomFromString): + """ + The element includes text that describes a book or book + result. In a search result feed, this may be a search result "snippet" that + contains the words around the user's search term. For a single volume feed, + this element may contain a synopsis of the book. + + The element can appear only as a child of + """ + + _tag = 'description' + _namespace = DC_NAMESPACE + + +class Format(_AtomFromString): + """ + The element describes the physical properties of the volume. + Currently, it indicates the number of pages in the book, but more + information may be added to this field in the future. + + This element can appear only as a child of . + """ + + _tag = 'format' + _namespace = DC_NAMESPACE + + +class Identifier(_AtomFromString): + """ + The element provides an unambiguous reference to a + particular book. + * Every contains at least one child. + * The first identifier is always the unique string Book Search has assigned + to the volume (such as s1gVAAAAYAAJ). This is the ID that appears in the + book's URL in the Book Search GUI, as well as in the URL of that book's + single item feed. + * Many books contain additional elements. These provide + alternate, external identifiers to the volume. Such identifiers may + include the ISBNs, ISSNs, Library of Congress Control Numbers (LCCNs), + and OCLC numbers; they are prepended with a corresponding namespace + prefix (such as "ISBN:"). + * Any can be passed to the Dynamic Links, used to + instantiate an Embedded Viewer, or even used to construct static links to + Book Search. + The element can appear only as a child of . + """ + + _tag = 'identifier' + _namespace = DC_NAMESPACE + + +class Publisher(_AtomFromString): + """ + The element contains the name of the entity responsible for + producing and distributing the volume (usually the specific edition of this + book). Examples of a publisher include a person, an organization, or a + service. + + This element can appear only as a child of . If there is more than + one publisher, multiple elements may appear. + """ + + _tag = 'publisher' + _namespace = DC_NAMESPACE + + +class Subject(_AtomFromString): + """ + The element identifies the topic of the book. Usually this is + a Library of Congress Subject Heading (LCSH) or Book Industry Standards + and Communications Subject Heading (BISAC). + + The element can appear only as a child of . There may + be multiple elements per entry. + """ + + _tag = 'subject' + _namespace = DC_NAMESPACE + + +class Title(_AtomFromString): + """ + The element contains the title of a book as it was published. If + a book has a subtitle, it appears as a second element in the book + result's . + """ + + _tag = 'title' + _namespace = DC_NAMESPACE + + +class Viewability(_AtomFromString): + """ + Google Book Search respects the user's local copyright restrictions. As a + result, previews or full views of some books are not available in all + locations. The element indicates whether a book is fully + viewable, can be previewed, or only has "about the book" information. These + three "viewability modes" are the same ones returned by the Dynamic Links + API. + + The element can appear only as a child of . + + The value attribute will take the form of the following URIs to represent + the relevant viewing capability: + + Full View: http://schemas.google.com/books/2008#view_all_pages + Limited Preview: http://schemas.google.com/books/2008#view_partial + Snippet View/No Preview: http://schemas.google.com/books/2008#view_no_pages + Unknown view: http://schemas.google.com/books/2008#view_unknown + """ + + _tag = 'viewability' + _namespace = BOOK_SEARCH_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, text=None, + extension_elements=None, extension_attributes=None): + self.value = value + _AtomFromString.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class Embeddability(_AtomFromString): + """ + Many of the books found on Google Book Search can be embedded on third-party + sites using the Embedded Viewer. The element indicates + whether a particular book result is available for embedding. By definition, + a book that cannot be previewed on Book Search cannot be embedded on third- + party sites. + + The element can appear only as a child of . + + The value attribute will take on one of the following URIs: + embeddable: http://schemas.google.com/books/2008#embeddable + not embeddable: http://schemas.google.com/books/2008#not_embeddable + """ + + _tag = 'embeddability' + _namespace = BOOK_SEARCH_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, text=None, extension_elements=None, + extension_attributes=None): + self.value = value + _AtomFromString.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class Review(_AtomFromString): + """ + When present, the element contains a user-generated review for + a given book. This element currently appears only in the user library and + user annotation feeds, as a child of . + + type: text, html, xhtml + xml:lang: id of the language, a guess, (always two letters?) + """ + + _tag = 'review' + _namespace = BOOK_SEARCH_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['type'] = 'type' + _attributes['{http://www.w3.org/XML/1998/namespace}lang'] = 'lang' + + def __init__(self, type=None, lang=None, text=None, + extension_elements=None, extension_attributes=None): + self.type = type + self.lang = lang + _AtomFromString.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class Rating(_AtomFromString): + """All attributes must take an integral string between 1 and 5. + The min, max, and average attributes represent 'community' ratings. The + value attribute is the user's (of the feed from which the item is fetched, + not necessarily the authenticated user) rating of the book. + """ + + _tag = 'rating' + _namespace = gdata.GDATA_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['min'] = 'min' + _attributes['max'] = 'max' + _attributes['average'] = 'average' + _attributes['value'] = 'value' + + def __init__(self, min=None, max=None, average=None, value=None, text=None, + extension_elements=None, extension_attributes=None): + self.min = min + self.max = max + self.average = average + self.value = value + _AtomFromString.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class Book(_AtomFromString, gdata.GDataEntry): + """ + Represents an from either a search, annotation, library, or single + item feed. Note that dc_title attribute is the proper title of the volume, + title is an atom element and may not represent the full title. + """ + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + for i in (Creator, Identifier, Publisher, Subject,): + _children['{%s}%s' % (i._namespace, i._tag)] = (i._tag, [i]) + for i in (Date, Description, Format, Viewability, Embeddability, + Review, Rating): # Review, Rating maybe only in anno/lib entrys + _children['{%s}%s' % (i._namespace, i._tag)] = (i._tag, i) + # there is an atom title as well, should we clobber that? + del(i) + _children['{%s}%s' % (Title._namespace, Title._tag)] = ('dc_title', [Title]) + + def to_dict(self): + """Returns a dictionary of the book's available metadata. If the data + cannot be discovered, it is not included as a key in the returned dict. + The possible keys are: authors, embeddability, date, description, + format, identifiers, publishers, rating, review, subjects, title, and + viewability. + + Notes: + * Plural keys will be lists + * Singular keys will be strings + * Title, despite usually being a list, joins the title and subtitle + with a space as a single string. + * embeddability and viewability only return the portion of the URI + after # + * identifiers is a list of tuples, where the first item of each tuple + is the type of identifier and the second item is the identifying + string. Note that while doing dict() on this tuple may be possible, + some items may have multiple of the same identifier and converting + to a dict may resulted in collisions/dropped data. + * Rating returns only the user's rating. See Rating class for precise + definition. + """ + d = {} + if self.GetAnnotationLink(): + d['annotation'] = self.GetAnnotationLink().href + if self.creator: + d['authors'] = [x.text for x in self.creator] + if self.embeddability: + d['embeddability'] = self.embeddability.value.split('#')[-1] + if self.date: + d['date'] = self.date.text + if self.description: + d['description'] = self.description.text + if self.format: + d['format'] = self.format.text + if self.identifier: + d['identifiers'] = [('google_id', self.identifier[0].text)] + for x in self.identifier[1:]: + l = x.text.split(':') # should we lower the case of the ids? + d['identifiers'].append((l[0], ':'.join(l[1:]))) + if self.GetInfoLink(): + d['info'] = self.GetInfoLink().href + if self.GetPreviewLink(): + d['preview'] = self.GetPreviewLink().href + if self.publisher: + d['publishers'] = [x.text for x in self.publisher] + if self.rating: + d['rating'] = self.rating.value + if self.review: + d['review'] = self.review.text + if self.subject: + d['subjects'] = [x.text for x in self.subject] + if self.GetThumbnailLink(): + d['thumbnail'] = self.GetThumbnailLink().href + if self.dc_title: + d['title'] = ' '.join([x.text for x in self.dc_title]) + if self.viewability: + d['viewability'] = self.viewability.value.split('#')[-1] + return d + + def __init__(self, creator=None, date=None, + description=None, format=None, author=None, identifier=None, + publisher=None, subject=None, dc_title=None, viewability=None, + embeddability=None, review=None, rating=None, category=None, + content=None, contributor=None, atom_id=None, link=None, + published=None, rights=None, source=None, summary=None, + title=None, control=None, updated=None, text=None, + extension_elements=None, extension_attributes=None): + self.creator = creator + self.date = date + self.description = description + self.format = format + self.identifier = identifier + self.publisher = publisher + self.subject = subject + self.dc_title = dc_title or [] + self.viewability = viewability + self.embeddability = embeddability + self.review = review + self.rating = rating + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, contributor=contributor, atom_id=atom_id, + link=link, published=published, rights=rights, source=source, + summary=summary, title=title, control=control, updated=updated, + text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + + def GetThumbnailLink(self): + """Returns the atom.Link object representing the thumbnail URI.""" + for i in self.link: + if i.rel == THUMBNAIL_REL: + return i + + def GetInfoLink(self): + """ + Returns the atom.Link object representing the human-readable info URI. + """ + for i in self.link: + if i.rel == INFO_REL: + return i + + def GetPreviewLink(self): + """Returns the atom.Link object representing the preview URI.""" + for i in self.link: + if i.rel == PREVIEW_REL: + return i + + def GetAnnotationLink(self): + """ + Returns the atom.Link object representing the Annotation URI. + Note that the use of www.books in the href of this link seems to make + this information useless. Using books.service.ANNOTATION_FEED and + BOOK_SERVER to construct your URI seems to work better. + """ + for i in self.link: + if i.rel == ANNOTATION_REL: + return i + + def set_rating(self, value): + """Set user's rating. Must be an integral string between 1 nad 5""" + assert (value in ('1','2','3','4','5')) + if not isinstance(self.rating, Rating): + self.rating = Rating() + self.rating.value = value + + def set_review(self, text, type='text', lang='en'): + """Set user's review text""" + self.review = Review(text=text, type=type, lang=lang) + + def get_label(self): + """Get users label for the item as a string""" + for i in self.category: + if i.scheme == LABEL_SCHEME: + return i.term + + def set_label(self, term): + """Clear pre-existing label for the item and set term as the label.""" + self.remove_label() + self.category.append(atom.Category(term=term, scheme=LABEL_SCHEME)) + + def remove_label(self): + """Clear the user's label for the item""" + ln = len(self.category) + for i, j in enumerate(self.category[::-1]): + if j.scheme == LABEL_SCHEME: + del(self.category[ln-1-i]) + + def clean_annotations(self): + """Clear all annotations from an item. Useful for taking an item from + another user's library/annotation feed and adding it to the + authenticated user's library without adopting annotations.""" + self.remove_label() + self.review = None + self.rating = None + + + def get_google_id(self): + """Get Google's ID of the item.""" + return self.id.text.split('/')[-1] + + +class BookFeed(_AtomFromString, gdata.GDataFeed): + """Represents a feed of entries from a search.""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _children['{%s}%s' % (Book._namespace, Book._tag)] = (Book._tag, [Book]) + + +if __name__ == '__main__': + import doctest + doctest.testfile('datamodels.txt') diff --git a/patches/gdata/books/data.py b/patches/gdata/books/data.py new file mode 100644 index 0000000..3f7f978 --- /dev/null +++ b/patches/gdata/books/data.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Contains the data classes of the Google Book Search Data API""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core +import atom.data +import gdata.data +import gdata.dublincore.data +import gdata.opensearch.data + + +GBS_TEMPLATE = '{http://schemas.google.com/books/2008/}%s' + + +class CollectionEntry(gdata.data.GDEntry): + """Describes an entry in a feed of collections.""" + + +class CollectionFeed(gdata.data.BatchFeed): + """Describes a Book Search collection feed.""" + entry = [CollectionEntry] + + +class Embeddability(atom.core.XmlElement): + """Describes an embeddability.""" + _qname = GBS_TEMPLATE % 'embeddability' + value = 'value' + + +class OpenAccess(atom.core.XmlElement): + """Describes an open access.""" + _qname = GBS_TEMPLATE % 'openAccess' + value = 'value' + + +class Review(atom.core.XmlElement): + """User-provided review.""" + _qname = GBS_TEMPLATE % 'review' + lang = 'lang' + type = 'type' + + +class Viewability(atom.core.XmlElement): + """Describes a viewability.""" + _qname = GBS_TEMPLATE % 'viewability' + value = 'value' + + +class VolumeEntry(gdata.data.GDEntry): + """Describes an entry in a feed of Book Search volumes.""" + comments = gdata.data.Comments + language = [gdata.dublincore.data.Language] + open_access = OpenAccess + format = [gdata.dublincore.data.Format] + dc_title = [gdata.dublincore.data.Title] + viewability = Viewability + embeddability = Embeddability + creator = [gdata.dublincore.data.Creator] + rating = gdata.data.Rating + description = [gdata.dublincore.data.Description] + publisher = [gdata.dublincore.data.Publisher] + date = [gdata.dublincore.data.Date] + subject = [gdata.dublincore.data.Subject] + identifier = [gdata.dublincore.data.Identifier] + review = Review + + +class VolumeFeed(gdata.data.BatchFeed): + """Describes a Book Search volume feed.""" + entry = [VolumeEntry] + + diff --git a/patches/gdata/books/service.py b/patches/gdata/books/service.py new file mode 100644 index 0000000..cbb846f --- /dev/null +++ b/patches/gdata/books/service.py @@ -0,0 +1,266 @@ +#!/usr/bin/python + +""" + Extend gdata.service.GDataService to support authenticated CRUD ops on + Books API + + http://code.google.com/apis/books/docs/getting-started.html + http://code.google.com/apis/books/docs/gdata/developers_guide_protocol.html + + TODO: (here and __init__) + * search based on label, review, or other annotations (possible?) + * edit (specifically, Put requests) seem to fail effect a change + + Problems With API: + * Adding a book with a review to the library adds a note, not a review. + This does not get included in the returned item. You see this by + looking at My Library through the website. + * Editing a review never edits a review (unless it is freshly added, but + see above). More generally, + * a Put request with changed annotations (label/rating/review) does NOT + change the data. Note: Put requests only work on the href from + GetEditLink (as per the spec). Do not try to PUT to the annotate or + library feeds, this will cause a 400 Invalid URI Bad Request response. + Attempting to Post to one of the feeds with the updated annotations + does not update them. See the following for (hopefully) a follow up: + google.com/support/forum/p/booksearch-apis/thread?tid=27fd7f68de438fc8 + * Attempts to workaround the edit problem continue to fail. For example, + removing the item, editing the data, readding the item, gives us only + our originally added data (annotations). This occurs even if we + completely shut python down, refetch the book from the public feed, + and re-add it. There is some kind of persistence going on that I + cannot change. This is likely due to the annotations being cached in + the annotation feed and the inability to edit (see Put, above) + * GetAnnotationLink has www.books.... as the server, but hitting www... + results in a bad URI error. + * Spec indicates there may be multiple labels, but there does not seem + to be a way to get the server to accept multiple labels, nor does the + web interface have an obvious way to have multiple labels. Multiple + labels are never returned. +""" + +__author__ = "James Sams " +__copyright__ = "Apache License v2.0" + +from shlex import split + +import gdata.service +try: + import books +except ImportError: + import gdata.books as books + + +BOOK_SERVER = "books.google.com" +GENERAL_FEED = "/books/feeds/volumes" +ITEM_FEED = "/books/feeds/volumes/" +LIBRARY_FEED = "/books/feeds/users/%s/collections/library/volumes" +ANNOTATION_FEED = "/books/feeds/users/%s/volumes" +PARTNER_FEED = "/books/feeds/p/%s/volumes" +BOOK_SERVICE = "print" +ACCOUNT_TYPE = "HOSTED_OR_GOOGLE" + + +class BookService(gdata.service.GDataService): + + def __init__(self, email=None, password=None, source=None, + server=BOOK_SERVER, account_type=ACCOUNT_TYPE, + exception_handlers=tuple(), **kwargs): + """source should be of form 'ProgramCompany - ProgramName - Version'""" + + gdata.service.GDataService.__init__(self, email=email, + password=password, service=BOOK_SERVICE, source=source, + server=server, **kwargs) + self.exception_handlers = exception_handlers + + def search(self, q, start_index="1", max_results="10", + min_viewability="none", feed=GENERAL_FEED, + converter=books.BookFeed.FromString): + """ + Query the Public search feed. q is either a search string or a + gdata.service.Query instance with a query set. + + min_viewability must be "none", "partial", or "full". + + If you change the feed to a single item feed, note that you will + probably need to change the converter to be Book.FromString + """ + + if not isinstance(q, gdata.service.Query): + q = gdata.service.Query(text_query=q) + if feed: + q.feed = feed + q['start-index'] = start_index + q['max-results'] = max_results + q['min-viewability'] = min_viewability + return self.Get(uri=q.ToUri(),converter=converter) + + def search_by_keyword(self, q='', feed=GENERAL_FEED, start_index="1", + max_results="10", min_viewability="none", **kwargs): + """ + Query the Public Search Feed by keyword. Non-keyword strings can be + set in q. This is quite fragile. Is there a function somewhere in + the Google library that will parse a query the same way that Google + does? + + Legal Identifiers are listed below and correspond to their meaning + at http://books.google.com/advanced_book_search: + all_words + exact_phrase + at_least_one + without_words + title + author + publisher + subject + isbn + lccn + oclc + seemingly unsupported: + publication_date: a sequence of two, two tuples: + ((min_month,min_year),(max_month,max_year)) + where month is one/two digit month, year is 4 digit, eg: + (('1','2000'),('10','2003')). Lower bound is inclusive, + upper bound is exclusive + """ + + for k, v in kwargs.items(): + if not v: + continue + k = k.lower() + if k == 'all_words': + q = "%s %s" % (q, v) + elif k == 'exact_phrase': + q = '%s "%s"' % (q, v.strip('"')) + elif k == 'at_least_one': + q = '%s %s' % (q, ' '.join(['OR "%s"' % x for x in split(v)])) + elif k == 'without_words': + q = '%s %s' % (q, ' '.join(['-"%s"' % x for x in split(v)])) + elif k in ('author','title', 'publisher'): + q = '%s %s' % (q, ' '.join(['in%s:"%s"'%(k,x) for x in split(v)])) + elif k == 'subject': + q = '%s %s' % (q, ' '.join(['%s:"%s"' % (k,x) for x in split(v)])) + elif k == 'isbn': + q = '%s ISBN%s' % (q, v) + elif k == 'issn': + q = '%s ISSN%s' % (q,v) + elif k == 'oclc': + q = '%s OCLC%s' % (q,v) + else: + raise ValueError("Unsupported search keyword") + return self.search(q.strip(),start_index=start_index, feed=feed, + max_results=max_results, + min_viewability=min_viewability) + + def search_library(self, q, id='me', **kwargs): + """Like search, but in a library feed. Default is the authenticated + user's feed. Change by setting id.""" + + if 'feed' in kwargs: + raise ValueError("kwarg 'feed' conflicts with library_id") + feed = LIBRARY_FEED % id + return self.search(q, feed=feed, **kwargs) + + def search_library_by_keyword(self, id='me', **kwargs): + """Hybrid of search_by_keyword and search_library + """ + + if 'feed' in kwargs: + raise ValueError("kwarg 'feed' conflicts with library_id") + feed = LIBRARY_FEED % id + return self.search_by_keyword(feed=feed,**kwargs) + + def search_annotations(self, q, id='me', **kwargs): + """Like search, but in an annotation feed. Default is the authenticated + user's feed. Change by setting id.""" + + if 'feed' in kwargs: + raise ValueError("kwarg 'feed' conflicts with library_id") + feed = ANNOTATION_FEED % id + return self.search(q, feed=feed, **kwargs) + + def search_annotations_by_keyword(self, id='me', **kwargs): + """Hybrid of search_by_keyword and search_annotations + """ + + if 'feed' in kwargs: + raise ValueError("kwarg 'feed' conflicts with library_id") + feed = ANNOTATION_FEED % id + return self.search_by_keyword(feed=feed,**kwargs) + + def add_item_to_library(self, item): + """Add the item, either an XML string or books.Book instance, to the + user's library feed""" + + feed = LIBRARY_FEED % 'me' + return self.Post(data=item, uri=feed, converter=books.Book.FromString) + + def remove_item_from_library(self, item): + """ + Remove the item, a books.Book instance, from the authenticated user's + library feed. Using an item retrieved from a public search will fail. + """ + + return self.Delete(item.GetEditLink().href) + + def add_annotation(self, item): + """ + Add the item, either an XML string or books.Book instance, to the + user's annotation feed. + """ + # do not use GetAnnotationLink, results in 400 Bad URI due to www + return self.Post(data=item, uri=ANNOTATION_FEED % 'me', + converter=books.Book.FromString) + + def edit_annotation(self, item): + """ + Send an edited item, a books.Book instance, to the user's annotation + feed. Note that whereas extra annotations in add_annotations, minus + ratings which are immutable once set, are simply added to the item in + the annotation feed, if an annotation has been removed from the item, + sending an edit request will remove that annotation. This should not + happen with add_annotation. + """ + + return self.Put(data=item, uri=item.GetEditLink().href, + converter=books.Book.FromString) + + def get_by_google_id(self, id): + return self.Get(ITEM_FEED + id, converter=books.Book.FromString) + + def get_library(self, id='me',feed=LIBRARY_FEED, start_index="1", + max_results="100", min_viewability="none", + converter=books.BookFeed.FromString): + """ + Return a generator object that will return gbook.Book instances until + the search feed no longer returns an item from the GetNextLink method. + Thus max_results is not the maximum number of items that will be + returned, but rather the number of items per page of searches. This has + been set high to reduce the required number of network requests. + """ + + q = gdata.service.Query() + q.feed = feed % id + q['start-index'] = start_index + q['max-results'] = max_results + q['min-viewability'] = min_viewability + x = self.Get(uri=q.ToUri(), converter=converter) + while 1: + for entry in x.entry: + yield entry + else: + l = x.GetNextLink() + if l: # hope the server preserves our preferences + x = self.Get(uri=l.href, converter=converter) + else: + break + + def get_annotations(self, id='me', start_index="1", max_results="100", + min_viewability="none", converter=books.BookFeed.FromString): + """ + Like get_library, but for the annotation feed + """ + + return self.get_library(id=id, feed=ANNOTATION_FEED, + max_results=max_results, min_viewability = min_viewability, + converter=converter) diff --git a/patches/gdata/calendar/__init__.py b/patches/gdata/calendar/__init__.py new file mode 100755 index 0000000..06c0410 --- /dev/null +++ b/patches/gdata/calendar/__init__.py @@ -0,0 +1,1044 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 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. + + +"""Contains extensions to ElementWrapper objects used with Google Calendar.""" + + +__author__ = 'api.vli (Vivian Li), api.rboyd (Ryan Boyd)' + + +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 +import gdata + + +# XML namespaces which are often used in Google Calendar entities. +GCAL_NAMESPACE = 'http://schemas.google.com/gCal/2005' +GCAL_TEMPLATE = '{http://schemas.google.com/gCal/2005}%s' +WEB_CONTENT_LINK_REL = '%s/%s' % (GCAL_NAMESPACE, 'webContent') +GACL_NAMESPACE = gdata.GACL_NAMESPACE +GACL_TEMPLATE = gdata.GACL_TEMPLATE + + + +class ValueAttributeContainer(atom.AtomBase): + """A parent class for all Calendar classes which have a value attribute. + + Children include Color, AccessLevel, Hidden + """ + + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +class Color(ValueAttributeContainer): + """The Google Calendar color element""" + + _tag = 'color' + _namespace = GCAL_NAMESPACE + _children = ValueAttributeContainer._children.copy() + _attributes = ValueAttributeContainer._attributes.copy() + + + +class AccessLevel(ValueAttributeContainer): + """The Google Calendar accesslevel element""" + + _tag = 'accesslevel' + _namespace = GCAL_NAMESPACE + _children = ValueAttributeContainer._children.copy() + _attributes = ValueAttributeContainer._attributes.copy() + + +class Hidden(ValueAttributeContainer): + """The Google Calendar hidden element""" + + _tag = 'hidden' + _namespace = GCAL_NAMESPACE + _children = ValueAttributeContainer._children.copy() + _attributes = ValueAttributeContainer._attributes.copy() + + +class Selected(ValueAttributeContainer): + """The Google Calendar selected element""" + + _tag = 'selected' + _namespace = GCAL_NAMESPACE + _children = ValueAttributeContainer._children.copy() + _attributes = ValueAttributeContainer._attributes.copy() + + +class Timezone(ValueAttributeContainer): + """The Google Calendar timezone element""" + + _tag = 'timezone' + _namespace = GCAL_NAMESPACE + _children = ValueAttributeContainer._children.copy() + _attributes = ValueAttributeContainer._attributes.copy() + + +class Where(atom.AtomBase): + """The Google Calendar Where element""" + + _tag = 'where' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['valueString'] = 'value_string' + + def __init__(self, value_string=None, extension_elements=None, + extension_attributes=None, text=None): + self.value_string = value_string + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class CalendarListEntry(gdata.GDataEntry, gdata.LinkFinder): + """A Google Calendar meta Entry flavor of an Atom Entry """ + + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}color' % GCAL_NAMESPACE] = ('color', Color) + _children['{%s}accesslevel' % GCAL_NAMESPACE] = ('access_level', + AccessLevel) + _children['{%s}hidden' % GCAL_NAMESPACE] = ('hidden', Hidden) + _children['{%s}selected' % GCAL_NAMESPACE] = ('selected', Selected) + _children['{%s}timezone' % GCAL_NAMESPACE] = ('timezone', Timezone) + _children['{%s}where' % gdata.GDATA_NAMESPACE] = ('where', Where) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + color=None, access_level=None, hidden=None, timezone=None, + selected=None, + where=None, + extension_elements=None, extension_attributes=None, text=None): + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, + updated=updated, text=None) + + self.color = color + self.access_level = access_level + self.hidden = hidden + self.selected = selected + self.timezone = timezone + self.where = where + + +class CalendarListFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Calendar meta feed flavor of an Atom Feed""" + + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [CalendarListEntry]) + + +class Scope(atom.AtomBase): + """The Google ACL scope element""" + + _tag = 'scope' + _namespace = GACL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + _attributes['type'] = 'type' + + def __init__(self, extension_elements=None, value=None, scope_type=None, + extension_attributes=None, text=None): + self.value = value + self.type = scope_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Role(ValueAttributeContainer): + """The Google Calendar timezone element""" + + _tag = 'role' + _namespace = GACL_NAMESPACE + _children = ValueAttributeContainer._children.copy() + _attributes = ValueAttributeContainer._attributes.copy() + + +class CalendarAclEntry(gdata.GDataEntry, gdata.LinkFinder): + """A Google Calendar ACL Entry flavor of an Atom Entry """ + + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}scope' % GACL_NAMESPACE] = ('scope', Scope) + _children['{%s}role' % GACL_NAMESPACE] = ('role', Role) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + scope=None, role=None, + extension_elements=None, extension_attributes=None, text=None): + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, + updated=updated, text=None) + self.scope = scope + self.role = role + + +class CalendarAclFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Calendar ACL feed flavor of an Atom Feed""" + + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [CalendarAclEntry]) + + +class CalendarEventCommentEntry(gdata.GDataEntry, gdata.LinkFinder): + """A Google Calendar event comments entry flavor of an Atom Entry""" + + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + + +class CalendarEventCommentFeed(gdata.GDataFeed, gdata.LinkFinder): + """A Google Calendar event comments feed flavor of an Atom Feed""" + + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [CalendarEventCommentEntry]) + + +class ExtendedProperty(gdata.ExtendedProperty): + """A transparent subclass of gdata.ExtendedProperty added to this module + for backwards compatibility.""" + + +class Reminder(atom.AtomBase): + """The Google Calendar reminder element""" + + _tag = 'reminder' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['absoluteTime'] = 'absolute_time' + _attributes['days'] = 'days' + _attributes['hours'] = 'hours' + _attributes['minutes'] = 'minutes' + _attributes['method'] = 'method' + + def __init__(self, absolute_time=None, + days=None, hours=None, minutes=None, method=None, + extension_elements=None, + extension_attributes=None, text=None): + self.absolute_time = absolute_time + if days is not None: + self.days = str(days) + else: + self.days = None + if hours is not None: + self.hours = str(hours) + else: + self.hours = None + if minutes is not None: + self.minutes = str(minutes) + else: + self.minutes = None + self.method = method + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class When(atom.AtomBase): + """The Google Calendar When element""" + + _tag = 'when' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}reminder' % gdata.GDATA_NAMESPACE] = ('reminder', [Reminder]) + _attributes['startTime'] = 'start_time' + _attributes['endTime'] = 'end_time' + + def __init__(self, start_time=None, end_time=None, reminder=None, + extension_elements=None, extension_attributes=None, text=None): + self.start_time = start_time + self.end_time = end_time + self.reminder = reminder or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Recurrence(atom.AtomBase): + """The Google Calendar Recurrence element""" + + _tag = 'recurrence' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + +class UriEnumElement(atom.AtomBase): + + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, tag, enum_map, attrib_name='value', + extension_elements=None, extension_attributes=None, text=None): + self.tag=tag + self.enum_map=enum_map + self.attrib_name=attrib_name + self.value=None + self.text=text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + def findKey(self, value): + res=[item[0] for item in self.enum_map.items() if item[1] == value] + if res is None or len(res) == 0: + return None + return res[0] + + def _ConvertElementAttributeToMember(self, attribute, value): + # Special logic to use the enum_map to set the value of the object's value member. + if attribute == self.attrib_name and value != '': + self.value = self.enum_map[value] + return + # Find the attribute in this class's list of attributes. + if self.__class__._attributes.has_key(attribute): + # Find the member of this class which corresponds to the XML attribute + # (lookup in current_class._attributes) and set this member to the + # desired value (using self.__dict__). + setattr(self, self.__class__._attributes[attribute], value) + else: + # The current class doesn't map this attribute, so try to parent class. + atom.ExtensionContainer._ConvertElementAttributeToMember(self, + attribute, + value) + + def _AddMembersToElementTree(self, tree): + # Convert the members of this class which are XML child nodes. + # This uses the class's _children dictionary to find the members which + # should become XML child nodes. + member_node_names = [values[0] for tag, values in + self.__class__._children.iteritems()] + for member_name in member_node_names: + member = getattr(self, member_name) + if member is None: + pass + elif isinstance(member, list): + for instance in member: + instance._BecomeChildElement(tree) + else: + member._BecomeChildElement(tree) + # Special logic to set the desired XML attribute. + key = self.findKey(self.value) + if key is not None: + tree.attrib[self.attrib_name]=key + # Convert the members of this class which are XML attributes. + for xml_attribute, member_name in self.__class__._attributes.iteritems(): + member = getattr(self, member_name) + if member is not None: + tree.attrib[xml_attribute] = member + # Lastly, call the parent's _AddMembersToElementTree to get any + # extension elements. + atom.ExtensionContainer._AddMembersToElementTree(self, tree) + + + +class AttendeeStatus(UriEnumElement): + """The Google Calendar attendeeStatus element""" + + _tag = 'attendeeStatus' + _namespace = gdata.GDATA_NAMESPACE + _children = UriEnumElement._children.copy() + _attributes = UriEnumElement._attributes.copy() + + attendee_enum = { + 'http://schemas.google.com/g/2005#event.accepted' : 'ACCEPTED', + 'http://schemas.google.com/g/2005#event.declined' : 'DECLINED', + 'http://schemas.google.com/g/2005#event.invited' : 'INVITED', + 'http://schemas.google.com/g/2005#event.tentative' : 'TENTATIVE'} + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + UriEnumElement.__init__(self, 'attendeeStatus', AttendeeStatus.attendee_enum, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +class AttendeeType(UriEnumElement): + """The Google Calendar attendeeType element""" + + _tag = 'attendeeType' + _namespace = gdata.GDATA_NAMESPACE + _children = UriEnumElement._children.copy() + _attributes = UriEnumElement._attributes.copy() + + attendee_type_enum = { + 'http://schemas.google.com/g/2005#event.optional' : 'OPTIONAL', + 'http://schemas.google.com/g/2005#event.required' : 'REQUIRED' } + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + UriEnumElement.__init__(self, 'attendeeType', + AttendeeType.attendee_type_enum, + extension_elements=extension_elements, + extension_attributes=extension_attributes,text=text) + + +class Visibility(UriEnumElement): + """The Google Calendar Visibility element""" + + _tag = 'visibility' + _namespace = gdata.GDATA_NAMESPACE + _children = UriEnumElement._children.copy() + _attributes = UriEnumElement._attributes.copy() + + visibility_enum = { + 'http://schemas.google.com/g/2005#event.confidential' : 'CONFIDENTIAL', + 'http://schemas.google.com/g/2005#event.default' : 'DEFAULT', + 'http://schemas.google.com/g/2005#event.private' : 'PRIVATE', + 'http://schemas.google.com/g/2005#event.public' : 'PUBLIC' } + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + UriEnumElement.__init__(self, 'visibility', Visibility.visibility_enum, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +class Transparency(UriEnumElement): + """The Google Calendar Transparency element""" + + _tag = 'transparency' + _namespace = gdata.GDATA_NAMESPACE + _children = UriEnumElement._children.copy() + _attributes = UriEnumElement._attributes.copy() + + transparency_enum = { + 'http://schemas.google.com/g/2005#event.opaque' : 'OPAQUE', + 'http://schemas.google.com/g/2005#event.transparent' : 'TRANSPARENT' } + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + UriEnumElement.__init__(self, tag='transparency', + enum_map=Transparency.transparency_enum, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +class Comments(atom.AtomBase): + """The Google Calendar comments element""" + + _tag = 'comments' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + gdata.FeedLink) + _attributes['rel'] = 'rel' + + def __init__(self, rel=None, feed_link=None, extension_elements=None, + extension_attributes=None, text=None): + self.rel = rel + self.feed_link = feed_link + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class EventStatus(UriEnumElement): + """The Google Calendar eventStatus element""" + + _tag = 'eventStatus' + _namespace = gdata.GDATA_NAMESPACE + _children = UriEnumElement._children.copy() + _attributes = UriEnumElement._attributes.copy() + + status_enum = { 'http://schemas.google.com/g/2005#event.canceled' : 'CANCELED', + 'http://schemas.google.com/g/2005#event.confirmed' : 'CONFIRMED', + 'http://schemas.google.com/g/2005#event.tentative' : 'TENTATIVE'} + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + UriEnumElement.__init__(self, tag='eventStatus', + enum_map=EventStatus.status_enum, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +class Who(UriEnumElement): + """The Google Calendar Who element""" + + _tag = 'who' + _namespace = gdata.GDATA_NAMESPACE + _children = UriEnumElement._children.copy() + _attributes = UriEnumElement._attributes.copy() + _children['{%s}attendeeStatus' % gdata.GDATA_NAMESPACE] = ( + 'attendee_status', AttendeeStatus) + _children['{%s}attendeeType' % gdata.GDATA_NAMESPACE] = ('attendee_type', + AttendeeType) + _attributes['valueString'] = 'name' + _attributes['email'] = 'email' + + relEnum = { 'http://schemas.google.com/g/2005#event.attendee' : 'ATTENDEE', + 'http://schemas.google.com/g/2005#event.organizer' : 'ORGANIZER', + 'http://schemas.google.com/g/2005#event.performer' : 'PERFORMER', + 'http://schemas.google.com/g/2005#event.speaker' : 'SPEAKER', + 'http://schemas.google.com/g/2005#message.bcc' : 'BCC', + 'http://schemas.google.com/g/2005#message.cc' : 'CC', + 'http://schemas.google.com/g/2005#message.from' : 'FROM', + 'http://schemas.google.com/g/2005#message.reply-to' : 'REPLY_TO', + 'http://schemas.google.com/g/2005#message.to' : 'TO' } + + def __init__(self, name=None, email=None, attendee_status=None, + attendee_type=None, rel=None, extension_elements=None, + extension_attributes=None, text=None): + UriEnumElement.__init__(self, 'who', Who.relEnum, attrib_name='rel', + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + self.name = name + self.email = email + self.attendee_status = attendee_status + self.attendee_type = attendee_type + self.rel = rel + + +class OriginalEvent(atom.AtomBase): + """The Google Calendar OriginalEvent element""" + + _tag = 'originalEvent' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + # TODO: The when tag used to map to a EntryLink, make sure it should really be a When. + _children['{%s}when' % gdata.GDATA_NAMESPACE] = ('when', When) + _attributes['id'] = 'id' + _attributes['href'] = 'href' + + def __init__(self, id=None, href=None, when=None, + extension_elements=None, extension_attributes=None, text=None): + self.id = id + self.href = href + self.when = when + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def GetCalendarEventEntryClass(): + return CalendarEventEntry + + +# This class is not completely defined here, because of a circular reference +# in which CalendarEventEntryLink and CalendarEventEntry refer to one another. +class CalendarEventEntryLink(gdata.EntryLink): + """An entryLink which contains a calendar event entry + + Within an event's recurranceExceptions, an entry link + points to a calendar event entry. This class exists + to capture the calendar specific extensions in the entry. + """ + + _tag = 'entryLink' + _namespace = gdata.GDATA_NAMESPACE + _children = gdata.EntryLink._children.copy() + _attributes = gdata.EntryLink._attributes.copy() + # The CalendarEventEntryLink should like CalendarEventEntry as a child but + # that class hasn't been defined yet, so we will wait until after defining + # CalendarEventEntry to list it in _children. + + +class RecurrenceException(atom.AtomBase): + """The Google Calendar RecurrenceException element""" + + _tag = 'recurrenceException' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}entryLink' % gdata.GDATA_NAMESPACE] = ('entry_link', + CalendarEventEntryLink) + _children['{%s}originalEvent' % gdata.GDATA_NAMESPACE] = ('original_event', + OriginalEvent) + _attributes['specialized'] = 'specialized' + + def __init__(self, specialized=None, entry_link=None, + original_event=None, extension_elements=None, + extension_attributes=None, text=None): + self.specialized = specialized + self.entry_link = entry_link + self.original_event = original_event + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class SendEventNotifications(atom.AtomBase): + """The Google Calendar sendEventNotifications element""" + + _tag = 'sendEventNotifications' + _namespace = GCAL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, extension_elements=None, + value=None, extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class QuickAdd(atom.AtomBase): + """The Google Calendar quickadd element""" + + _tag = 'quickadd' + _namespace = GCAL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, extension_elements=None, + value=None, extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + def _TransferToElementTree(self, element_tree): + if self.value: + element_tree.attrib['value'] = self.value + element_tree.tag = GCAL_TEMPLATE % 'quickadd' + atom.AtomBase._TransferToElementTree(self, element_tree) + return element_tree + + def _TakeAttributeFromElementTree(self, attribute, element_tree): + if attribute == 'value': + self.value = element_tree.attrib[attribute] + del element_tree.attrib[attribute] + else: + atom.AtomBase._TakeAttributeFromElementTree(self, attribute, + element_tree) + + +class SyncEvent(atom.AtomBase): + _tag = 'syncEvent' + _namespace = GCAL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value='false', extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class UID(atom.AtomBase): + _tag = 'uid' + _namespace = GCAL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Sequence(atom.AtomBase): + _tag = 'sequence' + _namespace = GCAL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class WebContentGadgetPref(atom.AtomBase): + + _tag = 'webContentGadgetPref' + _namespace = GCAL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + _attributes['value'] = 'value' + + """The Google Calendar Web Content Gadget Preferences element""" + + def __init__(self, name=None, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class WebContent(atom.AtomBase): + + _tag = 'webContent' + _namespace = GCAL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}webContentGadgetPref' % GCAL_NAMESPACE] = ('gadget_pref', + [WebContentGadgetPref]) + _attributes['url'] = 'url' + _attributes['width'] = 'width' + _attributes['height'] = 'height' + + def __init__(self, url=None, width=None, height=None, text=None, + gadget_pref=None, extension_elements=None, extension_attributes=None): + self.url = url + self.width = width + self.height = height + self.text = text + self.gadget_pref = gadget_pref or [] + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class WebContentLink(atom.Link): + + _tag = 'link' + _namespace = atom.ATOM_NAMESPACE + _children = atom.Link._children.copy() + _attributes = atom.Link._attributes.copy() + _children['{%s}webContent' % GCAL_NAMESPACE] = ('web_content', WebContent) + + def __init__(self, title=None, href=None, link_type=None, + web_content=None): + atom.Link.__init__(self, rel=WEB_CONTENT_LINK_REL, title=title, href=href, + link_type=link_type) + self.web_content = web_content + + +class GuestsCanInviteOthers(atom.AtomBase): + """Indicates whether event attendees may invite others to the event. + + This element may only be changed by the organizer of the event. If not + included as part of the event entry, this element will default to true + during a POST request, and will inherit its previous value during a PUT + request. + """ + _tag = 'guestsCanInviteOthers' + _namespace = GCAL_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value='true', *args, **kwargs): + atom.AtomBase.__init__(self, *args, **kwargs) + self.value = value + + +class GuestsCanSeeGuests(atom.AtomBase): + """Indicates whether attendees can see other people invited to the event. + + The organizer always sees all attendees. Guests always see themselves. This + property affects what attendees see in the event's guest list via both the + Calendar UI and API feeds. + + This element may only be changed by the organizer of the event. + + If not included as part of the event entry, this element will default to + true during a POST request, and will inherit its previous value during a + PUT request. + """ + _tag = 'guestsCanSeeGuests' + _namespace = GCAL_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value='true', *args, **kwargs): + atom.AtomBase.__init__(self, *args, **kwargs) + self.value = value + + +class GuestsCanModify(atom.AtomBase): + """Indicates whether event attendees may modify the original event. + + If yes, changes are visible to organizer and other attendees. Otherwise, + any changes made by attendees will be restricted to that attendee's + calendar. + + This element may only be changed by the organizer of the event, and may + be set to 'true' only if both gCal:guestsCanInviteOthers and + gCal:guestsCanSeeGuests are set to true in the same PUT/POST request. + Otherwise, request fails with HTTP error code 400 (Bad Request). + + If not included as part of the event entry, this element will default to + false during a POST request, and will inherit its previous value during a + PUT request.""" + _tag = 'guestsCanModify' + _namespace = GCAL_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value='false', *args, **kwargs): + atom.AtomBase.__init__(self, *args, **kwargs) + self.value = value + + + +class CalendarEventEntry(gdata.BatchEntry): + """A Google Calendar flavor of an Atom Entry """ + + _tag = gdata.BatchEntry._tag + _namespace = gdata.BatchEntry._namespace + _children = gdata.BatchEntry._children.copy() + _attributes = gdata.BatchEntry._attributes.copy() + # This class also contains WebContentLinks but converting those members + # is handled in a special version of _ConvertElementTreeToMember. + _children['{%s}where' % gdata.GDATA_NAMESPACE] = ('where', [Where]) + _children['{%s}when' % gdata.GDATA_NAMESPACE] = ('when', [When]) + _children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', [Who]) + _children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = ( + 'extended_property', [ExtendedProperty]) + _children['{%s}visibility' % gdata.GDATA_NAMESPACE] = ('visibility', + Visibility) + _children['{%s}transparency' % gdata.GDATA_NAMESPACE] = ('transparency', + Transparency) + _children['{%s}eventStatus' % gdata.GDATA_NAMESPACE] = ('event_status', + EventStatus) + _children['{%s}recurrence' % gdata.GDATA_NAMESPACE] = ('recurrence', + Recurrence) + _children['{%s}recurrenceException' % gdata.GDATA_NAMESPACE] = ( + 'recurrence_exception', [RecurrenceException]) + _children['{%s}sendEventNotifications' % GCAL_NAMESPACE] = ( + 'send_event_notifications', SendEventNotifications) + _children['{%s}quickadd' % GCAL_NAMESPACE] = ('quick_add', QuickAdd) + _children['{%s}comments' % gdata.GDATA_NAMESPACE] = ('comments', Comments) + _children['{%s}originalEvent' % gdata.GDATA_NAMESPACE] = ('original_event', + OriginalEvent) + _children['{%s}sequence' % GCAL_NAMESPACE] = ('sequence', Sequence) + _children['{%s}reminder' % gdata.GDATA_NAMESPACE] = ('reminder', [Reminder]) + _children['{%s}syncEvent' % GCAL_NAMESPACE] = ('sync_event', SyncEvent) + _children['{%s}uid' % GCAL_NAMESPACE] = ('uid', UID) + _children['{%s}guestsCanInviteOthers' % GCAL_NAMESPACE] = ( + 'guests_can_invite_others', GuestsCanInviteOthers) + _children['{%s}guestsCanModify' % GCAL_NAMESPACE] = ( + 'guests_can_modify', GuestsCanModify) + _children['{%s}guestsCanSeeGuests' % GCAL_NAMESPACE] = ( + 'guests_can_see_guests', GuestsCanSeeGuests) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + transparency=None, comments=None, event_status=None, + send_event_notifications=None, visibility=None, + recurrence=None, recurrence_exception=None, + where=None, when=None, who=None, quick_add=None, + extended_property=None, original_event=None, + batch_operation=None, batch_id=None, batch_status=None, + sequence=None, reminder=None, sync_event=None, uid=None, + guests_can_invite_others=None, guests_can_modify=None, + guests_can_see_guests=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.BatchEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + batch_operation=batch_operation, batch_id=batch_id, + batch_status=batch_status, + title=title, updated=updated) + + self.transparency = transparency + self.comments = comments + self.event_status = event_status + self.send_event_notifications = send_event_notifications + self.visibility = visibility + self.recurrence = recurrence + self.recurrence_exception = recurrence_exception or [] + self.where = where or [] + self.when = when or [] + self.who = who or [] + self.quick_add = quick_add + self.extended_property = extended_property or [] + self.original_event = original_event + self.sequence = sequence + self.reminder = reminder or [] + self.sync_event = sync_event + self.uid = uid + self.text = text + self.guests_can_invite_others = guests_can_invite_others + self.guests_can_modify = guests_can_modify + self.guests_can_see_guests = guests_can_see_guests + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + # We needed to add special logic to _ConvertElementTreeToMember because we + # want to make links with a rel of WEB_CONTENT_LINK_REL into a + # WebContentLink + def _ConvertElementTreeToMember(self, child_tree): + # Special logic to handle Web Content links + if (child_tree.tag == '{%s}link' % atom.ATOM_NAMESPACE and + child_tree.attrib['rel'] == WEB_CONTENT_LINK_REL): + if self.link is None: + self.link = [] + self.link.append(atom._CreateClassFromElementTree(WebContentLink, + child_tree)) + return + # Find the element's tag in this class's list of child members + if self.__class__._children.has_key(child_tree.tag): + member_name = self.__class__._children[child_tree.tag][0] + member_class = self.__class__._children[child_tree.tag][1] + # If the class member is supposed to contain a list, make sure the + # matching member is set to a list, then append the new member + # instance to the list. + if isinstance(member_class, list): + if getattr(self, member_name) is None: + setattr(self, member_name, []) + getattr(self, member_name).append(atom._CreateClassFromElementTree( + member_class[0], child_tree)) + else: + setattr(self, member_name, + atom._CreateClassFromElementTree(member_class, child_tree)) + else: + atom.ExtensionContainer._ConvertElementTreeToMember(self, child_tree) + + + def GetWebContentLink(self): + """Finds the first link with rel set to WEB_CONTENT_REL + + Returns: + A gdata.calendar.WebContentLink or none if none of the links had rel + equal to WEB_CONTENT_REL + """ + + for a_link in self.link: + if a_link.rel == WEB_CONTENT_LINK_REL: + return a_link + return None + + +def CalendarEventEntryFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarEventEntry, xml_string) + + +def CalendarEventCommentEntryFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarEventCommentEntry, xml_string) + + +CalendarEventEntryLink._children = {'{%s}entry' % atom.ATOM_NAMESPACE: + ('entry', CalendarEventEntry)} + + +def CalendarEventEntryLinkFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarEventEntryLink, xml_string) + + +class CalendarEventFeed(gdata.BatchFeed, gdata.LinkFinder): + """A Google Calendar event feed flavor of an Atom Feed""" + + _tag = gdata.BatchFeed._tag + _namespace = gdata.BatchFeed._namespace + _children = gdata.BatchFeed._children.copy() + _attributes = gdata.BatchFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [CalendarEventEntry]) + _children['{%s}timezone' % GCAL_NAMESPACE] = ('timezone', Timezone) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, entry=None, + total_results=None, start_index=None, items_per_page=None, + interrupted=None, timezone=None, + extension_elements=None, extension_attributes=None, text=None): + gdata.BatchFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + interrupted=interrupted, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + self.timezone = timezone + + +def CalendarListEntryFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarListEntry, xml_string) + + +def CalendarAclEntryFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarAclEntry, xml_string) + + +def CalendarListFeedFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarListFeed, xml_string) + + +def CalendarAclFeedFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarAclFeed, xml_string) + + +def CalendarEventFeedFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarEventFeed, xml_string) + + +def CalendarEventCommentFeedFromString(xml_string): + return atom.CreateClassFromXMLString(CalendarEventCommentFeed, xml_string) diff --git a/patches/gdata/calendar/client.py b/patches/gdata/calendar/client.py new file mode 100755 index 0000000..414338d --- /dev/null +++ b/patches/gdata/calendar/client.py @@ -0,0 +1,538 @@ +#!/usr/bin/python +# +# Copyright (C) 2011 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. + +"""CalendarClient extends the GDataService to streamline Google Calendar operations. + + CalendarService: Provides methods to query feeds and manipulate items. Extends + GDataService. + + DictionaryToParamList: Function which converts a dictionary into a list of + URL arguments (represented as strings). This is a + utility function used in CRUD operations. +""" + + +__author__ = 'alainv (Alain Vongsouvanh)' + + +import urllib +import gdata.client +import gdata.calendar.data +import atom.data +import atom.http_core +import gdata.gauth + + +DEFAULT_BATCH_URL = ('https://www.google.com/calendar/feeds/default/private' + '/full/batch') + + +class CalendarClient(gdata.client.GDClient): + """Client for the Google Calendar service.""" + api_version = '2' + auth_service = 'cl' + server = "www.google.com" + contact_list = "default" + auth_scopes = gdata.gauth.AUTH_SCOPES['cl'] + + def __init__(self, domain=None, auth_token=None, **kwargs): + """Constructs a new client for the Calendar API. + + Args: + domain: string The Google Apps domain (if any). + kwargs: The other parameters to pass to the gdata.client.GDClient + constructor. + """ + gdata.client.GDClient.__init__(self, auth_token=auth_token, **kwargs) + self.domain = domain + + def get_calendar_feed_uri(self, feed='', projection='full', scheme="https"): + """Builds a feed URI. + + Args: + projection: The projection to apply to the feed contents, for example + 'full', 'base', 'base/12345', 'full/batch'. Default value: 'full'. + scheme: The URL scheme such as 'http' or 'https', None to return a + relative URI without hostname. + + Returns: + A feed URI using the given scheme and projection. + Example: '/calendar/feeds/default/owncalendars/full'. + """ + prefix = scheme and '%s://%s' % (scheme, self.server) or '' + suffix = feed and '/%s/%s' % (feed, projection) or '' + return '%s/calendar/feeds/default%s' % (prefix, suffix) + + GetCalendarFeedUri = get_calendar_feed_uri + + def get_calendar_event_feed_uri(self, calendar='default', visibility='private', + projection='full', scheme="https"): + """Builds a feed URI. + + Args: + projection: The projection to apply to the feed contents, for example + 'full', 'base', 'base/12345', 'full/batch'. Default value: 'full'. + scheme: The URL scheme such as 'http' or 'https', None to return a + relative URI without hostname. + + Returns: + A feed URI using the given scheme and projection. + Example: '/calendar/feeds/default/private/full'. + """ + prefix = scheme and '%s://%s' % (scheme, self.server) or '' + return '%s/calendar/feeds/%s/%s/%s' % (prefix, calendar, + visibility, projection) + + GetCalendarEventFeedUri = get_calendar_event_feed_uri + + def get_calendars_feed(self, uri, + desired_class=gdata.calendar.data.CalendarFeed, + auth_token=None, **kwargs): + """Obtains a calendar feed. + + Args: + uri: The uri of the calendar feed to request. + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.get_feed(uri, auth_token=auth_token, + desired_class=desired_class, **kwargs) + + GetCalendarsFeed = get_calendars_feed + + def get_own_calendars_feed(self, + desired_class=gdata.calendar.data.CalendarFeed, + auth_token=None, **kwargs): + """Obtains a feed containing the calendars owned by the current user. + + Args: + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.GetCalendarsFeed(uri=self.GetCalendarFeedUri(feed='owncalendars'), + desired_class=desired_class, auth_token=auth_token, + **kwargs) + + GetOwnCalendarsFeed = get_own_calendars_feed + + def get_all_calendars_feed(self, desired_class=gdata.calendar.data.CalendarFeed, + auth_token=None, **kwargs): + """Obtains a feed containing all the ccalendars the current user has access to. + + Args: + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.GetCalendarsFeed(uri=self.GetCalendarFeedUri(feed='allcalendars'), + desired_class=desired_class, auth_token=auth_token, + **kwargs) + + GetAllCalendarsFeed = get_all_calendars_feed + + def get_calendar_entry(self, uri, desired_class=gdata.calendar.data.CalendarEntry, + auth_token=None, **kwargs): + """Obtains a single calendar entry. + + Args: + uri: The uri of the desired calendar entry. + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarEntry. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.get_entry(uri, auth_token=auth_token, desired_class=desired_class, + **kwargs) + + GetCalendarEntry = get_calendar_entry + + def get_calendar_event_feed(self, uri=None, + desired_class=gdata.calendar.data.CalendarEventFeed, + auth_token=None, **kwargs): + """Obtains a feed of events for the desired calendar. + + Args: + uri: The uri of the desired calendar entry. + Defaults to https://www.google.com/calendar/feeds/default/private/full. + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarEventFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + uri = uri or self.GetCalendarEventFeedUri() + return self.get_feed(uri, auth_token=auth_token, + desired_class=desired_class, **kwargs) + + GetCalendarEventFeed = get_calendar_event_feed + + def get_event_entry(self, uri, desired_class=gdata.calendar.data.CalendarEventEntry, + auth_token=None, **kwargs): + """Obtains a single event entry. + + Args: + uri: The uri of the desired calendar event entry. + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarEventEntry. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.get_entry(uri, auth_token=auth_token, desired_class=desired_class, + **kwargs) + + GetEventEntry = get_event_entry + + def get_calendar_acl_feed(self, uri='https://www.google.com/calendar/feeds/default/acl/full', + desired_class=gdata.calendar.data.CalendarAclFeed, + auth_token=None, **kwargs): + """Obtains an Access Control List feed. + + Args: + uri: The uri of the desired Acl feed. + Defaults to https://www.google.com/calendar/feeds/default/acl/full. + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarAclFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.get_feed(uri, auth_token=auth_token, desired_class=desired_class, + **kwargs) + + GetCalendarAclFeed = get_calendar_acl_feed + + def get_calendar_acl_entry(self, uri, desired_class=gdata.calendar.data.CalendarAclEntry, + auth_token=None, **kwargs): + """Obtains a single Access Control List entry. + + Args: + uri: The uri of the desired Acl feed. + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.calendar.data.CalendarAclEntry. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.get_entry(uri, auth_token=auth_token, desired_class=desired_class, + **kwargs) + + GetCalendarAclEntry = get_calendar_acl_entry + + def insert_calendar(self, new_calendar, insert_uri=None, auth_token=None, **kwargs): + """Adds an new calendar to Google Calendar. + + Args: + new_calendar: atom.Entry or subclass A new calendar which is to be added to + Google Calendar. + insert_uri: the URL to post new contacts to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the contact created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + insert_uri = insert_uri or self.GetCalendarFeedUri(feed='owncalendars') + return self.Post(new_calendar, insert_uri, + auth_token=auth_token, **kwargs) + + InsertCalendar = insert_calendar + + def insert_calendar_subscription(self, calendar, insert_uri=None, + auth_token=None, **kwargs): + """Subscribes the authenticated user to the provided calendar. + + Args: + calendar: The calendar to which the user should be subscribed. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the subscription created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + insert_uri = insert_uri or self.GetCalendarFeedUri(feed='allcalendars') + return self.Post(calendar, insert_uri, auth_token=auth_token, **kwargs) + + InsertCalendarSubscription = insert_calendar_subscription + + def insert_event(self, new_event, insert_uri=None, auth_token=None, **kwargs): + """Adds an new event to Google Calendar. + + Args: + new_event: atom.Entry or subclass A new event which is to be added to + Google Calendar. + insert_uri: the URL to post new contacts to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the contact created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + insert_uri = insert_uri or self.GetCalendarEventFeedUri() + return self.Post(new_event, insert_uri, + auth_token=auth_token, **kwargs) + + + InsertEvent = insert_event + + def insert_acl_entry(self, new_acl_entry, + insert_uri = 'https://www.google.com/calendar/feeds/default/acl/full', + auth_token=None, **kwargs): + """Adds an new Acl entry to Google Calendar. + + Args: + new_acl_event: atom.Entry or subclass A new acl which is to be added to + Google Calendar. + insert_uri: the URL to post new contacts to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the contact created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + return self.Post(new_acl_entry, insert_uri, auth_token=auth_token, **kwargs) + + InsertAclEntry = insert_acl_entry + + def execute_batch(self, batch_feed, url, desired_class=None): + """Sends a batch request feed to the server. + + Args: + batch_feed: gdata.contacts.CalendarEventFeed A feed containing batch + request entries. Each entry contains the operation to be performed + on the data contained in the entry. For example an entry with an + operation type of insert will be used as if the individual entry + had been inserted. + url: str The batch URL to which these operations should be applied. + converter: Function (optional) The function used to convert the server's + response to an object. + + Returns: + The results of the batch request's execution on the server. If the + default converter is used, this is stored in a ContactsFeed. + """ + return self.Post(batch_feed, url, desired_class=desired_class) + + ExecuteBatch = execute_batch + + def update(self, entry, auth_token=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: + + Returns: + A new Entry object of a matching type to the entry which was passed in. + """ + return gdata.client.GDClient.Update(self, entry, auth_token=auth_token, + force=True, **kwargs) + + Update = update + + +class CalendarEventQuery(gdata.client.Query): + """ + Create a custom Calendar Query + + Full specs can be found at: U{Calendar query parameters reference + } + """ + + def __init__(self, feed=None, ctz=None, fields=None, futureevents=None, + max_attendees=None, orderby=None, recurrence_expansion_start=None, + recurrence_expansion_end=None, singleevents=None, showdeleted=None, + showhidden=None, sortorder=None, start_min=None, start_max=None, + updated_min=None, **kwargs): + """ + @param max_results: The maximum number of entries to return. If you want + to receive all of the contacts, rather than only the default maximum, you + can specify a very large number for max-results. + @param start-index: The 1-based index of the first result to be retrieved. + @param updated-min: The lower bound on entry update dates. + @param group: Constrains the results to only the contacts belonging to the + group specified. Value of this parameter specifies group ID + @param orderby: Sorting criterion. The only supported value is + lastmodified. + @param showdeleted: Include deleted contacts in the returned contacts feed + @pram sortorder: Sorting order direction. Can be either ascending or + descending. + @param requirealldeleted: Only relevant if showdeleted and updated-min + are also provided. It dictates the behavior of the server in case it + detects that placeholders of some entries deleted since the point in + time specified as updated-min may have been lost. + """ + gdata.client.Query.__init__(self, **kwargs) + self.ctz = ctz + self.fields = fields + self.futureevents = futureevents + self.max_attendees = max_attendees + self.orderby = orderby + self.recurrence_expansion_start = recurrence_expansion_start + self.recurrence_expansion_end = recurrence_expansion_end + self.singleevents = singleevents + self.showdeleted = showdeleted + self.showhidden = showhidden + self.sortorder = sortorder + self.start_min = start_min + self.start_max = start_max + self.updated_min = updated_min + + def modify_request(self, http_request): + if self.ctz: + gdata.client._add_query_param('ctz', self.ctz, http_request) + if self.fields: + gdata.client._add_query_param('fields', self.fields, http_request) + if self.futureevents: + gdata.client._add_query_param('futureevents', self.futureevents, http_request) + if self.max_attendees: + gdata.client._add_query_param('max-attendees', self.max_attendees, http_request) + if self.orderby: + gdata.client._add_query_param('orderby', self.orderby, http_request) + if self.recurrence_expansion_start: + gdata.client._add_query_param('recurrence-expansion-start', + self.recurrence_expansion_start, http_request) + if self.recurrence_expansion_end: + gdata.client._add_query_param('recurrence-expansion-end', + self.recurrence_expansion_end, http_request) + if self.singleevents: + gdata.client._add_query_param('singleevents', self.singleevents, http_request) + if self.showdeleted: + gdata.client._add_query_param('showdeleted', self.showdeleted, http_request) + if self.showhidden: + gdata.client._add_query_param('showhidden', self.showhidden, http_request) + if self.sortorder: + gdata.client._add_query_param('sortorder', self.sortorder, http_request) + if self.start_min: + gdata.client._add_query_param('start-min', self.start_min, http_request) + if self.start_max: + gdata.client._add_query_param('start-max', self.start_max, http_request) + if self.updated_min: + gdata.client._add_query_param('updated-min', self.updated_min, http_request) + gdata.client.Query.modify_request(self, http_request) + + ModifyRequest = modify_request + + + diff --git a/patches/gdata/calendar/data.py b/patches/gdata/calendar/data.py new file mode 100644 index 0000000..e5974cc --- /dev/null +++ b/patches/gdata/calendar/data.py @@ -0,0 +1,327 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""Contains the data classes of the Google Calendar Data API""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core +import atom.data +import gdata.acl.data +import gdata.data +import gdata.geo.data +import gdata.opensearch.data + + +GCAL_NAMESPACE = 'http://schemas.google.com/gCal/2005' +GCAL_TEMPLATE = '{http://schemas.google.com/gCal/2005/}%s' +WEB_CONTENT_LINK_REL = '%s/%s' % (GCAL_NAMESPACE, 'webContent') + + +class AccessLevelProperty(atom.core.XmlElement): + """Describes how much a given user may do with an event or calendar""" + _qname = GCAL_TEMPLATE % 'accesslevel' + value = 'value' + + +class AllowGSync2Property(atom.core.XmlElement): + """Whether the user is permitted to run Google Apps Sync""" + _qname = GCAL_TEMPLATE % 'allowGSync2' + value = 'value' + + +class AllowGSyncProperty(atom.core.XmlElement): + """Whether the user is permitted to run Google Apps Sync""" + _qname = GCAL_TEMPLATE % 'allowGSync' + value = 'value' + + +class AnyoneCanAddSelfProperty(atom.core.XmlElement): + """Whether anyone can add self as attendee""" + _qname = GCAL_TEMPLATE % 'anyoneCanAddSelf' + value = 'value' + + +class CalendarAclRole(gdata.acl.data.AclRole): + """Describes the Calendar roles of an entry in the Calendar access control list""" + _qname = gdata.acl.data.GACL_TEMPLATE % 'role' + + +class CalendarCommentEntry(gdata.data.GDEntry): + """Describes an entry in a feed of a Calendar event's comments""" + + +class CalendarCommentFeed(gdata.data.GDFeed): + """Describes feed of a Calendar event's comments""" + entry = [CalendarCommentEntry] + + +class CalendarComments(gdata.data.Comments): + """Describes a container of a feed link for Calendar comment entries""" + _qname = gdata.data.GD_TEMPLATE % 'comments' + + +class CalendarExtendedProperty(gdata.data.ExtendedProperty): + """Defines a value for the realm attribute that is used only in the calendar API""" + _qname = gdata.data.GD_TEMPLATE % 'extendedProperty' + + +class CalendarWhere(gdata.data.Where): + """Extends the base Where class with Calendar extensions""" + _qname = gdata.data.GD_TEMPLATE % 'where' + + +class ColorProperty(atom.core.XmlElement): + """Describes the color of a calendar""" + _qname = GCAL_TEMPLATE % 'color' + value = 'value' + + +class GuestsCanInviteOthersProperty(atom.core.XmlElement): + """Whether guests can invite others to the event""" + _qname = GCAL_TEMPLATE % 'guestsCanInviteOthers' + value = 'value' + + +class GuestsCanModifyProperty(atom.core.XmlElement): + """Whether guests can modify event""" + _qname = GCAL_TEMPLATE % 'guestsCanModify' + value = 'value' + + +class GuestsCanSeeGuestsProperty(atom.core.XmlElement): + """Whether guests can see other attendees""" + _qname = GCAL_TEMPLATE % 'guestsCanSeeGuests' + value = 'value' + + +class HiddenProperty(atom.core.XmlElement): + """Describes whether a calendar is hidden""" + _qname = GCAL_TEMPLATE % 'hidden' + value = 'value' + + +class IcalUIDProperty(atom.core.XmlElement): + """Describes the UID in the ical export of the event""" + _qname = GCAL_TEMPLATE % 'uid' + value = 'value' + + +class OverrideNameProperty(atom.core.XmlElement): + """Describes the override name property of a calendar""" + _qname = GCAL_TEMPLATE % 'overridename' + value = 'value' + + +class PrivateCopyProperty(atom.core.XmlElement): + """Indicates whether this is a private copy of the event, changes to which should not be sent to other calendars""" + _qname = GCAL_TEMPLATE % 'privateCopy' + value = 'value' + + +class QuickAddProperty(atom.core.XmlElement): + """Describes whether gd:content is for quick-add processing""" + _qname = GCAL_TEMPLATE % 'quickadd' + value = 'value' + + +class ResourceProperty(atom.core.XmlElement): + """Describes whether gd:who is a resource such as a conference room""" + _qname = GCAL_TEMPLATE % 'resource' + value = 'value' + id = 'id' + + +class EventWho(gdata.data.Who): + """Extends the base Who class with Calendar extensions""" + _qname = gdata.data.GD_TEMPLATE % 'who' + resource = ResourceProperty + + +class SelectedProperty(atom.core.XmlElement): + """Describes whether a calendar is selected""" + _qname = GCAL_TEMPLATE % 'selected' + value = 'value' + + +class SendAclNotificationsProperty(atom.core.XmlElement): + """Describes whether to send ACL notifications to grantees""" + _qname = GCAL_TEMPLATE % 'sendAclNotifications' + value = 'value' + + +class CalendarAclEntry(gdata.acl.data.AclEntry): + """Describes an entry in a feed of a Calendar access control list (ACL)""" + send_acl_notifications = SendAclNotificationsProperty + + +class CalendarAclFeed(gdata.data.GDFeed): + """Describes a Calendar access contorl list (ACL) feed""" + entry = [CalendarAclEntry] + + +class SendEventNotificationsProperty(atom.core.XmlElement): + """Describes whether to send event notifications to other participants of the event""" + _qname = GCAL_TEMPLATE % 'sendEventNotifications' + value = 'value' + + +class SequenceNumberProperty(atom.core.XmlElement): + """Describes sequence number of an event""" + _qname = GCAL_TEMPLATE % 'sequence' + value = 'value' + + +class CalendarRecurrenceExceptionEntry(gdata.data.GDEntry): + """Describes an entry used by a Calendar recurrence exception entry link""" + uid = IcalUIDProperty + sequence = SequenceNumberProperty + + +class CalendarRecurrenceException(gdata.data.RecurrenceException): + """Describes an exception to a recurring Calendar event""" + _qname = gdata.data.GD_TEMPLATE % 'recurrenceException' + + +class SettingsProperty(atom.core.XmlElement): + """User preference name-value pair""" + _qname = GCAL_TEMPLATE % 'settingsProperty' + name = 'name' + value = 'value' + + +class SettingsEntry(gdata.data.GDEntry): + """Describes a Calendar Settings property entry""" + settings_property = SettingsProperty + + +class CalendarSettingsFeed(gdata.data.GDFeed): + """Personal settings for Calendar application""" + entry = [SettingsEntry] + + +class SuppressReplyNotificationsProperty(atom.core.XmlElement): + """Lists notification methods to be suppressed for this reply""" + _qname = GCAL_TEMPLATE % 'suppressReplyNotifications' + methods = 'methods' + + +class SyncEventProperty(atom.core.XmlElement): + """Describes whether this is a sync scenario where the Ical UID and Sequence number are honored during inserts and updates""" + _qname = GCAL_TEMPLATE % 'syncEvent' + value = 'value' + + +class When(gdata.data.When): + """Extends the gd:when element to add reminders""" + reminder = [gdata.data.Reminder] + + +class CalendarEventEntry(gdata.data.BatchEntry): + """Describes a Calendar event entry""" + quick_add = QuickAddProperty + send_event_notifications = SendEventNotificationsProperty + sync_event = SyncEventProperty + anyone_can_add_self = AnyoneCanAddSelfProperty + extended_property = [CalendarExtendedProperty] + sequence = SequenceNumberProperty + guests_can_invite_others = GuestsCanInviteOthersProperty + guests_can_modify = GuestsCanModifyProperty + guests_can_see_guests = GuestsCanSeeGuestsProperty + georss_where = gdata.geo.data.GeoRssWhere + private_copy = PrivateCopyProperty + suppress_reply_notifications = SuppressReplyNotificationsProperty + uid = IcalUIDProperty + where = [gdata.data.Where] + when = [When] + who = [gdata.data.Who] + transparency = gdata.data.Transparency + comments = gdata.data.Comments + event_status = gdata.data.EventStatus + visibility = gdata.data.Visibility + recurrence = gdata.data.Recurrence + recurrence_exception = [gdata.data.RecurrenceException] + original_event = gdata.data.OriginalEvent + reminder = [gdata.data.Reminder] + + +class TimeZoneProperty(atom.core.XmlElement): + """Describes the time zone of a calendar""" + _qname = GCAL_TEMPLATE % 'timezone' + value = 'value' + + +class TimesCleanedProperty(atom.core.XmlElement): + """Describes how many times calendar was cleaned via Manage Calendars""" + _qname = GCAL_TEMPLATE % 'timesCleaned' + value = 'value' + + +class CalendarEntry(gdata.data.GDEntry): + """Describes a Calendar entry in the feed of a user's calendars""" + timezone = TimeZoneProperty + overridename = OverrideNameProperty + hidden = HiddenProperty + selected = SelectedProperty + times_cleaned = TimesCleanedProperty + color = ColorProperty + where = [CalendarWhere] + accesslevel = AccessLevelProperty + + +class CalendarEventFeed(gdata.data.BatchFeed): + """Describes a Calendar event feed""" + allow_g_sync2 = AllowGSync2Property + timezone = TimeZoneProperty + entry = [CalendarEventEntry] + times_cleaned = TimesCleanedProperty + allow_g_sync = AllowGSyncProperty + + +class CalendarFeed(gdata.data.GDFeed): + """Describes a feed of Calendars""" + entry = [CalendarEntry] + + +class WebContentGadgetPref(atom.core.XmlElement): + """Describes a single web content gadget preference""" + _qname = GCAL_TEMPLATE % 'webContentGadgetPref' + name = 'name' + value = 'value' + + +class WebContent(atom.core.XmlElement): + """Describes a "web content" extension""" + _qname = GCAL_TEMPLATE % 'webContent' + height = 'height' + width = 'width' + web_content_gadget_pref = [WebContentGadgetPref] + url = 'url' + display = 'display' + + +class WebContentLink(atom.data.Link): + """Describes a "web content" link""" + def __init__(self, title=None, href=None, link_type=None, + web_content=None): + atom.data.Link.__init__(self, rel=WEB_CONTENT_LINK_REL, title=title, href=href, + link_type=link_type) + + web_content = WebContent + diff --git a/patches/gdata/calendar/service.py b/patches/gdata/calendar/service.py new file mode 100755 index 0000000..53a94e3 --- /dev/null +++ b/patches/gdata/calendar/service.py @@ -0,0 +1,595 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 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. + +"""CalendarService extends the GDataService to streamline Google Calendar operations. + + CalendarService: Provides methods to query feeds and manipulate items. Extends + GDataService. + + DictionaryToParamList: Function which converts a dictionary into a list of + URL arguments (represented as strings). This is a + utility function used in CRUD operations. +""" + + +__author__ = 'api.vli (Vivian Li)' + + +import urllib +import gdata +import atom.service +import gdata.service +import gdata.calendar +import atom + + +DEFAULT_BATCH_URL = ('http://www.google.com/calendar/feeds/default/private' + '/full/batch') + + +class Error(Exception): + pass + + +class RequestError(Error): + pass + + +class CalendarService(gdata.service.GDataService): + """Client for the Google Calendar service.""" + + def __init__(self, email=None, password=None, source=None, + server='www.google.com', additional_headers=None, **kwargs): + """Creates a client for the Google Calendar service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'www.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='cl', source=source, + server=server, additional_headers=additional_headers, **kwargs) + + def GetCalendarEventFeed(self, uri='/calendar/feeds/default/private/full'): + return self.Get(uri, converter=gdata.calendar.CalendarEventFeedFromString) + + def GetCalendarEventEntry(self, uri): + return self.Get(uri, converter=gdata.calendar.CalendarEventEntryFromString) + + def GetCalendarListFeed(self, uri='/calendar/feeds/default/allcalendars/full'): + return self.Get(uri, converter=gdata.calendar.CalendarListFeedFromString) + + def GetAllCalendarsFeed(self, uri='/calendar/feeds/default/allcalendars/full'): + return self.Get(uri, converter=gdata.calendar.CalendarListFeedFromString) + + def GetOwnCalendarsFeed(self, uri='/calendar/feeds/default/owncalendars/full'): + return self.Get(uri, converter=gdata.calendar.CalendarListFeedFromString) + + def GetCalendarListEntry(self, uri): + return self.Get(uri, converter=gdata.calendar.CalendarListEntryFromString) + + def GetCalendarAclFeed(self, uri='/calendar/feeds/default/acl/full'): + return self.Get(uri, converter=gdata.calendar.CalendarAclFeedFromString) + + def GetCalendarAclEntry(self, uri): + return self.Get(uri, converter=gdata.calendar.CalendarAclEntryFromString) + + def GetCalendarEventCommentFeed(self, uri): + return self.Get(uri, converter=gdata.calendar.CalendarEventCommentFeedFromString) + + def GetCalendarEventCommentEntry(self, uri): + return self.Get(uri, converter=gdata.calendar.CalendarEventCommentEntryFromString) + + def Query(self, uri, converter=None): + """Performs a query and returns a resulting feed or entry. + + Args: + feed: string The feed which is to be queried + + Returns: + On success, a GDataFeed or Entry depending on which is sent from the + server. + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + if converter: + result = self.Get(uri, converter=converter) + else: + result = self.Get(uri) + return result + + def CalendarQuery(self, query): + if isinstance(query, CalendarEventQuery): + return self.Query(query.ToUri(), + converter=gdata.calendar.CalendarEventFeedFromString) + elif isinstance(query, CalendarListQuery): + return self.Query(query.ToUri(), + converter=gdata.calendar.CalendarListFeedFromString) + elif isinstance(query, CalendarEventCommentQuery): + return self.Query(query.ToUri(), + converter=gdata.calendar.CalendarEventCommentFeedFromString) + else: + return self.Query(query.ToUri()) + + def InsertEvent(self, new_event, insert_uri, url_params=None, + escape_params=True): + """Adds an event to Google Calendar. + + Args: + new_event: atom.Entry or subclass A new event which is to be added to + Google Calendar. + insert_uri: the URL to post new events to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the event created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + return self.Post(new_event, insert_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarEventEntryFromString) + + def InsertCalendarSubscription(self, calendar, url_params=None, + escape_params=True): + """Subscribes the authenticated user to the provided calendar. + + Args: + calendar: The calendar to which the user should be subscribed. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the subscription created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + insert_uri = '/calendar/feeds/default/allcalendars/full' + return self.Post(calendar, insert_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarListEntryFromString) + + def InsertCalendar(self, new_calendar, url_params=None, + escape_params=True): + """Creates a new calendar. + + Args: + new_calendar: The calendar to be created + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the calendar created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + insert_uri = '/calendar/feeds/default/owncalendars/full' + response = self.Post(new_calendar, insert_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarListEntryFromString) + return response + + def UpdateCalendar(self, calendar, url_params=None, + escape_params=True): + """Updates a calendar. + + Args: + calendar: The calendar which should be updated + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the calendar created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + update_uri = calendar.GetEditLink().href + response = self.Put(data=calendar, uri=update_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarListEntryFromString) + return response + + def InsertAclEntry(self, new_entry, insert_uri, url_params=None, + escape_params=True): + """Adds an ACL entry (rule) to Google Calendar. + + Args: + new_entry: atom.Entry or subclass A new ACL entry which is to be added to + Google Calendar. + insert_uri: the URL to post new entries to the ACL feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the ACL entry created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + return self.Post(new_entry, insert_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarAclEntryFromString) + + def InsertEventComment(self, new_entry, insert_uri, url_params=None, + escape_params=True): + """Adds an entry to Google Calendar. + + Args: + new_entry: atom.Entry or subclass A new entry which is to be added to + Google Calendar. + insert_uri: the URL to post new entrys to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the comment created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + return self.Post(new_entry, insert_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarEventCommentEntryFromString) + + def _RemoveStandardUrlPrefix(self, url): + url_prefix = 'http://%s/' % self.server + if url.startswith(url_prefix): + return url[len(url_prefix) - 1:] + return url + + def DeleteEvent(self, edit_uri, extra_headers=None, + url_params=None, escape_params=True): + """Removes an event with the specified ID from Google Calendar. + + Args: + edit_uri: string The edit URL of the entry to be deleted. Example: + 'http://www.google.com/calendar/feeds/default/private/full/abx' + url_params: dict (optional) Additional URL parameters to be included + in the deletion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful delete, a httplib.HTTPResponse containing the server's + response to the DELETE request. + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + edit_uri = self._RemoveStandardUrlPrefix(edit_uri) + return self.Delete('%s' % edit_uri, + url_params=url_params, escape_params=escape_params) + + def DeleteAclEntry(self, edit_uri, extra_headers=None, + url_params=None, escape_params=True): + """Removes an ACL entry at the given edit_uri from Google Calendar. + + Args: + edit_uri: string The edit URL of the entry to be deleted. Example: + 'http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/default' + url_params: dict (optional) Additional URL parameters to be included + in the deletion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful delete, a httplib.HTTPResponse containing the server's + response to the DELETE request. + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + edit_uri = self._RemoveStandardUrlPrefix(edit_uri) + return self.Delete('%s' % edit_uri, + url_params=url_params, escape_params=escape_params) + + def DeleteCalendarEntry(self, edit_uri, extra_headers=None, + url_params=None, escape_params=True): + """Removes a calendar entry at the given edit_uri from Google Calendar. + + Args: + edit_uri: string The edit URL of the entry to be deleted. Example: + 'http://www.google.com/calendar/feeds/default/allcalendars/abcdef@group.calendar.google.com' + url_params: dict (optional) Additional URL parameters to be included + in the deletion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful delete, True is returned + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + return self.Delete(edit_uri, url_params=url_params, + escape_params=escape_params) + + def UpdateEvent(self, edit_uri, updated_event, url_params=None, + escape_params=True): + """Updates an existing event. + + Args: + edit_uri: string The edit link URI for the element being updated + updated_event: string, atom.Entry, or subclass containing + the Atom Entry which will replace the event which is + stored at the edit_url + url_params: dict (optional) Additional URL parameters to be included + in the update request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful update, a httplib.HTTPResponse containing the server's + response to the PUT request. + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + edit_uri = self._RemoveStandardUrlPrefix(edit_uri) + return self.Put(updated_event, '%s' % edit_uri, + url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarEventEntryFromString) + + def UpdateAclEntry(self, edit_uri, updated_rule, url_params=None, + escape_params=True): + """Updates an existing ACL rule. + + Args: + edit_uri: string The edit link URI for the element being updated + updated_rule: string, atom.Entry, or subclass containing + the Atom Entry which will replace the event which is + stored at the edit_url + url_params: dict (optional) Additional URL parameters to be included + in the update request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful update, a httplib.HTTPResponse containing the server's + response to the PUT request. + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + edit_uri = self._RemoveStandardUrlPrefix(edit_uri) + return self.Put(updated_rule, '%s' % edit_uri, + url_params=url_params, + escape_params=escape_params, + converter=gdata.calendar.CalendarAclEntryFromString) + + def ExecuteBatch(self, batch_feed, url, + converter=gdata.calendar.CalendarEventFeedFromString): + """Sends a batch request feed to the server. + + The batch request needs to be sent to the batch URL for a particular + calendar. You can find the URL by calling GetBatchLink().href on the + CalendarEventFeed. + + Args: + batch_feed: gdata.calendar.CalendarEventFeed A feed containing batch + request entries. Each entry contains the operation to be performed + on the data contained in the entry. For example an entry with an + operation type of insert will be used as if the individual entry + had been inserted. + url: str The batch URL for the Calendar to which these operations should + be applied. + converter: Function (optional) The function used to convert the server's + response to an object. The default value is + CalendarEventFeedFromString. + + Returns: + The results of the batch request's execution on the server. If the + default converter is used, this is stored in a CalendarEventFeed. + """ + return self.Post(batch_feed, url, converter=converter) + + +class CalendarEventQuery(gdata.service.Query): + + def __init__(self, user='default', visibility='private', projection='full', + text_query=None, params=None, categories=None): + gdata.service.Query.__init__(self, + feed='http://www.google.com/calendar/feeds/%s/%s/%s' % ( + urllib.quote(user), + urllib.quote(visibility), + urllib.quote(projection)), + text_query=text_query, params=params, categories=categories) + + def _GetStartMin(self): + if 'start-min' in self.keys(): + return self['start-min'] + else: + return None + + def _SetStartMin(self, val): + self['start-min'] = val + + start_min = property(_GetStartMin, _SetStartMin, + doc="""The start-min query parameter""") + + def _GetStartMax(self): + if 'start-max' in self.keys(): + return self['start-max'] + else: + return None + + def _SetStartMax(self, val): + self['start-max'] = val + + start_max = property(_GetStartMax, _SetStartMax, + doc="""The start-max query parameter""") + + def _GetOrderBy(self): + if 'orderby' in self.keys(): + return self['orderby'] + else: + return None + + def _SetOrderBy(self, val): + if val is not 'lastmodified' and val is not 'starttime': + raise Error, "Order By must be either 'lastmodified' or 'starttime'" + self['orderby'] = val + + orderby = property(_GetOrderBy, _SetOrderBy, + doc="""The orderby query parameter""") + + def _GetSortOrder(self): + if 'sortorder' in self.keys(): + return self['sortorder'] + else: + return None + + def _SetSortOrder(self, val): + if (val is not 'ascending' and val is not 'descending' + and val is not 'a' and val is not 'd' and val is not 'ascend' + and val is not 'descend'): + raise Error, "Sort order must be either ascending, ascend, " + ( + "a or descending, descend, or d") + self['sortorder'] = val + + sortorder = property(_GetSortOrder, _SetSortOrder, + doc="""The sortorder query parameter""") + + def _GetSingleEvents(self): + if 'singleevents' in self.keys(): + return self['singleevents'] + else: + return None + + def _SetSingleEvents(self, val): + self['singleevents'] = val + + singleevents = property(_GetSingleEvents, _SetSingleEvents, + doc="""The singleevents query parameter""") + + def _GetFutureEvents(self): + if 'futureevents' in self.keys(): + return self['futureevents'] + else: + return None + + def _SetFutureEvents(self, val): + self['futureevents'] = val + + futureevents = property(_GetFutureEvents, _SetFutureEvents, + doc="""The futureevents query parameter""") + + def _GetRecurrenceExpansionStart(self): + if 'recurrence-expansion-start' in self.keys(): + return self['recurrence-expansion-start'] + else: + return None + + def _SetRecurrenceExpansionStart(self, val): + self['recurrence-expansion-start'] = val + + recurrence_expansion_start = property(_GetRecurrenceExpansionStart, + _SetRecurrenceExpansionStart, + doc="""The recurrence-expansion-start query parameter""") + + def _GetRecurrenceExpansionEnd(self): + if 'recurrence-expansion-end' in self.keys(): + return self['recurrence-expansion-end'] + else: + return None + + def _SetRecurrenceExpansionEnd(self, val): + self['recurrence-expansion-end'] = val + + recurrence_expansion_end = property(_GetRecurrenceExpansionEnd, + _SetRecurrenceExpansionEnd, + doc="""The recurrence-expansion-end query parameter""") + + def _SetTimezone(self, val): + self['ctz'] = val + + def _GetTimezone(self): + if 'ctz' in self.keys(): + return self['ctz'] + else: + return None + + ctz = property(_GetTimezone, _SetTimezone, + doc="""The ctz query parameter which sets report time on the server.""") + + +class CalendarListQuery(gdata.service.Query): + """Queries the Google Calendar meta feed""" + + def __init__(self, userId=None, text_query=None, + params=None, categories=None): + if userId is None: + userId = 'default' + + gdata.service.Query.__init__(self, feed='http://www.google.com/calendar/feeds/' + +userId, + text_query=text_query, params=params, + categories=categories) + +class CalendarEventCommentQuery(gdata.service.Query): + """Queries the Google Calendar event comments feed""" + + def __init__(self, feed=None): + gdata.service.Query.__init__(self, feed=feed) diff --git a/patches/gdata/calendar_resource/__init__.py b/patches/gdata/calendar_resource/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/patches/gdata/calendar_resource/__init__.py @@ -0,0 +1 @@ + diff --git a/patches/gdata/calendar_resource/client.py b/patches/gdata/calendar_resource/client.py new file mode 100644 index 0000000..54d2ea8 --- /dev/null +++ b/patches/gdata/calendar_resource/client.py @@ -0,0 +1,200 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""CalendarResourceClient simplifies Calendar Resources API calls. + +CalendarResourceClient extends gdata.client.GDClient to ease interaction with +the Google Apps Calendar Resources API. These interactions include the ability +to create, retrieve, update, and delete calendar resources in a Google Apps +domain. +""" + + +__author__ = 'Vic Fryzel ' + + +import gdata.calendar_resource.data +import gdata.client +import urllib + + +# Feed URI template. This must end with a / +# The strings in this template are eventually replaced with the API version +# and Google Apps domain name, respectively. +RESOURCE_FEED_TEMPLATE = '/a/feeds/calendar/resource/%s/%s/' + + +class CalendarResourceClient(gdata.client.GDClient): + """Client extension for the Google Calendar Resource API service. + + Attributes: + host: string The hostname for the Calendar Resouce API service. + api_version: string The version of the Calendar Resource API. + """ + + host = 'apps-apis.google.com' + api_version = '2.0' + auth_service = 'apps' + auth_scopes = gdata.gauth.AUTH_SCOPES['apps'] + ssl = True + + def __init__(self, domain, auth_token=None, **kwargs): + """Constructs a new client for the Calendar Resource API. + + Args: + domain: string The Google Apps domain with Calendar Resources. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the calendar resource + data. + kwargs: The other parameters to pass to the gdata.client.GDClient + constructor. + """ + gdata.client.GDClient.__init__(self, auth_token=auth_token, **kwargs) + self.domain = domain + + def make_resource_feed_uri(self, resource_id=None, params=None): + """Creates a resource feed URI for the Calendar Resource API. + + Using this client's Google Apps domain, create a feed URI for calendar + resources in that domain. If a resource_id is provided, return a URI + for that specific resource. If params are provided, append them as GET + params. + + Args: + resource_id: string (optional) The ID of the calendar resource for which + to make a feed URI. + params: dict (optional) key -> value params to append as GET vars to the + URI. Example: params={'start': 'my-resource-id'} + Returns: + A string giving the URI for calendar resources for this client's Google + Apps domain. + """ + uri = RESOURCE_FEED_TEMPLATE % (self.api_version, self.domain) + if resource_id: + uri += resource_id + if params: + uri += '?' + urllib.urlencode(params) + return uri + + MakeResourceFeedUri = make_resource_feed_uri + + def get_resource_feed(self, uri=None, **kwargs): + """Fetches a ResourceFeed of calendar resources at the given URI. + + Args: + uri: string The URI of the feed to pull. + kwargs: The other parameters to pass to gdata.client.GDClient.get_feed(). + + Returns: + A ResourceFeed object representing the feed at the given URI. + """ + + if uri is None: + uri = self.MakeResourceFeedUri() + return self.get_feed( + uri, + desired_class=gdata.calendar_resource.data.CalendarResourceFeed, + **kwargs) + + GetResourceFeed = get_resource_feed + + def get_resource(self, uri=None, resource_id=None, **kwargs): + """Fetches a single calendar resource by resource ID. + + Args: + uri: string The base URI of the feed from which to fetch the resource. + resource_id: string The string ID of the Resource to fetch. + kwargs: The other parameters to pass to gdata.client.GDClient.get_entry(). + + Returns: + A Resource object representing the calendar resource with the given + base URI and resource ID. + """ + + if uri is None: + uri = self.MakeResourceFeedUri(resource_id) + return self.get_entry( + uri, + desired_class=gdata.calendar_resource.data.CalendarResourceEntry, + **kwargs) + + GetResource = get_resource + + def create_resource(self, resource_id, resource_common_name=None, + resource_description=None, resource_type=None, **kwargs): + """Creates a calendar resource with the given properties. + + Args: + resource_id: string The resource ID of the calendar resource. + resource_common_name: string (optional) The common name of the resource. + resource_description: string (optional) The description of the resource. + resource_type: string (optional) The type of the resource. + kwargs: The other parameters to pass to gdata.client.GDClient.post(). + + Returns: + gdata.calendar_resource.data.CalendarResourceEntry of the new resource. + """ + new_resource = gdata.calendar_resource.data.CalendarResourceEntry( + resource_id=resource_id, + resource_common_name=resource_common_name, + resource_description=resource_description, + resource_type=resource_type) + return self.post(new_resource, self.MakeResourceFeedUri(), **kwargs) + + CreateResource = create_resource + + def update_resource(self, resource_id, resource_common_name=None, + resource_description=None, resource_type=None, **kwargs): + """Updates the calendar resource with the given resource ID. + + Args: + resource_id: string The resource ID of the calendar resource to update. + resource_common_name: string (optional) The common name to give the + resource. + resource_description: string (optional) The description to give the + resource. + resource_type: string (optional) The type to give the resource. + kwargs: The other parameters to pass to gdata.client.GDClient.update(). + + Returns: + gdata.calendar_resource.data.CalendarResourceEntry of the updated + resource. + """ + new_resource = gdata.calendar_resource.data.CalendarResourceEntry( + resource_id=resource_id, + resource_common_name=resource_common_name, + resource_description=resource_description, + resource_type=resource_type) + return self.update( + new_resource, + **kwargs) + + UpdateResource = update_resource + + def delete_resource(self, resource_id, **kwargs): + """Deletes the calendar resource with the given resource ID. + + Args: + resource_id: string The resource ID of the calendar resource to delete. + kwargs: The other parameters to pass to gdata.client.GDClient.delete() + + Returns: + An HTTP response object. See gdata.client.request(). + """ + + return self.delete(self.MakeResourceFeedUri(resource_id), **kwargs) + + DeleteResource = delete_resource diff --git a/patches/gdata/calendar_resource/data.py b/patches/gdata/calendar_resource/data.py new file mode 100644 index 0000000..82c152a --- /dev/null +++ b/patches/gdata/calendar_resource/data.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""Data model for parsing and generating XML for the Calendar Resource API.""" + + +__author__ = 'Vic Fryzel ' + + +import atom.core +import atom.data +import gdata.apps +import gdata.apps_property +import gdata.data + + +# This is required to work around a naming conflict between the Google +# Spreadsheets API and Python's built-in property function +pyproperty = property + + +# The apps:property name of the resourceId property +RESOURCE_ID_NAME = 'resourceId' +# The apps:property name of the resourceCommonName property +RESOURCE_COMMON_NAME_NAME = 'resourceCommonName' +# The apps:property name of the resourceDescription property +RESOURCE_DESCRIPTION_NAME = 'resourceDescription' +# The apps:property name of the resourceType property +RESOURCE_TYPE_NAME = 'resourceType' +# The apps:property name of the resourceEmail property +RESOURCE_EMAIL_NAME = 'resourceEmail' + + +class CalendarResourceEntry(gdata.data.GDEntry): + """Represents a Calendar Resource entry in object form.""" + + property = [gdata.apps_property.AppsProperty] + + def _GetProperty(self, name): + """Get the apps:property value with the given name. + + Args: + name: string Name of the apps:property value to get. + + Returns: + The apps:property value with the given name, or None if the name was + invalid. + """ + + for p in self.property: + if p.name == name: + return p.value + return None + + def _SetProperty(self, name, value): + """Set the apps:property value with the given name to the given value. + + Args: + name: string Name of the apps:property value to set. + value: string Value to give the apps:property value with the given name. + """ + + for i in range(len(self.property)): + if self.property[i].name == name: + self.property[i].value = value + return + self.property.append(gdata.apps_property.AppsProperty(name=name, value=value)) + + def GetResourceId(self): + """Get the resource ID of this Calendar Resource object. + + Returns: + The resource ID of this Calendar Resource object as a string or None. + """ + + return self._GetProperty(RESOURCE_ID_NAME) + + def SetResourceId(self, value): + """Set the resource ID of this Calendar Resource object. + + Args: + value: string The new resource ID value to give this object. + """ + + self._SetProperty(RESOURCE_ID_NAME, value) + + resource_id = pyproperty(GetResourceId, SetResourceId) + + def GetResourceCommonName(self): + """Get the common name of this Calendar Resource object. + + Returns: + The common name of this Calendar Resource object as a string or None. + """ + + return self._GetProperty(RESOURCE_COMMON_NAME_NAME) + + def SetResourceCommonName(self, value): + """Set the common name of this Calendar Resource object. + + Args: + value: string The new common name value to give this object. + """ + + self._SetProperty(RESOURCE_COMMON_NAME_NAME, value) + + resource_common_name = pyproperty( + GetResourceCommonName, + SetResourceCommonName) + + def GetResourceDescription(self): + """Get the description of this Calendar Resource object. + + Returns: + The description of this Calendar Resource object as a string or None. + """ + + return self._GetProperty(RESOURCE_DESCRIPTION_NAME) + + def SetResourceDescription(self, value): + """Set the description of this Calendar Resource object. + + Args: + value: string The new description value to give this object. + """ + + self._SetProperty(RESOURCE_DESCRIPTION_NAME, value) + + resource_description = pyproperty( + GetResourceDescription, + SetResourceDescription) + + def GetResourceType(self): + """Get the type of this Calendar Resource object. + + Returns: + The type of this Calendar Resource object as a string or None. + """ + + return self._GetProperty(RESOURCE_TYPE_NAME) + + def SetResourceType(self, value): + """Set the type value of this Calendar Resource object. + + Args: + value: string The new type value to give this object. + """ + + self._SetProperty(RESOURCE_TYPE_NAME, value) + + resource_type = pyproperty(GetResourceType, SetResourceType) + + def GetResourceEmail(self): + """Get the email of this Calendar Resource object. + + Returns: + The email of this Calendar Resource object as a string or None. + """ + + return self._GetProperty(RESOURCE_EMAIL_NAME) + + resource_email = pyproperty(GetResourceEmail) + + def __init__(self, resource_id=None, resource_common_name=None, + resource_description=None, resource_type=None, *args, **kwargs): + """Constructs a new CalendarResourceEntry object with the given arguments. + + Args: + resource_id: string (optional) The resource ID to give this new object. + resource_common_name: string (optional) The common name to give this new + object. + resource_description: string (optional) The description to give this new + object. + resource_type: string (optional) The type to give this new object. + args: The other parameters to pass to gdata.entry.GDEntry constructor. + kwargs: The other parameters to pass to gdata.entry.GDEntry constructor. + """ + super(CalendarResourceEntry, self).__init__(*args, **kwargs) + if resource_id: + self.resource_id = resource_id + if resource_common_name: + self.resource_common_name = resource_common_name + if resource_description: + self.resource_description = resource_description + if resource_type: + self.resource_type = resource_type + + +class CalendarResourceFeed(gdata.data.GDFeed): + """Represents a feed of CalendarResourceEntry objects.""" + + # Override entry so that this feed knows how to type its list of entries. + entry = [CalendarResourceEntry] diff --git a/patches/gdata/client.py b/patches/gdata/client.py new file mode 100644 index 0000000..7aaff46 --- /dev/null +++ b/patches/gdata/client.py @@ -0,0 +1,1131 @@ +#!/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 + + 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 it's 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: + m = re.compile('[\?\&]gsessionid=(\w*)').search(location) + if m is not None: + self.__gsessionid = m.group(1) + # Make a recursive call with the gsession ID in the URI to follow + # the redirect. + return self.request(method=method, uri=uri, 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.. 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: + None + + 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) + + 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 + + #TODO: implement batch requests. + #def batch(feed, uri, auth_token=None, converter=None, **kwargs): + # pass + + # 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): + """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. + """ + 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 + + 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' + + + 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 + + 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 + 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): + """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. + + Returns: + The final Atom entry as created on the server. The entry will be + parsed accoring to 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. + """ + http_request = atom.http_core.HttpRequest() + + # Send empty POST 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'] = self.total_file_size + + if headers is not None: + http_request.headers.update(headers) + + response = self.client.request(method='POST', + uri=resumable_media_link, + auth_token=auth_token, + http_request=http_request) + + self.upload_uri = (response.getheader('location') or + response.getheader('Location')) + + _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='POST', 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): + """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. + + 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) + + 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): + """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. + + 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. + """ + # Need to override the POST request for a resumable update (required). + customer_headers = {'X-HTTP-Method-Override': 'PUT'} + + if headers is not None: + customer_headers.update(headers) + + if isinstance(entry_or_resumable_edit_link, gdata.data.GDEntry): + resumable_edit_link = entry_or_resumable_edit_link.find_url( + 'http://schemas.google.com/g/2005#resumable-edit-media') + customer_headers['If-Match'] = entry_or_resumable_edit_link.etag + else: + resumable_edit_link = entry_or_resumable_edit_link + + if force: + customer_headers['If-Match'] = '*' + + return self.upload_file(resumable_edit_link, headers=customer_headers, + auth_token=auth_token) + + 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 diff --git a/patches/gdata/client.pyc b/patches/gdata/client.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3f7815633eb3397645b486f7207d9b0af790424 GIT binary patch literal 42940 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 literal 0 HcmV?d00001 diff --git a/patches/gdata/codesearch/__init__.py b/patches/gdata/codesearch/__init__.py new file mode 100644 index 0000000..fa23ef0 --- /dev/null +++ b/patches/gdata/codesearch/__init__.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007 Benoit Chesneau +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +"""Contains extensions to Atom objects used by Google Codesearch""" + +__author__ = 'Benoit Chesneau' + + +import atom +import gdata + + +CODESEARCH_NAMESPACE='http://schemas.google.com/codesearch/2006' +CODESEARCH_TEMPLATE='{http://shema.google.com/codesearch/2006}%s' + + +class Match(atom.AtomBase): + """ The Google Codesearch match element """ + _tag = 'match' + _namespace = CODESEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['lineNumber'] = 'line_number' + _attributes['type'] = 'type' + + def __init__(self, line_number=None, type=None, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.type = type + self.line_number = line_number + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class File(atom.AtomBase): + """ The Google Codesearch file element""" + _tag = 'file' + _namespace = CODESEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + + def __init__(self, name=None, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.name = name + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Package(atom.AtomBase): + """ The Google Codesearch package element""" + _tag = 'package' + _namespace = CODESEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + _attributes['uri'] = 'uri' + + def __init__(self, name=None, uri=None, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.name = name + self.uri = uri + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class CodesearchEntry(gdata.GDataEntry): + """ Google codesearch atom entry""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + + _children['{%s}file' % CODESEARCH_NAMESPACE] = ('file', File) + _children['{%s}package' % CODESEARCH_NAMESPACE] = ('package', Package) + _children['{%s}match' % CODESEARCH_NAMESPACE] = ('match', [Match]) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + match=None, + extension_elements=None, extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, + updated=updated, text=None) + + self.match = match or [] + + +def CodesearchEntryFromString(xml_string): + """Converts an XML string into a CodesearchEntry object. + + Args: + xml_string: string The XML describing a Codesearch feed entry. + + Returns: + A CodesearchEntry object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(CodesearchEntry, xml_string) + + +class CodesearchFeed(gdata.GDataFeed): + """feed containing list of Google codesearch Items""" + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [CodesearchEntry]) + + +def CodesearchFeedFromString(xml_string): + """Converts an XML string into a CodesearchFeed object. + Args: + xml_string: string The XML describing a Codesearch feed. + Returns: + A CodeseartchFeed object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(CodesearchFeed, xml_string) diff --git a/patches/gdata/codesearch/service.py b/patches/gdata/codesearch/service.py new file mode 100644 index 0000000..1243d61 --- /dev/null +++ b/patches/gdata/codesearch/service.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007 Benoit Chesneau +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +"""CodesearchService extends GDataService to streamline Google Codesearch +operations""" + + +__author__ = 'Benoit Chesneau' + + +import atom +import gdata.service +import gdata.codesearch + + +class CodesearchService(gdata.service.GDataService): + """Client extension for Google codesearch service""" + ssl = True + + def __init__(self, email=None, password=None, source=None, + server='www.google.com', additional_headers=None, **kwargs): + """Creates a client for the Google codesearch service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'www.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='codesearch', + source=source, server=server, additional_headers=additional_headers, + **kwargs) + + def Query(self, uri, converter=gdata.codesearch.CodesearchFeedFromString): + """Queries the Codesearch feed and returns the resulting feed of + entries. + + Args: + uri: string The full URI to be queried. This can contain query + parameters, a hostname, or simply the relative path to a Document + List feed. The DocumentQuery object is useful when constructing + query parameters. + converter: func (optional) A function which will be executed on the + retrieved item, generally to render it into a Python object. + By default the CodesearchFeedFromString function is used to + return a CodesearchFeed object. This is because most feed + queries will result in a feed and not a single entry. + + Returns : + A CodesearchFeed objects representing the feed returned by the server + """ + return self.Get(uri, converter=converter) + + def GetSnippetsFeed(self, text_query=None): + """Retrieve Codesearch feed for a keyword + + Args: + text_query : string (optional) The contents of the q query parameter. This + string is URL escaped upon conversion to a URI. + Returns: + A CodesearchFeed objects representing the feed returned by the server + """ + + query=gdata.codesearch.service.CodesearchQuery(text_query=text_query) + feed = self.Query(query.ToUri()) + return feed + + +class CodesearchQuery(gdata.service.Query): + """Object used to construct the query to the Google Codesearch feed. here only as a shorcut""" + + def __init__(self, feed='/codesearch/feeds/search', text_query=None, + params=None, categories=None): + """Constructor for Codesearch Query. + + Args: + feed: string (optional) The path for the feed. (e.g. '/codesearch/feeds/search') + text_query: string (optional) The contents of the q query parameter. This + string is URL escaped upon conversion to a URI. + params: dict (optional) Parameter value string pairs which become URL + params when translated to a URI. These parameters are added to + the query's items. + categories: list (optional) List of category strings which should be + included as query categories. See gdata.service.Query for + additional documentation. + + Yelds: + A CodesearchQuery object to construct a URI based on Codesearch feed + """ + + gdata.service.Query.__init__(self, feed, text_query, params, categories) diff --git a/patches/gdata/contacts/__init__.py b/patches/gdata/contacts/__init__.py new file mode 100644 index 0000000..41e7c31 --- /dev/null +++ b/patches/gdata/contacts/__init__.py @@ -0,0 +1,740 @@ +#!/usr/bin/env python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""Contains extensions to ElementWrapper objects used with Google Contacts.""" + +__author__ = 'dbrattli (Dag Brattli)' + + +import atom +import gdata + + +## Constants from http://code.google.com/apis/gdata/elements.html ## +REL_HOME = 'http://schemas.google.com/g/2005#home' +REL_WORK = 'http://schemas.google.com/g/2005#work' +REL_OTHER = 'http://schemas.google.com/g/2005#other' + +# AOL Instant Messenger protocol +IM_AIM = 'http://schemas.google.com/g/2005#AIM' +IM_MSN = 'http://schemas.google.com/g/2005#MSN' # MSN Messenger protocol +IM_YAHOO = 'http://schemas.google.com/g/2005#YAHOO' # Yahoo Messenger protocol +IM_SKYPE = 'http://schemas.google.com/g/2005#SKYPE' # Skype protocol +IM_QQ = 'http://schemas.google.com/g/2005#QQ' # QQ protocol +# Google Talk protocol +IM_GOOGLE_TALK = 'http://schemas.google.com/g/2005#GOOGLE_TALK' +IM_ICQ = 'http://schemas.google.com/g/2005#ICQ' # ICQ protocol +IM_JABBER = 'http://schemas.google.com/g/2005#JABBER' # Jabber protocol +IM_NETMEETING = 'http://schemas.google.com/g/2005#netmeeting' # NetMeeting + +PHOTO_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#photo' +PHOTO_EDIT_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#edit-photo' + +# Different phone types, for more info see: +# http://code.google.com/apis/gdata/docs/2.0/elements.html#gdPhoneNumber +PHONE_CAR = 'http://schemas.google.com/g/2005#car' +PHONE_FAX = 'http://schemas.google.com/g/2005#fax' +PHONE_GENERAL = 'http://schemas.google.com/g/2005#general' +PHONE_HOME = REL_HOME +PHONE_HOME_FAX = 'http://schemas.google.com/g/2005#home_fax' +PHONE_INTERNAL = 'http://schemas.google.com/g/2005#internal-extension' +PHONE_MOBILE = 'http://schemas.google.com/g/2005#mobile' +PHONE_OTHER = REL_OTHER +PHONE_PAGER = 'http://schemas.google.com/g/2005#pager' +PHONE_SATELLITE = 'http://schemas.google.com/g/2005#satellite' +PHONE_VOIP = 'http://schemas.google.com/g/2005#voip' +PHONE_WORK = REL_WORK +PHONE_WORK_FAX = 'http://schemas.google.com/g/2005#work_fax' +PHONE_WORK_MOBILE = 'http://schemas.google.com/g/2005#work_mobile' +PHONE_WORK_PAGER = 'http://schemas.google.com/g/2005#work_pager' +PHONE_MAIN = 'http://schemas.google.com/g/2005#main' +PHONE_ASSISTANT = 'http://schemas.google.com/g/2005#assistant' +PHONE_CALLBACK = 'http://schemas.google.com/g/2005#callback' +PHONE_COMPANY_MAIN = 'http://schemas.google.com/g/2005#company_main' +PHONE_ISDN = 'http://schemas.google.com/g/2005#isdn' +PHONE_OTHER_FAX = 'http://schemas.google.com/g/2005#other_fax' +PHONE_RADIO = 'http://schemas.google.com/g/2005#radio' +PHONE_TELEX = 'http://schemas.google.com/g/2005#telex' +PHONE_TTY_TDD = 'http://schemas.google.com/g/2005#tty_tdd' + +EXTERNAL_ID_ORGANIZATION = 'organization' + +RELATION_MANAGER = 'manager' + +CONTACTS_NAMESPACE = 'http://schemas.google.com/contact/2008' + + +class GDataBase(atom.AtomBase): + """The Google Contacts intermediate class from atom.AtomBase.""" + + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, text=None, + extension_elements=None, extension_attributes=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class ContactsBase(GDataBase): + """The Google Contacts intermediate class for Contacts namespace.""" + + _namespace = CONTACTS_NAMESPACE + + +class OrgName(GDataBase): + """The Google Contacts OrgName element.""" + + _tag = 'orgName' + + +class OrgTitle(GDataBase): + """The Google Contacts OrgTitle element.""" + + _tag = 'orgTitle' + + +class OrgDepartment(GDataBase): + """The Google Contacts OrgDepartment element.""" + + _tag = 'orgDepartment' + + +class OrgJobDescription(GDataBase): + """The Google Contacts OrgJobDescription element.""" + + _tag = 'orgJobDescription' + + +class Where(GDataBase): + """The Google Contacts Where element.""" + + _tag = 'where' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['rel'] = 'rel' + _attributes['label'] = 'label' + _attributes['valueString'] = 'value_string' + + def __init__(self, value_string=None, rel=None, label=None, + text=None, extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.rel = rel + self.label = label + self.value_string = value_string + + +class When(GDataBase): + """The Google Contacts When element.""" + + _tag = 'when' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['startTime'] = 'start_time' + _attributes['endTime'] = 'end_time' + _attributes['label'] = 'label' + + def __init__(self, start_time=None, end_time=None, label=None, + text=None, extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.start_time = start_time + self.end_time = end_time + self.label = label + + +class Organization(GDataBase): + """The Google Contacts Organization element.""" + + _tag = 'organization' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['label'] = 'label' + _attributes['rel'] = 'rel' + _attributes['primary'] = 'primary' + _children['{%s}orgName' % GDataBase._namespace] = ( + 'org_name', OrgName) + _children['{%s}orgTitle' % GDataBase._namespace] = ( + 'org_title', OrgTitle) + _children['{%s}orgDepartment' % GDataBase._namespace] = ( + 'org_department', OrgDepartment) + _children['{%s}orgJobDescription' % GDataBase._namespace] = ( + 'org_job_description', OrgJobDescription) + #_children['{%s}where' % GDataBase._namespace] = ('where', Where) + + def __init__(self, label=None, rel=None, primary='false', org_name=None, + org_title=None, org_department=None, org_job_description=None, + where=None, text=None, + extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.label = label + self.rel = rel or REL_OTHER + self.primary = primary + self.org_name = org_name + self.org_title = org_title + self.org_department = org_department + self.org_job_description = org_job_description + self.where = where + + +class PostalAddress(GDataBase): + """The Google Contacts PostalAddress element.""" + + _tag = 'postalAddress' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['rel'] = 'rel' + _attributes['primary'] = 'primary' + + def __init__(self, primary=None, rel=None, text=None, + extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.rel = rel or REL_OTHER + self.primary = primary + + +class FormattedAddress(GDataBase): + """The Google Contacts FormattedAddress element.""" + + _tag = 'formattedAddress' + + +class StructuredPostalAddress(GDataBase): + """The Google Contacts StructuredPostalAddress element.""" + + _tag = 'structuredPostalAddress' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['rel'] = 'rel' + _attributes['primary'] = 'primary' + _children['{%s}formattedAddress' % GDataBase._namespace] = ( + 'formatted_address', FormattedAddress) + + def __init__(self, rel=None, primary=None, + formatted_address=None, text=None, + extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.rel = rel or REL_OTHER + self.primary = primary + self.formatted_address = formatted_address + + +class IM(GDataBase): + """The Google Contacts IM element.""" + + _tag = 'im' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['address'] = 'address' + _attributes['primary'] = 'primary' + _attributes['protocol'] = 'protocol' + _attributes['label'] = 'label' + _attributes['rel'] = 'rel' + + def __init__(self, primary='false', rel=None, address=None, protocol=None, + label=None, text=None, + extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.protocol = protocol + self.address = address + self.primary = primary + self.rel = rel or REL_OTHER + self.label = label + + +class Email(GDataBase): + """The Google Contacts Email element.""" + + _tag = 'email' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['address'] = 'address' + _attributes['primary'] = 'primary' + _attributes['rel'] = 'rel' + _attributes['label'] = 'label' + + def __init__(self, label=None, rel=None, address=None, primary='false', + text=None, extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.label = label + self.rel = rel or REL_OTHER + self.address = address + self.primary = primary + + +class PhoneNumber(GDataBase): + """The Google Contacts PhoneNumber element.""" + + _tag = 'phoneNumber' + _children = GDataBase._children.copy() + _attributes = GDataBase._attributes.copy() + _attributes['label'] = 'label' + _attributes['rel'] = 'rel' + _attributes['uri'] = 'uri' + _attributes['primary'] = 'primary' + + def __init__(self, label=None, rel=None, uri=None, primary='false', + text=None, extension_elements=None, extension_attributes=None): + GDataBase.__init__(self, text=text, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.label = label + self.rel = rel or REL_OTHER + self.uri = uri + self.primary = primary + + +class Nickname(ContactsBase): + """The Google Contacts Nickname element.""" + + _tag = 'nickname' + + +class Occupation(ContactsBase): + """The Google Contacts Occupation element.""" + + _tag = 'occupation' + + +class Gender(ContactsBase): + """The Google Contacts Gender element.""" + + _tag = 'gender' + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, + text=None, extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.value = value + + +class Birthday(ContactsBase): + """The Google Contacts Birthday element.""" + + _tag = 'birthday' + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['when'] = 'when' + + def __init__(self, when=None, + text=None, extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.when = when + + +class Relation(ContactsBase): + """The Google Contacts Relation element.""" + + _tag = 'relation' + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['label'] = 'label' + _attributes['rel'] = 'rel' + + def __init__(self, label=None, rel=None, + text=None, extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.label = label + self.rel = rel + + +def RelationFromString(xml_string): + return atom.CreateClassFromXMLString(Relation, xml_string) + + +class UserDefinedField(ContactsBase): + """The Google Contacts UserDefinedField element.""" + + _tag = 'userDefinedField' + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['key'] = 'key' + _attributes['value'] = 'value' + + def __init__(self, key=None, value=None, + text=None, extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.key = key + self.value = value + + +def UserDefinedFieldFromString(xml_string): + return atom.CreateClassFromXMLString(UserDefinedField, xml_string) + + +class Website(ContactsBase): + """The Google Contacts Website element.""" + + _tag = 'website' + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['href'] = 'href' + _attributes['label'] = 'label' + _attributes['primary'] = 'primary' + _attributes['rel'] = 'rel' + + def __init__(self, href=None, label=None, primary='false', rel=None, + text=None, extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.href = href + self.label = label + self.primary = primary + self.rel = rel + + +def WebsiteFromString(xml_string): + return atom.CreateClassFromXMLString(Website, xml_string) + + +class ExternalId(ContactsBase): + """The Google Contacts ExternalId element.""" + + _tag = 'externalId' + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['label'] = 'label' + _attributes['rel'] = 'rel' + _attributes['value'] = 'value' + + def __init__(self, label=None, rel=None, value=None, + text=None, extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.label = label + self.rel = rel + self.value = value + + +def ExternalIdFromString(xml_string): + return atom.CreateClassFromXMLString(ExternalId, xml_string) + + +class Event(ContactsBase): + """The Google Contacts Event element.""" + + _tag = 'event' + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['label'] = 'label' + _attributes['rel'] = 'rel' + _children['{%s}when' % ContactsBase._namespace] = ('when', When) + + def __init__(self, label=None, rel=None, when=None, + text=None, extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.label = label + self.rel = rel + self.when = when + + +def EventFromString(xml_string): + return atom.CreateClassFromXMLString(Event, xml_string) + + +class Deleted(GDataBase): + """The Google Contacts Deleted element.""" + + _tag = 'deleted' + + +class GroupMembershipInfo(ContactsBase): + """The Google Contacts GroupMembershipInfo element.""" + + _tag = 'groupMembershipInfo' + + _children = ContactsBase._children.copy() + _attributes = ContactsBase._attributes.copy() + _attributes['deleted'] = 'deleted' + _attributes['href'] = 'href' + + def __init__(self, deleted=None, href=None, text=None, + extension_elements=None, extension_attributes=None): + ContactsBase.__init__(self, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.deleted = deleted + self.href = href + + +class PersonEntry(gdata.BatchEntry): + """Base class for ContactEntry and ProfileEntry.""" + + _children = gdata.BatchEntry._children.copy() + _children['{%s}organization' % gdata.GDATA_NAMESPACE] = ( + 'organization', [Organization]) + _children['{%s}phoneNumber' % gdata.GDATA_NAMESPACE] = ( + 'phone_number', [PhoneNumber]) + _children['{%s}nickname' % CONTACTS_NAMESPACE] = ('nickname', Nickname) + _children['{%s}occupation' % CONTACTS_NAMESPACE] = ('occupation', Occupation) + _children['{%s}gender' % CONTACTS_NAMESPACE] = ('gender', Gender) + _children['{%s}birthday' % CONTACTS_NAMESPACE] = ('birthday', Birthday) + _children['{%s}postalAddress' % gdata.GDATA_NAMESPACE] = ('postal_address', + [PostalAddress]) + _children['{%s}structuredPostalAddress' % gdata.GDATA_NAMESPACE] = ( + 'structured_postal_address', [StructuredPostalAddress]) + _children['{%s}email' % gdata.GDATA_NAMESPACE] = ('email', [Email]) + _children['{%s}im' % gdata.GDATA_NAMESPACE] = ('im', [IM]) + _children['{%s}relation' % CONTACTS_NAMESPACE] = ('relation', [Relation]) + _children['{%s}userDefinedField' % CONTACTS_NAMESPACE] = ( + 'user_defined_field', [UserDefinedField]) + _children['{%s}website' % CONTACTS_NAMESPACE] = ('website', [Website]) + _children['{%s}externalId' % CONTACTS_NAMESPACE] = ( + 'external_id', [ExternalId]) + _children['{%s}event' % CONTACTS_NAMESPACE] = ('event', [Event]) + # The following line should be removed once the Python support + # for GData 2.0 is mature. + _attributes = gdata.BatchEntry._attributes.copy() + _attributes['{%s}etag' % gdata.GDATA_NAMESPACE] = 'etag' + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, organization=None, phone_number=None, + nickname=None, occupation=None, gender=None, birthday=None, + postal_address=None, structured_postal_address=None, email=None, + im=None, relation=None, user_defined_field=None, website=None, + external_id=None, event=None, batch_operation=None, + batch_id=None, batch_status=None, text=None, + extension_elements=None, extension_attributes=None, etag=None): + gdata.BatchEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, + batch_operation=batch_operation, + batch_id=batch_id, batch_status=batch_status, + title=title, updated=updated) + self.organization = organization or [] + self.phone_number = phone_number or [] + self.nickname = nickname + self.occupation = occupation + self.gender = gender + self.birthday = birthday + self.postal_address = postal_address or [] + self.structured_postal_address = structured_postal_address or [] + self.email = email or [] + self.im = im or [] + self.relation = relation or [] + self.user_defined_field = user_defined_field or [] + self.website = website or [] + self.external_id = external_id or [] + self.event = event or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + # The following line should be removed once the Python support + # for GData 2.0 is mature. + self.etag = etag + + +class ContactEntry(PersonEntry): + """A Google Contact flavor of an Atom Entry.""" + + _children = PersonEntry._children.copy() + + _children['{%s}deleted' % gdata.GDATA_NAMESPACE] = ('deleted', Deleted) + _children['{%s}groupMembershipInfo' % CONTACTS_NAMESPACE] = ( + 'group_membership_info', [GroupMembershipInfo]) + _children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = ( + 'extended_property', [gdata.ExtendedProperty]) + # Overwrite the organization rule in PersonEntry so that a ContactEntry + # may only contain one element. + _children['{%s}organization' % gdata.GDATA_NAMESPACE] = ( + 'organization', Organization) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, organization=None, phone_number=None, + nickname=None, occupation=None, gender=None, birthday=None, + postal_address=None, structured_postal_address=None, email=None, + im=None, relation=None, user_defined_field=None, website=None, + external_id=None, event=None, batch_operation=None, + batch_id=None, batch_status=None, text=None, + extension_elements=None, extension_attributes=None, etag=None, + deleted=None, extended_property=None, + group_membership_info=None): + PersonEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, updated=updated, + organization=organization, phone_number=phone_number, + nickname=nickname, occupation=occupation, + gender=gender, birthday=birthday, + postal_address=postal_address, + structured_postal_address=structured_postal_address, + email=email, im=im, relation=relation, + user_defined_field=user_defined_field, + website=website, external_id=external_id, event=event, + batch_operation=batch_operation, batch_id=batch_id, + batch_status=batch_status, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes, etag=etag) + self.deleted = deleted + self.extended_property = extended_property or [] + self.group_membership_info = group_membership_info or [] + + def GetPhotoLink(self): + for a_link in self.link: + if a_link.rel == PHOTO_LINK_REL: + return a_link + return None + + def GetPhotoEditLink(self): + for a_link in self.link: + if a_link.rel == PHOTO_EDIT_LINK_REL: + return a_link + return None + + +def ContactEntryFromString(xml_string): + return atom.CreateClassFromXMLString(ContactEntry, xml_string) + + +class ContactsFeed(gdata.BatchFeed, gdata.LinkFinder): + """A Google Contacts feed flavor of an Atom Feed.""" + + _children = gdata.BatchFeed._children.copy() + + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ContactEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.BatchFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def ContactsFeedFromString(xml_string): + return atom.CreateClassFromXMLString(ContactsFeed, xml_string) + + +class GroupEntry(gdata.BatchEntry): + """Represents a contact group.""" + _children = gdata.BatchEntry._children.copy() + _children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = ( + 'extended_property', [gdata.ExtendedProperty]) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, + rights=None, source=None, summary=None, control=None, + title=None, updated=None, + extended_property=None, batch_operation=None, batch_id=None, + batch_status=None, + extension_elements=None, extension_attributes=None, text=None): + gdata.BatchEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + batch_operation=batch_operation, + batch_id=batch_id, batch_status=batch_status, + title=title, updated=updated) + self.extended_property = extended_property or [] + + +def GroupEntryFromString(xml_string): + return atom.CreateClassFromXMLString(GroupEntry, xml_string) + + +class GroupsFeed(gdata.BatchFeed): + """A Google contact groups feed flavor of an Atom Feed.""" + _children = gdata.BatchFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GroupEntry]) + + +def GroupsFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GroupsFeed, xml_string) + + +class ProfileEntry(PersonEntry): + """A Google Profiles flavor of an Atom Entry.""" + + +def ProfileEntryFromString(xml_string): + """Converts an XML string into a ProfileEntry object. + + Args: + xml_string: string The XML describing a Profile entry. + + Returns: + A ProfileEntry object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(ProfileEntry, xml_string) + + +class ProfilesFeed(gdata.BatchFeed, gdata.LinkFinder): + """A Google Profiles feed flavor of an Atom Feed.""" + + _children = gdata.BatchFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ProfileEntry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.BatchFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def ProfilesFeedFromString(xml_string): + """Converts an XML string into a ProfilesFeed object. + + Args: + xml_string: string The XML describing a Profiles feed. + + Returns: + A ProfilesFeed object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(ProfilesFeed, xml_string) diff --git a/patches/gdata/contacts/client.py b/patches/gdata/contacts/client.py new file mode 100644 index 0000000..d79d3c9 --- /dev/null +++ b/patches/gdata/contacts/client.py @@ -0,0 +1,497 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. +from types import ListType, DictionaryType + + +"""Contains a client to communicate with the Contacts servers. + +For documentation on the Contacts API, see: +http://code.google.com/apis/contatcs/ +""" + +__author__ = 'vinces1979@gmail.com (Vince Spicer)' + + +import gdata.client +import gdata.contacts.data +import atom.data +import atom.http_core +import gdata.gauth + +DEFAULT_BATCH_URL = ('http://www.google.com/m8/feeds/contacts/default/full' + '/batch') +DEFAULT_PROFILES_BATCH_URL = ('http://www.google.com' + '/m8/feeds/profiles/default/full/batch') + +class ContactsClient(gdata.client.GDClient): + api_version = '3' + auth_service = 'cp' + server = "www.google.com" + contact_list = "default" + auth_scopes = gdata.gauth.AUTH_SCOPES['cp'] + ssl = True + + + def __init__(self, domain=None, auth_token=None, **kwargs): + """Constructs a new client for the Email Settings API. + + Args: + domain: string The Google Apps domain (if any). + kwargs: The other parameters to pass to the gdata.client.GDClient + constructor. + """ + gdata.client.GDClient.__init__(self, auth_token=auth_token, **kwargs) + self.domain = domain + + def get_feed_uri(self, kind='contacts', contact_list=None, projection='full', + scheme="http"): + """Builds a feed URI. + + Args: + kind: The type of feed to return, typically 'groups' or 'contacts'. + Default value: 'contacts'. + contact_list: The contact list to return a feed for. + Default value: self.contact_list. + projection: The projection to apply to the feed contents, for example + 'full', 'base', 'base/12345', 'full/batch'. Default value: 'full'. + scheme: The URL scheme such as 'http' or 'https', None to return a + relative URI without hostname. + + Returns: + A feed URI using the given kind, contact list, and projection. + Example: '/m8/feeds/contacts/default/full'. + """ + contact_list = contact_list or self.contact_list + if kind == 'profiles': + contact_list = 'domain/%s' % self.domain + prefix = scheme and '%s://%s' % (scheme, self.server) or '' + return '%s/m8/feeds/%s/%s/%s' % (prefix, kind, contact_list, projection) + + GetFeedUri = get_feed_uri + + def get_contact(self, uri, desired_class=gdata.contacts.data.ContactEntry, + auth_token=None, **kwargs): + return self.get_entry(uri, auth_token=auth_token, + desired_class=desired_class, **kwargs) + + + GetContact = get_contact + + + def create_contact(self, new_contact, insert_uri=None, auth_token=None, **kwargs): + """Adds an new contact to Google Contacts. + + Args: + new_contact: atom.Entry or subclass A new contact which is to be added to + Google Contacts. + insert_uri: the URL to post new contacts to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the contact created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + insert_uri = insert_uri or self.GetFeedUri() + return self.Post(new_contact, insert_uri, + auth_token=auth_token, **kwargs) + + CreateContact = create_contact + + def add_contact(self, new_contact, insert_uri=None, auth_token=None, + billing_information=None, birthday=None, calendar_link=None, **kwargs): + """Adds an new contact to Google Contacts. + + Args: + new_contact: atom.Entry or subclass A new contact which is to be added to + Google Contacts. + insert_uri: the URL to post new contacts to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the contact created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + + contact = gdata.contacts.data.ContactEntry() + + if billing_information is not None: + if not isinstance(billing_information, gdata.contacts.data.BillingInformation): + billing_information = gdata.contacts.data.BillingInformation(text=billing_information) + + contact.billing_information = billing_information + + if birthday is not None: + if not isinstance(birthday, gdata.contacts.data.Birthday): + birthday = gdata.contacts.data.Birthday(when=birthday) + + contact.birthday = birthday + + if calendar_link is not None: + if type(calendar_link) is not ListType: + calendar_link = [calendar_link] + + for link in calendar_link: + if not isinstance(link, gdata.contacts.data.CalendarLink): + if type(link) is not DictionaryType: + raise TypeError, "calendar_link Requires dictionary not %s" % type(link) + + link = gdata.contacts.data.CalendarLink( + rel=link.get("rel", None), + label=link.get("label", None), + primary=link.get("primary", None), + href=link.get("href", None), + ) + + contact.calendar_link.append(link) + + insert_uri = insert_uri or self.GetFeedUri() + return self.Post(contact, insert_uri, + auth_token=auth_token, **kwargs) + + AddContact = add_contact + + def get_contacts(self, desired_class=gdata.contacts.data.ContactsFeed, + auth_token=None, **kwargs): + """Obtains a feed with the contacts belonging to the current user. + + Args: + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + desired_class: class descended from atom.core.XmlElement to which a + successful response should be converted. If there is no + converter function specified (desired_class=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. Defaults to + gdata.spreadsheets.data.SpreadsheetsFeed. + """ + return self.get_feed(self.GetFeedUri(), auth_token=auth_token, + desired_class=desired_class, **kwargs) + + GetContacts = get_contacts + + def get_group(self, uri=None, desired_class=gdata.contacts.data.GroupEntry, + auth_token=None, **kwargs): + """ Get a single groups details + Args: + uri: the group uri or id + """ + return self.get_entry(uri, desired_class=desired_class, auth_token=auth_token, **kwargs) + + GetGroup = get_group + + def get_groups(self, uri=None, desired_class=gdata.contacts.data.GroupsFeed, + auth_token=None, **kwargs): + uri = uri or self.GetFeedUri('groups') + return self.get_feed(uri, desired_class=desired_class, auth_token=auth_token, **kwargs) + + GetGroups = get_groups + + def create_group(self, new_group, insert_uri=None, url_params=None, + desired_class=None): + insert_uri = insert_uri or self.GetFeedUri('groups') + return self.Post(new_group, insert_uri, url_params=url_params, + desired_class=desired_class) + + CreateGroup = create_group + + def update_group(self, edit_uri, updated_group, url_params=None, + escape_params=True, desired_class=None): + return self.Put(updated_group, self._CleanUri(edit_uri), + url_params=url_params, + escape_params=escape_params, + desired_class=desired_class) + + UpdateGroup = update_group + + def delete_group(self, group_object, auth_token=None, force=False, **kws): + return self.Delete(group_object, auth_token=auth_token, force=force, **kws ) + + DeleteGroup = delete_group + + def change_photo(self, media, contact_entry_or_url, content_type=None, + content_length=None): + """Change the photo for the contact by uploading a new photo. + + Performs a PUT against the photo edit URL to send the binary data for the + photo. + + Args: + media: filename, file-like-object, or a gdata.MediaSource object to send. + contact_entry_or_url: ContactEntry or str If it is a ContactEntry, this + method will search for an edit photo link URL and + perform a PUT to the URL. + content_type: str (optional) the mime type for the photo data. This is + necessary if media is a file or file name, but if media + is a MediaSource object then the media object can contain + the mime type. If media_type is set, it will override the + mime type in the media object. + content_length: int or str (optional) Specifying the content length is + only required if media is a file-like object. If media + is a filename, the length is determined using + os.path.getsize. If media is a MediaSource object, it is + assumed that it already contains the content length. + """ + if isinstance(contact_entry_or_url, gdata.contacts.data.ContactEntry): + url = contact_entry_or_url.GetPhotoEditLink().href + else: + url = contact_entry_or_url + if isinstance(media, gdata.MediaSource): + payload = media + # If the media object is a file-like object, then use it as the file + # handle in the in the MediaSource. + elif hasattr(media, 'read'): + payload = gdata.MediaSource(file_handle=media, + content_type=content_type, content_length=content_length) + # Assume that the media object is a file name. + else: + payload = gdata.MediaSource(content_type=content_type, + content_length=content_length, file_path=media) + return self.Put(payload, url) + + ChangePhoto = change_photo + + def get_photo(self, contact_entry_or_url): + """Retrives the binary data for the contact's profile photo as a string. + + Args: + contact_entry_or_url: a gdata.contacts.ContactEntry objecr or a string + containing the photo link's URL. If the contact entry does not + contain a photo link, the image will not be fetched and this method + will return None. + """ + # TODO: add the ability to write out the binary image data to a file, + # reading and writing a chunk at a time to avoid potentially using up + # large amounts of memory. + url = None + if isinstance(contact_entry_or_url, gdata.contacts.data.ContactEntry): + photo_link = contact_entry_or_url.GetPhotoLink() + if photo_link: + url = photo_link.href + else: + url = contact_entry_or_url + if url: + return self.Get(url).read() + else: + return None + + GetPhoto = get_photo + + def delete_photo(self, contact_entry_or_url): + url = None + if isinstance(contact_entry_or_url, gdata.contacts.data.ContactEntry): + url = contact_entry_or_url.GetPhotoEditLink().href + else: + url = contact_entry_or_url + if url: + self.Delete(url) + + DeletePhoto = delete_photo + + def get_profiles_feed(self, uri=None): + """Retrieves a feed containing all domain's profiles. + + Args: + uri: string (optional) the URL to retrieve the profiles feed, + for example /m8/feeds/profiles/default/full + + Returns: + On success, a ProfilesFeed containing the profiles. + On failure, raises a RequestError. + """ + + uri = uri or self.GetFeedUri('profiles') + return self.Get(uri, + desired_class=gdata.contacts.data.ProfilesFeed) + + GetProfilesFeed = get_profiles_feed + + def get_profile(self, uri): + """Retrieves a domain's profile for the user. + + Args: + uri: string the URL to retrieve the profiles feed, + for example /m8/feeds/profiles/default/full/username + + Returns: + On success, a ProfileEntry containing the profile for the user. + On failure, raises a RequestError + """ + return self.Get(uri, + desired_class=gdata.contacts.data.ProfileEntry) + + GetProfile = get_profile + + def update_profile(self, updated_profile, auth_token=None, force=False, **kwargs): + """Updates an existing profile. + + Args: + updated_profile: atom.Entry or subclass containing + the Atom Entry which will replace the profile which is + stored at the edit_url. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of ContactsClient. + 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. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful update, a httplib.HTTPResponse containing the server's + response to the PUT request. + On failure, raises a RequestError. + """ + return self.Update(updated_profile, auth_token=auth_token, force=force, **kwargs) + + UpdateProfile = update_profile + + def execute_batch(self, batch_feed, url=DEFAULT_BATCH_URL, desired_class=None): + """Sends a batch request feed to the server. + + Args: + batch_feed: gdata.contacts.ContactFeed A feed containing batch + request entries. Each entry contains the operation to be performed + on the data contained in the entry. For example an entry with an + operation type of insert will be used as if the individual entry + had been inserted. + url: str The batch URL to which these operations should be applied. + converter: Function (optional) The function used to convert the server's + response to an object. + + Returns: + The results of the batch request's execution on the server. If the + default converter is used, this is stored in a ContactsFeed. + """ + return self.Post(batch_feed, url, desired_class=desired_class) + + ExecuteBatch = execute_batch + + def execute_batch_profiles(self, batch_feed, url, + desired_class=gdata.contacts.data.ProfilesFeed): + """Sends a batch request feed to the server. + + Args: + batch_feed: gdata.profiles.ProfilesFeed A feed containing batch + request entries. Each entry contains the operation to be performed + on the data contained in the entry. For example an entry with an + operation type of insert will be used as if the individual entry + had been inserted. + url: string The batch URL to which these operations should be applied. + converter: Function (optional) The function used to convert the server's + response to an object. The default value is + gdata.profiles.ProfilesFeedFromString. + + Returns: + The results of the batch request's execution on the server. If the + default converter is used, this is stored in a ProfilesFeed. + """ + return self.Post(batch_feed, url, desired_class=desired_class) + + ExecuteBatchProfiles = execute_batch_profiles + + def _CleanUri(self, uri): + """Sanitizes a feed URI. + + Args: + uri: The URI to sanitize, can be relative or absolute. + + Returns: + The given URI without its http://server prefix, if any. + Keeps the leading slash of the URI. + """ + url_prefix = 'http://%s' % self.server + if uri.startswith(url_prefix): + uri = uri[len(url_prefix):] + return uri + +class ContactsQuery(gdata.client.Query): + """ + Create a custom Contacts Query + + Full specs can be found at: U{Contacts query parameters reference + } + """ + + def __init__(self, feed=None, group=None, orderby=None, showdeleted=None, + sortorder=None, requirealldeleted=None, **kwargs): + """ + @param max_results: The maximum number of entries to return. If you want + to receive all of the contacts, rather than only the default maximum, you + can specify a very large number for max-results. + @param start-index: The 1-based index of the first result to be retrieved. + @param updated-min: The lower bound on entry update dates. + @param group: Constrains the results to only the contacts belonging to the + group specified. Value of this parameter specifies group ID + @param orderby: Sorting criterion. The only supported value is + lastmodified. + @param showdeleted: Include deleted contacts in the returned contacts feed + @pram sortorder: Sorting order direction. Can be either ascending or + descending. + @param requirealldeleted: Only relevant if showdeleted and updated-min + are also provided. It dictates the behavior of the server in case it + detects that placeholders of some entries deleted since the point in + time specified as updated-min may have been lost. + """ + gdata.client.Query.__init__(self, **kwargs) + self.group = group + self.orderby = orderby + self.sortorder = sortorder + self.showdeleted = showdeleted + + def modify_request(self, http_request): + if self.group: + gdata.client._add_query_param('group', self.group, http_request) + if self.orderby: + gdata.client._add_query_param('orderby', self.orderby, http_request) + if self.sortorder: + gdata.client._add_query_param('sortorder', self.sortorder, http_request) + if self.showdeleted: + gdata.client._add_query_param('showdeleted', self.showdeleted, http_request) + gdata.client.Query.modify_request(self, http_request) + + ModifyRequest = modify_request + + +class ProfilesQuery(gdata.client.Query): + def __init__(self, feed=None): + self.feed = feed or 'http://www.google.com/m8/feeds/profiles/default/full' diff --git a/patches/gdata/contacts/data.py b/patches/gdata/contacts/data.py new file mode 100644 index 0000000..8b04612 --- /dev/null +++ b/patches/gdata/contacts/data.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + +"""Data model classes for parsing and generating XML for the Contacts API.""" + + +__author__ = 'vinces1979@gmail.com (Vince Spicer)' + + +import atom.core +import gdata +import gdata.data + + +PHOTO_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#photo' +PHOTO_EDIT_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#edit-photo' + +EXTERNAL_ID_ORGANIZATION = 'organization' + +RELATION_MANAGER = 'manager' + +CONTACTS_NAMESPACE = 'http://schemas.google.com/contact/2008' +CONTACTS_TEMPLATE = '{%s}%%s' % CONTACTS_NAMESPACE + + +class BillingInformation(atom.core.XmlElement): + """ + gContact:billingInformation + Specifies billing information of the entity represented by the contact. The element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'billingInformation' + + +class Birthday(atom.core.XmlElement): + """ + Stores birthday date of the person represented by the contact. The element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'birthday' + when = 'when' + + +class CalendarLink(atom.core.XmlElement): + """ + Storage for URL of the contact's calendar. The element can be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'calendarLink' + rel = 'rel' + label = 'label' + primary = 'primary' + href = 'href' + + +class DirectoryServer(atom.core.XmlElement): + """ + A directory server associated with this contact. + May not be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'directoryServer' + + +class Event(atom.core.XmlElement): + """ + These elements describe events associated with a contact. + They may be repeated + """ + + _qname = CONTACTS_TEMPLATE % 'event' + label = 'label' + rel = 'rel' + when = gdata.data.When + + +class ExternalId(atom.core.XmlElement): + """ + Describes an ID of the contact in an external system of some kind. + This element may be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'externalId' + label = 'label' + rel = 'rel' + value = 'value' + + +def ExternalIdFromString(xml_string): + return atom.core.parse(ExternalId, xml_string) + + +class Gender(atom.core.XmlElement): + """ + Specifies the gender of the person represented by the contact. + The element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'directoryServer' + value = 'value' + + +class Hobby(atom.core.XmlElement): + """ + Describes an ID of the contact in an external system of some kind. + This element may be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'hobby' + + +class Initials(atom.core.XmlElement): + """ Specifies the initials of the person represented by the contact. The + element cannot be repeated. """ + + _qname = CONTACTS_TEMPLATE % 'initials' + + +class Jot(atom.core.XmlElement): + """ + Storage for arbitrary pieces of information about the contact. Each jot + has a type specified by the rel attribute and a text value. + The element can be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'jot' + rel = 'rel' + + +class Language(atom.core.XmlElement): + """ + Specifies the preferred languages of the contact. + The element can be repeated. + + The language must be specified using one of two mutually exclusive methods: + using the freeform @label attribute, or using the @code attribute, whose value + must conform to the IETF BCP 47 specification. + """ + + _qname = CONTACTS_TEMPLATE % 'language' + code = 'code' + label = 'label' + + +class MaidenName(atom.core.XmlElement): + """ + Specifies maiden name of the person represented by the contact. + The element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'maidenName' + + +class Mileage(atom.core.XmlElement): + """ + Specifies the mileage for the entity represented by the contact. + Can be used for example to document distance needed for reimbursement + purposes. The value is not interpreted. The element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'mileage' + + +class NickName(atom.core.XmlElement): + """ + Specifies the nickname of the person represented by the contact. + The element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'nickname' + + +class Occupation(atom.core.XmlElement): + """ + Specifies the occupation/profession of the person specified by the contact. + The element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'occupation' + + +class Priority(atom.core.XmlElement): + """ + Classifies importance of the contact into 3 categories: + * Low + * Normal + * High + + The priority element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'priority' + + +class Relation(atom.core.XmlElement): + """ + This element describe another entity (usually a person) that is in a + relation of some kind with the contact. + """ + + _qname = CONTACTS_TEMPLATE % 'relation' + rel = 'rel' + label = 'label' + + +class Sensitivity(atom.core.XmlElement): + """ + Classifies sensitivity of the contact into the following categories: + * Confidential + * Normal + * Personal + * Private + + The sensitivity element cannot be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'sensitivity' + rel = 'rel' + + +class UserDefinedField(atom.core.XmlElement): + """ + Represents an arbitrary key-value pair attached to the contact. + """ + + _qname = CONTACTS_TEMPLATE % 'userDefinedField' + key = 'key' + value = 'value' + + +def UserDefinedFieldFromString(xml_string): + return atom.core.parse(UserDefinedField, xml_string) + + +class Website(atom.core.XmlElement): + """ + Describes websites associated with the contact, including links. + May be repeated. + """ + + _qname = CONTACTS_TEMPLATE % 'website' + + href = 'href' + label = 'label' + primary = 'primary' + rel = 'rel' + + +def WebsiteFromString(xml_string): + return atom.core.parse(Website, xml_string) + + +class HouseName(atom.core.XmlElement): + """ + Used in places where houses or buildings have names (and + not necessarily numbers), eg. "The Pillars". + """ + + _qname = CONTACTS_TEMPLATE % 'housename' + + +class Street(atom.core.XmlElement): + """ + Can be street, avenue, road, etc. This element also includes the house + number and room/apartment/flat/floor number. + """ + + _qname = CONTACTS_TEMPLATE % 'street' + + +class POBox(atom.core.XmlElement): + """ + Covers actual P.O. boxes, drawers, locked bags, etc. This is usually but not + always mutually exclusive with street + """ + + _qname = CONTACTS_TEMPLATE % 'pobox' + + +class Neighborhood(atom.core.XmlElement): + """ + This is used to disambiguate a street address when a city contains more than + one street with the same name, or to specify a small place whose mail is + routed through a larger postal town. In China it could be a county or a + minor city. + """ + + _qname = CONTACTS_TEMPLATE % 'neighborhood' + + +class City(atom.core.XmlElement): + """ + Can be city, village, town, borough, etc. This is the postal town and not + necessarily the place of residence or place of business. + """ + + _qname = CONTACTS_TEMPLATE % 'city' + + +class SubRegion(atom.core.XmlElement): + """ + Handles administrative districts such as U.S. or U.K. counties that are not + used for mail addressing purposes. Subregion is not intended for + delivery addresses. + """ + + _qname = CONTACTS_TEMPLATE % 'subregion' + + +class Region(atom.core.XmlElement): + """ + A state, province, county (in Ireland), Land (in Germany), + departement (in France), etc. + """ + + _qname = CONTACTS_TEMPLATE % 'region' + + +class PostalCode(atom.core.XmlElement): + """ + Postal code. Usually country-wide, but sometimes specific to the + city (e.g. "2" in "Dublin 2, Ireland" addresses). + """ + + _qname = CONTACTS_TEMPLATE % 'postcode' + + +class Country(atom.core.XmlElement): + """ The name or code of the country. """ + + _qname = CONTACTS_TEMPLATE % 'country' + + +class PersonEntry(gdata.data.BatchEntry): + """Represents a google contact""" + + billing_information = BillingInformation + birthday = Birthday + calendar_link = [CalendarLink] + directory_server = DirectoryServer + event = [Event] + external_id = [ExternalId] + gender = Gender + hobby = [Hobby] + initals = Initials + jot = [Jot] + language= [Language] + maiden_name = MaidenName + mileage = Mileage + nickname = NickName + occupation = Occupation + priority = Priority + relation = [Relation] + sensitivity = Sensitivity + user_defined_field = [UserDefinedField] + website = [Website] + + name = gdata.data.Name + phone_number = [gdata.data.PhoneNumber] + organization = gdata.data.Organization + postal_address = [gdata.data.PostalAddress] + email = [gdata.data.Email] + im = [gdata.data.Im] + structured_postal_address = [gdata.data.StructuredPostalAddress] + extended_property = [gdata.data.ExtendedProperty] + + +class Deleted(atom.core.XmlElement): + """If present, indicates that this contact has been deleted.""" + _qname = gdata.GDATA_TEMPLATE % 'deleted' + + +class GroupMembershipInfo(atom.core.XmlElement): + """ + Identifies the group to which the contact belongs or belonged. + The group is referenced by its id. + """ + + _qname = CONTACTS_TEMPLATE % 'groupMembershipInfo' + + href = 'href' + deleted = 'deleted' + + +class ContactEntry(PersonEntry): + """A Google Contacts flavor of an Atom Entry.""" + + deleted = Deleted + group_membership_info = [GroupMembershipInfo] + organization = gdata.data.Organization + + def GetPhotoLink(self): + for a_link in self.link: + if a_link.rel == PHOTO_LINK_REL: + return a_link + return None + + def GetPhotoEditLink(self): + for a_link in self.link: + if a_link.rel == PHOTO_EDIT_LINK_REL: + return a_link + return None + + +class ContactsFeed(gdata.data.BatchFeed): + """A collection of Contacts.""" + entry = [ContactEntry] + + +class SystemGroup(atom.core.XmlElement): + """The contacts systemGroup element. + + When used within a contact group entry, indicates that the group in + question is one of the predefined system groups.""" + + _qname = CONTACTS_TEMPLATE % 'systemGroup' + id = 'id' + + +class GroupEntry(gdata.data.BatchEntry): + """Represents a contact group.""" + extended_property = [gdata.data.ExtendedProperty] + system_group = SystemGroup + + +class GroupsFeed(gdata.data.BatchFeed): + """A Google contact groups feed flavor of an Atom Feed.""" + entry = [GroupEntry] + + +class ProfileEntry(PersonEntry): + """A Google Profiles flavor of an Atom Entry.""" + + +def ProfileEntryFromString(xml_string): + """Converts an XML string into a ProfileEntry object. + + Args: + xml_string: string The XML describing a Profile entry. + + Returns: + A ProfileEntry object corresponding to the given XML. + """ + return atom.core.parse(ProfileEntry, xml_string) + + +class ProfilesFeed(gdata.data.BatchFeed): + """A Google Profiles feed flavor of an Atom Feed.""" + _qname = atom.data.ATOM_TEMPLATE % 'feed' + entry = [ProfileEntry] + + +def ProfilesFeedFromString(xml_string): + """Converts an XML string into a ProfilesFeed object. + + Args: + xml_string: string The XML describing a Profiles feed. + + Returns: + A ProfilesFeed object corresponding to the given XML. + """ + return atom.core.parse(ProfilesFeed, xml_string) + + diff --git a/patches/gdata/contacts/service.py b/patches/gdata/contacts/service.py new file mode 100644 index 0000000..4b017c0 --- /dev/null +++ b/patches/gdata/contacts/service.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""ContactsService extends the GDataService for Google Contacts operations. + + ContactsService: Provides methods to query feeds and manipulate items. + Extends GDataService. + + DictionaryToParamList: Function which converts a dictionary into a list of + URL arguments (represented as strings). This is a + utility function used in CRUD operations. +""" + +__author__ = 'dbrattli (Dag Brattli)' + + +import gdata +import gdata.calendar +import gdata.service + + +DEFAULT_BATCH_URL = ('http://www.google.com/m8/feeds/contacts/default/full' + '/batch') +DEFAULT_PROFILES_BATCH_URL = ('http://www.google.com' + '/m8/feeds/profiles/default/full/batch') + +GDATA_VER_HEADER = 'GData-Version' + + +class Error(Exception): + pass + + +class RequestError(Error): + pass + + +class ContactsService(gdata.service.GDataService): + """Client for the Google Contacts service.""" + + def __init__(self, email=None, password=None, source=None, + server='www.google.com', additional_headers=None, + contact_list='default', **kwargs): + """Creates a client for the Contacts service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'www.google.com'. + contact_list: string (optional) The name of the default contact list to + use when no URI is specified to the methods of the service. + Default value: 'default' (the logged in user's contact list). + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + + self.contact_list = contact_list + gdata.service.GDataService.__init__( + self, email=email, password=password, service='cp', source=source, + server=server, additional_headers=additional_headers, **kwargs) + + def GetFeedUri(self, kind='contacts', contact_list=None, projection='full', + scheme=None): + """Builds a feed URI. + + Args: + kind: The type of feed to return, typically 'groups' or 'contacts'. + Default value: 'contacts'. + contact_list: The contact list to return a feed for. + Default value: self.contact_list. + projection: The projection to apply to the feed contents, for example + 'full', 'base', 'base/12345', 'full/batch'. Default value: 'full'. + scheme: The URL scheme such as 'http' or 'https', None to return a + relative URI without hostname. + + Returns: + A feed URI using the given kind, contact list, and projection. + Example: '/m8/feeds/contacts/default/full'. + """ + contact_list = contact_list or self.contact_list + if kind == 'profiles': + contact_list = 'domain/%s' % contact_list + prefix = scheme and '%s://%s' % (scheme, self.server) or '' + return '%s/m8/feeds/%s/%s/%s' % (prefix, kind, contact_list, projection) + + def GetContactsFeed(self, uri=None): + uri = uri or self.GetFeedUri() + return self.Get(uri, converter=gdata.contacts.ContactsFeedFromString) + + def GetContact(self, uri): + return self.Get(uri, converter=gdata.contacts.ContactEntryFromString) + + def CreateContact(self, new_contact, insert_uri=None, url_params=None, + escape_params=True): + """Adds an new contact to Google Contacts. + + Args: + new_contact: atom.Entry or subclass A new contact which is to be added to + Google Contacts. + insert_uri: the URL to post new contacts to the feed + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful insert, an entry containing the contact created + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + insert_uri = insert_uri or self.GetFeedUri() + return self.Post(new_contact, insert_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.contacts.ContactEntryFromString) + + def UpdateContact(self, edit_uri, updated_contact, url_params=None, + escape_params=True): + """Updates an existing contact. + + Args: + edit_uri: string The edit link URI for the element being updated + updated_contact: string, atom.Entry or subclass containing + the Atom Entry which will replace the contact which is + stored at the edit_url + url_params: dict (optional) Additional URL parameters to be included + in the update request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful update, a httplib.HTTPResponse containing the server's + response to the PUT request. + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + return self.Put(updated_contact, self._CleanUri(edit_uri), + url_params=url_params, + escape_params=escape_params, + converter=gdata.contacts.ContactEntryFromString) + + def DeleteContact(self, edit_uri, extra_headers=None, + url_params=None, escape_params=True): + """Removes an contact with the specified ID from Google Contacts. + + Args: + edit_uri: string The edit URL of the entry to be deleted. Example: + '/m8/feeds/contacts/default/full/xxx/yyy' + url_params: dict (optional) Additional URL parameters to be included + in the deletion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + On successful delete, a httplib.HTTPResponse containing the server's + response to the DELETE request. + On failure, a RequestError is raised of the form: + {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response} + """ + return self.Delete(self._CleanUri(edit_uri), + url_params=url_params, escape_params=escape_params) + + def GetGroupsFeed(self, uri=None): + uri = uri or self.GetFeedUri('groups') + return self.Get(uri, converter=gdata.contacts.GroupsFeedFromString) + + def CreateGroup(self, new_group, insert_uri=None, url_params=None, + escape_params=True): + insert_uri = insert_uri or self.GetFeedUri('groups') + return self.Post(new_group, insert_uri, url_params=url_params, + escape_params=escape_params, + converter=gdata.contacts.GroupEntryFromString) + + def UpdateGroup(self, edit_uri, updated_group, url_params=None, + escape_params=True): + return self.Put(updated_group, self._CleanUri(edit_uri), + url_params=url_params, + escape_params=escape_params, + converter=gdata.contacts.GroupEntryFromString) + + def DeleteGroup(self, edit_uri, extra_headers=None, + url_params=None, escape_params=True): + return self.Delete(self._CleanUri(edit_uri), + url_params=url_params, escape_params=escape_params) + + def ChangePhoto(self, media, contact_entry_or_url, content_type=None, + content_length=None): + """Change the photo for the contact by uploading a new photo. + + Performs a PUT against the photo edit URL to send the binary data for the + photo. + + Args: + media: filename, file-like-object, or a gdata.MediaSource object to send. + contact_entry_or_url: ContactEntry or str If it is a ContactEntry, this + method will search for an edit photo link URL and + perform a PUT to the URL. + content_type: str (optional) the mime type for the photo data. This is + necessary if media is a file or file name, but if media + is a MediaSource object then the media object can contain + the mime type. If media_type is set, it will override the + mime type in the media object. + content_length: int or str (optional) Specifying the content length is + only required if media is a file-like object. If media + is a filename, the length is determined using + os.path.getsize. If media is a MediaSource object, it is + assumed that it already contains the content length. + """ + if isinstance(contact_entry_or_url, gdata.contacts.ContactEntry): + url = contact_entry_or_url.GetPhotoEditLink().href + else: + url = contact_entry_or_url + if isinstance(media, gdata.MediaSource): + payload = media + # If the media object is a file-like object, then use it as the file + # handle in the in the MediaSource. + elif hasattr(media, 'read'): + payload = gdata.MediaSource(file_handle=media, + content_type=content_type, content_length=content_length) + # Assume that the media object is a file name. + else: + payload = gdata.MediaSource(content_type=content_type, + content_length=content_length, file_path=media) + return self.Put(payload, url) + + def GetPhoto(self, contact_entry_or_url): + """Retrives the binary data for the contact's profile photo as a string. + + Args: + contact_entry_or_url: a gdata.contacts.ContactEntry objecr or a string + containing the photo link's URL. If the contact entry does not + contain a photo link, the image will not be fetched and this method + will return None. + """ + # TODO: add the ability to write out the binary image data to a file, + # reading and writing a chunk at a time to avoid potentially using up + # large amounts of memory. + url = None + if isinstance(contact_entry_or_url, gdata.contacts.ContactEntry): + photo_link = contact_entry_or_url.GetPhotoLink() + if photo_link: + url = photo_link.href + else: + url = contact_entry_or_url + if url: + return self.Get(url, converter=str) + else: + return None + + def DeletePhoto(self, contact_entry_or_url): + url = None + if isinstance(contact_entry_or_url, gdata.contacts.ContactEntry): + url = contact_entry_or_url.GetPhotoEditLink().href + else: + url = contact_entry_or_url + if url: + self.Delete(url) + + def GetProfilesFeed(self, uri=None): + """Retrieves a feed containing all domain's profiles. + + Args: + uri: string (optional) the URL to retrieve the profiles feed, + for example /m8/feeds/profiles/default/full + + Returns: + On success, a ProfilesFeed containing the profiles. + On failure, raises a RequestError. + """ + + uri = uri or self.GetFeedUri('profiles') + return self.Get(uri, + converter=gdata.contacts.ProfilesFeedFromString) + + def GetProfile(self, uri): + """Retrieves a domain's profile for the user. + + Args: + uri: string the URL to retrieve the profiles feed, + for example /m8/feeds/profiles/default/full/username + + Returns: + On success, a ProfileEntry containing the profile for the user. + On failure, raises a RequestError + """ + return self.Get(uri, + converter=gdata.contacts.ProfileEntryFromString) + + def UpdateProfile(self, edit_uri, updated_profile, url_params=None, + escape_params=True): + """Updates an existing profile. + + Args: + edit_uri: string The edit link URI for the element being updated + updated_profile: string atom.Entry or subclass containing + the Atom Entry which will replace the profile which is + stored at the edit_url. + url_params: dict (optional) Additional URL parameters to be included + in the update request. + escape_params: boolean (optional) If true, the url_params will be + escaped before they are included in the request. + + Returns: + On successful update, a httplib.HTTPResponse containing the server's + response to the PUT request. + On failure, raises a RequestError. + """ + return self.Put(updated_profile, self._CleanUri(edit_uri), + url_params=url_params, escape_params=escape_params, + converter=gdata.contacts.ProfileEntryFromString) + + def ExecuteBatch(self, batch_feed, url, + converter=gdata.contacts.ContactsFeedFromString): + """Sends a batch request feed to the server. + + Args: + batch_feed: gdata.contacts.ContactFeed A feed containing batch + request entries. Each entry contains the operation to be performed + on the data contained in the entry. For example an entry with an + operation type of insert will be used as if the individual entry + had been inserted. + url: str The batch URL to which these operations should be applied. + converter: Function (optional) The function used to convert the server's + response to an object. The default value is ContactsFeedFromString. + + Returns: + The results of the batch request's execution on the server. If the + default converter is used, this is stored in a ContactsFeed. + """ + return self.Post(batch_feed, url, converter=converter) + + def ExecuteBatchProfiles(self, batch_feed, url, + converter=gdata.contacts.ProfilesFeedFromString): + """Sends a batch request feed to the server. + + Args: + batch_feed: gdata.profiles.ProfilesFeed A feed containing batch + request entries. Each entry contains the operation to be performed + on the data contained in the entry. For example an entry with an + operation type of insert will be used as if the individual entry + had been inserted. + url: string The batch URL to which these operations should be applied. + converter: Function (optional) The function used to convert the server's + response to an object. The default value is + gdata.profiles.ProfilesFeedFromString. + + Returns: + The results of the batch request's execution on the server. If the + default converter is used, this is stored in a ProfilesFeed. + """ + return self.Post(batch_feed, url, converter=converter) + + def _CleanUri(self, uri): + """Sanitizes a feed URI. + + Args: + uri: The URI to sanitize, can be relative or absolute. + + Returns: + The given URI without its http://server prefix, if any. + Keeps the leading slash of the URI. + """ + url_prefix = 'http://%s' % self.server + if uri.startswith(url_prefix): + uri = uri[len(url_prefix):] + return uri + +class ContactsQuery(gdata.service.Query): + + def __init__(self, feed=None, text_query=None, params=None, + categories=None, group=None): + self.feed = feed or '/m8/feeds/contacts/default/full' + if group: + self._SetGroup(group) + gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query, + params=params, categories=categories) + + def _GetGroup(self): + if 'group' in self: + return self['group'] + else: + return None + + def _SetGroup(self, group_id): + self['group'] = group_id + + group = property(_GetGroup, _SetGroup, + doc='The group query parameter to find only contacts in this group') + +class GroupsQuery(gdata.service.Query): + + def __init__(self, feed=None, text_query=None, params=None, + categories=None): + self.feed = feed or '/m8/feeds/groups/default/full' + gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query, + params=params, categories=categories) + + +class ProfilesQuery(gdata.service.Query): + """Constructs a query object for the profiles feed.""" + + def __init__(self, feed=None, text_query=None, params=None, + categories=None): + self.feed = feed or '/m8/feeds/profiles/default/full' + gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query, + params=params, categories=categories) diff --git a/patches/gdata/contentforshopping/__init__.py b/patches/gdata/contentforshopping/__init__.py new file mode 100644 index 0000000..5a5ed8e --- /dev/null +++ b/patches/gdata/contentforshopping/__init__.py @@ -0,0 +1,21 @@ +#!/usr/bin/python +# +# Copyright (C) 2010-2011 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. + + +"""Support for the Content API for Shopping + +See: http://code.google.com/apis/shopping/content/index.html +""" diff --git a/patches/gdata/contentforshopping/client.py b/patches/gdata/contentforshopping/client.py new file mode 100644 index 0000000..815201e --- /dev/null +++ b/patches/gdata/contentforshopping/client.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# +# Copyright (C) 2010-2011 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. + + +"""Extend the gdata client for the Content API for Shopping. + +TODO: + +1. Proper MCA Support. +2. Better datafeed Support. +""" + + +__author__ = 'afshar (Ali Afshar)' + + +import gdata.client +import atom.data + +from gdata.contentforshopping.data import (ProductEntry, ProductFeed, + DatafeedFeed, ClientAccountFeed, ClientAccount) + + +CFS_VERSION = 'v1' +CFS_HOST = 'content.googleapis.com' +CFS_URI = 'https://%s/content' % CFS_HOST +CFS_PROJECTION = 'generic' + + +class ContentForShoppingClient(gdata.client.GDClient): + """Client for Content for Shopping API. + + :param account_id: Merchant account ID. This value will be used by default + for all requests, but may be overridden on a + request-by-request basis. + :param api_version: The version of the API to target. Default value: 'v1'. + :param **kwargs: Pass all addtional keywords to the GDClient constructor. + """ + + api_version = '1.0' + + def __init__(self, account_id=None, api_version=CFS_VERSION, **kwargs): + self.cfs_account_id = account_id + self.cfs_api_version = api_version + gdata.client.GDClient.__init__(self, **kwargs) + + def _create_uri(self, account_id, resource, path=(), use_projection=True): + """Create a request uri from the given arguments. + + If arguments are None, use the default client attributes. + """ + account_id = account_id or self.cfs_account_id + if account_id is None: + raise ValueError('No Account ID set. ' + 'Either set for the client, or per request') + segments = [CFS_URI, self.cfs_api_version, account_id, resource] + if use_projection: + segments.append(CFS_PROJECTION) + segments.extend(path) + return '/'.join(segments) + + def _create_product_id(self, id, country, language): + return 'online:%s:%s:%s' % (language, country, id) + + def _create_batch_feed(self, entries, operation, feed=None): + if feed is None: + feed = ProductFeed() + for entry in entries: + entry.batch_operation = gdata.data.BatchOperation(type=operation) + feed.entry.append(entry) + return feed + + def get_products(self, start_index=None, max_results=None, account_id=None, + auth_token=None): + """Get a feed of products for the account. + + :param max_results: The maximum number of results to return (default 25, + maximum 250). + :param start_index: The starting index of the feed to return (default 1, + maximum 10000) + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + uri = self._create_uri(account_id, 'items/products') + return self.get_feed(uri, auth_token=auth_token, + desired_class=gdata.contentforshopping.data.ProductFeed) + + def get_product(self, id, country, language, account_id=None, + auth_token=None): + """Get a product by id, country and language. + + :param id: The product ID + :param country: The country (target_country) + :param language: The language (content_language) + """ + pid = self._create_product_id(id, country, language) + uri = self._create_uri(account_id, 'items/products', [pid]) + return self.get_entry(uri, desired_class=ProductEntry, + auth_token=auth_token) + + def insert_product(self, product, account_id=None, auth_token=None): + """Create a new product, by posting the product entry feed. + + :param product: A :class:`gdata.contentforshopping.data.ProductEntry` with + the required product data. + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + uri = self._create_uri(account_id, 'items/products') + return self.post(product, uri=uri, auth_token=auth_token) + + def insert_products(self, products, account_id=None, auth_token=None): + """Insert the products using a batch request + + :param products: A list of product entries + """ + feed = self._create_batch_feed(products, 'insert') + return self.batch(feed) + + def delete_products(self, products, account_id=None, auth_token=None): + """Delete the products using a batch request. + + :param products: A list of product entries + + .. note:: Entries must have the atom:id element set. + """ + feed = self._create_batch_feed(products, 'delete') + return self.batch(feed) + + def update_products(self, products, account_id=None, auth_token=None): + """Update the products using a batch request + + :param products: A list of product entries + + .. note:: Entries must have the atom:id element set. + """ + feed = self._create_batch_feed(products, 'update') + return self.batch(feed) + + def batch(self, feed, account_id=None, auth_token=None): + """Send a batch request. + + :param feed: The feed of batch entries to send. + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + uri = self._create_uri(account_id, 'items/products', ['batch']) + return self.post(feed, uri=uri, auth_token=auth_token, + desired_class=ProductFeed) + + def update_product(self, product, account_id=None, + auth_token=None): + """Update a product, by putting the product entry feed. + + :param product: A :class:`gdata.contentforshopping.data.ProductEntry` with + the required product data. + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + pid = self._create_product_id(product.id.text, product.target_country.text, + product.content_language.text) + uri = self._create_uri(account_id, 'items/products', [pid]) + return self.update(product, uri=uri, auth_token=auth_token) + + def get_datafeeds(self, account_id=None): + """Get the feed of datafeeds. + """ + uri = self._create_uri(account_id, 'datafeeds/products', + use_projection=False) + return self.get_feed(uri, desired_class=DatafeedFeed) + + def insert_datafeed(self, entry, account_id=None, auth_token=None): + """Insert a datafeed. + """ + uri = self._create_uri(account_id, 'datafeeds/products', + use_projection=False) + return self.post(entry, uri=uri, auth_token=auth_token) + + def get_client_accounts(self, account_id=None, auth_token=None): + """Get the feed of managed accounts + + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + uri = self._create_uri(account_id, 'managedaccounts/products', + use_projection=False) + return self.get_feed(uri, desired_class=ClientAccountFeed, + auth_token=auth_token) + + def insert_client_account(self, entry, account_id=None, auth_token=None): + """Insert a client account entry + + :param entry: An entry of type ClientAccount + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + uri = self._create_uri(account_id, 'managedaccounts/products', + use_projection=False) + return self.post(entry, uri=uri, auth_token=auth_token) + + def update_client_account(self, entry, client_account_id, account_id=None, auth_token=None): + """Update a client account + + :param entry: An entry of type ClientAccount to update to + :param client_account_id: The client account ID + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + uri = self._create_uri(account_id, 'managedaccounts/products', + [client_account_id], use_projection=False) + return self.update(entry, uri=uri, auth_token=auth_token) + + def delete_client_account(self, client_account_id, account_id=None, + auth_token=None): + """Delete a client account + + :param client_account_id: The client account ID + :param account_id: The Merchant Center Account ID. If ommitted the default + Account ID will be used for this client + """ + + uri = self._create_uri(account_id, 'managedaccounts/products', + [client_account_id], use_projection=False) + return self.delete(uri, auth_token=auth_token) diff --git a/patches/gdata/contentforshopping/data.py b/patches/gdata/contentforshopping/data.py new file mode 100644 index 0000000..2f09a42 --- /dev/null +++ b/patches/gdata/contentforshopping/data.py @@ -0,0 +1,1175 @@ +#!/usr/bin/python +# +# Copyright (C) 2010-2011 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. + + +"""GData definitions for Content API for Shopping""" + + +__author__ = 'afshar (Ali Afshar)' + + +import atom.core +import atom.data +import gdata.data + + +SC_NAMESPACE_TEMPLATE = ('{http://schemas.google.com/' + 'structuredcontent/2009}%s') +SCP_NAMESPACE_TEMPLATE = ('{http://schemas.google.com/' + 'structuredcontent/2009/products}%s') + + +class ProductId(atom.core.XmlElement): + """sc:id element + + It is required that all inserted products are provided with a unique + alphanumeric ID, in this element. + """ + _qname = SC_NAMESPACE_TEMPLATE % 'id' + + +class RequiredDestination(atom.core.XmlElement): + """sc:required_destination element + + This element defines the required destination for a product, namely + "ProductSearch", "ProductAds" or "CommerceSearch". It should be added to the + app:control element (ProductEntry's "control" attribute) to specify where the + product should appear in search APIs. + + By default, when omitted, the api attempts to upload to as many destinations + as possible. + """ + _qname = SC_NAMESPACE_TEMPLATE % 'required_destination' + dest = 'dest' + + +class ExcludedDestination(atom.core.XmlElement): + """sc:excluded_destination element + + This element defines the required destination for a product, namely + "ProductSearch", "ProductAds" or "CommerceSearch". It should be added to the + app:control element (ProductEntry's "control" attribute) to specify where the + product should not appear in search APIs. + + By default, when omitted, the api attempts to upload to as many destinations + as possible. + """ + _qname = SC_NAMESPACE_TEMPLATE % 'excluded_destination' + dest = 'dest' + + +class ProductControl(atom.data.Control): + """ + app:control element + + overridden to provide additional elements in the sc namespace. + """ + required_destination = RequiredDestination + excluded_destination = ExcludedDestination + + +class ContentLanguage(atom.core.XmlElement): + """ + sc:content_language element + + Language used in the item content for the product + """ + _qname = SC_NAMESPACE_TEMPLATE % 'content_language' + + +class TargetCountry(atom.core.XmlElement): + """ + sc:target_country element + + The target country of the product + """ + _qname = SC_NAMESPACE_TEMPLATE % 'target_country' + + +class ImageLink(atom.core.XmlElement): + """sc:image_link element + + This is the URL of an associated image for a product. Please use full size + images (400x400 pixels or larger), not thumbnails. + """ + _qname = SC_NAMESPACE_TEMPLATE % 'image_link' + + +class ExpirationDate(atom.core.XmlElement): + """sc:expiration_date + + This is the date when the product listing will expire. If omitted, this will + default to 30 days after the product was created. + """ + _qname = SC_NAMESPACE_TEMPLATE % 'expiration_date' + + +class Adult(atom.core.XmlElement): + """sc:adult element + + Indicates whether the content is targeted towards adults, with possible + values of "true" or "false". Defaults to "false". + """ + _qname = SC_NAMESPACE_TEMPLATE % 'adult' + + +class Author(atom.core.XmlElement): + """ + scp:author element + + Defines the author of the information, recommended for books. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'author' + + +class Availability(atom.core.XmlElement): + """ + scp:availability element + + The retailer's suggested label for product availability. Supported values + include: 'in stock', 'out of stock', 'limited availability'. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'availability' + + +class Brand(atom.core.XmlElement): + """ + scp:brand element + + The brand of the product + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'brand' + + +class Color(atom.core.XmlElement): + """scp:color element + + The color of the product. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'color' + + +class Condition(atom.core.XmlElement): + """scp:condition element + + The condition of the product, one of "new", "used", "refurbished" + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'condition' + + +class Edition(atom.core.XmlElement): + """scp:edition element + + The edition of the product. Recommended for products with multiple editions + such as collectors' editions etc, such as books. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'edition' + + +class Feature(atom.core.XmlElement): + """scp:feature element + + A product feature. A product may have multiple features, each being text, for + example a smartphone may have features: "wifi", "gps" etc. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'feature' + + +class FeaturedProduct(atom.core.XmlElement): + """scp:featured_product element + + Used to indicate that this item is a special, featured product; Supported + values are: "true", "false". + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'featured_product' + + +class Genre(atom.core.XmlElement): + """scp:genre element + + Describes the genre of a product, eg "comedy". Strongly recommended for media. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'genre' + + +class Gtin(atom.core.XmlElement): + """scp:gtin element + + GTIN of the product (isbn/upc/ean) + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'gtin' + + +class Manufacturer(atom.core.XmlElement): + """scp:manufacturer element + + Manufacturer of the product. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'manufacturer' + + +class Mpn(atom.core.XmlElement): + """scp:mpn element + + Manufacturer's Part Number. A unique code determined by the manufacturer for + the product. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'mpn' + + +class Price(atom.core.XmlElement): + """scp:price element + + The price of the product. The unit attribute must be set, and should represent + the currency. + + Note: Required Element + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'price' + unit = 'unit' + + +class ProductType(atom.core.XmlElement): + """scp:product_type element + + Describes the type of product. A taxonomy of available product types is + listed at http://www.google.com/basepages/producttype/taxonomy.txt and the + entire line in the taxonomy should be included, for example "Electronics > + Video > Projectors". + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'product_type' + + +class Quantity(atom.core.XmlElement): + """scp:quantity element + + The number of items available. A value of 0 indicates items that are + currently out of stock. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'quantity' + + +class ShippingCountry(atom.core.XmlElement): + """scp:shipping_country element + + The two-letter ISO 3166 country code for the country to which an item will + ship. + + This element should be placed inside the scp:shipping element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'shipping_country' + + +class ShippingPrice(atom.core.XmlElement): + """scp:shipping_price element + + Fixed shipping price, represented as a number. Specify the currency as the + "unit" attribute". + + This element should be placed inside the scp:shipping element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'shipping_price' + unit = 'unit' + + +class ShippingRegion(atom.core.XmlElement): + """scp:shipping_region element + + The geographic region to which a shipping rate applies, e.g., in the US, the + two-letter state abbreviation, ZIP code, or ZIP code range using * wildcard. + + This element should be placed inside the scp:shipping element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'shipping_region' + + +class ShippingService(atom.core.XmlElement): + """scp:shipping_service element + + A free-form description of the service class or delivery speed. + + This element should be placed inside the scp:shipping element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'shipping_service' + + +class Shipping(atom.core.XmlElement): + """scp:shipping element + + Container for the shipping rules as provided by the shipping_country, + shipping_price, shipping_region and shipping_service tags. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'shipping' + shipping_price = ShippingPrice + shipping_country = ShippingCountry + shipping_service = ShippingService + shipping_region = ShippingRegion + + +class ShippingWeight(atom.core.XmlElement): + """scp:shipping_weight element + + The shipping weight of a product. Requires a value and a unit using the unit + attribute. Valid units include lb, pound, oz, ounce, g, gram, kg, kilogram. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'shipping_weight' + unit = 'unit' + + +class Size(atom.core.XmlElement): + """scp:size element + + Available sizes of an item. Appropriate values include: "small", "medium", + "large", etc. The product enttry may contain multiple sizes, to indicate the + available sizes. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'size' + + +class TaxRate(atom.core.XmlElement): + """scp:tax_rate element + + The tax rate as a percent of the item price, i.e., number, as a percentage. + + This element should be placed inside the scp:tax (Tax class) element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'tax_rate' + + +class TaxCountry(atom.core.XmlElement): + """scp:tax_country element + + The country an item is taxed in (as a two-letter ISO 3166 country code). + + This element should be placed inside the scp:tax (Tax class) element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'tax_country' + + +class TaxRegion(atom.core.XmlElement): + """scp:tax_region element + + The geographic region that a tax rate applies to, e.g., in the US, the + two-letter state abbreviation, ZIP code, or ZIP code range using * wildcard. + + This element should be placed inside the scp:tax (Tax class) element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'tax_region' + + +class TaxShip(atom.core.XmlElement): + """scp:tax_ship element + + Whether tax is charged on shipping for this product. The default value is + "false". + + This element should be placed inside the scp:tax (Tax class) element. + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'tax_ship' + + +class Tax(atom.core.XmlElement): + """scp:tax element + + Container for the tax rules for this product. Containing the tax_rate, + tax_country, tax_region, and tax_ship elements + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'tax' + tax_rate = TaxRate + tax_country = TaxCountry + tax_region = TaxRegion + tax_ship = TaxShip + + +class Year(atom.core.XmlElement): + """scp:year element + + The year the product was produced. Expects four digits + """ + _qname = SCP_NAMESPACE_TEMPLATE % 'year' + + +class ProductEntry(gdata.data.BatchEntry): + """Product entry containing product information + + The elements of this entry that are used are made up of five different + namespaces. They are: + + atom: - Atom + app: - Atom Publishing Protocol + gd: - Google Data API + sc: - Content API for Shopping, general attributes + scp: - Content API for Shopping, product attributes + + Only the sc and scp namespace elements are defined here, but additional useful + elements are defined in superclasses, and are documented here because they are + part of the required Content for Shopping API. + + .. attribute:: title + + The title of the product. + + This should be a :class:`atom.data.Title` element, for example:: + + entry = ProductEntry() + entry.title = atom.data.Title(u'32GB MP3 Player') + + .. attribute:: author + + The author of the product. + + This should be a :class:`Author` element, for example:: + + entry = ProductEntry() + entry.author = atom.data.Author(u'Isaac Asimov') + + .. attribute:: availability + + The avilability of a product. + + This should be an :class:`Availability` instance, for example:: + + entry = ProductEntry() + entry.availability = Availability('in stock') + + .. attribute:: brand + + The brand of a product. + + This should be a :class:`Brand` element, for example:: + + entry = ProductEntry() + entry.brand = Brand(u'Sony') + + .. attribute:: color + + The color of a product. + + This should be a :class:`Color` element, for example:: + + entry = ProductEntry() + entry.color = Color(u'purple') + + .. attribute:: condition + + The condition of a product. + + This should be a :class:`Condition` element, for example:: + + entry = ProductEntry() + entry.condition = Condition(u'new') + + .. attribute:: content_language + + The language for the product. + + This should be a :class:`ContentLanguage` element, for example:: + + entry = ProductEntry() + entry.content_language = ContentLanguage('EN') + + .. attribute:: edition + + The edition of the product. + + This should be a :class:`Edition` element, for example:: + + entry = ProductEntry() + entry.edition = Edition('1') + + .. attribute:: expiration + + The expiration date of this product listing. + + This should be a :class:`ExpirationDate` element, for example:: + + entry = ProductEntry() + entry.expiration_date = ExpirationDate('2011-22-03') + + .. attribute:: feature + + A list of features for this product. + + Each feature should be a :class:`Feature` element, for example:: + + entry = ProductEntry() + entry.feature.append(Feature(u'wifi')) + entry.feature.append(Feature(u'gps')) + + .. attribute:: featured_product + + Whether the product is featured. + + This should be a :class:`FeaturedProduct` element, for example:: + + entry = ProductEntry() + entry.featured_product = FeaturedProduct('true') + + .. attribute:: genre + + The genre of the product. + + This should be a :class:`Genre` element, for example:: + + entry = ProductEntry() + entry.genre = Genre(u'comedy') + + .. attribute:: image_link + + A list of links to images of the product. Each link should be an + :class:`ImageLink` element, for example:: + + entry = ProductEntry() + entry.image_link.append(ImageLink('http://myshop/cdplayer.jpg')) + + .. attribute:: manufacturer + + The manufacturer of the product. + + This should be a :class:`Manufacturer` element, for example:: + + entry = ProductEntry() + entry.manufacturer = Manufacturer('Sony') + + .. attribute:: mpn + + The manufacturer's part number for this product. + + This should be a :class:`Mpn` element, for example:: + + entry = ProductEntry() + entry.mpn = Mpn('cd700199US') + + .. attribute:: price + + The price for this product. + + This should be a :class:`Price` element, including a unit argument to + indicate the currency, for example:: + + entry = ProductEntry() + entry.price = Price('20.00', unit='USD') + + .. attribute:: gtin + + The gtin for this product. + + This should be a :class:`Gtin` element, for example:: + + entry = ProductEntry() + entry.gtin = Gtin('A888998877997') + + .. attribute:: product_type + + The type of product. + + This should be a :class:`ProductType` element, for example:: + + entry = ProductEntry() + entry.product_type = ProductType("Electronics > Video > Projectors") + + .. attribute:: publisher + + The publisher of this product. + + This should be a :class:`Publisher` element, for example:: + + entry = ProductEntry() + entry.publisher = Publisher(u'Oxford University Press') + + .. attribute:: quantity + + The quantity of product available in stock. + + This should be a :class:`Quantity` element, for example:: + + entry = ProductEntry() + entry.quantity = Quantity('100') + + .. attribute:: shipping + + The shipping rules for the product. + + This should be a :class:`Shipping` with the necessary rules embedded as + elements, for example:: + + entry = ProductEntry() + entry.shipping = Shipping() + entry.shipping.shipping_price = ShippingPrice('10.00', unit='USD') + + .. attribute:: shipping_weight + + The shipping weight for this product. + + This should be a :class:`ShippingWeight` element, including a unit parameter + for the unit of weight, for example:: + + entry = ProductEntry() + entry.shipping_weight = ShippingWeight('10.45', unit='kg') + + .. attribute:: size + + A list of the available sizes for this product. + + Each item in this list should be a :class:`Size` element, for example:: + + entry = ProductEntry() + entry.size.append(Size('Small')) + entry.size.append(Size('Medium')) + entry.size.append(Size('Large')) + + .. attribute:: target_country + + The target country for the product. + + This should be a :class:`TargetCountry` element, for example:: + + entry = ProductEntry() + entry.target_country = TargetCountry('US') + + .. attribute:: tax + + The tax rules for this product. + + This should be a :class:`Tax` element, with the tax rule elements embedded + within, for example:: + + entry = ProductEntry() + entry.tax = Tax() + entry.tax.tax_rate = TaxRate('17.5') + + .. attribute:: year + + The year the product was created. + + This should be a :class:`Year` element, for example:: + + entry = ProductEntry() + entry.year = Year('2001') + + + #TODO Document these atom elements which are part of the required API + + <link> + <entry> + <id> + <category> + <content> + <author> + <created> + <updated> +""" + + author = Author + product_id = ProductId + availability = Availability + brand = Brand + color = Color + condition = Condition + content_language = ContentLanguage + edition = Edition + expiration_date = ExpirationDate + feature = [Feature] + featured_product = FeaturedProduct + genre = Genre + image_link = [ImageLink] + manufacturer = Manufacturer + mpn = Mpn + price = Price + gtin = Gtin + product_type = ProductType + quantity = Quantity + shipping = Shipping + shipping_weight = ShippingWeight + size = [Size] + target_country = TargetCountry + tax = Tax + year = Year + control = ProductControl + + +# opensearch needs overriding for wrong version +# see http://code.google.com/p/gdata-python-client/issues/detail?id=483 +class TotalResults(gdata.data.TotalResults): + + _qname = gdata.data.TotalResults._qname[1] + + +class ItemsPerPage(gdata.data.ItemsPerPage): + + _qname = gdata.data.ItemsPerPage._qname[1] + + +class StartIndex(gdata.data.StartIndex): + + _qname = gdata.data.StartIndex._qname[1] + + +class ProductFeed(gdata.data.BatchFeed): + """Represents a feed of a merchant's products.""" + entry = [ProductEntry] + total_results = TotalResults + items_per_page = ItemsPerPage + start_index = StartIndex + + +def build_entry(product_id=None, title=None, content=None, link=None, condition=None, + target_country=None, content_language=None, price=None, + price_unit=None, tax_rate=None, shipping_price=None, + shipping_price_unit=None, image_links=(), expiration_date=None, + adult=None, author=None, brand=None, color=None, edition=None, + features=(), featured_product=None, genre=None, + manufacturer=None, mpn=None, gtin=None, product_type=None, + quantity=None, shipping_country=None, shipping_region=None, + shipping_service=None, shipping_weight=None, + shipping_weight_unit=None, sizes=(), tax_country=None, + tax_region=None, tax_ship=None, year=None, product=None): + """Create a new product with the required attributes. + + This function exists as an alternative constructor to help alleviate the + boilerplate involved in creating product definitions. You may well want to + fine-tune your products after creating them. + + Documentation of each attribute attempts to explain the "long-hand" way of + achieving the same goal. + + :param product_id: The unique ID for this product. + + This is equivalent to creating and setting an product_id element:: + + entry = ProductEntry() + entry.product_id = ProductId(product_id) + + :param title: The title of this product. + + This is equivalent to creating and setting a title element:: + + entry = ProductEntry + entry.title = atom.data.Title(title) + + :param content: The description of this product. + + This is equivalent to creating and setting the content element:: + + entry.content = atom.data.Content(content) + + :param link: The uri of the link to a page describing the product. + + This is equivalent to creating and setting the link element:: + + entry.link = atom.data.Link(href=link, rel='alternate', + type='text/html') + + :param condition: The condition of the product. + + This is equivalent to creating and setting the condition element:: + + entry.condition = Condition(condition) + + :param target_country: The target country of the product + + This is equivalent to creating and setting the target_country element:: + + entry.target_country = TargetCountry(target_country) + + :param content_language: The language of the content + + This is equivalent to creating and setting the content_language element:: + + entry.content_language = ContentLanguage(content_language) + + :param price: The price of the product + + This is equivalent to creating and setting the price element, using the + price_unit parameter as the unit:: + + entry.price = Price(price, unit=price_unit) + + :param price_unit: The price unit of the product + + See price parameter. + + :param tax_rate: The tax rate for this product + + This is equivalent to creating and setting the tax element and its required + children:: + + entry.tax = Tax() + entry.tax.tax_rate = TaxRate(tax_rate) + + :param shipping_price: Thie price of shipping for this product + + This is equivalent to creating and setting the shipping element and its + required children. The unit for the price is taken from the + shipping_price_unit parameter:: + + entry.shipping = Shipping() + entry.shipping.shipping_price = ShippingPrice(shipping_price, + unit=shipping_price_unit) + + :param shipping_price_unit: The unit of the shipping price + + See shipping_price + + :param image_links: A sequence of links for images for this product. + + This is equivalent to creating a single image_link element for each image:: + + for image_link in image_links: + entry.image_link.append(ImageLink(image_link)) + + :param expiration_date: The date that this product listing expires + + This is equivalent to creating and setting an expiration_date element:: + + entry.expiration_date = ExpirationDate(expiration_date) + + :param adult: Whether this product listing contains adult content + + This is equivalent to creating and setting the adult element:: + + entry.adult = Adult(adult) + + :param author: The author of the product + + This is equivalent to creating and setting the author element:: + + entry.author = Author(author) + + :param brand: The brand of the product + + This is equivalent to creating and setting the brand element:: + + entry.brand = Brand(brand) + + :param color: The color of the product + + This is equivalent to creating and setting the color element:: + + entry.color = Color(color) + + :param edition: The edition of the product + + This is equivalent to creating and setting the edition element:: + + entry.edition = Edition('1') + + :param features=(): Features for this product + + Each feature in the provided sequence will create a Feature element in the + entry, equivalent to:: + + for feature in features: + entry.feature.append(Feature(feature))) + + :param featured_product: Whether this product is featured + + This is equivalent to creating and setting the featured_product element:: + + entry.featured_product = FeaturedProduct(featured_product) + + :param genre: The genre of the product + + This is equivalent to creating and setting the genre element:: + + entry.genre = Genre(genre) + + :param manufacturer: The manufacturer of the product + + This is equivalent to creating and setting the manufacturer element:: + + entry.manufacturer = Manufacturer(manufacturer) + + :param mpn: The manufacturer's part number for a product + + This is equivalent to creating and setting the mpn element:: + + entry.mpn = Mpn(mpn) + + :param gtin: The gtin for a product + + This is equivalent to creating and setting the gtin element:: + + entry.gtin = Gtin(gtin) + + :param product_type: The type of a product + + This is equivalent to creating and setting the product_type element:: + + entry.product_type = ProductType(product_type) + + :param quantity: The quantity of the product in stock + + This is equivalent to creating and setting the quantity element:: + + entry.quantity = Quantity(quantity) + + :param shipping_country: The country that this product can be shipped to + + This is equivalent to creating a Shipping element, and creating and setting + the required element within:: + + entry.shipping = Shipping() + entry.shipping.shipping_country = ShippingCountry(shipping_country) + + :param shipping_region: The region that this product can be shipped to + + This is equivalent to creating a Shipping element, and creating and setting + the required element within:: + + entry.shipping = Shipping() + entry.shipping.shipping_region = ShippingRegion(shipping_region) + + :param shipping_service: The service for shipping. + + This is equivalent to creating a Shipping element, and creating and setting + the required element within:: + + entry.shipping = Shipping() + entry.shipping.shipping_service = ShippingRegion(shipping_service) + + :param shipping_weight: The shipping weight of a product + + Along with the shipping_weight_unit, this is equivalent to creating and + setting the shipping_weight element:: + + entry.shipping_weight = ShippingWeight(shipping_weight, + unit=shipping_weight_unit) + + :param shipping_weight_unit: The unit of shipping weight + + See shipping_weight. + + :param: The sizes that are available for this product. + + Each size of a list will add a size element to the entry, like so:: + + for size in sizes: + product.size.append(Size(size)) + + :param tax_country: The country that tax rules will apply + + This is equivalent to creating a Tax element, and creating and setting the + required sub-element:: + + entry.tax = Tax() + entry.tax.tax_country = TaxCountry(tax_country) + + :param tax_region: The region that the tax rule applies in + + This is equivalent to creating a Tax element, and creating and setting the + required sub-element:: + + entry.tax = Tax() + entry.tax.tax_region = TaxRegion(tax_region) + + :param tax_ship: Whether shipping cost is taxable + + This is equivalent to creating a Tax element, and creating and setting the + required sub-element:: + + entry.tax = Tax() + entry.tax.tax_ship = TaxShip(tax_ship) + + :param year: The year the product was created + + This is equivalent to creating and setting a year element:: + + entry.year = Year('2001') + """ + + product = product or ProductEntry() + if product_id is not None: + product.product_id = ProductId(product_id) + if content is not None: + product.content = atom.data.Content(content) + if title is not None: + product.title = atom.data.Title(title) + if condition is not None: + product.condition = Condition(condition) + if price is not None: + product.price = Price(price, unit=price_unit) + if content_language is not None: + product.content_language = ContentLanguage(content_language) + if target_country is not None: + product.target_country = TargetCountry(target_country) + if tax_rate is not None: + product.tax = Tax() + product.tax.tax_rate = TaxRate(tax_rate) + if shipping_price is not None: + if shipping_price_unit is None: + raise ValueError('Must provide shipping_price_unit if ' + 'shipping_price is provided') + product.shipping = Shipping() + product.shipping.shipping_price = ShippingPrice(shipping_price, + unit=shipping_price_unit) + if link is not None: + product.link.append(atom.data.Link(href=link, type='text/html', + rel='alternate')) + for image_link in image_links: + product.image_link.append(ImageLink(image_link)) + if expiration_date is not None: + product.expiration_date = ExpirationDate(expiration_date) + if adult is not None: + product.adult = Adult(adult) + if author is not None: + product.author = Author(author) + if brand is not None: + product.brand = Brand(brand) + if color is not None: + product.color = Color(color) + if edition is not None: + product.edition = Edition(edition) + for feature in features: + product.feature.append(Feature(feature)) + if featured_product is not None: + product.featured_product = FeaturedProduct(featured_product) + if genre is not None: + product.genre = Genre(genre) + if manufacturer is not None: + product.manufacturer = Manufacturer(manufacturer) + if mpn is not None: + product.mpn = Mpn(mpn) + if gtin is not None: + product.gtin = Gtin(gtin) + if product_type is not None: + product.product_type = ProductType(product_type) + if quantity is not None: + product.quantity = Quantity(quantity) + if shipping_country is not None: + product.shipping.shipping_country = ShippingCountry( + shipping_country) + if shipping_region is not None: + product.shipping.shipping_region = ShippingRegion(shipping_region) + if shipping_service is not None: + product.shipping.shipping_service = ShippingService( + shipping_service) + if shipping_weight is not None: + product.shipping_weight = ShippingWeight(shipping_weight) + if shipping_weight_unit is not None: + product.shipping_weight.unit = shipping_weight_unit + for size in sizes: + product.size.append(Size(size)) + if tax_country is not None: + product.tax.tax_country = TaxCountry(tax_country) + if tax_region is not None: + product.tax.tax_region = TaxRegion(tax_region) + if tax_ship is not None: + product.tax.tax_ship = TaxShip(tax_ship) + if year is not None: + product.year = Year(year) + return product + + +class Edited(atom.core.XmlElement): + """sc:edited element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'edited' + + +class AttributeLanguage(atom.core.XmlElement): + """sc:attribute_language element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'attribute_language' + + +class Channel(atom.core.XmlElement): + """sc:channel element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'channel' + + +class FeedFileName(atom.core.XmlElement): + """sc:feed_file_name element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'feed_file_name' + + +class FeedType(atom.core.XmlElement): + """sc:feed_type element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'feed_type' + + +class UseQuotedFields(atom.core.XmlElement): + """sc:use_quoted_fields element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'use_quoted_fields' + + +class FileFormat(atom.core.XmlElement): + """sc:file_format element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'file_format' + use_quoted_fields = UseQuotedFields + format = 'format' + + +class ProcessingStatus(atom.core.XmlElement): + """sc:processing_status element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'processing_status' + + +class DatafeedEntry(gdata.data.GDEntry): + """An entry for a Datafeed + """ + content_language = ContentLanguage + target_country = TargetCountry + feed_file_name = FeedFileName + file_format = FileFormat + attribute_language = AttributeLanguage + processing_status = ProcessingStatus + edited = Edited + feed_type = FeedType + + +class DatafeedFeed(gdata.data.GDFeed): + """A datafeed feed + """ + entry = [DatafeedEntry] + + +class AdultContent(atom.core.XmlElement): + """sc:adult_content element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'adult_content' + + +class InternalId(atom.core.XmlElement): + """sc:internal_id element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'internal_id' + + +class ReviewsUrl(atom.core.XmlElement): + """sc:reviews_url element + """ + _qname = SC_NAMESPACE_TEMPLATE % 'reviews_url' + + +class ClientAccount(gdata.data.GDEntry): + """A multiclient account entry + """ + adult_content = AdultContent + internal_id = InternalId + reviews_url = ReviewsUrl + + +class ClientAccountFeed(gdata.data.GDFeed): + """A multiclient account feed + """ + entry = [ClientAccount] diff --git a/patches/gdata/core.py b/patches/gdata/core.py new file mode 100644 index 0000000..0661ec6 --- /dev/null +++ b/patches/gdata/core.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python +# +# Copyright (C) 2010 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. + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +"""Provides classes and methods for working with JSON-C. + +This module is experimental and subject to backwards incompatible changes. + + Jsonc: Class which represents JSON-C data and provides pythonic member + access which is a bit cleaner than working with plain old dicts. + parse_json: Converts a JSON-C string into a Jsonc object. + jsonc_to_string: Converts a Jsonc object into a string of JSON-C. +""" + + +try: + import simplejson +except ImportError: + try: + # Try to import from django, should work on App Engine + from django.utils import simplejson + except ImportError: + # Should work for Python2.6 and higher. + import json as simplejson + + +def _convert_to_jsonc(x): + """Builds a Jsonc objects which wraps the argument's members.""" + + if isinstance(x, dict): + jsonc_obj = Jsonc() + # Recursively transform all members of the dict. + # When converting a dict, we do not convert _name items into private + # Jsonc members. + for key, value in x.iteritems(): + jsonc_obj._dict[key] = _convert_to_jsonc(value) + return jsonc_obj + elif isinstance(x, list): + # Recursively transform all members of the list. + members = [] + for item in x: + members.append(_convert_to_jsonc(item)) + return members + else: + # Return the base object. + return x + + +def parse_json(json_string): + """Converts a JSON-C string into a Jsonc object. + + Args: + json_string: str or unicode The JSON to be parsed. + + Returns: + A new Jsonc object. + """ + + return _convert_to_jsonc(simplejson.loads(json_string)) + + +def parse_json_file(json_file): + return _convert_to_jsonc(simplejson.load(json_file)) + + +def jsonc_to_string(jsonc_obj): + """Converts a Jsonc object into a string of JSON-C.""" + + return simplejson.dumps(_convert_to_object(jsonc_obj)) + + +def prettify_jsonc(jsonc_obj, indentation=2): + """Converts a Jsonc object to a pretified (intented) JSON string.""" + + return simplejson.dumps(_convert_to_object(jsonc_obj), indent=indentation) + + + +def _convert_to_object(jsonc_obj): + """Creates a new dict or list which has the data in the Jsonc object. + + Used to convert the Jsonc object to a plain old Python object to simplify + conversion to a JSON-C string. + + Args: + jsonc_obj: A Jsonc object to be converted into simple Python objects + (dicts, lists, etc.) + + Returns: + Either a dict, list, or other object with members converted from Jsonc + objects to the corresponding simple Python object. + """ + + if isinstance(jsonc_obj, Jsonc): + plain = {} + for key, value in jsonc_obj._dict.iteritems(): + plain[key] = _convert_to_object(value) + return plain + elif isinstance(jsonc_obj, list): + plain = [] + for item in jsonc_obj: + plain.append(_convert_to_object(item)) + return plain + else: + return jsonc_obj + + +def _to_jsonc_name(member_name): + """Converts a Python style member name to a JSON-C style name. + + JSON-C uses camelCaseWithLower while Python tends to use + lower_with_underscores so this method converts as follows: + + spam becomes spam + spam_and_eggs becomes spamAndEggs + + Args: + member_name: str or unicode The Python syle name which should be + converted to JSON-C style. + + Returns: + The JSON-C style name as a str or unicode. + """ + + characters = [] + uppercase_next = False + for character in member_name: + if character == '_': + uppercase_next = True + elif uppercase_next: + characters.append(character.upper()) + uppercase_next = False + else: + characters.append(character) + return ''.join(characters) + + +class Jsonc(object): + """Represents JSON-C data in an easy to access object format. + + To access the members of a JSON structure which looks like this: + { + "data": { + "totalItems": 800, + "items": [ + { + "content": { + "1": "rtsp://v5.cache3.c.youtube.com/CiILENy.../0/0/0/video.3gp" + }, + "viewCount": 220101, + "commentCount": 22, + "favoriteCount": 201 + } + ] + }, + "apiVersion": "2.0" + } + + You would do the following: + x = gdata.core.parse_json(the_above_string) + # Gives you 800 + x.data.total_items + # Should be 22 + x.data.items[0].comment_count + # The apiVersion is '2.0' + x.api_version + + To create a Jsonc object which would produce the above JSON, you would do: + gdata.core.Jsonc( + api_version='2.0', + data=gdata.core.Jsonc( + total_items=800, + items=[ + gdata.core.Jsonc( + view_count=220101, + comment_count=22, + favorite_count=201, + content={ + '1': ('rtsp://v5.cache3.c.youtube.com' + '/CiILENy.../0/0/0/video.3gp')})])) + or + x = gdata.core.Jsonc() + x.api_version = '2.0' + x.data = gdata.core.Jsonc() + x.data.total_items = 800 + x.data.items = [] + # etc. + + How it works: + The JSON-C data is stored in an internal dictionary (._dict) and the + getattr, setattr, and delattr methods rewrite the name which you provide + to mirror the expected format in JSON-C. (For more details on name + conversion see _to_jsonc_name.) You may also access members using + getitem, setitem, delitem as you would for a dictionary. For example + x.data.total_items is equivalent to x['data']['totalItems'] + (Not all dict methods are supported so if you need something other than + the item operations, then you will want to use the ._dict member). + + You may need to use getitem or the _dict member to access certain + properties in cases where the JSON-C syntax does not map neatly to Python + objects. For example the YouTube Video feed has some JSON like this: + "content": {"1": "rtsp://v5.cache3.c.youtube.com..."...} + You cannot do x.content.1 in Python, so you would use the getitem as + follows: + x.content['1'] + or you could use the _dict member as follows: + x.content._dict['1'] + + If you need to create a new object with such a mapping you could use. + + x.content = gdata.core.Jsonc(_dict={'1': 'rtsp://cache3.c.youtube.com...'}) + """ + + def __init__(self, _dict=None, **kwargs): + json = _dict or {} + for key, value in kwargs.iteritems(): + if key.startswith('_'): + object.__setattr__(self, key, value) + else: + json[_to_jsonc_name(key)] = _convert_to_jsonc(value) + + object.__setattr__(self, '_dict', json) + + def __setattr__(self, name, value): + if name.startswith('_'): + object.__setattr__(self, name, value) + else: + object.__getattribute__( + self, '_dict')[_to_jsonc_name(name)] = _convert_to_jsonc(value) + + def __getattr__(self, name): + if name.startswith('_'): + object.__getattribute__(self, name) + else: + try: + return object.__getattribute__(self, '_dict')[_to_jsonc_name(name)] + except KeyError: + raise AttributeError( + 'No member for %s or [\'%s\']' % (name, _to_jsonc_name(name))) + + + def __delattr__(self, name): + if name.startswith('_'): + object.__delattr__(self, name) + else: + try: + del object.__getattribute__(self, '_dict')[_to_jsonc_name(name)] + except KeyError: + raise AttributeError( + 'No member for %s (or [\'%s\'])' % (name, _to_jsonc_name(name))) + + # For container methods pass-through to the underlying dict. + def __getitem__(self, key): + return self._dict[key] + + def __setitem__(self, key, value): + self._dict[key] = value + + def __delitem__(self, key): + del self._dict[key] diff --git a/patches/gdata/data.py b/patches/gdata/data.py new file mode 100644 index 0000000..3bf1850 --- /dev/null +++ b/patches/gdata/data.py @@ -0,0 +1,1219 @@ +#!/usr/bin/env python +# +# Copyright (C) 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 classes and constants for the XML in the Google Data namespace. + +Documentation for the raw XML which these classes represent can be found here: +http://code.google.com/apis/gdata/docs/2.0/elements.html +""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import os +import atom.core +import atom.data + + +GDATA_TEMPLATE = '{http://schemas.google.com/g/2005}%s' +GD_TEMPLATE = GDATA_TEMPLATE +OPENSEARCH_TEMPLATE_V1 = '{http://a9.com/-/spec/opensearchrss/1.0/}%s' +OPENSEARCH_TEMPLATE_V2 = '{http://a9.com/-/spec/opensearch/1.1/}%s' +BATCH_TEMPLATE = '{http://schemas.google.com/gdata/batch}%s' + + +# Labels used in batch request entries to specify the desired CRUD operation. +BATCH_INSERT = 'insert' +BATCH_UPDATE = 'update' +BATCH_DELETE = 'delete' +BATCH_QUERY = 'query' + +EVENT_LOCATION = 'http://schemas.google.com/g/2005#event' +ALTERNATE_LOCATION = 'http://schemas.google.com/g/2005#event.alternate' +PARKING_LOCATION = 'http://schemas.google.com/g/2005#event.parking' + +CANCELED_EVENT = 'http://schemas.google.com/g/2005#event.canceled' +CONFIRMED_EVENT = 'http://schemas.google.com/g/2005#event.confirmed' +TENTATIVE_EVENT = 'http://schemas.google.com/g/2005#event.tentative' + +CONFIDENTIAL_EVENT = 'http://schemas.google.com/g/2005#event.confidential' +DEFAULT_EVENT = 'http://schemas.google.com/g/2005#event.default' +PRIVATE_EVENT = 'http://schemas.google.com/g/2005#event.private' +PUBLIC_EVENT = 'http://schemas.google.com/g/2005#event.public' + +OPAQUE_EVENT = 'http://schemas.google.com/g/2005#event.opaque' +TRANSPARENT_EVENT = 'http://schemas.google.com/g/2005#event.transparent' + +CHAT_MESSAGE = 'http://schemas.google.com/g/2005#message.chat' +INBOX_MESSAGE = 'http://schemas.google.com/g/2005#message.inbox' +SENT_MESSAGE = 'http://schemas.google.com/g/2005#message.sent' +SPAM_MESSAGE = 'http://schemas.google.com/g/2005#message.spam' +STARRED_MESSAGE = 'http://schemas.google.com/g/2005#message.starred' +UNREAD_MESSAGE = 'http://schemas.google.com/g/2005#message.unread' + +BCC_RECIPIENT = 'http://schemas.google.com/g/2005#message.bcc' +CC_RECIPIENT = 'http://schemas.google.com/g/2005#message.cc' +SENDER = 'http://schemas.google.com/g/2005#message.from' +REPLY_TO = 'http://schemas.google.com/g/2005#message.reply-to' +TO_RECIPIENT = 'http://schemas.google.com/g/2005#message.to' + +ASSISTANT_REL = 'http://schemas.google.com/g/2005#assistant' +CALLBACK_REL = 'http://schemas.google.com/g/2005#callback' +CAR_REL = 'http://schemas.google.com/g/2005#car' +COMPANY_MAIN_REL = 'http://schemas.google.com/g/2005#company_main' +FAX_REL = 'http://schemas.google.com/g/2005#fax' +HOME_REL = 'http://schemas.google.com/g/2005#home' +HOME_FAX_REL = 'http://schemas.google.com/g/2005#home_fax' +ISDN_REL = 'http://schemas.google.com/g/2005#isdn' +MAIN_REL = 'http://schemas.google.com/g/2005#main' +MOBILE_REL = 'http://schemas.google.com/g/2005#mobile' +OTHER_REL = 'http://schemas.google.com/g/2005#other' +OTHER_FAX_REL = 'http://schemas.google.com/g/2005#other_fax' +PAGER_REL = 'http://schemas.google.com/g/2005#pager' +RADIO_REL = 'http://schemas.google.com/g/2005#radio' +TELEX_REL = 'http://schemas.google.com/g/2005#telex' +TTL_TDD_REL = 'http://schemas.google.com/g/2005#tty_tdd' +WORK_REL = 'http://schemas.google.com/g/2005#work' +WORK_FAX_REL = 'http://schemas.google.com/g/2005#work_fax' +WORK_MOBILE_REL = 'http://schemas.google.com/g/2005#work_mobile' +WORK_PAGER_REL = 'http://schemas.google.com/g/2005#work_pager' +NETMEETING_REL = 'http://schemas.google.com/g/2005#netmeeting' +OVERALL_REL = 'http://schemas.google.com/g/2005#overall' +PRICE_REL = 'http://schemas.google.com/g/2005#price' +QUALITY_REL = 'http://schemas.google.com/g/2005#quality' +EVENT_REL = 'http://schemas.google.com/g/2005#event' +EVENT_ALTERNATE_REL = 'http://schemas.google.com/g/2005#event.alternate' +EVENT_PARKING_REL = 'http://schemas.google.com/g/2005#event.parking' + +AIM_PROTOCOL = 'http://schemas.google.com/g/2005#AIM' +MSN_PROTOCOL = 'http://schemas.google.com/g/2005#MSN' +YAHOO_MESSENGER_PROTOCOL = 'http://schemas.google.com/g/2005#YAHOO' +SKYPE_PROTOCOL = 'http://schemas.google.com/g/2005#SKYPE' +QQ_PROTOCOL = 'http://schemas.google.com/g/2005#QQ' +GOOGLE_TALK_PROTOCOL = 'http://schemas.google.com/g/2005#GOOGLE_TALK' +ICQ_PROTOCOL = 'http://schemas.google.com/g/2005#ICQ' +JABBER_PROTOCOL = 'http://schemas.google.com/g/2005#JABBER' + +REGULAR_COMMENTS = 'http://schemas.google.com/g/2005#regular' +REVIEW_COMMENTS = 'http://schemas.google.com/g/2005#reviews' + +MAIL_BOTH = 'http://schemas.google.com/g/2005#both' +MAIL_LETTERS = 'http://schemas.google.com/g/2005#letters' +MAIL_PARCELS = 'http://schemas.google.com/g/2005#parcels' +MAIL_NEITHER = 'http://schemas.google.com/g/2005#neither' + +GENERAL_ADDRESS = 'http://schemas.google.com/g/2005#general' +LOCAL_ADDRESS = 'http://schemas.google.com/g/2005#local' + +OPTIONAL_ATENDEE = 'http://schemas.google.com/g/2005#event.optional' +REQUIRED_ATENDEE = 'http://schemas.google.com/g/2005#event.required' + +ATTENDEE_ACCEPTED = 'http://schemas.google.com/g/2005#event.accepted' +ATTENDEE_DECLINED = 'http://schemas.google.com/g/2005#event.declined' +ATTENDEE_INVITED = 'http://schemas.google.com/g/2005#event.invited' +ATTENDEE_TENTATIVE = 'http://schemas.google.com/g/2005#event.tentative' + +FULL_PROJECTION = 'full' +VALUES_PROJECTION = 'values' +BASIC_PROJECTION = 'basic' + +PRIVATE_VISIBILITY = 'private' +PUBLIC_VISIBILITY = 'public' + +OPAQUE_TRANSPARENCY = 'http://schemas.google.com/g/2005#event.opaque' +TRANSPARENT_TRANSPARENCY = 'http://schemas.google.com/g/2005#event.transparent' + +CONFIDENTIAL_EVENT_VISIBILITY = 'http://schemas.google.com/g/2005#event.confidential' +DEFAULT_EVENT_VISIBILITY = 'http://schemas.google.com/g/2005#event.default' +PRIVATE_EVENT_VISIBILITY = 'http://schemas.google.com/g/2005#event.private' +PUBLIC_EVENT_VISIBILITY = 'http://schemas.google.com/g/2005#event.public' + +CANCELED_EVENT_STATUS = 'http://schemas.google.com/g/2005#event.canceled' +CONFIRMED_EVENT_STATUS = 'http://schemas.google.com/g/2005#event.confirmed' +TENTATIVE_EVENT_STATUS = 'http://schemas.google.com/g/2005#event.tentative' + +ACL_REL = 'http://schemas.google.com/acl/2007#accessControlList' + + +class Error(Exception): + pass + + +class MissingRequiredParameters(Error): + pass + + +class LinkFinder(atom.data.LinkFinder): + """Mixin used in Feed and Entry classes to simplify link lookups by type. + + Provides lookup methods for edit, edit-media, post, ACL and other special + links which are common across Google Data APIs. + """ + + def find_html_link(self): + """Finds the first link with rel of alternate and type of text/html.""" + for link in self.link: + if link.rel == 'alternate' and link.type == 'text/html': + return link.href + return None + + FindHtmlLink = find_html_link + + def get_html_link(self): + for a_link in self.link: + if a_link.rel == 'alternate' and a_link.type == 'text/html': + return a_link + return None + + GetHtmlLink = get_html_link + + def find_post_link(self): + """Get the URL to which new entries should be POSTed. + + The POST target URL is used to insert new entries. + + Returns: + A str for the URL in the link with a rel matching the POST type. + """ + return self.find_url('http://schemas.google.com/g/2005#post') + + FindPostLink = find_post_link + + def get_post_link(self): + return self.get_link('http://schemas.google.com/g/2005#post') + + GetPostLink = get_post_link + + def find_acl_link(self): + acl_link = self.get_acl_link() + if acl_link: + return acl_link.href + + return None + + FindAclLink = find_acl_link + + def get_acl_link(self): + """Searches for a link or feed_link (if present) with the rel for ACL.""" + + acl_link = self.get_link(ACL_REL) + if acl_link: + return acl_link + elif hasattr(self, 'feed_link'): + for a_feed_link in self.feed_link: + if a_feed_link.rel == ACL_REL: + return a_feed_link + + return None + + GetAclLink = get_acl_link + + def find_feed_link(self): + return self.find_url('http://schemas.google.com/g/2005#feed') + + FindFeedLink = find_feed_link + + def get_feed_link(self): + return self.get_link('http://schemas.google.com/g/2005#feed') + + GetFeedLink = get_feed_link + + def find_previous_link(self): + return self.find_url('previous') + + FindPreviousLink = find_previous_link + + def get_previous_link(self): + return self.get_link('previous') + + GetPreviousLink = get_previous_link + + +class TotalResults(atom.core.XmlElement): + """opensearch:TotalResults for a GData feed.""" + _qname = (OPENSEARCH_TEMPLATE_V1 % 'totalResults', + OPENSEARCH_TEMPLATE_V2 % 'totalResults') + + +class StartIndex(atom.core.XmlElement): + """The opensearch:startIndex element in GData feed.""" + _qname = (OPENSEARCH_TEMPLATE_V1 % 'startIndex', + OPENSEARCH_TEMPLATE_V2 % 'startIndex') + + +class ItemsPerPage(atom.core.XmlElement): + """The opensearch:itemsPerPage element in GData feed.""" + _qname = (OPENSEARCH_TEMPLATE_V1 % 'itemsPerPage', + OPENSEARCH_TEMPLATE_V2 % 'itemsPerPage') + + +class ExtendedProperty(atom.core.XmlElement): + """The Google Data extendedProperty element. + + Used to store arbitrary key-value information specific to your + application. The value can either be a text string stored as an XML + attribute (.value), or an XML node (XmlBlob) as a child element. + + This element is used in the Google Calendar data API and the Google + Contacts data API. + """ + _qname = GDATA_TEMPLATE % 'extendedProperty' + name = 'name' + value = 'value' + + def get_xml_blob(self): + """Returns the XML blob as an atom.core.XmlElement. + + Returns: + An XmlElement representing the blob's XML, or None if no + blob was set. + """ + if self._other_elements: + return self._other_elements[0] + else: + return None + + GetXmlBlob = get_xml_blob + + def set_xml_blob(self, blob): + """Sets the contents of the extendedProperty to XML as a child node. + + Since the extendedProperty is only allowed one child element as an XML + blob, setting the XML blob will erase any preexisting member elements + in this object. + + Args: + blob: str or atom.core.XmlElement representing the XML blob stored in + the extendedProperty. + """ + # Erase any existing extension_elements, clears the child nodes from the + # extendedProperty. + if isinstance(blob, atom.core.XmlElement): + self._other_elements = [blob] + else: + self._other_elements = [atom.core.parse(str(blob))] + + SetXmlBlob = set_xml_blob + + +class GDEntry(atom.data.Entry, LinkFinder): + """Extends Atom Entry to provide data processing""" + etag = '{http://schemas.google.com/g/2005}etag' + + def get_id(self): + if self.id is not None and self.id.text is not None: + return self.id.text.strip() + return None + + GetId = get_id + + def is_media(self): + if self.find_edit_media_link(): + return True + return False + + IsMedia = is_media + + def find_media_link(self): + """Returns the URL to the media content, if the entry is a media entry. + Otherwise returns None. + """ + if self.is_media(): + return self.content.src + return None + + FindMediaLink = find_media_link + + +class GDFeed(atom.data.Feed, LinkFinder): + """A Feed from a GData service.""" + etag = '{http://schemas.google.com/g/2005}etag' + total_results = TotalResults + start_index = StartIndex + items_per_page = ItemsPerPage + entry = [GDEntry] + + def get_id(self): + if self.id is not None and self.id.text is not None: + return self.id.text.strip() + return None + + GetId = get_id + + def get_generator(self): + if self.generator and self.generator.text: + return self.generator.text.strip() + return None + + +class BatchId(atom.core.XmlElement): + """Identifies a single operation in a batch request.""" + _qname = BATCH_TEMPLATE % 'id' + + +class BatchOperation(atom.core.XmlElement): + """The CRUD operation which this batch entry represents.""" + _qname = BATCH_TEMPLATE % 'operation' + type = 'type' + + +class BatchStatus(atom.core.XmlElement): + """The batch:status element present in a batch response entry. + + A status element contains the code (HTTP response code) and + reason as elements. In a single request these fields would + be part of the HTTP response, but in a batch request each + Entry operation has a corresponding Entry in the response + feed which includes status information. + + See http://code.google.com/apis/gdata/batch.html#Handling_Errors + """ + _qname = BATCH_TEMPLATE % 'status' + code = 'code' + reason = 'reason' + content_type = 'content-type' + + +class BatchEntry(GDEntry): + """An atom:entry for use in batch requests. + + The BatchEntry contains additional members to specify the operation to be + performed on this entry and a batch ID so that the server can reference + individual operations in the response feed. For more information, see: + http://code.google.com/apis/gdata/batch.html + """ + batch_operation = BatchOperation + batch_id = BatchId + batch_status = BatchStatus + + +class BatchInterrupted(atom.core.XmlElement): + """The batch:interrupted element sent if batch request was interrupted. + + Only appears in a feed if some of the batch entries could not be processed. + See: http://code.google.com/apis/gdata/batch.html#Handling_Errors + """ + _qname = BATCH_TEMPLATE % 'interrupted' + reason = 'reason' + success = 'success' + failures = 'failures' + parsed = 'parsed' + + +class BatchFeed(GDFeed): + """A feed containing a list of batch request entries.""" + interrupted = BatchInterrupted + entry = [BatchEntry] + + def add_batch_entry(self, entry=None, id_url_string=None, + batch_id_string=None, operation_string=None): + """Logic for populating members of a BatchEntry and adding to the feed. + + If the entry is not a BatchEntry, it is converted to a BatchEntry so + that the batch specific members will be present. + + The id_url_string can be used in place of an entry if the batch operation + applies to a URL. For example query and delete operations require just + the URL of an entry, no body is sent in the HTTP request. If an + id_url_string is sent instead of an entry, a BatchEntry is created and + added to the feed. + + This method also assigns the desired batch id to the entry so that it + can be referenced in the server's response. If the batch_id_string is + None, this method will assign a batch_id to be the index at which this + entry will be in the feed's entry list. + + Args: + entry: BatchEntry, atom.data.Entry, or another Entry flavor (optional) + The entry which will be sent to the server as part of the batch + request. The item must have a valid atom id so that the server + knows which entry this request references. + id_url_string: str (optional) The URL of the entry to be acted on. You + can find this URL in the text member of the atom id for an entry. + If an entry is not sent, this id will be used to construct a new + BatchEntry which will be added to the request feed. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. Note that batch_ids should either always be specified or + never, mixing could potentially result in duplicate batch ids. + operation_string: str (optional) The desired batch operation which will + set the batch_operation.type member of the entry. Options are + 'insert', 'update', 'delete', and 'query' + + Raises: + MissingRequiredParameters: Raised if neither an id_ url_string nor an + entry are provided in the request. + + Returns: + The added entry. + """ + if entry is None and id_url_string is None: + raise MissingRequiredParameters('supply either an entry or URL string') + if entry is None and id_url_string is not None: + entry = BatchEntry(id=atom.data.Id(text=id_url_string)) + if batch_id_string is not None: + entry.batch_id = BatchId(text=batch_id_string) + elif entry.batch_id is None or entry.batch_id.text is None: + entry.batch_id = BatchId(text=str(len(self.entry))) + if operation_string is not None: + entry.batch_operation = BatchOperation(type=operation_string) + self.entry.append(entry) + return entry + + AddBatchEntry = add_batch_entry + + def add_insert(self, entry, batch_id_string=None): + """Add an insert request to the operations in this batch request feed. + + If the entry doesn't yet have an operation or a batch id, these will + be set to the insert operation and a batch_id specified as a parameter. + + Args: + entry: BatchEntry The entry which will be sent in the batch feed as an + insert request. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. Note that batch_ids should either always be specified or + never, mixing could potentially result in duplicate batch ids. + """ + self.add_batch_entry(entry=entry, batch_id_string=batch_id_string, + operation_string=BATCH_INSERT) + + AddInsert = add_insert + + def add_update(self, entry, batch_id_string=None): + """Add an update request to the list of batch operations in this feed. + + Sets the operation type of the entry to insert if it is not already set + and assigns the desired batch id to the entry so that it can be + referenced in the server's response. + + Args: + entry: BatchEntry The entry which will be sent to the server as an + update (HTTP PUT) request. The item must have a valid atom id + so that the server knows which entry to replace. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. See also comments for AddInsert. + """ + self.add_batch_entry(entry=entry, batch_id_string=batch_id_string, + operation_string=BATCH_UPDATE) + + AddUpdate = add_update + + def add_delete(self, url_string=None, entry=None, batch_id_string=None): + """Adds a delete request to the batch request feed. + + This method takes either the url_string which is the atom id of the item + to be deleted, or the entry itself. The atom id of the entry must be + present so that the server knows which entry should be deleted. + + Args: + url_string: str (optional) The URL of the entry to be deleted. You can + find this URL in the text member of the atom id for an entry. + entry: BatchEntry (optional) The entry to be deleted. + batch_id_string: str (optional) + + Raises: + MissingRequiredParameters: Raised if neither a url_string nor an entry + are provided in the request. + """ + self.add_batch_entry(entry=entry, id_url_string=url_string, + batch_id_string=batch_id_string, operation_string=BATCH_DELETE) + + AddDelete = add_delete + + def add_query(self, url_string=None, entry=None, batch_id_string=None): + """Adds a query request to the batch request feed. + + This method takes either the url_string which is the query URL + whose results will be added to the result feed. The query URL will + be encapsulated in a BatchEntry, and you may pass in the BatchEntry + with a query URL instead of sending a url_string. + + Args: + url_string: str (optional) + entry: BatchEntry (optional) + batch_id_string: str (optional) + + Raises: + MissingRequiredParameters + """ + self.add_batch_entry(entry=entry, id_url_string=url_string, + batch_id_string=batch_id_string, operation_string=BATCH_QUERY) + + AddQuery = add_query + + def find_batch_link(self): + return self.find_url('http://schemas.google.com/g/2005#batch') + + FindBatchLink = find_batch_link + + +class EntryLink(atom.core.XmlElement): + """The gd:entryLink element. + + Represents a logically nested entry. For example, a <gd:who> + representing a contact might have a nested entry from a contact feed. + """ + _qname = GDATA_TEMPLATE % 'entryLink' + entry = GDEntry + rel = 'rel' + read_only = 'readOnly' + href = 'href' + + +class FeedLink(atom.core.XmlElement): + """The gd:feedLink element. + + Represents a logically nested feed. For example, a calendar feed might + have a nested feed representing all comments on entries. + """ + _qname = GDATA_TEMPLATE % 'feedLink' + feed = GDFeed + rel = 'rel' + read_only = 'readOnly' + count_hint = 'countHint' + href = 'href' + + +class AdditionalName(atom.core.XmlElement): + """The gd:additionalName element. + + Specifies additional (eg. middle) name of the person. + Contains an attribute for the phonetic representaton of the name. + """ + _qname = GDATA_TEMPLATE % 'additionalName' + yomi = 'yomi' + + +class Comments(atom.core.XmlElement): + """The gd:comments element. + + Contains a comments feed for the enclosing entry (such as a calendar event). + """ + _qname = GDATA_TEMPLATE % 'comments' + rel = 'rel' + feed_link = FeedLink + + +class Country(atom.core.XmlElement): + """The gd:country element. + + Country name along with optional country code. The country code is + given in accordance with ISO 3166-1 alpha-2: + http://www.iso.org/iso/iso-3166-1_decoding_table + """ + _qname = GDATA_TEMPLATE % 'country' + code = 'code' + + +class EmailImParent(atom.core.XmlElement): + address = 'address' + label = 'label' + rel = 'rel' + primary = 'primary' + + +class Email(EmailImParent): + """The gd:email element. + + An email address associated with the containing entity (which is + usually an entity representing a person or a location). + """ + _qname = GDATA_TEMPLATE % 'email' + display_name = 'displayName' + + +class FamilyName(atom.core.XmlElement): + """The gd:familyName element. + + Specifies family name of the person, eg. "Smith". + """ + _qname = GDATA_TEMPLATE % 'familyName' + yomi = 'yomi' + + +class Im(EmailImParent): + """The gd:im element. + + An instant messaging address associated with the containing entity. + """ + _qname = GDATA_TEMPLATE % 'im' + protocol = 'protocol' + + +class GivenName(atom.core.XmlElement): + """The gd:givenName element. + + Specifies given name of the person, eg. "John". + """ + _qname = GDATA_TEMPLATE % 'givenName' + yomi = 'yomi' + + +class NamePrefix(atom.core.XmlElement): + """The gd:namePrefix element. + + Honorific prefix, eg. 'Mr' or 'Mrs'. + """ + _qname = GDATA_TEMPLATE % 'namePrefix' + + +class NameSuffix(atom.core.XmlElement): + """The gd:nameSuffix element. + + Honorific suffix, eg. 'san' or 'III'. + """ + _qname = GDATA_TEMPLATE % 'nameSuffix' + + +class FullName(atom.core.XmlElement): + """The gd:fullName element. + + Unstructured representation of the name. + """ + _qname = GDATA_TEMPLATE % 'fullName' + + +class Name(atom.core.XmlElement): + """The gd:name element. + + Allows storing person's name in a structured way. Consists of + given name, additional name, family name, prefix, suffix and full name. + """ + _qname = GDATA_TEMPLATE % 'name' + given_name = GivenName + additional_name = AdditionalName + family_name = FamilyName + name_prefix = NamePrefix + name_suffix = NameSuffix + full_name = FullName + + +class OrgDepartment(atom.core.XmlElement): + """The gd:orgDepartment element. + + Describes a department within an organization. Must appear within a + gd:organization element. + """ + _qname = GDATA_TEMPLATE % 'orgDepartment' + + +class OrgJobDescription(atom.core.XmlElement): + """The gd:orgJobDescription element. + + Describes a job within an organization. Must appear within a + gd:organization element. + """ + _qname = GDATA_TEMPLATE % 'orgJobDescription' + + +class OrgName(atom.core.XmlElement): + """The gd:orgName element. + + The name of the organization. Must appear within a gd:organization + element. + + Contains a Yomigana attribute (Japanese reading aid) for the + organization name. + """ + _qname = GDATA_TEMPLATE % 'orgName' + yomi = 'yomi' + + +class OrgSymbol(atom.core.XmlElement): + """The gd:orgSymbol element. + + Provides a symbol of an organization. Must appear within a + gd:organization element. + """ + _qname = GDATA_TEMPLATE % 'orgSymbol' + + +class OrgTitle(atom.core.XmlElement): + """The gd:orgTitle element. + + The title of a person within an organization. Must appear within a + gd:organization element. + """ + _qname = GDATA_TEMPLATE % 'orgTitle' + + +class Organization(atom.core.XmlElement): + """The gd:organization element. + + An organization, typically associated with a contact. + """ + _qname = GDATA_TEMPLATE % 'organization' + label = 'label' + primary = 'primary' + rel = 'rel' + department = OrgDepartment + job_description = OrgJobDescription + name = OrgName + symbol = OrgSymbol + title = OrgTitle + + +class When(atom.core.XmlElement): + """The gd:when element. + + Represents a period of time or an instant. + """ + _qname = GDATA_TEMPLATE % 'when' + end = 'endTime' + start = 'startTime' + value = 'valueString' + + +class OriginalEvent(atom.core.XmlElement): + """The gd:originalEvent element. + + Equivalent to the Recurrence ID property specified in section 4.8.4.4 + of RFC 2445. Appears in every instance of a recurring event, to identify + the original event. + + Contains a <gd:when> element specifying the original start time of the + instance that has become an exception. + """ + _qname = GDATA_TEMPLATE % 'originalEvent' + id = 'id' + href = 'href' + when = When + + +class PhoneNumber(atom.core.XmlElement): + """The gd:phoneNumber element. + + A phone number associated with the containing entity (which is usually + an entity representing a person or a location). + """ + _qname = GDATA_TEMPLATE % 'phoneNumber' + label = 'label' + rel = 'rel' + uri = 'uri' + primary = 'primary' + + +class PostalAddress(atom.core.XmlElement): + """The gd:postalAddress element.""" + _qname = GDATA_TEMPLATE % 'postalAddress' + label = 'label' + rel = 'rel' + uri = 'uri' + primary = 'primary' + + +class Rating(atom.core.XmlElement): + """The gd:rating element. + + Represents a numeric rating of the enclosing entity, such as a + comment. Each rating supplies its own scale, although it may be + normalized by a service; for example, some services might convert all + ratings to a scale from 1 to 5. + """ + _qname = GDATA_TEMPLATE % 'rating' + average = 'average' + max = 'max' + min = 'min' + num_raters = 'numRaters' + rel = 'rel' + value = 'value' + + +class Recurrence(atom.core.XmlElement): + """The gd:recurrence element. + + Represents the dates and times when a recurring event takes place. + + The string that defines the recurrence consists of a set of properties, + each of which is defined in the iCalendar standard (RFC 2445). + + Specifically, the string usually begins with a DTSTART property that + indicates the starting time of the first instance of the event, and + often a DTEND property or a DURATION property to indicate when the + first instance ends. Next come RRULE, RDATE, EXRULE, and/or EXDATE + properties, which collectively define a recurring event and its + exceptions (but see below). (See section 4.8.5 of RFC 2445 for more + information about these recurrence component properties.) Last comes a + VTIMEZONE component, providing detailed timezone rules for any timezone + ID mentioned in the preceding properties. + + Google services like Google Calendar don't generally generate EXRULE + and EXDATE properties to represent exceptions to recurring events; + instead, they generate <gd:recurrenceException> elements. However, + Google services may include EXRULE and/or EXDATE properties anyway; + for example, users can import events and exceptions into Calendar, and + if those imported events contain EXRULE or EXDATE properties, then + Calendar will provide those properties when it sends a <gd:recurrence> + element. + + Note the the use of <gd:recurrenceException> means that you can't be + sure just from examining a <gd:recurrence> element whether there are + any exceptions to the recurrence description. To ensure that you find + all exceptions, look for <gd:recurrenceException> elements in the feed, + and use their <gd:originalEvent> elements to match them up with + <gd:recurrence> elements. + """ + _qname = GDATA_TEMPLATE % 'recurrence' + + +class RecurrenceException(atom.core.XmlElement): + """The gd:recurrenceException element. + + Represents an event that's an exception to a recurring event-that is, + an instance of a recurring event in which one or more aspects of the + recurring event (such as attendance list, time, or location) have been + changed. + + Contains a <gd:originalEvent> element that specifies the original + recurring event that this event is an exception to. + + When you change an instance of a recurring event, that instance becomes + an exception. Depending on what change you made to it, the exception + behaves in either of two different ways when the original recurring + event is changed: + + - If you add, change, or remove comments, attendees, or attendee + responses, then the exception remains tied to the original event, and + changes to the original event also change the exception. + - If you make any other changes to the exception (such as changing the + time or location) then the instance becomes "specialized," which means + that it's no longer as tightly tied to the original event. If you + change the original event, specialized exceptions don't change. But + see below. + + For example, say you have a meeting every Tuesday and Thursday at + 2:00 p.m. If you change the attendance list for this Thursday's meeting + (but not for the regularly scheduled meeting), then it becomes an + exception. If you change the time for this Thursday's meeting (but not + for the regularly scheduled meeting), then it becomes specialized. + + Regardless of whether an exception is specialized or not, if you do + something that deletes the instance that the exception was derived from, + then the exception is deleted. Note that changing the day or time of a + recurring event deletes all instances, and creates new ones. + + For example, after you've specialized this Thursday's meeting, say you + change the recurring meeting to happen on Monday, Wednesday, and Friday. + That change deletes all of the recurring instances of the + Tuesday/Thursday meeting, including the specialized one. + + If a particular instance of a recurring event is deleted, then that + instance appears as a <gd:recurrenceException> containing a + <gd:entryLink> that has its <gd:eventStatus> set to + "http://schemas.google.com/g/2005#event.canceled". (For more + information about canceled events, see RFC 2445.) + """ + _qname = GDATA_TEMPLATE % 'recurrenceException' + specialized = 'specialized' + entry_link = EntryLink + original_event = OriginalEvent + + +class Reminder(atom.core.XmlElement): + """The gd:reminder element. + + A time interval, indicating how long before the containing entity's start + time or due time attribute a reminder should be issued. Alternatively, + may specify an absolute time at which a reminder should be issued. Also + specifies a notification method, indicating what medium the system + should use to remind the user. + """ + _qname = GDATA_TEMPLATE % 'reminder' + absolute_time = 'absoluteTime' + method = 'method' + days = 'days' + hours = 'hours' + minutes = 'minutes' + + +class Transparency(atom.core.XmlElement): + """The gd:transparency element: + + Extensible enum corresponding to the TRANSP property defined in RFC 244. + """ + _qname = GDATA_TEMPLATE % 'transparency' + value = 'value' + + +class Agent(atom.core.XmlElement): + """The gd:agent element. + + The agent who actually receives the mail. Used in work addresses. + Also for 'in care of' or 'c/o'. + """ + _qname = GDATA_TEMPLATE % 'agent' + + +class HouseName(atom.core.XmlElement): + """The gd:housename element. + + Used in places where houses or buildings have names (and not + necessarily numbers), eg. "The Pillars". + """ + _qname = GDATA_TEMPLATE % 'housename' + + +class Street(atom.core.XmlElement): + """The gd:street element. + + Can be street, avenue, road, etc. This element also includes the + house number and room/apartment/flat/floor number. + """ + _qname = GDATA_TEMPLATE % 'street' + + +class PoBox(atom.core.XmlElement): + """The gd:pobox element. + + Covers actual P.O. boxes, drawers, locked bags, etc. This is usually + but not always mutually exclusive with street. + """ + _qname = GDATA_TEMPLATE % 'pobox' + + +class Neighborhood(atom.core.XmlElement): + """The gd:neighborhood element. + + This is used to disambiguate a street address when a city contains more + than one street with the same name, or to specify a small place whose + mail is routed through a larger postal town. In China it could be a + county or a minor city. + """ + _qname = GDATA_TEMPLATE % 'neighborhood' + + +class City(atom.core.XmlElement): + """The gd:city element. + + Can be city, village, town, borough, etc. This is the postal town and + not necessarily the place of residence or place of business. + """ + _qname = GDATA_TEMPLATE % 'city' + + +class Subregion(atom.core.XmlElement): + """The gd:subregion element. + + Handles administrative districts such as U.S. or U.K. counties that are + not used for mail addressing purposes. Subregion is not intended for + delivery addresses. + """ + _qname = GDATA_TEMPLATE % 'subregion' + + +class Region(atom.core.XmlElement): + """The gd:region element. + + A state, province, county (in Ireland), Land (in Germany), + departement (in France), etc. + """ + _qname = GDATA_TEMPLATE % 'region' + + +class Postcode(atom.core.XmlElement): + """The gd:postcode element. + + Postal code. Usually country-wide, but sometimes specific to the + city (e.g. "2" in "Dublin 2, Ireland" addresses). + """ + _qname = GDATA_TEMPLATE % 'postcode' + + +class Country(atom.core.XmlElement): + """The gd:country element. + + The name or code of the country. + """ + _qname = GDATA_TEMPLATE % 'country' + + +class FormattedAddress(atom.core.XmlElement): + """The gd:formattedAddress element. + + The full, unstructured postal address. + """ + _qname = GDATA_TEMPLATE % 'formattedAddress' + + +class StructuredPostalAddress(atom.core.XmlElement): + """The gd:structuredPostalAddress element. + + Postal address split into components. It allows to store the address + in locale independent format. The fields can be interpreted and used + to generate formatted, locale dependent address. The following elements + reperesent parts of the address: agent, house name, street, P.O. box, + neighborhood, city, subregion, region, postal code, country. The + subregion element is not used for postal addresses, it is provided for + extended uses of addresses only. In order to store postal address in an + unstructured form formatted address field is provided. + """ + _qname = GDATA_TEMPLATE % 'structuredPostalAddress' + rel = 'rel' + mail_class = 'mailClass' + usage = 'usage' + label = 'label' + primary = 'primary' + agent = Agent + house_name = HouseName + street = Street + po_box = PoBox + neighborhood = Neighborhood + city = City + subregion = Subregion + region = Region + postcode = Postcode + country = Country + formatted_address = FormattedAddress + + +class Where(atom.core.XmlElement): + """The gd:where element. + + A place (such as an event location) associated with the containing + entity. The type of the association is determined by the rel attribute; + the details of the location are contained in an embedded or linked-to + Contact entry. + + A <gd:where> element is more general than a <gd:geoPt> element. The + former identifies a place using a text description and/or a Contact + entry, while the latter identifies a place using a specific geographic + location. + """ + _qname = GDATA_TEMPLATE % 'where' + label = 'label' + rel = 'rel' + value = 'valueString' + entry_link = EntryLink + + +class AttendeeType(atom.core.XmlElement): + """The gd:attendeeType element.""" + _qname = GDATA_TEMPLATE % 'attendeeType' + value = 'value' + + +class AttendeeStatus(atom.core.XmlElement): + """The gd:attendeeStatus element.""" + _qname = GDATA_TEMPLATE % 'attendeeStatus' + value = 'value' + + +class EventStatus(atom.core.XmlElement): + """The gd:eventStatus element.""" + _qname = GDATA_TEMPLATE % 'eventStatus' + value = 'value' + + +class Visibility(atom.core.XmlElement): + """The gd:visibility element.""" + _qname = GDATA_TEMPLATE % 'visibility' + value = 'value' + + +class Who(atom.core.XmlElement): + """The gd:who element. + + A person associated with the containing entity. The type of the + association is determined by the rel attribute; the details about the + person are contained in an embedded or linked-to Contact entry. + + The <gd:who> element can be used to specify email senders and + recipients, calendar event organizers, and so on. + """ + _qname = GDATA_TEMPLATE % 'who' + email = 'email' + rel = 'rel' + value = 'valueString' + attendee_status = AttendeeStatus + attendee_type = AttendeeType + entry_link = EntryLink + + +class Deleted(atom.core.XmlElement): + """gd:deleted when present, indicates the containing entry is deleted.""" + _qname = GD_TEMPLATE % 'deleted' + + +class Money(atom.core.XmlElement): + """Describes money""" + _qname = GD_TEMPLATE % 'money' + amount = 'amount' + currency_code = 'currencyCode' + + +class MediaSource(object): + """GData Entries can refer to media sources, so this class provides a + place to store references to these objects along with some metadata. + """ + + def __init__(self, file_handle=None, content_type=None, content_length=None, + file_path=None, file_name=None): + """Creates an object of type MediaSource. + + Args: + file_handle: A file handle pointing to the file to be encapsulated in the + MediaSource. + content_type: string The MIME type of the file. Required if a file_handle + is given. + content_length: int The size of the file. Required if a file_handle is + given. + file_path: string (optional) A full path name to the file. Used in + place of a file_handle. + file_name: string The name of the file without any path information. + Required if a file_handle is given. + """ + self.file_handle = file_handle + self.content_type = content_type + self.content_length = content_length + self.file_name = file_name + + if (file_handle is None and content_type is not None and + file_path is not None): + self.set_file_handle(file_path, content_type) + + def set_file_handle(self, file_name, content_type): + """A helper function which can create a file handle from a given filename + and set the content type and length all at once. + + Args: + file_name: string The path and file name to the file containing the media + content_type: string A MIME type representing the type of the media + """ + + self.file_handle = open(file_name, 'rb') + self.content_type = content_type + self.content_length = os.path.getsize(file_name) + self.file_name = os.path.basename(file_name) + + SetFileHandle = set_file_handle + + def modify_request(self, http_request): + http_request.add_body_part(self.file_handle, self.content_type, + self.content_length) + return http_request + + ModifyRequest = modify_request diff --git a/patches/gdata/data.pyc b/patches/gdata/data.pyc new file mode 100644 index 0000000000000000000000000000000000000000..536174fc2a88ad11f30510374bb6cfe75d6723e6 GIT binary patch literal 51152 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 literal 0 HcmV?d00001 diff --git a/patches/gdata/docs/__init__.py b/patches/gdata/docs/__init__.py new file mode 100644 index 0000000..8031bc9 --- /dev/null +++ b/patches/gdata/docs/__init__.py @@ -0,0 +1,269 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""Contains extensions to Atom objects used with Google Documents.""" + +__author__ = ('api.jfisher (Jeff Fisher), ' + 'api.eric@google.com (Eric Bidelman)') + +import atom +import gdata + + +DOCUMENTS_NAMESPACE = 'http://schemas.google.com/docs/2007' + + +class Scope(atom.AtomBase): + """The DocList ACL scope element""" + + _tag = 'scope' + _namespace = gdata.GACL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + _attributes['type'] = 'type' + + def __init__(self, value=None, type=None, extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.type = type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Role(atom.AtomBase): + """The DocList ACL role element""" + + _tag = 'role' + _namespace = gdata.GACL_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class FeedLink(atom.AtomBase): + """The DocList gd:feedLink element""" + + _tag = 'feedLink' + _namespace = gdata.GDATA_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['rel'] = 'rel' + _attributes['href'] = 'href' + + def __init__(self, href=None, rel=None, text=None, extension_elements=None, + extension_attributes=None): + self.href = href + self.rel = rel + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class ResourceId(atom.AtomBase): + """The DocList gd:resourceId element""" + + _tag = 'resourceId' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + def __init__(self, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class LastModifiedBy(atom.Person): + """The DocList gd:lastModifiedBy element""" + + _tag = 'lastModifiedBy' + _namespace = gdata.GDATA_NAMESPACE + + +class LastViewed(atom.Person): + """The DocList gd:lastViewed element""" + + _tag = 'lastViewed' + _namespace = gdata.GDATA_NAMESPACE + + +class WritersCanInvite(atom.AtomBase): + """The DocList docs:writersCanInvite element""" + + _tag = 'writersCanInvite' + _namespace = DOCUMENTS_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['value'] = 'value' + + +class DocumentListEntry(gdata.GDataEntry): + """The Google Documents version of an Atom Entry""" + + _tag = gdata.GDataEntry._tag + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feedLink', FeedLink) + _children['{%s}resourceId' % gdata.GDATA_NAMESPACE] = ('resourceId', + ResourceId) + _children['{%s}lastModifiedBy' % gdata.GDATA_NAMESPACE] = ('lastModifiedBy', + LastModifiedBy) + _children['{%s}lastViewed' % gdata.GDATA_NAMESPACE] = ('lastViewed', + LastViewed) + _children['{%s}writersCanInvite' % DOCUMENTS_NAMESPACE] = ( + 'writersCanInvite', WritersCanInvite) + + def __init__(self, resourceId=None, feedLink=None, lastViewed=None, + lastModifiedBy=None, writersCanInvite=None, author=None, + category=None, content=None, atom_id=None, link=None, + published=None, title=None, updated=None, text=None, + extension_elements=None, extension_attributes=None): + self.feedLink = feedLink + self.lastViewed = lastViewed + self.lastModifiedBy = lastModifiedBy + self.resourceId = resourceId + self.writersCanInvite = writersCanInvite + gdata.GDataEntry.__init__( + self, author=author, category=category, content=content, + atom_id=atom_id, link=link, published=published, title=title, + updated=updated, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + def GetAclLink(self): + """Extracts the DocListEntry's <gd:feedLink>. + + Returns: + A FeedLink object. + """ + return self.feedLink + + def GetDocumentType(self): + """Extracts the type of document from the DocListEntry. + + This method returns the type of document the DocListEntry + represents. Possible values are document, presentation, + spreadsheet, folder, or pdf. + + Returns: + A string representing the type of document. + """ + if self.category: + for category in self.category: + if category.scheme == gdata.GDATA_NAMESPACE + '#kind': + return category.label + else: + return None + + +def DocumentListEntryFromString(xml_string): + """Converts an XML string into a DocumentListEntry object. + + Args: + xml_string: string The XML describing a Document List feed entry. + + Returns: + A DocumentListEntry object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(DocumentListEntry, xml_string) + + +class DocumentListAclEntry(gdata.GDataEntry): + """A DocList ACL Entry flavor of an Atom Entry""" + + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}scope' % gdata.GACL_NAMESPACE] = ('scope', Scope) + _children['{%s}role' % gdata.GACL_NAMESPACE] = ('role', Role) + + def __init__(self, category=None, atom_id=None, link=None, + title=None, updated=None, scope=None, role=None, + extension_elements=None, extension_attributes=None, text=None): + gdata.GDataEntry.__init__(self, author=None, category=category, + content=None, atom_id=atom_id, link=link, + published=None, title=title, + updated=updated, text=None) + self.scope = scope + self.role = role + + +def DocumentListAclEntryFromString(xml_string): + """Converts an XML string into a DocumentListAclEntry object. + + Args: + xml_string: string The XML describing a Document List ACL feed entry. + + Returns: + A DocumentListAclEntry object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(DocumentListAclEntry, xml_string) + + +class DocumentListFeed(gdata.GDataFeed): + """A feed containing a list of Google Documents Items""" + + _tag = gdata.GDataFeed._tag + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [DocumentListEntry]) + + +def DocumentListFeedFromString(xml_string): + """Converts an XML string into a DocumentListFeed object. + + Args: + xml_string: string The XML describing a DocumentList feed. + + Returns: + A DocumentListFeed object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(DocumentListFeed, xml_string) + + +class DocumentListAclFeed(gdata.GDataFeed): + """A DocList ACL feed flavor of a Atom feed""" + + _tag = gdata.GDataFeed._tag + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [DocumentListAclEntry]) + + +def DocumentListAclFeedFromString(xml_string): + """Converts an XML string into a DocumentListAclFeed object. + + Args: + xml_string: string The XML describing a DocumentList feed. + + Returns: + A DocumentListFeed object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(DocumentListAclFeed, xml_string) diff --git a/patches/gdata/docs/client.py b/patches/gdata/docs/client.py new file mode 100755 index 0000000..e0eba88 --- /dev/null +++ b/patches/gdata/docs/client.py @@ -0,0 +1,644 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""DocsClient extends gdata.client.GDClient to streamline DocList API calls.""" + + +__author__ = 'e.bidelman (Eric Bidelman)' + +import mimetypes +import urllib +import atom.data +import atom.http_core +import gdata.client +import gdata.docs.data +import gdata.gauth + + +# Feed URI templates +DOCLIST_FEED_URI = '/feeds/default/private/full/' +FOLDERS_FEED_TEMPLATE = DOCLIST_FEED_URI + '%s/contents' +ACL_FEED_TEMPLATE = DOCLIST_FEED_URI + '%s/acl' +REVISIONS_FEED_TEMPLATE = DOCLIST_FEED_URI + '%s/revisions' + + +class DocsClient(gdata.client.GDClient): + """Client extension for the Google Documents List API.""" + + host = 'docs.google.com' # default server for the API + api_version = '3.0' # default major version for the service. + auth_service = 'writely' + auth_scopes = gdata.gauth.AUTH_SCOPES['writely'] + ssl = True + + def __init__(self, auth_token=None, **kwargs): + """Constructs a new client for the DocList API. + + Args: + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: The other parameters to pass to gdata.client.GDClient constructor. + """ + gdata.client.GDClient.__init__(self, auth_token=auth_token, **kwargs) + + def get_file_content(self, uri, auth_token=None, **kwargs): + """Fetches the file content from the specified uri. + + This method is useful for downloading/exporting a file within enviornments + like Google App Engine, where the user does not have the ability to write + the file to a local disk. + + Args: + uri: str The full URL to fetch the file contents from. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.request(). + + Returns: + The binary file content. + + Raises: + gdata.client.RequestError: on error response from server. + """ + server_response = self.request('GET', uri, auth_token=auth_token, **kwargs) + if server_response.status != 200: + raise gdata.client.RequestError, {'status': server_response.status, + 'reason': server_response.reason, + 'body': server_response.read()} + return server_response.read() + + GetFileContent = get_file_content + + def _download_file(self, uri, file_path, auth_token=None, **kwargs): + """Downloads a file to disk from the specified URI. + + Note: to download a file in memory, use the GetFileContent() method. + + Args: + uri: str The full URL to download the file from. + file_path: str The full path to save the file to. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.get_file_content(). + + Raises: + gdata.client.RequestError: on error response from server. + """ + f = open(file_path, 'wb') + try: + f.write(self.get_file_content(uri, auth_token=auth_token, **kwargs)) + except gdata.client.RequestError, e: + f.close() + raise e + f.flush() + f.close() + + _DownloadFile = _download_file + + def get_doclist(self, uri=None, limit=None, auth_token=None, **kwargs): + """Retrieves the main doclist feed containing the user's items. + + Args: + uri: str (optional) A URI to query the doclist feed. + limit: int (optional) A maximum cap for the number of results to + return in the feed. By default, the API returns a maximum of 100 + per page. Thus, if you set limit=5000, you will get <= 5000 + documents (guarenteed no more than 5000), and will need to follow the + feed's next links (feed.GetNextLink()) to the rest. See + get_everything(). Similarly, if you set limit=50, only <= 50 + documents are returned. Note: if the max-results parameter is set in + the uri parameter, it is chosen over a value set for limit. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + gdata.docs.data.DocList feed. + """ + if uri is None: + uri = DOCLIST_FEED_URI + + if isinstance(uri, (str, unicode)): + uri = atom.http_core.Uri.parse_uri(uri) + + # Add max-results param if it wasn't included in the uri. + if limit is not None and not 'max-results' in uri.query: + uri.query['max-results'] = limit + + return self.get_feed(uri, desired_class=gdata.docs.data.DocList, + auth_token=auth_token, **kwargs) + + GetDocList = get_doclist + + def get_doc(self, resource_id, etag=None, auth_token=None, uri=None, + **kwargs): + """Retrieves a particular document given by its resource id. + + Args: + resource_id: str The document/item's resource id. Example spreadsheet: + 'spreadsheet%3A0A1234567890'. + etag: str (optional) The document/item's etag value to be used in a + conditional GET. See http://code.google.com/apis/documents/docs/3.0/ + developers_guide_protocol.html#RetrievingCached. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + uri: (optional) URI to use for this request. Translates to uri + + resource_id. + kwargs: Other parameters to pass to self.get_entry(). + + Returns: + A gdata.docs.data.DocsEntry object representing the retrieved entry. + + Raises: + ValueError if the resource_id is not a valid format. + """ + match = gdata.docs.data.RESOURCE_ID_PATTERN.match(resource_id) + if match is None: + raise ValueError, 'Invalid resource id: %s' % resource_id + if uri is None: + uri = DOCLIST_FEED_URI + return self.get_entry( + uri + resource_id, etag=etag, + desired_class=gdata.docs.data.DocsEntry, + auth_token=auth_token, **kwargs) + + GetDoc = get_doc + + def get_everything(self, uri=None, auth_token=None, **kwargs): + """Retrieves the user's entire doc list. + + The method makes multiple HTTP requests (by following the feed's next links) + in order to fetch the user's entire document list. + + Args: + uri: str (optional) A URI to query the doclist feed with. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.GetDocList(). + + Returns: + A list of gdata.docs.data.DocsEntry objects representing the retrieved + entries. + """ + if uri is None: + uri = DOCLIST_FEED_URI + + feed = self.GetDocList(uri=uri, auth_token=auth_token, **kwargs) + entries = feed.entry + + while feed.GetNextLink() is not None: + feed = self.GetDocList( + feed.GetNextLink().href, auth_token=auth_token, **kwargs) + entries.extend(feed.entry) + + return entries + + GetEverything = get_everything + + def get_acl_permissions(self, resource_id, auth_token=None, uri=None, + **kwargs): + """Retrieves a the ACL sharing permissions for a document. + + Args: + resource_id: str The document/item's resource id. Example for pdf: + 'pdf%3A0A1234567890'. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + uri: (optional) URI to use when making this request. Must be a template, + with a %s for where to put the resource_id. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + A gdata.docs.data.AclFeed object representing the document's ACL entries. + + Raises: + ValueError if the resource_id is not a valid format. + """ + match = gdata.docs.data.RESOURCE_ID_PATTERN.match(resource_id) + if match is None: + raise ValueError, 'Invalid resource id: %s' % resource_id + if uri is None: + uri = ACL_FEED_TEMPLATE + + return self.get_feed( + uri % resource_id, desired_class=gdata.docs.data.AclFeed, + auth_token=auth_token, **kwargs) + + GetAclPermissions = get_acl_permissions + + def get_revisions(self, resource_id, auth_token=None, uri=None, **kwargs): + """Retrieves the revision history for a document. + + Args: + resource_id: str The document/item's resource id. Example for pdf: + 'pdf%3A0A1234567890'. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + uri: (optional) URI to use when making this request. Must be a template, + with a %s for where to put the resource_id. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + A gdata.docs.data.RevisionFeed representing the document's revisions. + + Raises: + ValueError if the resource_id is not a valid format. + """ + match = gdata.docs.data.RESOURCE_ID_PATTERN.match(resource_id) + if match is None: + raise ValueError, 'Invalid resource id: %s' % resource_id + if uri is None: + uri = REVISIONS_FEED_TEMPLATE + + return self.get_feed( + uri % resource_id, + desired_class=gdata.docs.data.RevisionFeed, auth_token=auth_token, + **kwargs) + + GetRevisions = get_revisions + + def create(self, doc_type, title, folder_or_id=None, writers_can_invite=None, + auth_token=None, uri=None, **kwargs): + """Creates a new item in the user's doclist. + + Args: + doc_type: str The type of object to create. For example: 'document', + 'spreadsheet', 'folder', 'presentation'. + title: str A title for the document. + folder_or_id: gdata.docs.data.DocsEntry or str (optional) Folder entry or + the resouce id of a folder to create the object under. Note: A valid + resource id for a folder is of the form: folder%3Afolder_id. + writers_can_invite: bool (optional) False prevents collaborators from + being able to invite others to edit or view the document. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + uri: (optional) URI to use when making this request. Must be a template, + with a %s for where to put the resource_id. + kwargs: Other parameters to pass to self.post(). + + Returns: + gdata.docs.data.DocsEntry containing information newly created item. + """ + entry = gdata.docs.data.DocsEntry(title=atom.data.Title(text=title)) + entry.category.append(gdata.docs.data.make_kind_category(doc_type)) + + if isinstance(writers_can_invite, gdata.docs.data.WritersCanInvite): + entry.writers_can_invite = writers_can_invite + elif isinstance(writers_can_invite, bool): + entry.writers_can_invite = gdata.docs.data.WritersCanInvite( + value=str(writers_can_invite).lower()) + + if uri is None: + uri = DOCLIST_FEED_URI + + if folder_or_id is not None: + if isinstance(folder_or_id, gdata.docs.data.DocsEntry): + # Verify that we're uploading the resource into to a folder. + if folder_or_id.get_document_type() == gdata.docs.data.FOLDER_LABEL: + uri = folder_or_id.content.src + else: + raise gdata.client.Error, 'Trying to upload item to a non-folder.' + else: + uri = FOLDERS_FEED_TEMPLATE % folder_or_id + + return self.post(entry, uri, auth_token=auth_token, **kwargs) + + Create = create + + def copy(self, source_entry, title, auth_token=None, uri=None, **kwargs): + """Copies a native Google document, spreadsheet, or presentation. + + Note: arbitrary file types and PDFs do not support this feature. + + Args: + source_entry: gdata.docs.data.DocsEntry An object representing the source + document/folder. + title: str A title for the new document. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + uri: (optional) URI to use when making this request. Must be a template, + with a %s for where to put the resource_id. + kwargs: Other parameters to pass to self.post(). + + Returns: + A gdata.docs.data.DocsEntry of the duplicated document. + """ + entry = gdata.docs.data.DocsEntry( + title=atom.data.Title(text=title), + id=atom.data.Id(text=source_entry.GetSelfLink().href)) + if uri is None: + uri = DOCLIST_FEED_URI + return self.post(entry, uri, auth_token=auth_token, **kwargs) + + Copy = copy + + def move(self, source_entry, folder_entry=None, + keep_in_folders=False, auth_token=None, delete_folder_uri=None, + folder_uri=None, **kwargs): + """Moves an item into a different folder (or to the root document list). + + Args: + source_entry: gdata.docs.data.DocsEntry An object representing the source + document/folder. + folder_entry: gdata.docs.data.DocsEntry (optional) An object representing + the destination folder. If None, set keep_in_folders to + True to remove the item from all parent folders. + keep_in_folders: boolean (optional) If True, the source entry + is not removed from any existing parent folders it is in. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + delete_folder_uri: (optional) URI to use when making this request. Must + be a template, with a %s for where to put the resource_id. + folder_uri: (optional) URI to use when making this request. Must be a + template, with a %s for where to put the resource_id. + kwargs: Other parameters to pass to self.post(). + + Returns: + A gdata.docs.data.DocsEntry of the moved entry or True if just moving the + item out of all folders (e.g. Move(source_entry)). + """ + entry = gdata.docs.data.DocsEntry(id=source_entry.id) + + # Remove the item from any folders it is already in. + if not keep_in_folders: + construct_uri = False + if delete_folder_uri is None: + construct_uri = True + for folder in source_entry.InFolders(): + if construct_uri: + uri = '%s/contents/%s' % ( + folder.href, + urllib.quote(source_entry.resource_id.text)) + else: + uri = delete_folder_uri % urllib.quote(source_entry.resource_id.text) + + self.delete(uri, force=True) + + # If we're moving the resource into a folder, verify it is a folder entry. + if folder_entry is not None: + if folder_entry.get_document_type() == gdata.docs.data.FOLDER_LABEL: + if folder_uri is None: + folder_uri = folder_entry.content.src + return self.post(entry, folder_uri, auth_token=auth_token, **kwargs) + else: + raise gdata.client.Error, 'Trying to move item into a non-folder.' + + return True + + Move = move + + def upload(self, media, title, folder_or_uri=None, content_type=None, + auth_token=None, **kwargs): + """Uploads a file to Google Docs. + + Args: + media: A gdata.data.MediaSource object containing the file to be + uploaded or a string of the filepath. + title: str The title of the document on the server after being + uploaded. + folder_or_uri: gdata.docs.data.DocsEntry or str (optional) An object with + a link to the folder or the uri to upload the file to. + Note: A valid uri for a folder is of the form: + /feeds/default/private/full/folder%3Afolder_id/contents + content_type: str (optional) The file's mimetype. If not provided, the + one in the media source object is used or the mimetype is inferred + from the filename (if media is a string). When media is a filename, + it is always recommended to pass in a content type. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.post(). + + Returns: + A gdata.docs.data.DocsEntry containing information about uploaded doc. + """ + uri = None + if folder_or_uri is not None: + if isinstance(folder_or_uri, gdata.docs.data.DocsEntry): + # Verify that we're uploading the resource into to a folder. + if folder_or_uri.get_document_type() == gdata.docs.data.FOLDER_LABEL: + uri = folder_or_uri.content.src + else: + raise gdata.client.Error, 'Trying to upload item to a non-folder.' + else: + uri = folder_or_uri + else: + uri = DOCLIST_FEED_URI + + # Create media source if media is a filepath. + if isinstance(media, (str, unicode)): + mimetype = mimetypes.guess_type(media)[0] + if mimetype is None and content_type is None: + raise ValueError, ("Unknown mimetype. Please pass in the file's " + "content_type") + else: + media = gdata.data.MediaSource(file_path=media, + content_type=content_type) + + entry = gdata.docs.data.DocsEntry(title=atom.data.Title(text=title)) + + return self.post(entry, uri, media_source=media, + desired_class=gdata.docs.data.DocsEntry, + auth_token=auth_token, **kwargs) + + Upload = upload + + def download(self, entry_or_id_or_url, file_path, extra_params=None, + auth_token=None, **kwargs): + """Downloads a file from the Document List to local disk. + + Note: to download a file in memory, use the GetFileContent() method. + + Args: + entry_or_id_or_url: gdata.docs.data.DocsEntry or string representing a + resource id or URL to download the document from (such as the content + src link). + file_path: str The full path to save the file to. + extra_params: dict (optional) A map of any further parameters to control + how the document is downloaded/exported. For example, exporting a + spreadsheet as a .csv: extra_params={'gid': 0, 'exportFormat': 'csv'} + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self._download_file(). + + Raises: + gdata.client.RequestError if the download URL is malformed or the server's + response was not successful. + ValueError if entry_or_id_or_url was a resource id for a filetype + in which the download link cannot be manually constructed (e.g. pdf). + """ + if isinstance(entry_or_id_or_url, gdata.docs.data.DocsEntry): + url = entry_or_id_or_url.content.src + else: + if gdata.docs.data.RESOURCE_ID_PATTERN.match(entry_or_id_or_url): + url = gdata.docs.data.make_content_link_from_resource_id( + entry_or_id_or_url) + else: + url = entry_or_id_or_url + + if extra_params is not None: + if 'exportFormat' in extra_params and url.find('/Export?') == -1: + raise gdata.client.Error, ('This entry type cannot be exported ' + 'as a different format.') + + if 'gid' in extra_params and url.find('spreadsheets') == -1: + raise gdata.client.Error, 'gid param is not valid for this doc type.' + + url += '&' + urllib.urlencode(extra_params) + + self._download_file(url, file_path, auth_token=auth_token, **kwargs) + + Download = download + + def export(self, entry_or_id_or_url, file_path, gid=None, auth_token=None, + **kwargs): + """Exports a document from the Document List in a different format. + + Args: + entry_or_id_or_url: gdata.docs.data.DocsEntry or string representing a + resource id or URL to download the document from (such as the content + src link). + file_path: str The full path to save the file to. The export + format is inferred from the the file extension. + gid: str (optional) grid id for downloading a single grid of a + spreadsheet. The param should only be used for .csv and .tsv + spreadsheet exports. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.download(). + + Raises: + gdata.client.RequestError if the download URL is malformed or the server's + response was not successful. + """ + extra_params = {} + + match = gdata.docs.data.FILE_EXT_PATTERN.match(file_path) + if match: + extra_params['exportFormat'] = match.group(1) + + if gid is not None: + extra_params['gid'] = gid + + self.download(entry_or_id_or_url, file_path, extra_params, + auth_token=auth_token, **kwargs) + + Export = export + + +class DocsQuery(gdata.client.Query): + + def __init__(self, title=None, title_exact=None, opened_min=None, + opened_max=None, edited_min=None, edited_max=None, owner=None, + writer=None, reader=None, show_folders=None, + show_deleted=None, ocr=None, target_language=None, + source_language=None, convert=None, **kwargs): + """Constructs a query URL for the Google Documents List API. + + Args: + title: str (optional) Specifies the search terms for the title of a + document. This parameter used without title_exact will only + submit partial queries, not exact queries. + title_exact: str (optional) Meaningless without title. Possible values + are 'true' and 'false'. Note: Matches are case-insensitive. + opened_min: str (optional) Lower bound on the last time a document was + opened by the current user. Use the RFC 3339 timestamp + format. For example: opened_min='2005-08-09T09:57:00-08:00'. + opened_max: str (optional) Upper bound on the last time a document was + opened by the current user. (See also opened_min.) + edited_min: str (optional) Lower bound on the last time a document was + edited by the current user. This value corresponds to the + edited.text value in the doc's entry object, which + represents changes to the document's content or metadata. + Use the RFC 3339 timestamp format. For example: + edited_min='2005-08-09T09:57:00-08:00' + edited_max: str (optional) Upper bound on the last time a document was + edited by the user. (See also edited_min.) + owner: str (optional) Searches for documents with a specific owner. Use + the email address of the owner. For example: + owner='user@gmail.com' + writer: str (optional) Searches for documents which can be written to + by specific users. Use a single email address or a comma + separated list of email addresses. For example: + writer='user1@gmail.com,user@example.com' + reader: str (optional) Searches for documents which can be read by + specific users. (See also writer.) + show_folders: str (optional) Specifies whether the query should return + folders as well as documents. Possible values are 'true' + and 'false'. Default is false. + show_deleted: str (optional) Specifies whether the query should return + documents which are in the trash as well as other + documents. Possible values are 'true' and 'false'. + Default is false. + ocr: str (optional) Specifies whether to attempt OCR on a .jpg, .png, or + .gif upload. Possible values are 'true' and 'false'. Default is + false. See OCR in the Protocol Guide: + http://code.google.com/apis/documents/docs/3.0/developers_guide_protocol.html#OCR + target_language: str (optional) Specifies the language to translate a + document into. See Document Translation in the Protocol + Guide for a table of possible values: + http://code.google.com/apis/documents/docs/3.0/developers_guide_protocol.html#DocumentTranslation + source_language: str (optional) Specifies the source language of the + original document. Optional when using the translation + service. If not provided, Google will attempt to + auto-detect the source language. See Document + Translation in the Protocol Guide for a table of + possible values (link in target_language). + convert: str (optional) Used when uploading arbitrary file types to + specity if document-type uploads should convert to a native + Google Docs format. Possible values are 'true' and 'false'. + The default is 'true'. + """ + gdata.client.Query.__init__(self, **kwargs) + self.convert = convert + self.title = title + self.title_exact = title_exact + self.opened_min = opened_min + self.opened_max = opened_max + self.edited_min = edited_min + self.edited_max = edited_max + self.owner = owner + self.writer = writer + self.reader = reader + self.show_folders = show_folders + self.show_deleted = show_deleted + self.ocr = ocr + self.target_language = target_language + self.source_language = source_language + + def modify_request(self, http_request): + gdata.client._add_query_param('convert', self.convert, http_request) + gdata.client._add_query_param('title', self.title, http_request) + gdata.client._add_query_param('title-exact', self.title_exact, + http_request) + gdata.client._add_query_param('opened-min', self.opened_min, http_request) + gdata.client._add_query_param('opened-max', self.opened_max, http_request) + gdata.client._add_query_param('edited-min', self.edited_min, http_request) + gdata.client._add_query_param('edited-max', self.edited_max, http_request) + gdata.client._add_query_param('owner', self.owner, http_request) + gdata.client._add_query_param('writer', self.writer, http_request) + gdata.client._add_query_param('reader', self.reader, http_request) + gdata.client._add_query_param('showfolders', self.show_folders, + http_request) + gdata.client._add_query_param('showdeleted', self.show_deleted, + http_request) + gdata.client._add_query_param('ocr', self.ocr, http_request) + gdata.client._add_query_param('targetLanguage', self.target_language, + http_request) + gdata.client._add_query_param('sourceLanguage', self.source_language, + http_request) + gdata.client.Query.modify_request(self, http_request) + + ModifyRequest = modify_request diff --git a/patches/gdata/docs/data.py b/patches/gdata/docs/data.py new file mode 100755 index 0000000..8e54d57 --- /dev/null +++ b/patches/gdata/docs/data.py @@ -0,0 +1,280 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""Data model classes for parsing and generating XML for the DocList Data API""" + +__author__ = 'e.bidelman (Eric Bidelman)' + + +import re +import atom.core +import atom.data +import gdata.acl.data +import gdata.data + +DOCUMENTS_NS = 'http://schemas.google.com/docs/2007' +DOCUMENTS_TEMPLATE = '{http://schemas.google.com/docs/2007}%s' +ACL_FEEDLINK_REL = 'http://schemas.google.com/acl/2007#accessControlList' +REVISION_FEEDLINK_REL = DOCUMENTS_NS + '/revisions' + +# XML Namespaces used in Google Documents entities. +DATA_KIND_SCHEME = 'http://schemas.google.com/g/2005#kind' +DOCUMENT_LABEL = 'document' +SPREADSHEET_LABEL = 'spreadsheet' +PRESENTATION_LABEL = 'presentation' +FOLDER_LABEL = 'folder' +PDF_LABEL = 'pdf' + +LABEL_SCHEME = 'http://schemas.google.com/g/2005/labels' +STARRED_LABEL_TERM = LABEL_SCHEME + '#starred' +TRASHED_LABEL_TERM = LABEL_SCHEME + '#trashed' +HIDDEN_LABEL_TERM = LABEL_SCHEME + '#hidden' +MINE_LABEL_TERM = LABEL_SCHEME + '#mine' +PRIVATE_LABEL_TERM = LABEL_SCHEME + '#private' +SHARED_WITH_DOMAIN_LABEL_TERM = LABEL_SCHEME + '#shared-with-domain' +VIEWED_LABEL_TERM = LABEL_SCHEME + '#viewed' + +DOCS_PARENT_LINK_REL = DOCUMENTS_NS + '#parent' +DOCS_PUBLISH_LINK_REL = DOCUMENTS_NS + '#publish' + +FILE_EXT_PATTERN = re.compile('.*\.([a-zA-Z]{3,}$)') +RESOURCE_ID_PATTERN = re.compile('^([a-z]*)(:|%3A)([\w-]*)$') + +# File extension/mimetype pairs of common format. +MIMETYPES = { + 'CSV': 'text/csv', + 'TSV': 'text/tab-separated-values', + 'TAB': 'text/tab-separated-values', + 'DOC': 'application/msword', + 'DOCX': ('application/vnd.openxmlformats-officedocument.' + 'wordprocessingml.document'), + 'ODS': 'application/x-vnd.oasis.opendocument.spreadsheet', + 'ODT': 'application/vnd.oasis.opendocument.text', + 'RTF': 'application/rtf', + 'SXW': 'application/vnd.sun.xml.writer', + 'TXT': 'text/plain', + 'XLS': 'application/vnd.ms-excel', + 'XLSX': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'PDF': 'application/pdf', + 'PNG': 'image/png', + 'PPT': 'application/vnd.ms-powerpoint', + 'PPS': 'application/vnd.ms-powerpoint', + 'HTM': 'text/html', + 'HTML': 'text/html', + 'ZIP': 'application/zip', + 'SWF': 'application/x-shockwave-flash' + } + + +def make_kind_category(label): + """Builds the appropriate atom.data.Category for the label passed in. + + Args: + label: str The value for the category entry. + + Returns: + An atom.data.Category or None if label is None. + """ + if label is None: + return None + + return atom.data.Category( + scheme=DATA_KIND_SCHEME, term='%s#%s' % (DOCUMENTS_NS, label), label=label) + +MakeKindCategory = make_kind_category + +def make_content_link_from_resource_id(resource_id): + """Constructs export URL for a given resource. + + Args: + resource_id: str The document/item's resource id. Example presentation: + 'presentation%3A0A1234567890'. + + Raises: + gdata.client.ValueError if the resource_id is not a valid format. + """ + match = RESOURCE_ID_PATTERN.match(resource_id) + + if match: + label = match.group(1) + doc_id = match.group(3) + if label == DOCUMENT_LABEL: + return '/feeds/download/documents/Export?docId=%s' % doc_id + if label == PRESENTATION_LABEL: + return '/feeds/download/presentations/Export?docId=%s' % doc_id + if label == SPREADSHEET_LABEL: + return ('https://spreadsheets.google.com/feeds/download/spreadsheets/' + 'Export?key=%s' % doc_id) + raise ValueError, ('Invalid resource id: %s, or manually creating the ' + 'download url for this type of doc is not possible' + % resource_id) + +MakeContentLinkFromResourceId = make_content_link_from_resource_id + + +class ResourceId(atom.core.XmlElement): + """The DocList gd:resourceId element.""" + _qname = gdata.data.GDATA_TEMPLATE % 'resourceId' + + +class LastModifiedBy(atom.data.Person): + """The DocList gd:lastModifiedBy element.""" + _qname = gdata.data.GDATA_TEMPLATE % 'lastModifiedBy' + + +class LastViewed(atom.data.Person): + """The DocList gd:lastViewed element.""" + _qname = gdata.data.GDATA_TEMPLATE % 'lastViewed' + + +class WritersCanInvite(atom.core.XmlElement): + """The DocList docs:writersCanInvite element.""" + _qname = DOCUMENTS_TEMPLATE % 'writersCanInvite' + value = 'value' + + +class QuotaBytesUsed(atom.core.XmlElement): + """The DocList gd:quotaBytesUsed element.""" + _qname = gdata.data.GDATA_TEMPLATE % 'quotaBytesUsed' + + +class Publish(atom.core.XmlElement): + """The DocList docs:publish element.""" + _qname = DOCUMENTS_TEMPLATE % 'publish' + value = 'value' + + +class PublishAuto(atom.core.XmlElement): + """The DocList docs:publishAuto element.""" + _qname = DOCUMENTS_TEMPLATE % 'publishAuto' + value = 'value' + + +class PublishOutsideDomain(atom.core.XmlElement): + """The DocList docs:publishOutsideDomain element.""" + _qname = DOCUMENTS_TEMPLATE % 'publishOutsideDomain' + value = 'value' + + +class DocsEntry(gdata.data.GDEntry): + """A DocList version of an Atom Entry.""" + + last_viewed = LastViewed + last_modified_by = LastModifiedBy + resource_id = ResourceId + writers_can_invite = WritersCanInvite + quota_bytes_used = QuotaBytesUsed + feed_link = [gdata.data.FeedLink] + + def get_document_type(self): + """Extracts the type of document this DocsEntry is. + + This method returns the type of document the DocsEntry represents. Possible + values are document, presentation, spreadsheet, folder, or pdf. + + Returns: + A string representing the type of document. + """ + if self.category: + for category in self.category: + if category.scheme == DATA_KIND_SCHEME: + return category.label + else: + return None + + GetDocumentType = get_document_type + + def get_acl_feed_link(self): + """Extracts the DocsEntry's ACL feed <gd:feedLink>. + + Returns: + A gdata.data.FeedLink object. + """ + for feed_link in self.feed_link: + if feed_link.rel == ACL_FEEDLINK_REL: + return feed_link + return None + + GetAclFeedLink = get_acl_feed_link + + def get_revisions_feed_link(self): + """Extracts the DocsEntry's revisions feed <gd:feedLink>. + + Returns: + A gdata.data.FeedLink object. + """ + for feed_link in self.feed_link: + if feed_link.rel == REVISION_FEEDLINK_REL: + return feed_link + return None + + GetRevisionsFeedLink = get_revisions_feed_link + + def in_folders(self): + """Returns the parents link(s) (folders) of this entry.""" + links = [] + for link in self.link: + if link.rel == DOCS_PARENT_LINK_REL and link.href: + links.append(link) + return links + + InFolders = in_folders + + +class Acl(gdata.acl.data.AclEntry): + """A document ACL entry.""" + + +class DocList(gdata.data.GDFeed): + """The main DocList feed containing a list of Google Documents.""" + entry = [DocsEntry] + + +class AclFeed(gdata.acl.data.AclFeed): + """A DocList ACL feed.""" + entry = [Acl] + + +class Revision(gdata.data.GDEntry): + """A document Revision entry.""" + publish = Publish + publish_auto = PublishAuto + publish_outside_domain = PublishOutsideDomain + + def find_publish_link(self): + """Get the link that points to the published document on the web. + + Returns: + A str for the URL in the link with a rel ending in #publish. + """ + return self.find_url(DOCS_PUBLISH_LINK_REL) + + FindPublishLink = find_publish_link + + def get_publish_link(self): + """Get the link that points to the published document on the web. + + Returns: + A gdata.data.Link for the link with a rel ending in #publish. + """ + return self.get_link(DOCS_PUBLISH_LINK_REL) + + GetPublishLink = get_publish_link + + +class RevisionFeed(gdata.data.GDFeed): + """A DocList Revision feed.""" + entry = [Revision] diff --git a/patches/gdata/docs/service.py b/patches/gdata/docs/service.py new file mode 100644 index 0000000..b6f39f6 --- /dev/null +++ b/patches/gdata/docs/service.py @@ -0,0 +1,618 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""DocsService extends the GDataService to streamline Google Documents + operations. + + DocsService: Provides methods to query feeds and manipulate items. + Extends GDataService. + + DocumentQuery: Queries a Google Document list feed. + + DocumentAclQuery: Queries a Google Document Acl feed. +""" + + +__author__ = ('api.jfisher (Jeff Fisher), ' + 'e.bidelman (Eric Bidelman)') + +import re +import atom +import gdata.service +import gdata.docs +import urllib + +# XML Namespaces used in Google Documents entities. +DATA_KIND_SCHEME = gdata.GDATA_NAMESPACE + '#kind' +DOCUMENT_LABEL = 'document' +SPREADSHEET_LABEL = 'spreadsheet' +PRESENTATION_LABEL = 'presentation' +FOLDER_LABEL = 'folder' +PDF_LABEL = 'pdf' + +LABEL_SCHEME = gdata.GDATA_NAMESPACE + '/labels' +STARRED_LABEL_TERM = LABEL_SCHEME + '#starred' +TRASHED_LABEL_TERM = LABEL_SCHEME + '#trashed' +HIDDEN_LABEL_TERM = LABEL_SCHEME + '#hidden' +MINE_LABEL_TERM = LABEL_SCHEME + '#mine' +PRIVATE_LABEL_TERM = LABEL_SCHEME + '#private' +SHARED_WITH_DOMAIN_LABEL_TERM = LABEL_SCHEME + '#shared-with-domain' +VIEWED_LABEL_TERM = LABEL_SCHEME + '#viewed' + +FOLDERS_SCHEME_PREFIX = gdata.docs.DOCUMENTS_NAMESPACE + '/folders/' + +# File extensions of documents that are permitted to be uploaded or downloaded. +SUPPORTED_FILETYPES = { + 'CSV': 'text/csv', + 'TSV': 'text/tab-separated-values', + 'TAB': 'text/tab-separated-values', + 'DOC': 'application/msword', + 'DOCX': ('application/vnd.openxmlformats-officedocument.' + 'wordprocessingml.document'), + 'ODS': 'application/x-vnd.oasis.opendocument.spreadsheet', + 'ODT': 'application/vnd.oasis.opendocument.text', + 'RTF': 'application/rtf', + 'SXW': 'application/vnd.sun.xml.writer', + 'TXT': 'text/plain', + 'XLS': 'application/vnd.ms-excel', + 'XLSX': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'PDF': 'application/pdf', + 'PNG': 'image/png', + 'PPT': 'application/vnd.ms-powerpoint', + 'PPS': 'application/vnd.ms-powerpoint', + 'HTM': 'text/html', + 'HTML': 'text/html', + 'ZIP': 'application/zip', + 'SWF': 'application/x-shockwave-flash' + } + + +class DocsService(gdata.service.GDataService): + + """Client extension for the Google Documents service Document List feed.""" + + __FILE_EXT_PATTERN = re.compile('.*\.([a-zA-Z]{3,}$)') + __RESOURCE_ID_PATTERN = re.compile('^([a-z]*)(:|%3A)([\w-]*)$') + + def __init__(self, email=None, password=None, source=None, + server='docs.google.com', additional_headers=None, **kwargs): + """Creates a client for the Google Documents service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'docs.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='writely', source=source, + server=server, additional_headers=additional_headers, **kwargs) + self.ssl = True + + def _MakeKindCategory(self, label): + if label is None: + return None + return atom.Category(scheme=DATA_KIND_SCHEME, + term=gdata.docs.DOCUMENTS_NAMESPACE + '#' + label, label=label) + + def _MakeContentLinkFromId(self, resource_id): + match = self.__RESOURCE_ID_PATTERN.match(resource_id) + label = match.group(1) + doc_id = match.group(3) + if label == DOCUMENT_LABEL: + return '/feeds/download/documents/Export?docId=%s' % doc_id + if label == PRESENTATION_LABEL: + return '/feeds/download/presentations/Export?docId=%s' % doc_id + if label == SPREADSHEET_LABEL: + return ('https://spreadsheets.google.com/feeds/download/spreadsheets/' + 'Export?key=%s' % doc_id) + raise ValueError, 'Invalid resource id: %s' % resource_id + + def _UploadFile(self, media_source, title, category, folder_or_uri=None): + """Uploads a file to the Document List feed. + + Args: + media_source: A gdata.MediaSource object containing the file to be + uploaded. + title: string The title of the document on the server after being + uploaded. + category: An atom.Category object specifying the appropriate document + type. + folder_or_uri: DocumentListEntry or string (optional) An object with a + link to a folder or a uri to a folder to upload to. + Note: A valid uri for a folder is of the form: + /feeds/folders/private/full/folder%3Afolder_id + + Returns: + A DocumentListEntry containing information about the document created on + the Google Documents service. + """ + if folder_or_uri: + try: + uri = folder_or_uri.content.src + except AttributeError: + uri = folder_or_uri + else: + uri = '/feeds/documents/private/full' + + entry = gdata.docs.DocumentListEntry() + entry.title = atom.Title(text=title) + if category is not None: + entry.category.append(category) + entry = self.Post(entry, uri, media_source=media_source, + extra_headers={'Slug': media_source.file_name}, + converter=gdata.docs.DocumentListEntryFromString) + return entry + + def _DownloadFile(self, uri, file_path): + """Downloads a file. + + Args: + uri: string The full Export URL to download the file from. + file_path: string The full path to save the file to. + + Raises: + RequestError: on error response from server. + """ + server_response = self.request('GET', uri) + response_body = server_response.read() + timeout = 5 + while server_response.status == 302 and timeout > 0: + server_response = self.request('GET', + server_response.getheader('Location')) + response_body = server_response.read() + timeout -= 1 + if server_response.status != 200: + raise gdata.service.RequestError, {'status': server_response.status, + 'reason': server_response.reason, + 'body': response_body} + f = open(file_path, 'wb') + f.write(response_body) + f.flush() + f.close() + + def MoveIntoFolder(self, source_entry, folder_entry): + """Moves a document into a folder in the Document List Feed. + + Args: + source_entry: DocumentListEntry An object representing the source + document/folder. + folder_entry: DocumentListEntry An object with a link to the destination + folder. + + Returns: + A DocumentListEntry containing information about the document created on + the Google Documents service. + """ + entry = gdata.docs.DocumentListEntry() + entry.id = source_entry.id + entry = self.Post(entry, folder_entry.content.src, + converter=gdata.docs.DocumentListEntryFromString) + return entry + + def Query(self, uri, converter=gdata.docs.DocumentListFeedFromString): + """Queries the Document List feed and returns the resulting feed of + entries. + + Args: + uri: string The full URI to be queried. This can contain query + parameters, a hostname, or simply the relative path to a Document + List feed. The DocumentQuery object is useful when constructing + query parameters. + converter: func (optional) A function which will be executed on the + retrieved item, generally to render it into a Python object. + By default the DocumentListFeedFromString function is used to + return a DocumentListFeed object. This is because most feed + queries will result in a feed and not a single entry. + """ + return self.Get(uri, converter=converter) + + def QueryDocumentListFeed(self, uri): + """Retrieves a DocumentListFeed by retrieving a URI based off the Document + List feed, including any query parameters. A DocumentQuery object can + be used to construct these parameters. + + Args: + uri: string The URI of the feed being retrieved possibly with query + parameters. + + Returns: + A DocumentListFeed object representing the feed returned by the server. + """ + return self.Get(uri, converter=gdata.docs.DocumentListFeedFromString) + + def GetDocumentListEntry(self, uri): + """Retrieves a particular DocumentListEntry by its unique URI. + + Args: + uri: string The unique URI of an entry in a Document List feed. + + Returns: + A DocumentListEntry object representing the retrieved entry. + """ + return self.Get(uri, converter=gdata.docs.DocumentListEntryFromString) + + def GetDocumentListFeed(self, uri=None): + """Retrieves a feed containing all of a user's documents. + + Args: + uri: string A full URI to query the Document List feed. + """ + if not uri: + uri = gdata.docs.service.DocumentQuery().ToUri() + return self.QueryDocumentListFeed(uri) + + def GetDocumentListAclEntry(self, uri): + """Retrieves a particular DocumentListAclEntry by its unique URI. + + Args: + uri: string The unique URI of an entry in a Document List feed. + + Returns: + A DocumentListAclEntry object representing the retrieved entry. + """ + return self.Get(uri, converter=gdata.docs.DocumentListAclEntryFromString) + + def GetDocumentListAclFeed(self, uri): + """Retrieves a feed containing all of a user's documents. + + Args: + uri: string The URI of a document's Acl feed to retrieve. + + Returns: + A DocumentListAclFeed object representing the ACL feed + returned by the server. + """ + return self.Get(uri, converter=gdata.docs.DocumentListAclFeedFromString) + + def Upload(self, media_source, title, folder_or_uri=None, label=None): + """Uploads a document inside of a MediaSource object to the Document List + feed with the given title. + + Args: + media_source: MediaSource The gdata.MediaSource object containing a + document file to be uploaded. + title: string The title of the document on the server after being + uploaded. + folder_or_uri: DocumentListEntry or string (optional) An object with a + link to a folder or a uri to a folder to upload to. + Note: A valid uri for a folder is of the form: + /feeds/folders/private/full/folder%3Afolder_id + label: optional label describing the type of the document to be created. + + Returns: + A DocumentListEntry containing information about the document created + on the Google Documents service. + """ + + return self._UploadFile(media_source, title, self._MakeKindCategory(label), + folder_or_uri) + + def Download(self, entry_or_id_or_url, file_path, export_format=None, + gid=None, extra_params=None): + """Downloads a document from the Document List. + + Args: + entry_or_id_or_url: a DocumentListEntry, or the resource id of an entry, + or a url to download from (such as the content src). + file_path: string The full path to save the file to. + export_format: the format to convert to, if conversion is required. + gid: grid id, for downloading a single grid of a spreadsheet + extra_params: a map of any further parameters to control how the document + is downloaded + + Raises: + RequestError if the service does not respond with success + """ + + if isinstance(entry_or_id_or_url, gdata.docs.DocumentListEntry): + url = entry_or_id_or_url.content.src + else: + if self.__RESOURCE_ID_PATTERN.match(entry_or_id_or_url): + url = self._MakeContentLinkFromId(entry_or_id_or_url) + else: + url = entry_or_id_or_url + + if export_format is not None: + if url.find('/Export?') == -1: + raise gdata.service.Error, ('This entry cannot be exported ' + 'as a different format') + url += '&exportFormat=%s' % export_format + + if gid is not None: + if url.find('spreadsheets') == -1: + raise gdata.service.Error, 'grid id param is not valid for this entry' + url += '&gid=%s' % gid + + if extra_params: + url += '&' + urllib.urlencode(extra_params) + + self._DownloadFile(url, file_path) + + def Export(self, entry_or_id_or_url, file_path, gid=None, extra_params=None): + """Downloads a document from the Document List in a different format. + + Args: + entry_or_id_or_url: a DocumentListEntry, or the resource id of an entry, + or a url to download from (such as the content src). + file_path: string The full path to save the file to. The export + format is inferred from the the file extension. + gid: grid id, for downloading a single grid of a spreadsheet + extra_params: a map of any further parameters to control how the document + is downloaded + + Raises: + RequestError if the service does not respond with success + """ + ext = None + match = self.__FILE_EXT_PATTERN.match(file_path) + if match: + ext = match.group(1) + self.Download(entry_or_id_or_url, file_path, ext, gid, extra_params) + + def CreateFolder(self, title, folder_or_uri=None): + """Creates a folder in the Document List feed. + + Args: + title: string The title of the folder on the server after being created. + folder_or_uri: DocumentListEntry or string (optional) An object with a + link to a folder or a uri to a folder to upload to. + Note: A valid uri for a folder is of the form: + /feeds/folders/private/full/folder%3Afolder_id + + Returns: + A DocumentListEntry containing information about the folder created on + the Google Documents service. + """ + if folder_or_uri: + try: + uri = folder_or_uri.content.src + except AttributeError: + uri = folder_or_uri + else: + uri = '/feeds/documents/private/full' + + folder_entry = gdata.docs.DocumentListEntry() + folder_entry.title = atom.Title(text=title) + folder_entry.category.append(self._MakeKindCategory(FOLDER_LABEL)) + folder_entry = self.Post(folder_entry, uri, + converter=gdata.docs.DocumentListEntryFromString) + + return folder_entry + + + def MoveOutOfFolder(self, source_entry): + """Moves a document into a folder in the Document List Feed. + + Args: + source_entry: DocumentListEntry An object representing the source + document/folder. + + Returns: + True if the entry was moved out. + """ + return self.Delete(source_entry.GetEditLink().href) + + # Deprecated methods + + #@atom.deprecated('Please use Upload instead') + def UploadPresentation(self, media_source, title, folder_or_uri=None): + """Uploads a presentation inside of a MediaSource object to the Document + List feed with the given title. + + This method is deprecated, use Upload instead. + + Args: + media_source: MediaSource The MediaSource object containing a + presentation file to be uploaded. + title: string The title of the presentation on the server after being + uploaded. + folder_or_uri: DocumentListEntry or string (optional) An object with a + link to a folder or a uri to a folder to upload to. + Note: A valid uri for a folder is of the form: + /feeds/folders/private/full/folder%3Afolder_id + + Returns: + A DocumentListEntry containing information about the presentation created + on the Google Documents service. + """ + return self._UploadFile( + media_source, title, self._MakeKindCategory(PRESENTATION_LABEL), + folder_or_uri=folder_or_uri) + + UploadPresentation = atom.deprecated('Please use Upload instead')( + UploadPresentation) + + #@atom.deprecated('Please use Upload instead') + def UploadSpreadsheet(self, media_source, title, folder_or_uri=None): + """Uploads a spreadsheet inside of a MediaSource object to the Document + List feed with the given title. + + This method is deprecated, use Upload instead. + + Args: + media_source: MediaSource The MediaSource object containing a spreadsheet + file to be uploaded. + title: string The title of the spreadsheet on the server after being + uploaded. + folder_or_uri: DocumentListEntry or string (optional) An object with a + link to a folder or a uri to a folder to upload to. + Note: A valid uri for a folder is of the form: + /feeds/folders/private/full/folder%3Afolder_id + + Returns: + A DocumentListEntry containing information about the spreadsheet created + on the Google Documents service. + """ + return self._UploadFile( + media_source, title, self._MakeKindCategory(SPREADSHEET_LABEL), + folder_or_uri=folder_or_uri) + + UploadSpreadsheet = atom.deprecated('Please use Upload instead')( + UploadSpreadsheet) + + #@atom.deprecated('Please use Upload instead') + def UploadDocument(self, media_source, title, folder_or_uri=None): + """Uploads a document inside of a MediaSource object to the Document List + feed with the given title. + + This method is deprecated, use Upload instead. + + Args: + media_source: MediaSource The gdata.MediaSource object containing a + document file to be uploaded. + title: string The title of the document on the server after being + uploaded. + folder_or_uri: DocumentListEntry or string (optional) An object with a + link to a folder or a uri to a folder to upload to. + Note: A valid uri for a folder is of the form: + /feeds/folders/private/full/folder%3Afolder_id + + Returns: + A DocumentListEntry containing information about the document created + on the Google Documents service. + """ + return self._UploadFile( + media_source, title, self._MakeKindCategory(DOCUMENT_LABEL), + folder_or_uri=folder_or_uri) + + UploadDocument = atom.deprecated('Please use Upload instead')( + UploadDocument) + + """Calling any of these functions is the same as calling Export""" + DownloadDocument = atom.deprecated('Please use Export instead')(Export) + DownloadPresentation = atom.deprecated('Please use Export instead')(Export) + DownloadSpreadsheet = atom.deprecated('Please use Export instead')(Export) + + """Calling any of these functions is the same as calling MoveIntoFolder""" + MoveDocumentIntoFolder = atom.deprecated( + 'Please use MoveIntoFolder instead')(MoveIntoFolder) + MovePresentationIntoFolder = atom.deprecated( + 'Please use MoveIntoFolder instead')(MoveIntoFolder) + MoveSpreadsheetIntoFolder = atom.deprecated( + 'Please use MoveIntoFolder instead')(MoveIntoFolder) + MoveFolderIntoFolder = atom.deprecated( + 'Please use MoveIntoFolder instead')(MoveIntoFolder) + + +class DocumentQuery(gdata.service.Query): + + """Object used to construct a URI to query the Google Document List feed""" + + def __init__(self, feed='/feeds/documents', visibility='private', + projection='full', text_query=None, params=None, + categories=None): + """Constructor for Document List Query + + Args: + feed: string (optional) The path for the feed. (e.g. '/feeds/documents') + visibility: string (optional) The visibility chosen for the current feed. + projection: string (optional) The projection chosen for the current feed. + text_query: string (optional) The contents of the q query parameter. This + string is URL escaped upon conversion to a URI. + params: dict (optional) Parameter value string pairs which become URL + params when translated to a URI. These parameters are added to + the query's items. + categories: list (optional) List of category strings which should be + included as query categories. See gdata.service.Query for + additional documentation. + + Yields: + A DocumentQuery object used to construct a URI based on the Document + List feed. + """ + self.visibility = visibility + self.projection = projection + gdata.service.Query.__init__(self, feed, text_query, params, categories) + + def ToUri(self): + """Generates a URI from the query parameters set in the object. + + Returns: + A string containing the URI used to retrieve entries from the Document + List feed. + """ + old_feed = self.feed + self.feed = '/'.join([old_feed, self.visibility, self.projection]) + new_feed = gdata.service.Query.ToUri(self) + self.feed = old_feed + return new_feed + + def AddNamedFolder(self, email, folder_name): + """Adds a named folder category, qualified by a schema. + + This function lets you query for documents that are contained inside a + named folder without fear of collision with other categories. + + Args: + email: string The email of the user who owns the folder. + folder_name: string The name of the folder. + + Returns: + The string of the category that was added to the object. + """ + + category = '{%s%s}%s' % (FOLDERS_SCHEME_PREFIX, email, folder_name) + self.categories.append(category) + return category + + def RemoveNamedFolder(self, email, folder_name): + """Removes a named folder category, qualified by a schema. + + Args: + email: string The email of the user who owns the folder. + folder_name: string The name of the folder. + + Returns: + The string of the category that was removed to the object. + """ + category = '{%s%s}%s' % (FOLDERS_SCHEME_PREFIX, email, folder_name) + self.categories.remove(category) + return category + + +class DocumentAclQuery(gdata.service.Query): + + """Object used to construct a URI to query a Document's ACL feed""" + + def __init__(self, resource_id, feed='/feeds/acl/private/full'): + """Constructor for Document ACL Query + + Args: + resource_id: string The resource id. (e.g. 'document%3Adocument_id', + 'spreadsheet%3Aspreadsheet_id', etc.) + feed: string (optional) The path for the feed. + (e.g. '/feeds/acl/private/full') + + Yields: + A DocumentAclQuery object used to construct a URI based on the Document + ACL feed. + """ + self.resource_id = resource_id + gdata.service.Query.__init__(self, feed) + + def ToUri(self): + """Generates a URI from the query parameters set in the object. + + Returns: + A string containing the URI used to retrieve entries from the Document + ACL feed. + """ + return '%s/%s' % (gdata.service.Query.ToUri(self), self.resource_id) diff --git a/patches/gdata/dublincore/__init__.py b/patches/gdata/dublincore/__init__.py new file mode 100644 index 0000000..22071f7 --- /dev/null +++ b/patches/gdata/dublincore/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# +# Copyright (C) 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. diff --git a/patches/gdata/dublincore/data.py b/patches/gdata/dublincore/data.py new file mode 100644 index 0000000..c6345c1 --- /dev/null +++ b/patches/gdata/dublincore/data.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Contains the data classes of the Dublin Core Metadata Initiative (DCMI) Extension""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core + + +DC_TEMPLATE = '{http://purl.org/dc/terms/}%s' + + +class Creator(atom.core.XmlElement): + """Entity primarily responsible for making the resource.""" + _qname = DC_TEMPLATE % 'creator' + + +class Date(atom.core.XmlElement): + """Point or period of time associated with an event in the lifecycle of the resource.""" + _qname = DC_TEMPLATE % 'date' + + +class Description(atom.core.XmlElement): + """Account of the resource.""" + _qname = DC_TEMPLATE % 'description' + + +class Format(atom.core.XmlElement): + """File format, physical medium, or dimensions of the resource.""" + _qname = DC_TEMPLATE % 'format' + + +class Identifier(atom.core.XmlElement): + """An unambiguous reference to the resource within a given context.""" + _qname = DC_TEMPLATE % 'identifier' + + +class Language(atom.core.XmlElement): + """Language of the resource.""" + _qname = DC_TEMPLATE % 'language' + + +class Publisher(atom.core.XmlElement): + """Entity responsible for making the resource available.""" + _qname = DC_TEMPLATE % 'publisher' + + +class Rights(atom.core.XmlElement): + """Information about rights held in and over the resource.""" + _qname = DC_TEMPLATE % 'rights' + + +class Subject(atom.core.XmlElement): + """Topic of the resource.""" + _qname = DC_TEMPLATE % 'subject' + + +class Title(atom.core.XmlElement): + """Name given to the resource.""" + _qname = DC_TEMPLATE % 'title' + + diff --git a/patches/gdata/exif/__init__.py b/patches/gdata/exif/__init__.py new file mode 100644 index 0000000..7f1f9c2 --- /dev/null +++ b/patches/gdata/exif/__init__.py @@ -0,0 +1,217 @@ +# -*-*- encoding: utf-8 -*-*- +# +# This is gdata.photos.exif, implementing the exif namespace in gdata +# +# $Id: __init__.py 81 2007-10-03 14:41:42Z havard.gulldahl $ +# +# Copyright 2007 HÃ¥vard Gulldahl +# Portions copyright 2007 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 maps elements from the {EXIF} namespace[1] to GData objects. +These elements describe image data, using exif attributes[2]. + +Picasa Web Albums uses the exif namespace to represent Exif data encoded +in a photo [3]. + +Picasa Web Albums uses the following exif elements: +exif:distance +exif:exposure +exif:flash +exif:focallength +exif:fstop +exif:imageUniqueID +exif:iso +exif:make +exif:model +exif:tags +exif:time + +[1]: http://schemas.google.com/photos/exif/2007. +[2]: http://en.wikipedia.org/wiki/Exif +[3]: http://code.google.com/apis/picasaweb/reference.html#exif_reference +""" + + +__author__ = u'havard@gulldahl.no'# (HÃ¥vard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__ +__license__ = 'Apache License v2' + + +import atom +import gdata + +EXIF_NAMESPACE = 'http://schemas.google.com/photos/exif/2007' + +class ExifBaseElement(atom.AtomBase): + """Base class for elements in the EXIF_NAMESPACE (%s). To add new elements, you only need to add the element tag name to self._tag + """ % EXIF_NAMESPACE + + _tag = '' + _namespace = EXIF_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, name=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +class Distance(ExifBaseElement): + "(float) The distance to the subject, e.g. 0.0" + + _tag = 'distance' +def DistanceFromString(xml_string): + return atom.CreateClassFromXMLString(Distance, xml_string) + +class Exposure(ExifBaseElement): + "(float) The exposure time used, e.g. 0.025 or 8.0E4" + + _tag = 'exposure' +def ExposureFromString(xml_string): + return atom.CreateClassFromXMLString(Exposure, xml_string) + +class Flash(ExifBaseElement): + """(string) Boolean value indicating whether the flash was used. + The .text attribute will either be `true' or `false' + + As a convenience, this object's .bool method will return what you want, + so you can say: + + flash_used = bool(Flash) + + """ + + _tag = 'flash' + def __bool__(self): + if self.text.lower() in ('true','false'): + return self.text.lower() == 'true' +def FlashFromString(xml_string): + return atom.CreateClassFromXMLString(Flash, xml_string) + +class Focallength(ExifBaseElement): + "(float) The focal length used, e.g. 23.7" + + _tag = 'focallength' +def FocallengthFromString(xml_string): + return atom.CreateClassFromXMLString(Focallength, xml_string) + +class Fstop(ExifBaseElement): + "(float) The fstop value used, e.g. 5.0" + + _tag = 'fstop' +def FstopFromString(xml_string): + return atom.CreateClassFromXMLString(Fstop, xml_string) + +class ImageUniqueID(ExifBaseElement): + "(string) The unique image ID for the photo. Generated by Google Photo servers" + + _tag = 'imageUniqueID' +def ImageUniqueIDFromString(xml_string): + return atom.CreateClassFromXMLString(ImageUniqueID, xml_string) + +class Iso(ExifBaseElement): + "(int) The iso equivalent value used, e.g. 200" + + _tag = 'iso' +def IsoFromString(xml_string): + return atom.CreateClassFromXMLString(Iso, xml_string) + +class Make(ExifBaseElement): + "(string) The make of the camera used, e.g. Fictitious Camera Company" + + _tag = 'make' +def MakeFromString(xml_string): + return atom.CreateClassFromXMLString(Make, xml_string) + +class Model(ExifBaseElement): + "(string) The model of the camera used,e.g AMAZING-100D" + + _tag = 'model' +def ModelFromString(xml_string): + return atom.CreateClassFromXMLString(Model, xml_string) + +class Time(ExifBaseElement): + """(int) The date/time the photo was taken, e.g. 1180294337000. + Represented as the number of milliseconds since January 1st, 1970. + + The value of this element will always be identical to the value + of the <gphoto:timestamp>. + + Look at this object's .isoformat() for a human friendly datetime string: + + photo_epoch = Time.text # 1180294337000 + photo_isostring = Time.isoformat() # '2007-05-27T19:32:17.000Z' + + Alternatively: + photo_datetime = Time.datetime() # (requires python >= 2.3) + """ + + _tag = 'time' + def isoformat(self): + """(string) Return the timestamp as a ISO 8601 formatted string, + e.g. '2007-05-27T19:32:17.000Z' + """ + import time + epoch = float(self.text)/1000 + return time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(epoch)) + + def datetime(self): + """(datetime.datetime) Return the timestamp as a datetime.datetime object + + Requires python 2.3 + """ + import datetime + epoch = float(self.text)/1000 + return datetime.datetime.fromtimestamp(epoch) + +def TimeFromString(xml_string): + return atom.CreateClassFromXMLString(Time, xml_string) + +class Tags(ExifBaseElement): + """The container for all exif elements. + The <exif:tags> element can appear as a child of a photo entry. + """ + + _tag = 'tags' + _children = atom.AtomBase._children.copy() + _children['{%s}fstop' % EXIF_NAMESPACE] = ('fstop', Fstop) + _children['{%s}make' % EXIF_NAMESPACE] = ('make', Make) + _children['{%s}model' % EXIF_NAMESPACE] = ('model', Model) + _children['{%s}distance' % EXIF_NAMESPACE] = ('distance', Distance) + _children['{%s}exposure' % EXIF_NAMESPACE] = ('exposure', Exposure) + _children['{%s}flash' % EXIF_NAMESPACE] = ('flash', Flash) + _children['{%s}focallength' % EXIF_NAMESPACE] = ('focallength', Focallength) + _children['{%s}iso' % EXIF_NAMESPACE] = ('iso', Iso) + _children['{%s}time' % EXIF_NAMESPACE] = ('time', Time) + _children['{%s}imageUniqueID' % EXIF_NAMESPACE] = ('imageUniqueID', ImageUniqueID) + + def __init__(self, extension_elements=None, extension_attributes=None, text=None): + ExifBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + self.fstop=None + self.make=None + self.model=None + self.distance=None + self.exposure=None + self.flash=None + self.focallength=None + self.iso=None + self.time=None + self.imageUniqueID=None +def TagsFromString(xml_string): + return atom.CreateClassFromXMLString(Tags, xml_string) + diff --git a/patches/gdata/finance/__init__.py b/patches/gdata/finance/__init__.py new file mode 100644 index 0000000..28ab898 --- /dev/null +++ b/patches/gdata/finance/__init__.py @@ -0,0 +1,486 @@ +#!/usr/bin/python +# +# Copyright (C) 2009 Tan Swee Heng +# +# 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. + + +"""Contains extensions to Atom objects used with Google Finance.""" + + +__author__ = 'thesweeheng@gmail.com' + + +import atom +import gdata + + +GD_NAMESPACE = 'http://schemas.google.com/g/2005' +GF_NAMESPACE = 'http://schemas.google.com/finance/2007' + + +class Money(atom.AtomBase): + """The <gd:money> element.""" + _tag = 'money' + _namespace = GD_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['amount'] = 'amount' + _attributes['currencyCode'] = 'currency_code' + + def __init__(self, amount=None, currency_code=None, **kwargs): + self.amount = amount + self.currency_code = currency_code + atom.AtomBase.__init__(self, **kwargs) + + def __str__(self): + return "%s %s" % (self.amount, self.currency_code) + + +def MoneyFromString(xml_string): + return atom.CreateClassFromXMLString(Money, xml_string) + + +class _Monies(atom.AtomBase): + """An element containing multiple <gd:money> in multiple currencies.""" + _namespace = GF_NAMESPACE + _children = atom.AtomBase._children.copy() + _children['{%s}money' % GD_NAMESPACE] = ('money', [Money]) + + def __init__(self, money=None, **kwargs): + self.money = money or [] + atom.AtomBase.__init__(self, **kwargs) + + def __str__(self): + return " / ".join(["%s" % i for i in self.money]) + + +class CostBasis(_Monies): + """The <gf:costBasis> element.""" + _tag = 'costBasis' + + +def CostBasisFromString(xml_string): + return atom.CreateClassFromXMLString(CostBasis, xml_string) + + +class DaysGain(_Monies): + """The <gf:daysGain> element.""" + _tag = 'daysGain' + + +def DaysGainFromString(xml_string): + return atom.CreateClassFromXMLString(DaysGain, xml_string) + + +class Gain(_Monies): + """The <gf:gain> element.""" + _tag = 'gain' + + +def GainFromString(xml_string): + return atom.CreateClassFromXMLString(Gain, xml_string) + + +class MarketValue(_Monies): + """The <gf:marketValue> element.""" + _tag = 'gain' + _tag = 'marketValue' + + +def MarketValueFromString(xml_string): + return atom.CreateClassFromXMLString(MarketValue, xml_string) + + +class Commission(_Monies): + """The <gf:commission> element.""" + _tag = 'commission' + + +def CommissionFromString(xml_string): + return atom.CreateClassFromXMLString(Commission, xml_string) + + +class Price(_Monies): + """The <gf:price> element.""" + _tag = 'price' + + +def PriceFromString(xml_string): + return atom.CreateClassFromXMLString(Price, xml_string) + + +class Symbol(atom.AtomBase): + """The <gf:symbol> element.""" + _tag = 'symbol' + _namespace = GF_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['fullName'] = 'full_name' + _attributes['exchange'] = 'exchange' + _attributes['symbol'] = 'symbol' + + def __init__(self, full_name=None, exchange=None, symbol=None, **kwargs): + self.full_name = full_name + self.exchange = exchange + self.symbol = symbol + atom.AtomBase.__init__(self, **kwargs) + + def __str__(self): + return "%s:%s (%s)" % (self.exchange, self.symbol, self.full_name) + + +def SymbolFromString(xml_string): + return atom.CreateClassFromXMLString(Symbol, xml_string) + + +class TransactionData(atom.AtomBase): + """The <gf:transactionData> element.""" + _tag = 'transactionData' + _namespace = GF_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['type'] = 'type' + _attributes['date'] = 'date' + _attributes['shares'] = 'shares' + _attributes['notes'] = 'notes' + _children = atom.AtomBase._children.copy() + _children['{%s}commission' % GF_NAMESPACE] = ('commission', Commission) + _children['{%s}price' % GF_NAMESPACE] = ('price', Price) + + def __init__(self, type=None, date=None, shares=None, + notes=None, commission=None, price=None, **kwargs): + self.type = type + self.date = date + self.shares = shares + self.notes = notes + self.commission = commission + self.price = price + atom.AtomBase.__init__(self, **kwargs) + + +def TransactionDataFromString(xml_string): + return atom.CreateClassFromXMLString(TransactionData, xml_string) + + +class TransactionEntry(gdata.GDataEntry): + """An entry of the transaction feed. + + A TransactionEntry contains TransactionData such as the transaction + type (Buy, Sell, Sell Short, or Buy to Cover), the number of units, + the date, the price, any commission, and any notes. + """ + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _children['{%s}transactionData' % GF_NAMESPACE] = ( + 'transaction_data', TransactionData) + + def __init__(self, transaction_data=None, **kwargs): + self.transaction_data = transaction_data + gdata.GDataEntry.__init__(self, **kwargs) + + def transaction_id(self): + return self.id.text.split("/")[-1] + + transaction_id = property(transaction_id, doc='The transaction ID.') + + +def TransactionEntryFromString(xml_string): + return atom.CreateClassFromXMLString(TransactionEntry, xml_string) + + +class TransactionFeed(gdata.GDataFeed): + """A feed that lists all of the transactions that have been recorded for + a particular position. + + A transaction is a collection of information about an instance of + buying or selling a particular security. The TransactionFeed lists all + of the transactions that have been recorded for a particular position + as a list of TransactionEntries. + """ + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [TransactionEntry]) + + +def TransactionFeedFromString(xml_string): + return atom.CreateClassFromXMLString(TransactionFeed, xml_string) + + +class TransactionFeedLink(atom.AtomBase): + """Link to TransactionFeed embedded in PositionEntry. + + If a PositionFeed is queried with transactions='true', TransactionFeeds + are inlined in the returned PositionEntries. These TransactionFeeds are + accessible via TransactionFeedLink's feed attribute. + """ + _tag = 'feedLink' + _namespace = GD_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['href'] = 'href' + _children = atom.AtomBase._children.copy() + _children['{%s}feed' % atom.ATOM_NAMESPACE] = ( + 'feed', TransactionFeed) + + def __init__(self, href=None, feed=None, **kwargs): + self.href = href + self.feed = feed + atom.AtomBase.__init__(self, **kwargs) + + +class PositionData(atom.AtomBase): + """The <gf:positionData> element.""" + _tag = 'positionData' + _namespace = GF_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['gainPercentage'] = 'gain_percentage' + _attributes['return1w'] = 'return1w' + _attributes['return4w'] = 'return4w' + _attributes['return3m'] = 'return3m' + _attributes['returnYTD'] = 'returnYTD' + _attributes['return1y'] = 'return1y' + _attributes['return3y'] = 'return3y' + _attributes['return5y'] = 'return5y' + _attributes['returnOverall'] = 'return_overall' + _attributes['shares'] = 'shares' + _children = atom.AtomBase._children.copy() + _children['{%s}costBasis' % GF_NAMESPACE] = ('cost_basis', CostBasis) + _children['{%s}daysGain' % GF_NAMESPACE] = ('days_gain', DaysGain) + _children['{%s}gain' % GF_NAMESPACE] = ('gain', Gain) + _children['{%s}marketValue' % GF_NAMESPACE] = ('market_value', MarketValue) + + def __init__(self, gain_percentage=None, + return1w=None, return4w=None, return3m=None, returnYTD=None, + return1y=None, return3y=None, return5y=None, return_overall=None, + shares=None, cost_basis=None, days_gain=None, + gain=None, market_value=None, **kwargs): + self.gain_percentage = gain_percentage + self.return1w = return1w + self.return4w = return4w + self.return3m = return3m + self.returnYTD = returnYTD + self.return1y = return1y + self.return3y = return3y + self.return5y = return5y + self.return_overall = return_overall + self.shares = shares + self.cost_basis = cost_basis + self.days_gain = days_gain + self.gain = gain + self.market_value = market_value + atom.AtomBase.__init__(self, **kwargs) + + +def PositionDataFromString(xml_string): + return atom.CreateClassFromXMLString(PositionData, xml_string) + + +class PositionEntry(gdata.GDataEntry): + """An entry of the position feed. + + A PositionEntry contains the ticker exchange and Symbol for a stock, + mutual fund, or other security, along with PositionData such as the + number of units of that security that the user holds, and performance + statistics. + """ + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _children['{%s}positionData' % GF_NAMESPACE] = ( + 'position_data', PositionData) + _children['{%s}symbol' % GF_NAMESPACE] = ('symbol', Symbol) + _children['{%s}feedLink' % GD_NAMESPACE] = ( + 'feed_link', TransactionFeedLink) + + def __init__(self, position_data=None, symbol=None, feed_link=None, + **kwargs): + self.position_data = position_data + self.symbol = symbol + self.feed_link = feed_link + gdata.GDataEntry.__init__(self, **kwargs) + + def position_title(self): + return self.title.text + + position_title = property(position_title, + doc='The position title as a string (i.e. position.title.text).') + + def ticker_id(self): + return self.id.text.split("/")[-1] + + ticker_id = property(ticker_id, doc='The position TICKER ID.') + + def transactions(self): + if self.feed_link.feed: + return self.feed_link.feed.entry + else: + return None + + transactions = property(transactions, doc=""" + Inlined TransactionEntries are returned if PositionFeed is queried + with transactions='true'.""") + + +def PositionEntryFromString(xml_string): + return atom.CreateClassFromXMLString(PositionEntry, xml_string) + + +class PositionFeed(gdata.GDataFeed): + """A feed that lists all of the positions in a particular portfolio. + + A position is a collection of information about a security that the + user holds. The PositionFeed lists all of the positions in a particular + portfolio as a list of PositionEntries. + """ + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [PositionEntry]) + + +def PositionFeedFromString(xml_string): + return atom.CreateClassFromXMLString(PositionFeed, xml_string) + + +class PositionFeedLink(atom.AtomBase): + """Link to PositionFeed embedded in PortfolioEntry. + + If a PortfolioFeed is queried with positions='true', the PositionFeeds + are inlined in the returned PortfolioEntries. These PositionFeeds are + accessible via PositionFeedLink's feed attribute. + """ + _tag = 'feedLink' + _namespace = GD_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['href'] = 'href' + _children = atom.AtomBase._children.copy() + _children['{%s}feed' % atom.ATOM_NAMESPACE] = ( + 'feed', PositionFeed) + + def __init__(self, href=None, feed=None, **kwargs): + self.href = href + self.feed = feed + atom.AtomBase.__init__(self, **kwargs) + + +class PortfolioData(atom.AtomBase): + """The <gf:portfolioData> element.""" + _tag = 'portfolioData' + _namespace = GF_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['currencyCode'] = 'currency_code' + _attributes['gainPercentage'] = 'gain_percentage' + _attributes['return1w'] = 'return1w' + _attributes['return4w'] = 'return4w' + _attributes['return3m'] = 'return3m' + _attributes['returnYTD'] = 'returnYTD' + _attributes['return1y'] = 'return1y' + _attributes['return3y'] = 'return3y' + _attributes['return5y'] = 'return5y' + _attributes['returnOverall'] = 'return_overall' + _children = atom.AtomBase._children.copy() + _children['{%s}costBasis' % GF_NAMESPACE] = ('cost_basis', CostBasis) + _children['{%s}daysGain' % GF_NAMESPACE] = ('days_gain', DaysGain) + _children['{%s}gain' % GF_NAMESPACE] = ('gain', Gain) + _children['{%s}marketValue' % GF_NAMESPACE] = ('market_value', MarketValue) + + def __init__(self, currency_code=None, gain_percentage=None, + return1w=None, return4w=None, return3m=None, returnYTD=None, + return1y=None, return3y=None, return5y=None, return_overall=None, + cost_basis=None, days_gain=None, gain=None, market_value=None, **kwargs): + self.currency_code = currency_code + self.gain_percentage = gain_percentage + self.return1w = return1w + self.return4w = return4w + self.return3m = return3m + self.returnYTD = returnYTD + self.return1y = return1y + self.return3y = return3y + self.return5y = return5y + self.return_overall = return_overall + self.cost_basis = cost_basis + self.days_gain = days_gain + self.gain = gain + self.market_value = market_value + atom.AtomBase.__init__(self, **kwargs) + + +def PortfolioDataFromString(xml_string): + return atom.CreateClassFromXMLString(PortfolioData, xml_string) + + +class PortfolioEntry(gdata.GDataEntry): + """An entry of the PortfolioFeed. + + A PortfolioEntry contains the portfolio's title along with PortfolioData + such as currency, total market value, and overall performance statistics. + """ + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _children['{%s}portfolioData' % GF_NAMESPACE] = ( + 'portfolio_data', PortfolioData) + _children['{%s}feedLink' % GD_NAMESPACE] = ( + 'feed_link', PositionFeedLink) + + def __init__(self, portfolio_data=None, feed_link=None, **kwargs): + self.portfolio_data = portfolio_data + self.feed_link = feed_link + gdata.GDataEntry.__init__(self, **kwargs) + + def portfolio_title(self): + return self.title.text + + def set_portfolio_title(self, portfolio_title): + self.title = atom.Title(text=portfolio_title, title_type='text') + + portfolio_title = property(portfolio_title, set_portfolio_title, + doc='The portfolio title as a string (i.e. portfolio.title.text).') + + def portfolio_id(self): + return self.id.text.split("/")[-1] + + portfolio_id = property(portfolio_id, + doc='The portfolio ID. Do not confuse with portfolio.id.') + + def positions(self): + if self.feed_link.feed: + return self.feed_link.feed.entry + else: + return None + + positions = property(positions, doc=""" + Inlined PositionEntries are returned if PortfolioFeed was queried + with positions='true'.""") + + +def PortfolioEntryFromString(xml_string): + return atom.CreateClassFromXMLString(PortfolioEntry, xml_string) + + +class PortfolioFeed(gdata.GDataFeed): + """A feed that lists all of the user's portfolios. + + A portfolio is a collection of positions that the user holds in various + securities, plus metadata. The PortfolioFeed lists all of the user's + portfolios as a list of PortfolioEntries. + """ + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [PortfolioEntry]) + + +def PortfolioFeedFromString(xml_string): + return atom.CreateClassFromXMLString(PortfolioFeed, xml_string) + + diff --git a/patches/gdata/finance/data.py b/patches/gdata/finance/data.py new file mode 100644 index 0000000..5e0caa8 --- /dev/null +++ b/patches/gdata/finance/data.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""Contains the data classes of the Google Finance Portfolio Data API""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core +import atom.data +import gdata.data +import gdata.opensearch.data + + +GF_TEMPLATE = '{http://schemas.google.com/finance/2007/}%s' + + +class Commission(atom.core.XmlElement): + """Commission for the transaction""" + _qname = GF_TEMPLATE % 'commission' + money = [gdata.data.Money] + + +class CostBasis(atom.core.XmlElement): + """Cost basis for the portfolio or position""" + _qname = GF_TEMPLATE % 'costBasis' + money = [gdata.data.Money] + + +class DaysGain(atom.core.XmlElement): + """Today's gain for the portfolio or position""" + _qname = GF_TEMPLATE % 'daysGain' + money = [gdata.data.Money] + + +class Gain(atom.core.XmlElement): + """Total gain for the portfolio or position""" + _qname = GF_TEMPLATE % 'gain' + money = [gdata.data.Money] + + +class MarketValue(atom.core.XmlElement): + """Market value for the portfolio or position""" + _qname = GF_TEMPLATE % 'marketValue' + money = [gdata.data.Money] + + +class PortfolioData(atom.core.XmlElement): + """Data for the portfolio""" + _qname = GF_TEMPLATE % 'portfolioData' + return_overall = 'returnOverall' + currency_code = 'currencyCode' + return3y = 'return3y' + return4w = 'return4w' + market_value = MarketValue + return_y_t_d = 'returnYTD' + cost_basis = CostBasis + gain_percentage = 'gainPercentage' + days_gain = DaysGain + return3m = 'return3m' + return5y = 'return5y' + return1w = 'return1w' + gain = Gain + return1y = 'return1y' + + +class PortfolioEntry(gdata.data.GDEntry): + """Describes an entry in a feed of Finance portfolios""" + portfolio_data = PortfolioData + + +class PortfolioFeed(gdata.data.GDFeed): + """Describes a Finance portfolio feed""" + entry = [PortfolioEntry] + + +class PositionData(atom.core.XmlElement): + """Data for the position""" + _qname = GF_TEMPLATE % 'positionData' + return_y_t_d = 'returnYTD' + return5y = 'return5y' + return_overall = 'returnOverall' + cost_basis = CostBasis + return3y = 'return3y' + return1y = 'return1y' + return4w = 'return4w' + shares = 'shares' + days_gain = DaysGain + gain_percentage = 'gainPercentage' + market_value = MarketValue + gain = Gain + return3m = 'return3m' + return1w = 'return1w' + + +class Price(atom.core.XmlElement): + """Price of the transaction""" + _qname = GF_TEMPLATE % 'price' + money = [gdata.data.Money] + + +class Symbol(atom.core.XmlElement): + """Stock symbol for the company""" + _qname = GF_TEMPLATE % 'symbol' + symbol = 'symbol' + exchange = 'exchange' + full_name = 'fullName' + + +class PositionEntry(gdata.data.GDEntry): + """Describes an entry in a feed of Finance positions""" + symbol = Symbol + position_data = PositionData + + +class PositionFeed(gdata.data.GDFeed): + """Describes a Finance position feed""" + entry = [PositionEntry] + + +class TransactionData(atom.core.XmlElement): + """Data for the transction""" + _qname = GF_TEMPLATE % 'transactionData' + shares = 'shares' + notes = 'notes' + date = 'date' + type = 'type' + commission = Commission + price = Price + + +class TransactionEntry(gdata.data.GDEntry): + """Describes an entry in a feed of Finance transactions""" + transaction_data = TransactionData + + +class TransactionFeed(gdata.data.GDFeed): + """Describes a Finance transaction feed""" + entry = [TransactionEntry] + + diff --git a/patches/gdata/finance/service.py b/patches/gdata/finance/service.py new file mode 100644 index 0000000..6e3eb86 --- /dev/null +++ b/patches/gdata/finance/service.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# +# Copyright (C) 2009 Tan Swee Heng +# +# 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. + + +"""Classes to interact with the Google Finance server.""" + + +__author__ = 'thesweeheng@gmail.com' + + +import gdata.service +import gdata.finance +import atom + + +class PortfolioQuery(gdata.service.Query): + """A query object for the list of a user's portfolios.""" + + def returns(self): + return self.get('returns', False) + + def set_returns(self, value): + if value is 'true' or value is True: + self['returns'] = 'true' + + returns = property(returns, set_returns, doc="The returns query parameter") + + def positions(self): + return self.get('positions', False) + + def set_positions(self, value): + if value is 'true' or value is True: + self['positions'] = 'true' + + positions = property(positions, set_positions, + doc="The positions query parameter") + + +class PositionQuery(gdata.service.Query): + """A query object for the list of a user's positions in a portfolio.""" + + def returns(self): + return self.get('returns', False) + + def set_returns(self, value): + if value is 'true' or value is True: + self['returns'] = 'true' + + returns = property(returns, set_returns, + doc="The returns query parameter") + + def transactions(self): + return self.get('transactions', False) + + def set_transactions(self, value): + if value is 'true' or value is True: + self['transactions'] = 'true' + + transactions = property(transactions, set_transactions, + doc="The transactions query parameter") + + +class FinanceService(gdata.service.GDataService): + + def __init__(self, email=None, password=None, source=None, + server='finance.google.com', **kwargs): + """Creates a client for the Finance service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'finance.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__(self, + email=email, password=password, service='finance', server=server, + **kwargs) + + def GetPortfolioFeed(self, query=None): + uri = '/finance/feeds/default/portfolios' + if query: + uri = PortfolioQuery(feed=uri, params=query).ToUri() + return self.Get(uri, converter=gdata.finance.PortfolioFeedFromString) + + def GetPositionFeed(self, portfolio_entry=None, portfolio_id=None, + query=None): + """ + Args: + portfolio_entry: PortfolioEntry (optional; see Notes) + portfolio_id: string (optional; see Notes) This may be obtained + from a PortfolioEntry's portfolio_id attribute. + query: PortfolioQuery (optional) + + Notes: + Either a PortfolioEntry OR a portfolio ID must be provided. + """ + if portfolio_entry: + uri = portfolio_entry.GetSelfLink().href + '/positions' + elif portfolio_id: + uri = '/finance/feeds/default/portfolios/%s/positions' % portfolio_id + if query: + uri = PositionQuery(feed=uri, params=query).ToUri() + return self.Get(uri, converter=gdata.finance.PositionFeedFromString) + + def GetTransactionFeed(self, position_entry=None, + portfolio_id=None, ticker_id=None): + """ + Args: + position_entry: PositionEntry (optional; see Notes) + portfolio_id: string (optional; see Notes) This may be obtained + from a PortfolioEntry's portfolio_id attribute. + ticker_id: string (optional; see Notes) This may be obtained from + a PositionEntry's ticker_id attribute. Alternatively it can + be constructed using the security's exchange and symbol, + e.g. 'NASDAQ:GOOG' + + Notes: + Either a PositionEntry OR (a portfolio ID AND ticker ID) must + be provided. + """ + if position_entry: + uri = position_entry.GetSelfLink().href + '/transactions' + elif portfolio_id and ticker_id: + uri = '/finance/feeds/default/portfolios/%s/positions/%s/transactions' \ + % (portfolio_id, ticker_id) + return self.Get(uri, converter=gdata.finance.TransactionFeedFromString) + + def GetPortfolio(self, portfolio_id=None, query=None): + uri = '/finance/feeds/default/portfolios/%s' % portfolio_id + if query: + uri = PortfolioQuery(feed=uri, params=query).ToUri() + return self.Get(uri, converter=gdata.finance.PortfolioEntryFromString) + + def AddPortfolio(self, portfolio_entry=None): + uri = '/finance/feeds/default/portfolios' + return self.Post(portfolio_entry, uri, + converter=gdata.finance.PortfolioEntryFromString) + + def UpdatePortfolio(self, portfolio_entry=None): + uri = portfolio_entry.GetEditLink().href + return self.Put(portfolio_entry, uri, + converter=gdata.finance.PortfolioEntryFromString) + + def DeletePortfolio(self, portfolio_entry=None): + uri = portfolio_entry.GetEditLink().href + return self.Delete(uri) + + def GetPosition(self, portfolio_id=None, ticker_id=None, query=None): + uri = '/finance/feeds/default/portfolios/%s/positions/%s' \ + % (portfolio_id, ticker_id) + if query: + uri = PositionQuery(feed=uri, params=query).ToUri() + return self.Get(uri, converter=gdata.finance.PositionEntryFromString) + + def DeletePosition(self, position_entry=None, + portfolio_id=None, ticker_id=None, transaction_feed=None): + """A position is deleted by deleting all its transactions. + + Args: + position_entry: PositionEntry (optional; see Notes) + portfolio_id: string (optional; see Notes) This may be obtained + from a PortfolioEntry's portfolio_id attribute. + ticker_id: string (optional; see Notes) This may be obtained from + a PositionEntry's ticker_id attribute. Alternatively it can + be constructed using the security's exchange and symbol, + e.g. 'NASDAQ:GOOG' + transaction_feed: TransactionFeed (optional; see Notes) + + Notes: + Either a PositionEntry OR (a portfolio ID AND ticker ID) OR + a TransactionFeed must be provided. + """ + if transaction_feed: + feed = transaction_feed + else: + if position_entry: + feed = self.GetTransactionFeed(position_entry=position_entry) + elif portfolio_id and ticker_id: + feed = self.GetTransactionFeed( + portfolio_id=portfolio_id, ticker_id=ticker_id) + for txn in feed.entry: + self.DeleteTransaction(txn) + return True + + def GetTransaction(self, portfolio_id=None, ticker_id=None, + transaction_id=None): + uri = '/finance/feeds/default/portfolios/%s/positions/%s/transactions/%s' \ + % (portfolio_id, ticker_id, transaction_id) + return self.Get(uri, converter=gdata.finance.TransactionEntryFromString) + + def AddTransaction(self, transaction_entry=None, transaction_feed = None, + position_entry=None, portfolio_id=None, ticker_id=None): + """ + Args: + transaction_entry: TransactionEntry (required) + transaction_feed: TransactionFeed (optional; see Notes) + position_entry: PositionEntry (optional; see Notes) + portfolio_id: string (optional; see Notes) This may be obtained + from a PortfolioEntry's portfolio_id attribute. + ticker_id: string (optional; see Notes) This may be obtained from + a PositionEntry's ticker_id attribute. Alternatively it can + be constructed using the security's exchange and symbol, + e.g. 'NASDAQ:GOOG' + + Notes: + Either a TransactionFeed OR a PositionEntry OR (a portfolio ID AND + ticker ID) must be provided. + """ + if transaction_feed: + uri = transaction_feed.GetPostLink().href + elif position_entry: + uri = position_entry.GetSelfLink().href + '/transactions' + elif portfolio_id and ticker_id: + uri = '/finance/feeds/default/portfolios/%s/positions/%s/transactions' \ + % (portfolio_id, ticker_id) + return self.Post(transaction_entry, uri, + converter=gdata.finance.TransactionEntryFromString) + + def UpdateTransaction(self, transaction_entry=None): + uri = transaction_entry.GetEditLink().href + return self.Put(transaction_entry, uri, + converter=gdata.finance.TransactionEntryFromString) + + def DeleteTransaction(self, transaction_entry=None): + uri = transaction_entry.GetEditLink().href + return self.Delete(uri) diff --git a/patches/gdata/gauth.py b/patches/gdata/gauth.py new file mode 100644 index 0000000..563656c --- /dev/null +++ b/patches/gdata/gauth.py @@ -0,0 +1,1306 @@ +#!/usr/bin/env python +# +# Copyright (C) 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 auth related token classes and functions for Google Data APIs. + +Token classes represent a user's authorization of this app to access their +data. Usually these are not created directly but by a GDClient object. + +ClientLoginToken +AuthSubToken +SecureAuthSubToken +OAuthHmacToken +OAuthRsaToken +TwoLeggedOAuthHmacToken +TwoLeggedOAuthRsaToken + +Functions which are often used in application code (as opposed to just within +the gdata-python-client library) are the following: + +generate_auth_sub_url +authorize_request_token + +The following are helper functions which are used to save and load auth token +objects in the App Engine datastore. These should only be used if you are using +this library within App Engine: + +ae_load +ae_save +""" + + +import time +import random +import urllib +import atom.http_core + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth=' +AUTHSUB_AUTH_LABEL = 'AuthSub token=' + + +# This dict provides the AuthSub and OAuth scopes for all services by service +# name. The service name (key) is used in ClientLogin requests. +AUTH_SCOPES = { + 'cl': ( # Google Calendar API + 'https://www.google.com/calendar/feeds/', + 'http://www.google.com/calendar/feeds/'), + 'gbase': ( # Google Base API + 'http://base.google.com/base/feeds/', + 'http://www.google.com/base/feeds/'), + 'blogger': ( # Blogger API + 'http://www.blogger.com/feeds/',), + 'codesearch': ( # Google Code Search API + 'http://www.google.com/codesearch/feeds/',), + 'cp': ( # Contacts API + 'https://www.google.com/m8/feeds/', + 'http://www.google.com/m8/feeds/'), + 'finance': ( # Google Finance API + 'http://finance.google.com/finance/feeds/',), + 'health': ( # Google Health API + 'https://www.google.com/health/feeds/',), + 'writely': ( # Documents List API + 'https://docs.google.com/feeds/', + 'http://docs.google.com/feeds/'), + 'lh2': ( # Picasa Web Albums API + 'http://picasaweb.google.com/data/',), + 'apps': ( # Google Apps Provisioning API + 'http://www.google.com/a/feeds/', + 'https://www.google.com/a/feeds/', + 'http://apps-apis.google.com/a/feeds/', + 'https://apps-apis.google.com/a/feeds/'), + 'weaver': ( # Health H9 Sandbox + 'https://www.google.com/h9/feeds/',), + 'wise': ( # Spreadsheets Data API + 'https://spreadsheets.google.com/feeds/', + 'http://spreadsheets.google.com/feeds/'), + 'sitemaps': ( # Google Webmaster Tools API + 'https://www.google.com/webmasters/tools/feeds/',), + 'youtube': ( # YouTube API + 'http://gdata.youtube.com/feeds/api/', + 'http://uploads.gdata.youtube.com/feeds/api', + 'http://gdata.youtube.com/action/GetUploadToken'), + 'books': ( # Google Books API + 'http://www.google.com/books/feeds/',), + 'analytics': ( # Google Analytics API + 'https://www.google.com/analytics/feeds/',), + 'jotspot': ( # Google Sites API + 'http://sites.google.com/feeds/', + 'https://sites.google.com/feeds/'), + 'local': ( # Google Maps Data API + 'http://maps.google.com/maps/feeds/',), + 'code': ( # Project Hosting Data API + 'http://code.google.com/feeds/issues',)} + + + +class Error(Exception): + pass + + +class UnsupportedTokenType(Error): + """Raised when token to or from blob is unable to convert the token.""" + pass + + +# ClientLogin functions and classes. +def generate_client_login_request_body(email, password, service, source, + account_type='HOSTED_OR_GOOGLE', captcha_token=None, + captcha_response=None): + """Creates the body of the autentication request + + See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request + for more details. + + Args: + email: str + password: str + service: str + source: str + account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid + values are 'GOOGLE' and 'HOSTED' + captcha_token: str (optional) + captcha_response: str (optional) + + Returns: + The HTTP body to send in a request for a client login token. + """ + # Create a POST body containing the user's credentials. + request_fields = {'Email': email, + 'Passwd': password, + 'accountType': account_type, + 'service': service, + 'source': source} + if captcha_token and captcha_response: + # Send the captcha token and response as part of the POST body if the + # user is responding to a captch challenge. + request_fields['logintoken'] = captcha_token + request_fields['logincaptcha'] = captcha_response + return urllib.urlencode(request_fields) + + +GenerateClientLoginRequestBody = generate_client_login_request_body + + +def get_client_login_token_string(http_body): + """Returns the token value for a ClientLoginToken. + + Reads the token from the server's response to a Client Login request and + creates the token value string to use in requests. + + Args: + http_body: str The body of the server's HTTP response to a Client Login + request + + Returns: + The token value string for a ClientLoginToken. + """ + for response_line in http_body.splitlines(): + if response_line.startswith('Auth='): + # Strip off the leading Auth= and return the Authorization value. + return response_line[5:] + return None + + +GetClientLoginTokenString = get_client_login_token_string + + +def get_captcha_challenge(http_body, + captcha_base_url='http://www.google.com/accounts/'): + """Returns the URL and token for a CAPTCHA challenge issued by the server. + + Args: + http_body: str The body of the HTTP response from the server which + contains the CAPTCHA challenge. + captcha_base_url: str This function returns a full URL for viewing the + challenge image which is built from the server's response. This + base_url is used as the beginning of the URL because the server + only provides the end of the URL. For example the server provides + 'Captcha?ctoken=Hi...N' and the URL for the image is + 'http://www.google.com/accounts/Captcha?ctoken=Hi...N' + + Returns: + A dictionary containing the information needed to repond to the CAPTCHA + challenge, the image URL and the ID token of the challenge. The + dictionary is in the form: + {'token': string identifying the CAPTCHA image, + 'url': string containing the URL of the image} + Returns None if there was no CAPTCHA challenge in the response. + """ + contains_captcha_challenge = False + captcha_parameters = {} + for response_line in http_body.splitlines(): + if response_line.startswith('Error=CaptchaRequired'): + contains_captcha_challenge = True + elif response_line.startswith('CaptchaToken='): + # Strip off the leading CaptchaToken= + captcha_parameters['token'] = response_line[13:] + elif response_line.startswith('CaptchaUrl='): + captcha_parameters['url'] = '%s%s' % (captcha_base_url, + response_line[11:]) + if contains_captcha_challenge: + return captcha_parameters + else: + return None + + +GetCaptchaChallenge = get_captcha_challenge + + +class ClientLoginToken(object): + + def __init__(self, token_string): + self.token_string = token_string + + def modify_request(self, http_request): + http_request.headers['Authorization'] = '%s%s' % (PROGRAMMATIC_AUTH_LABEL, + self.token_string) + + ModifyRequest = modify_request + + +# AuthSub functions and classes. +def _to_uri(str_or_uri): + if isinstance(str_or_uri, (str, unicode)): + return atom.http_core.Uri.parse_uri(str_or_uri) + return str_or_uri + + +def generate_auth_sub_url(next, scopes, secure=False, session=True, + request_url=atom.http_core.parse_uri( + 'https://www.google.com/accounts/AuthSubRequest'), + domain='default', scopes_param_prefix='auth_sub_scopes'): + """Constructs a URI for requesting a multiscope AuthSub token. + + The generated token will contain a URL parameter to pass along the + requested scopes to the next URL. When the Google Accounts page + redirects the broswser to the 'next' URL, it appends the single use + AuthSub token value to the URL as a URL parameter with the key 'token'. + However, the information about which scopes were requested is not + included by Google Accounts. This method adds the scopes to the next + URL before making the request so that the redirect will be sent to + a page, and both the token value and the list of scopes for which the token + was requested. + + Args: + next: atom.http_core.Uri or string The URL user will be sent to after + authorizing this web application to access their data. + scopes: list containint strings or atom.http_core.Uri objects. The URLs + of the services to be accessed. Could also be a single string + or single atom.http_core.Uri for requesting just one scope. + secure: boolean (optional) Determines whether or not the issued token + is a secure token. + session: boolean (optional) Determines whether or not the issued token + can be upgraded to a session token. + request_url: atom.http_core.Uri or str The beginning of the request URL. + This is normally + 'http://www.google.com/accounts/AuthSubRequest' or + '/accounts/AuthSubRequest' + domain: The domain which the account is part of. This is used for Google + Apps accounts, the default value is 'default' which means that + the requested account is a Google Account (@gmail.com for + example) + scopes_param_prefix: str (optional) The requested scopes are added as a + URL parameter to the next URL so that the page at + the 'next' URL can extract the token value and the + valid scopes from the URL. The key for the URL + parameter defaults to 'auth_sub_scopes' + + Returns: + An atom.http_core.Uri which the user's browser should be directed to in + order to authorize this application to access their information. + """ + if isinstance(next, (str, unicode)): + next = atom.http_core.Uri.parse_uri(next) + # If the user passed in a string instead of a list for scopes, convert to + # a single item tuple. + if isinstance(scopes, (str, unicode, atom.http_core.Uri)): + scopes = (scopes,) + scopes_string = ' '.join([str(scope) for scope in scopes]) + next.query[scopes_param_prefix] = scopes_string + + if isinstance(request_url, (str, unicode)): + request_url = atom.http_core.Uri.parse_uri(request_url) + request_url.query['next'] = str(next) + request_url.query['scope'] = scopes_string + if session: + request_url.query['session'] = '1' + else: + request_url.query['session'] = '0' + if secure: + request_url.query['secure'] = '1' + else: + request_url.query['secure'] = '0' + request_url.query['hd'] = domain + return request_url + + +def auth_sub_string_from_url(url, scopes_param_prefix='auth_sub_scopes'): + """Finds the token string (and scopes) after the browser is redirected. + + After the Google Accounts AuthSub pages redirect the user's broswer back to + the web application (using the 'next' URL from the request) the web app must + extract the token from the current page's URL. The token is provided as a + URL parameter named 'token' and if generate_auth_sub_url was used to create + the request, the token's valid scopes are included in a URL parameter whose + name is specified in scopes_param_prefix. + + Args: + url: atom.url.Url or str representing the current URL. The token value + and valid scopes should be included as URL parameters. + scopes_param_prefix: str (optional) The URL parameter key which maps to + the list of valid scopes for the token. + + Returns: + A tuple containing the token value as a string, and a tuple of scopes + (as atom.http_core.Uri objects) which are URL prefixes under which this + token grants permission to read and write user data. + (token_string, (scope_uri, scope_uri, scope_uri, ...)) + If no scopes were included in the URL, the second value in the tuple is + None. If there was no token param in the url, the tuple returned is + (None, None) + """ + if isinstance(url, (str, unicode)): + url = atom.http_core.Uri.parse_uri(url) + if 'token' not in url.query: + return (None, None) + token = url.query['token'] + # TODO: decide whether no scopes should be None or (). + scopes = None # Default to None for no scopes. + if scopes_param_prefix in url.query: + scopes = tuple(url.query[scopes_param_prefix].split(' ')) + return (token, scopes) + + +AuthSubStringFromUrl = auth_sub_string_from_url + + +def auth_sub_string_from_body(http_body): + """Extracts the AuthSub token from an HTTP body string. + + Used to find the new session token after making a request to upgrade a + single use AuthSub token. + + Args: + http_body: str The repsonse from the server which contains the AuthSub + key. For example, this function would find the new session token + from the server's response to an upgrade token request. + + Returns: + The raw token value string to use in an AuthSubToken object. + """ + for response_line in http_body.splitlines(): + if response_line.startswith('Token='): + # Strip off Token= and return the token value string. + return response_line[6:] + return None + + +class AuthSubToken(object): + + def __init__(self, token_string, scopes=None): + self.token_string = token_string + self.scopes = scopes or [] + + def modify_request(self, http_request): + """Sets Authorization header, allows app to act on the user's behalf.""" + http_request.headers['Authorization'] = '%s%s' % (AUTHSUB_AUTH_LABEL, + self.token_string) + + ModifyRequest = modify_request + + def from_url(str_or_uri): + """Creates a new AuthSubToken using information in the URL. + + Uses auth_sub_string_from_url. + + Args: + str_or_uri: The current page's URL (as a str or atom.http_core.Uri) + which should contain a token query parameter since the + Google auth server redirected the user's browser to this + URL. + """ + token_and_scopes = auth_sub_string_from_url(str_or_uri) + return AuthSubToken(token_and_scopes[0], token_and_scopes[1]) + + from_url = staticmethod(from_url) + FromUrl = from_url + + def _upgrade_token(self, http_body): + """Replaces the token value with a session token from the auth server. + + Uses the response of a token upgrade request to modify this token. Uses + auth_sub_string_from_body. + """ + self.token_string = auth_sub_string_from_body(http_body) + + +# Functions and classes for Secure-mode AuthSub +def build_auth_sub_data(http_request, timestamp, nonce): + """Creates the data string which must be RSA-signed in secure requests. + + For more details see the documenation on secure AuthSub requests: + http://code.google.com/apis/accounts/docs/AuthSub.html#signingrequests + + Args: + http_request: The request being made to the server. The Request's URL + must be complete before this signature is calculated as any changes + to the URL will invalidate the signature. + nonce: str Random 64-bit, unsigned number encoded as an ASCII string in + decimal format. The nonce/timestamp pair should always be unique to + prevent replay attacks. + timestamp: Integer representing the time the request is sent. The + timestamp should be expressed in number of seconds after January 1, + 1970 00:00:00 GMT. + """ + return '%s %s %s %s' % (http_request.method, str(http_request.uri), + str(timestamp), nonce) + + +def generate_signature(data, rsa_key): + """Signs the data string for a secure AuthSub request.""" + import base64 + try: + from tlslite.utils import keyfactory + except ImportError: + from gdata.tlslite.utils import keyfactory + private_key = keyfactory.parsePrivateKey(rsa_key) + signed = private_key.hashAndSign(data) + # Python2.3 and lower does not have the base64.b64encode function. + if hasattr(base64, 'b64encode'): + return base64.b64encode(signed) + else: + return base64.encodestring(signed).replace('\n', '') + + +class SecureAuthSubToken(AuthSubToken): + + def __init__(self, token_string, rsa_private_key, scopes=None): + self.token_string = token_string + self.scopes = scopes or [] + self.rsa_private_key = rsa_private_key + + def from_url(str_or_uri, rsa_private_key): + """Creates a new SecureAuthSubToken using information in the URL. + + Uses auth_sub_string_from_url. + + Args: + str_or_uri: The current page's URL (as a str or atom.http_core.Uri) + which should contain a token query parameter since the Google auth + server redirected the user's browser to this URL. + rsa_private_key: str the private RSA key cert used to sign all requests + made with this token. + """ + token_and_scopes = auth_sub_string_from_url(str_or_uri) + return SecureAuthSubToken(token_and_scopes[0], rsa_private_key, + token_and_scopes[1]) + + from_url = staticmethod(from_url) + FromUrl = from_url + + def modify_request(self, http_request): + """Sets the Authorization header and includes a digital signature. + + Calculates a digital signature using the private RSA key, a timestamp + (uses now at the time this method is called) and a random nonce. + + Args: + http_request: The atom.http_core.HttpRequest which contains all of the + information needed to send a request to the remote server. The + URL and the method of the request must be already set and cannot be + changed after this token signs the request, or the signature will + not be valid. + """ + timestamp = str(int(time.time())) + nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) + data = build_auth_sub_data(http_request, timestamp, nonce) + signature = generate_signature(data, self.rsa_private_key) + http_request.headers['Authorization'] = ( + '%s%s sigalg="rsa-sha1" data="%s" sig="%s"' % (AUTHSUB_AUTH_LABEL, + self.token_string, data, signature)) + + ModifyRequest = modify_request + + +# OAuth functions and classes. +RSA_SHA1 = 'RSA-SHA1' +HMAC_SHA1 = 'HMAC-SHA1' + + +def build_oauth_base_string(http_request, consumer_key, nonce, signaure_type, + timestamp, version, next='oob', token=None, + verifier=None): + """Generates the base string to be signed in the OAuth request. + + Args: + http_request: The request being made to the server. The Request's URL + must be complete before this signature is calculated as any changes + to the URL will invalidate the signature. + consumer_key: Domain identifying the third-party web application. This is + the domain used when registering the application with Google. It + identifies who is making the request on behalf of the user. + nonce: Random 64-bit, unsigned number encoded as an ASCII string in decimal + format. The nonce/timestamp pair should always be unique to prevent + replay attacks. + signaure_type: either RSA_SHA1 or HMAC_SHA1 + timestamp: Integer representing the time the request is sent. The + timestamp should be expressed in number of seconds after January 1, + 1970 00:00:00 GMT. + version: The OAuth version used by the requesting web application. This + value must be '1.0' or '1.0a'. If not provided, Google assumes version + 1.0 is in use. + next: The URL the user should be redirected to after granting access + to a Google service(s). It can include url-encoded query parameters. + The default value is 'oob'. (This is the oauth_callback.) + token: The string for the OAuth request token or OAuth access token. + verifier: str Sent as the oauth_verifier and required when upgrading a + request token to an access token. + """ + # First we must build the canonical base string for the request. + params = http_request.uri.query.copy() + params['oauth_consumer_key'] = consumer_key + params['oauth_nonce'] = nonce + params['oauth_signature_method'] = signaure_type + params['oauth_timestamp'] = str(timestamp) + if next is not None: + params['oauth_callback'] = str(next) + if token is not None: + params['oauth_token'] = token + if version is not None: + params['oauth_version'] = version + if verifier is not None: + params['oauth_verifier'] = verifier + # We need to get the key value pairs in lexigraphically sorted order. + sorted_keys = None + try: + sorted_keys = sorted(params.keys()) + # The sorted function is not available in Python2.3 and lower + except NameError: + sorted_keys = params.keys() + sorted_keys.sort() + pairs = [] + for key in sorted_keys: + pairs.append('%s=%s' % (urllib.quote(key, safe='~'), + urllib.quote(params[key], safe='~'))) + # We want to escape /'s too, so use safe='~' + all_parameters = urllib.quote('&'.join(pairs), safe='~') + normailzed_host = http_request.uri.host.lower() + normalized_scheme = (http_request.uri.scheme or 'http').lower() + non_default_port = None + if (http_request.uri.port is not None + and ((normalized_scheme == 'https' and http_request.uri.port != 443) + or (normalized_scheme == 'http' and http_request.uri.port != 80))): + non_default_port = http_request.uri.port + path = http_request.uri.path or '/' + request_path = None + if not path.startswith('/'): + path = '/%s' % path + if non_default_port is not None: + # Set the only safe char in url encoding to ~ since we want to escape / + # as well. + request_path = urllib.quote('%s://%s:%s%s' % ( + normalized_scheme, normailzed_host, non_default_port, path), safe='~') + else: + # Set the only safe char in url encoding to ~ since we want to escape / + # as well. + request_path = urllib.quote('%s://%s%s' % ( + normalized_scheme, normailzed_host, path), safe='~') + # TODO: ensure that token escaping logic is correct, not sure if the token + # value should be double escaped instead of single. + base_string = '&'.join((http_request.method.upper(), request_path, + all_parameters)) + # Now we have the base string, we can calculate the oauth_signature. + return base_string + + +def generate_hmac_signature(http_request, consumer_key, consumer_secret, + timestamp, nonce, version, next='oob', + token=None, token_secret=None, verifier=None): + import hmac + import base64 + base_string = build_oauth_base_string( + http_request, consumer_key, nonce, HMAC_SHA1, timestamp, version, + next, token, verifier=verifier) + hash_key = None + hashed = None + if token_secret is not None: + hash_key = '%s&%s' % (urllib.quote(consumer_secret, safe='~'), + urllib.quote(token_secret, safe='~')) + else: + hash_key = '%s&' % urllib.quote(consumer_secret, safe='~') + try: + import hashlib + hashed = hmac.new(hash_key, base_string, hashlib.sha1) + except ImportError: + import sha + hashed = hmac.new(hash_key, base_string, sha) + # Python2.3 does not have base64.b64encode. + if hasattr(base64, 'b64encode'): + return base64.b64encode(hashed.digest()) + else: + return base64.encodestring(hashed.digest()).replace('\n', '') + + +def generate_rsa_signature(http_request, consumer_key, rsa_key, + timestamp, nonce, version, next='oob', + token=None, token_secret=None, verifier=None): + import base64 + try: + from tlslite.utils import keyfactory + except ImportError: + from gdata.tlslite.utils import keyfactory + base_string = build_oauth_base_string( + http_request, consumer_key, nonce, RSA_SHA1, timestamp, version, + next, token, verifier=verifier) + private_key = keyfactory.parsePrivateKey(rsa_key) + # Sign using the key + signed = private_key.hashAndSign(base_string) + # Python2.3 does not have base64.b64encode. + if hasattr(base64, 'b64encode'): + return base64.b64encode(signed) + else: + return base64.encodestring(signed).replace('\n', '') + + +def generate_auth_header(consumer_key, timestamp, nonce, signature_type, + signature, version='1.0', next=None, token=None, + verifier=None): + """Builds the Authorization header to be sent in the request. + + Args: + consumer_key: Identifies the application making the request (str). + timestamp: + nonce: + signature_type: One of either HMAC_SHA1 or RSA_SHA1 + signature: The HMAC or RSA signature for the request as a base64 + encoded string. + version: The version of the OAuth protocol that this request is using. + Default is '1.0' + next: The URL of the page that the user's browser should be sent to + after they authorize the token. (Optional) + token: str The OAuth token value to be used in the oauth_token parameter + of the header. + verifier: str The OAuth verifier which must be included when you are + upgrading a request token to an access token. + """ + params = { + 'oauth_consumer_key': consumer_key, + 'oauth_version': version, + 'oauth_nonce': nonce, + 'oauth_timestamp': str(timestamp), + 'oauth_signature_method': signature_type, + 'oauth_signature': signature} + if next is not None: + params['oauth_callback'] = str(next) + if token is not None: + params['oauth_token'] = token + if verifier is not None: + params['oauth_verifier'] = verifier + pairs = [ + '%s="%s"' % ( + k, urllib.quote(v, safe='~')) for k, v in params.iteritems()] + return 'OAuth %s' % (', '.join(pairs)) + + +REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken' +ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken' + + +def generate_request_for_request_token( + consumer_key, signature_type, scopes, rsa_key=None, consumer_secret=None, + auth_server_url=REQUEST_TOKEN_URL, next='oob', version='1.0'): + """Creates request to be sent to auth server to get an OAuth request token. + + Args: + consumer_key: + signature_type: either RSA_SHA1 or HMAC_SHA1. The rsa_key must be + provided if the signature type is RSA but if the signature method + is HMAC, the consumer_secret must be used. + scopes: List of URL prefixes for the data which we want to access. For + example, to request access to the user's Blogger and Google Calendar + data, we would request + ['http://www.blogger.com/feeds/', + 'https://www.google.com/calendar/feeds/', + 'http://www.google.com/calendar/feeds/'] + rsa_key: Only used if the signature method is RSA_SHA1. + consumer_secret: Only used if the signature method is HMAC_SHA1. + auth_server_url: The URL to which the token request should be directed. + Defaults to 'https://www.google.com/accounts/OAuthGetRequestToken'. + next: The URL of the page that the user's browser should be sent to + after they authorize the token. (Optional) + version: The OAuth version used by the requesting web application. + Defaults to '1.0a' + + Returns: + An atom.http_core.HttpRequest object with the URL, Authorization header + and body filled in. + """ + request = atom.http_core.HttpRequest(auth_server_url, 'POST') + # Add the requested auth scopes to the Auth request URL. + if scopes: + request.uri.query['scope'] = ' '.join(scopes) + + timestamp = str(int(time.time())) + nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) + signature = None + if signature_type == HMAC_SHA1: + signature = generate_hmac_signature( + request, consumer_key, consumer_secret, timestamp, nonce, version, + next=next) + elif signature_type == RSA_SHA1: + signature = generate_rsa_signature( + request, consumer_key, rsa_key, timestamp, nonce, version, next=next) + else: + return None + + request.headers['Authorization'] = generate_auth_header( + consumer_key, timestamp, nonce, signature_type, signature, version, + next) + request.headers['Content-Length'] = '0' + return request + + +def generate_request_for_access_token( + request_token, auth_server_url=ACCESS_TOKEN_URL): + """Creates a request to ask the OAuth server for an access token. + + Requires a request token which the user has authorized. See the + documentation on OAuth with Google Data for more details: + http://code.google.com/apis/accounts/docs/OAuth.html#AccessToken + + Args: + request_token: An OAuthHmacToken or OAuthRsaToken which the user has + approved using their browser. + auth_server_url: (optional) The URL at which the OAuth access token is + requested. Defaults to + https://www.google.com/accounts/OAuthGetAccessToken + + Returns: + A new HttpRequest object which can be sent to the OAuth server to + request an OAuth Access Token. + """ + http_request = atom.http_core.HttpRequest(auth_server_url, 'POST') + http_request.headers['Content-Length'] = '0' + return request_token.modify_request(http_request) + + +def oauth_token_info_from_body(http_body): + """Exracts an OAuth request token from the server's response. + + Returns: + A tuple of strings containing the OAuth token and token secret. If + neither of these are present in the body, returns (None, None) + """ + token = None + token_secret = None + for pair in http_body.split('&'): + if pair.startswith('oauth_token='): + token = urllib.unquote(pair[len('oauth_token='):]) + if pair.startswith('oauth_token_secret='): + token_secret = urllib.unquote(pair[len('oauth_token_secret='):]) + return (token, token_secret) + + +def hmac_token_from_body(http_body, consumer_key, consumer_secret, + auth_state): + token_value, token_secret = oauth_token_info_from_body(http_body) + token = OAuthHmacToken(consumer_key, consumer_secret, token_value, + token_secret, auth_state) + return token + + +def rsa_token_from_body(http_body, consumer_key, rsa_private_key, + auth_state): + token_value, token_secret = oauth_token_info_from_body(http_body) + token = OAuthRsaToken(consumer_key, rsa_private_key, token_value, + token_secret, auth_state) + return token + + +DEFAULT_DOMAIN = 'default' +OAUTH_AUTHORIZE_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken' + + +def generate_oauth_authorization_url( + token, next=None, hd=DEFAULT_DOMAIN, hl=None, btmpl=None, + auth_server=OAUTH_AUTHORIZE_URL): + """Creates a URL for the page where the request token can be authorized. + + Args: + token: str The request token from the OAuth server. + next: str (optional) URL the user should be redirected to after granting + access to a Google service(s). It can include url-encoded query + parameters. + hd: str (optional) Identifies a particular hosted domain account to be + accessed (for example, 'mycollege.edu'). Uses 'default' to specify a + regular Google account ('username@gmail.com'). + hl: str (optional) An ISO 639 country code identifying what language the + approval page should be translated in (for example, 'hl=en' for + English). The default is the user's selected language. + btmpl: str (optional) Forces a mobile version of the approval page. The + only accepted value is 'mobile'. + auth_server: str (optional) The start of the token authorization web + page. Defaults to + 'https://www.google.com/accounts/OAuthAuthorizeToken' + + Returns: + An atom.http_core.Uri pointing to the token authorization page where the + user may allow or deny this app to access their Google data. + """ + uri = atom.http_core.Uri.parse_uri(auth_server) + uri.query['oauth_token'] = token + uri.query['hd'] = hd + if next is not None: + uri.query['oauth_callback'] = str(next) + if hl is not None: + uri.query['hl'] = hl + if btmpl is not None: + uri.query['btmpl'] = btmpl + return uri + + +def oauth_token_info_from_url(url): + """Exracts an OAuth access token from the redirected page's URL. + + Returns: + A tuple of strings containing the OAuth token and the OAuth verifier which + need to sent when upgrading a request token to an access token. + """ + if isinstance(url, (str, unicode)): + url = atom.http_core.Uri.parse_uri(url) + token = None + verifier = None + if 'oauth_token' in url.query: + token = urllib.unquote(url.query['oauth_token']) + if 'oauth_verifier' in url.query: + verifier = urllib.unquote(url.query['oauth_verifier']) + return (token, verifier) + + +def authorize_request_token(request_token, url): + """Adds information to request token to allow it to become an access token. + + Modifies the request_token object passed in by setting and unsetting the + necessary fields to allow this token to form a valid upgrade request. + + Args: + request_token: The OAuth request token which has been authorized by the + user. In order for this token to be upgraded to an access token, + certain fields must be extracted from the URL and added to the token + so that they can be passed in an upgrade-token request. + url: The URL of the current page which the user's browser was redirected + to after they authorized access for the app. This function extracts + information from the URL which is needed to upgraded the token from + a request token to an access token. + + Returns: + The same token object which was passed in. + """ + token, verifier = oauth_token_info_from_url(url) + request_token.token = token + request_token.verifier = verifier + request_token.auth_state = AUTHORIZED_REQUEST_TOKEN + return request_token + + +AuthorizeRequestToken = authorize_request_token + + +def upgrade_to_access_token(request_token, server_response_body): + """Extracts access token information from response to an upgrade request. + + Once the server has responded with the new token info for the OAuth + access token, this method modifies the request_token to set and unset + necessary fields to create valid OAuth authorization headers for requests. + + Args: + request_token: An OAuth token which this function modifies to allow it + to be used as an access token. + server_response_body: str The server's response to an OAuthAuthorizeToken + request. This should contain the new token and token_secret which + are used to generate the signature and parameters of the Authorization + header in subsequent requests to Google Data APIs. + + Returns: + The same token object which was passed in. + """ + token, token_secret = oauth_token_info_from_body(server_response_body) + request_token.token = token + request_token.token_secret = token_secret + request_token.auth_state = ACCESS_TOKEN + request_token.next = None + request_token.verifier = None + return request_token + + +UpgradeToAccessToken = upgrade_to_access_token + + +REQUEST_TOKEN = 1 +AUTHORIZED_REQUEST_TOKEN = 2 +ACCESS_TOKEN = 3 + + +class OAuthHmacToken(object): + SIGNATURE_METHOD = HMAC_SHA1 + + def __init__(self, consumer_key, consumer_secret, token, token_secret, + auth_state, next=None, verifier=None): + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.token = token + self.token_secret = token_secret + self.auth_state = auth_state + self.next = next + self.verifier = verifier # Used to convert request token to access token. + + def generate_authorization_url( + self, google_apps_domain=DEFAULT_DOMAIN, language=None, btmpl=None, + auth_server=OAUTH_AUTHORIZE_URL): + """Creates the URL at which the user can authorize this app to access. + + Args: + google_apps_domain: str (optional) If the user should be signing in + using an account under a known Google Apps domain, provide the + domain name ('example.com') here. If not provided, 'default' + will be used, and the user will be prompted to select an account + if they are signed in with a Google Account and Google Apps + accounts. + language: str (optional) An ISO 639 country code identifying what + language the approval page should be translated in (for example, + 'en' for English). The default is the user's selected language. + btmpl: str (optional) Forces a mobile version of the approval page. The + only accepted value is 'mobile'. + auth_server: str (optional) The start of the token authorization web + page. Defaults to + 'https://www.google.com/accounts/OAuthAuthorizeToken' + """ + return generate_oauth_authorization_url( + self.token, hd=google_apps_domain, hl=language, btmpl=btmpl, + auth_server=auth_server) + + GenerateAuthorizationUrl = generate_authorization_url + + def modify_request(self, http_request): + """Sets the Authorization header in the HTTP request using the token. + + Calculates an HMAC signature using the information in the token to + indicate that the request came from this application and that this + application has permission to access a particular user's data. + + Returns: + The same HTTP request object which was passed in. + """ + timestamp = str(int(time.time())) + nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) + signature = generate_hmac_signature( + http_request, self.consumer_key, self.consumer_secret, timestamp, + nonce, version='1.0', next=self.next, token=self.token, + token_secret=self.token_secret, verifier=self.verifier) + http_request.headers['Authorization'] = generate_auth_header( + self.consumer_key, timestamp, nonce, HMAC_SHA1, signature, + version='1.0', next=self.next, token=self.token, + verifier=self.verifier) + return http_request + + ModifyRequest = modify_request + + +class OAuthRsaToken(OAuthHmacToken): + SIGNATURE_METHOD = RSA_SHA1 + + def __init__(self, consumer_key, rsa_private_key, token, token_secret, + auth_state, next=None, verifier=None): + self.consumer_key = consumer_key + self.rsa_private_key = rsa_private_key + self.token = token + self.token_secret = token_secret + self.auth_state = auth_state + self.next = next + self.verifier = verifier # Used to convert request token to access token. + + def modify_request(self, http_request): + """Sets the Authorization header in the HTTP request using the token. + + Calculates an RSA signature using the information in the token to + indicate that the request came from this application and that this + application has permission to access a particular user's data. + + Returns: + The same HTTP request object which was passed in. + """ + timestamp = str(int(time.time())) + nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) + signature = generate_rsa_signature( + http_request, self.consumer_key, self.rsa_private_key, timestamp, + nonce, version='1.0', next=self.next, token=self.token, + token_secret=self.token_secret, verifier=self.verifier) + http_request.headers['Authorization'] = generate_auth_header( + self.consumer_key, timestamp, nonce, RSA_SHA1, signature, + version='1.0', next=self.next, token=self.token, + verifier=self.verifier) + return http_request + + ModifyRequest = modify_request + + +class TwoLeggedOAuthHmacToken(OAuthHmacToken): + + def __init__(self, consumer_key, consumer_secret, requestor_id): + self.requestor_id = requestor_id + OAuthHmacToken.__init__( + self, consumer_key, consumer_secret, None, None, ACCESS_TOKEN, + next=None, verifier=None) + + def modify_request(self, http_request): + """Sets the Authorization header in the HTTP request using the token. + + Calculates an HMAC signature using the information in the token to + indicate that the request came from this application and that this + application has permission to access a particular user's data using 2LO. + + Returns: + The same HTTP request object which was passed in. + """ + http_request.uri.query['xoauth_requestor_id'] = self.requestor_id + return OAuthHmacToken.modify_request(self, http_request) + + ModifyRequest = modify_request + + +class TwoLeggedOAuthRsaToken(OAuthRsaToken): + + def __init__(self, consumer_key, rsa_private_key, requestor_id): + self.requestor_id = requestor_id + OAuthRsaToken.__init__( + self, consumer_key, rsa_private_key, None, None, ACCESS_TOKEN, + next=None, verifier=None) + + def modify_request(self, http_request): + """Sets the Authorization header in the HTTP request using the token. + + Calculates an RSA signature using the information in the token to + indicate that the request came from this application and that this + application has permission to access a particular user's data using 2LO. + + Returns: + The same HTTP request object which was passed in. + """ + http_request.uri.query['xoauth_requestor_id'] = self.requestor_id + return OAuthRsaToken.modify_request(self, http_request) + + ModifyRequest = modify_request + + +def _join_token_parts(*args): + """"Escapes and combines all strings passed in. + + Used to convert a token object's members into a string instead of + using pickle. + + Note: A None value will be converted to an empty string. + + Returns: + A string in the form 1x|member1|member2|member3... + """ + return '|'.join([urllib.quote_plus(a or '') for a in args]) + + +def _split_token_parts(blob): + """Extracts and unescapes fields from the provided binary string. + + Reverses the packing performed by _join_token_parts. Used to extract + the members of a token object. + + Note: An empty string from the blob will be interpreted as None. + + Args: + blob: str A string of the form 1x|member1|member2|member3 as created + by _join_token_parts + + Returns: + A list of unescaped strings. + """ + return [urllib.unquote_plus(part) or None for part in blob.split('|')] + + +def token_to_blob(token): + """Serializes the token data as a string for storage in a datastore. + + Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken, + OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken, + TwoLeggedOAuthHmacToken. + + Args: + token: A token object which must be of one of the supported token classes. + + Raises: + UnsupportedTokenType if the token is not one of the supported token + classes listed above. + + Returns: + A string represenging this token. The string can be converted back into + an equivalent token object using token_from_blob. Note that any members + which are set to '' will be set to None when the token is deserialized + by token_from_blob. + """ + if isinstance(token, ClientLoginToken): + return _join_token_parts('1c', token.token_string) + # Check for secure auth sub type first since it is a subclass of + # AuthSubToken. + elif isinstance(token, SecureAuthSubToken): + return _join_token_parts('1s', token.token_string, token.rsa_private_key, + *token.scopes) + elif isinstance(token, AuthSubToken): + return _join_token_parts('1a', token.token_string, *token.scopes) + elif isinstance(token, TwoLeggedOAuthRsaToken): + return _join_token_parts( + '1rtl', token.consumer_key, token.rsa_private_key, token.requestor_id) + elif isinstance(token, TwoLeggedOAuthHmacToken): + return _join_token_parts( + '1htl', token.consumer_key, token.consumer_secret, token.requestor_id) + # Check RSA OAuth token first since the OAuthRsaToken is a subclass of + # OAuthHmacToken. + elif isinstance(token, OAuthRsaToken): + return _join_token_parts( + '1r', token.consumer_key, token.rsa_private_key, token.token, + token.token_secret, str(token.auth_state), token.next, + token.verifier) + elif isinstance(token, OAuthHmacToken): + return _join_token_parts( + '1h', token.consumer_key, token.consumer_secret, token.token, + token.token_secret, str(token.auth_state), token.next, + token.verifier) + else: + raise UnsupportedTokenType( + 'Unable to serialize token of type %s' % type(token)) + + +TokenToBlob = token_to_blob + + +def token_from_blob(blob): + """Deserializes a token string from the datastore back into a token object. + + Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken, + OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken, + TwoLeggedOAuthHmacToken. + + Args: + blob: string created by token_to_blob. + + Raises: + UnsupportedTokenType if the token is not one of the supported token + classes listed above. + + Returns: + A new token object with members set to the values serialized in the + blob string. Note that any members which were set to '' in the original + token will now be None. + """ + parts = _split_token_parts(blob) + if parts[0] == '1c': + return ClientLoginToken(parts[1]) + elif parts[0] == '1a': + return AuthSubToken(parts[1], parts[2:]) + elif parts[0] == '1s': + return SecureAuthSubToken(parts[1], parts[2], parts[3:]) + elif parts[0] == '1rtl': + return TwoLeggedOAuthRsaToken(parts[1], parts[2], parts[3]) + elif parts[0] == '1htl': + return TwoLeggedOAuthHmacToken(parts[1], parts[2], parts[3]) + elif parts[0] == '1r': + auth_state = int(parts[5]) + return OAuthRsaToken(parts[1], parts[2], parts[3], parts[4], auth_state, + parts[6], parts[7]) + elif parts[0] == '1h': + auth_state = int(parts[5]) + return OAuthHmacToken(parts[1], parts[2], parts[3], parts[4], auth_state, + parts[6], parts[7]) + else: + raise UnsupportedTokenType( + 'Unable to deserialize token with type marker of %s' % parts[0]) + + +TokenFromBlob = token_from_blob + + +def dump_tokens(tokens): + return ','.join([token_to_blob(t) for t in tokens]) + + +def load_tokens(blob): + return [token_from_blob(s) for s in blob.split(',')] + + +def find_scopes_for_services(service_names=None): + """Creates a combined list of scope URLs for the desired services. + + This method searches the AUTH_SCOPES dictionary. + + Args: + service_names: list of strings (optional) Each name must be a key in the + AUTH_SCOPES dictionary. If no list is provided (None) then + the resulting list will contain all scope URLs in the + AUTH_SCOPES dict. + + Returns: + A list of URL strings which are the scopes needed to access these services + when requesting a token using AuthSub or OAuth. + """ + result_scopes = [] + if service_names is None: + for service_name, scopes in AUTH_SCOPES.iteritems(): + result_scopes.extend(scopes) + else: + for service_name in service_names: + result_scopes.extend(AUTH_SCOPES[service_name]) + return result_scopes + + +FindScopesForServices = find_scopes_for_services + + +def ae_save(token, token_key): + """Stores an auth token in the App Engine datastore. + + This is a convenience method for using the library with App Engine. + Recommended usage is to associate the auth token with the current_user. + If a user is signed in to the app using the App Engine users API, you + can use + gdata.gauth.ae_save(some_token, users.get_current_user().user_id()) + If you are not using the Users API you are free to choose whatever + string you would like for a token_string. + + Args: + token: an auth token object. Must be one of ClientLoginToken, + AuthSubToken, SecureAuthSubToken, OAuthRsaToken, or OAuthHmacToken + (see token_to_blob). + token_key: str A unique identified to be used when you want to retrieve + the token. If the user is signed in to App Engine using the + users API, I recommend using the user ID for the token_key: + users.get_current_user().user_id() + """ + import gdata.alt.app_engine + key_name = ''.join(('gd_auth_token', token_key)) + return gdata.alt.app_engine.set_token(key_name, token_to_blob(token)) + + +AeSave = ae_save + + +def ae_load(token_key): + """Retrieves a token object from the App Engine datastore. + + This is a convenience method for using the library with App Engine. + See also ae_save. + + Args: + token_key: str The unique key associated with the desired token when it + was saved using ae_save. + + Returns: + A token object if there was a token associated with the token_key or None + if the key could not be found. + """ + import gdata.alt.app_engine + key_name = ''.join(('gd_auth_token', token_key)) + token_string = gdata.alt.app_engine.get_token(key_name) + if token_string is not None: + return token_from_blob(token_string) + else: + return None + + +AeLoad = ae_load + + +def ae_delete(token_key): + """Removes the token object from the App Engine datastore.""" + import gdata.alt.app_engine + key_name = ''.join(('gd_auth_token', token_key)) + gdata.alt.app_engine.delete_token(key_name) + + +AeDelete = ae_delete diff --git a/patches/gdata/gauth.pyc b/patches/gdata/gauth.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04e4fdc4cb4a9f2c2a020fca824c748adce6196e GIT binary patch literal 49134 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 Cb5m^q literal 0 HcmV?d00001 diff --git a/patches/gdata/geo/__init__.py b/patches/gdata/geo/__init__.py new file mode 100644 index 0000000..1fcf604 --- /dev/null +++ b/patches/gdata/geo/__init__.py @@ -0,0 +1,185 @@ +# -*-*- encoding: utf-8 -*-*- +# +# This is gdata.photos.geo, implementing geological positioning in gdata structures +# +# $Id: __init__.py 81 2007-10-03 14:41:42Z havard.gulldahl $ +# +# Copyright 2007 HÃ¥vard Gulldahl +# Portions copyright 2007 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. + +"""Picasa Web Albums uses the georss and gml namespaces for +elements defined in the GeoRSS and Geography Markup Language specifications. + +Specifically, Picasa Web Albums uses the following elements: + +georss:where +gml:Point +gml:pos + +http://code.google.com/apis/picasaweb/reference.html#georss_reference + + +Picasa Web Albums also accepts geographic-location data in two other formats: +W3C format and plain-GeoRSS (without GML) format. +""" +# +#Over the wire, the Picasa Web Albums only accepts and sends the +#elements mentioned above, but this module will let you seamlessly convert +#between the different formats (TODO 2007-10-18 hg) + +__author__ = u'havard@gulldahl.no'# (HÃ¥vard Gulldahl)' #BUG: api chokes on non-ascii chars in __author__ +__license__ = 'Apache License v2' + + +import atom +import gdata + +GEO_NAMESPACE = 'http://www.w3.org/2003/01/geo/wgs84_pos#' +GML_NAMESPACE = 'http://www.opengis.net/gml' +GEORSS_NAMESPACE = 'http://www.georss.org/georss' + +class GeoBaseElement(atom.AtomBase): + """Base class for elements. + + To add new elements, you only need to add the element tag name to self._tag + and the namespace to self._namespace + """ + + _tag = '' + _namespace = GML_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, name=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +class Pos(GeoBaseElement): + """(string) Specifies a latitude and longitude, separated by a space, + e.g. `35.669998 139.770004'""" + + _tag = 'pos' +def PosFromString(xml_string): + return atom.CreateClassFromXMLString(Pos, xml_string) + +class Point(GeoBaseElement): + """(container) Specifies a particular geographical point, by means of + a <gml:pos> element.""" + + _tag = 'Point' + _children = atom.AtomBase._children.copy() + _children['{%s}pos' % GML_NAMESPACE] = ('pos', Pos) + def __init__(self, pos=None, extension_elements=None, extension_attributes=None, text=None): + GeoBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + if pos is None: + pos = Pos() + self.pos=pos +def PointFromString(xml_string): + return atom.CreateClassFromXMLString(Point, xml_string) + +class Where(GeoBaseElement): + """(container) Specifies a geographical location or region. + A container element, containing a single <gml:Point> element. + (Not to be confused with <gd:where>.) + + Note that the (only) child attribute, .Point, is title-cased. + This reflects the names of elements in the xml stream + (principle of least surprise). + + As a convenience, you can get a tuple of (lat, lon) with Where.location(), + and set the same data with Where.setLocation( (lat, lon) ). + + Similarly, there are methods to set and get only latitude and longitude. + """ + + _tag = 'where' + _namespace = GEORSS_NAMESPACE + _children = atom.AtomBase._children.copy() + _children['{%s}Point' % GML_NAMESPACE] = ('Point', Point) + def __init__(self, point=None, extension_elements=None, extension_attributes=None, text=None): + GeoBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + if point is None: + point = Point() + self.Point=point + def location(self): + "(float, float) Return Where.Point.pos.text as a (lat,lon) tuple" + try: + return tuple([float(z) for z in self.Point.pos.text.split(' ')]) + except AttributeError: + return tuple() + def set_location(self, latlon): + """(bool) Set Where.Point.pos.text from a (lat,lon) tuple. + + Arguments: + lat (float): The latitude in degrees, from -90.0 to 90.0 + lon (float): The longitude in degrees, from -180.0 to 180.0 + + Returns True on success. + + """ + + assert(isinstance(latlon[0], float)) + assert(isinstance(latlon[1], float)) + try: + self.Point.pos.text = "%s %s" % (latlon[0], latlon[1]) + return True + except AttributeError: + return False + def latitude(self): + "(float) Get the latitude value of the geo-tag. See also .location()" + lat, lon = self.location() + return lat + + def longitude(self): + "(float) Get the longtitude value of the geo-tag. See also .location()" + lat, lon = self.location() + return lon + + longtitude = longitude + + def set_latitude(self, lat): + """(bool) Set the latitude value of the geo-tag. + + Args: + lat (float): The new latitude value + + See also .set_location() + """ + _lat, lon = self.location() + return self.set_location(lat, lon) + + def set_longitude(self, lon): + """(bool) Set the longtitude value of the geo-tag. + + Args: + lat (float): The new latitude value + + See also .set_location() + """ + lat, _lon = self.location() + return self.set_location(lat, lon) + + set_longtitude = set_longitude + +def WhereFromString(xml_string): + return atom.CreateClassFromXMLString(Where, xml_string) + diff --git a/patches/gdata/geo/data.py b/patches/gdata/geo/data.py new file mode 100644 index 0000000..2aec911 --- /dev/null +++ b/patches/gdata/geo/data.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Contains the data classes of the Geography Extension""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core + + +GEORSS_TEMPLATE = '{http://www.georss.org/georss/}%s' +GML_TEMPLATE = '{http://www.opengis.net/gml/}%s' +GEO_TEMPLATE = '{http://www.w3.org/2003/01/geo/wgs84_pos#/}%s' + + +class GeoLat(atom.core.XmlElement): + """Describes a W3C latitude.""" + _qname = GEO_TEMPLATE % 'lat' + + +class GeoLong(atom.core.XmlElement): + """Describes a W3C longitude.""" + _qname = GEO_TEMPLATE % 'long' + + +class GeoRssBox(atom.core.XmlElement): + """Describes a geographical region.""" + _qname = GEORSS_TEMPLATE % 'box' + + +class GeoRssPoint(atom.core.XmlElement): + """Describes a geographical location.""" + _qname = GEORSS_TEMPLATE % 'point' + + +class GmlLowerCorner(atom.core.XmlElement): + """Describes a lower corner of a region.""" + _qname = GML_TEMPLATE % 'lowerCorner' + + +class GmlPos(atom.core.XmlElement): + """Describes a latitude and longitude.""" + _qname = GML_TEMPLATE % 'pos' + + +class GmlPoint(atom.core.XmlElement): + """Describes a particular geographical point.""" + _qname = GML_TEMPLATE % 'Point' + pos = GmlPos + + +class GmlUpperCorner(atom.core.XmlElement): + """Describes an upper corner of a region.""" + _qname = GML_TEMPLATE % 'upperCorner' + + +class GmlEnvelope(atom.core.XmlElement): + """Describes a Gml geographical region.""" + _qname = GML_TEMPLATE % 'Envelope' + lower_corner = GmlLowerCorner + upper_corner = GmlUpperCorner + + +class GeoRssWhere(atom.core.XmlElement): + """Describes a geographical location or region.""" + _qname = GEORSS_TEMPLATE % 'where' + Point = GmlPoint + Envelope = GmlEnvelope + + +class W3CPoint(atom.core.XmlElement): + """Describes a W3C geographical location.""" + _qname = GEO_TEMPLATE % 'Point' + long = GeoLong + lat = GeoLat + + diff --git a/patches/gdata/health/__init__.py b/patches/gdata/health/__init__.py new file mode 100644 index 0000000..1904ecd --- /dev/null +++ b/patches/gdata/health/__init__.py @@ -0,0 +1,229 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""Contains extensions to Atom objects used with Google Health.""" + +__author__ = 'api.eric@google.com (Eric Bidelman)' + +import atom +import gdata + + +CCR_NAMESPACE = 'urn:astm-org:CCR' +METADATA_NAMESPACE = 'http://schemas.google.com/health/metadata' + + +class Ccr(atom.AtomBase): + """Represents a Google Health <ContinuityOfCareRecord>.""" + + _tag = 'ContinuityOfCareRecord' + _namespace = CCR_NAMESPACE + _children = atom.AtomBase._children.copy() + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + def GetAlerts(self): + """Helper for extracting Alert/Allergy data from the CCR. + + Returns: + A list of ExtensionElements (one for each allergy found) or None if + no allergies where found in this CCR. + """ + try: + body = self.FindExtensions('Body')[0] + return body.FindChildren('Alerts')[0].FindChildren('Alert') + except: + return None + + def GetAllergies(self): + """Alias for GetAlerts().""" + return self.GetAlerts() + + def GetProblems(self): + """Helper for extracting Problem/Condition data from the CCR. + + Returns: + A list of ExtensionElements (one for each problem found) or None if + no problems where found in this CCR. + """ + try: + body = self.FindExtensions('Body')[0] + return body.FindChildren('Problems')[0].FindChildren('Problem') + except: + return None + + def GetConditions(self): + """Alias for GetProblems().""" + return self.GetProblems() + + def GetProcedures(self): + """Helper for extracting Procedure data from the CCR. + + Returns: + A list of ExtensionElements (one for each procedure found) or None if + no procedures where found in this CCR. + """ + try: + body = self.FindExtensions('Body')[0] + return body.FindChildren('Procedures')[0].FindChildren('Procedure') + except: + return None + + def GetImmunizations(self): + """Helper for extracting Immunization data from the CCR. + + Returns: + A list of ExtensionElements (one for each immunization found) or None if + no immunizations where found in this CCR. + """ + try: + body = self.FindExtensions('Body')[0] + return body.FindChildren('Immunizations')[0].FindChildren('Immunization') + except: + return None + + def GetMedications(self): + """Helper for extracting Medication data from the CCR. + + Returns: + A list of ExtensionElements (one for each medication found) or None if + no medications where found in this CCR. + """ + try: + body = self.FindExtensions('Body')[0] + return body.FindChildren('Medications')[0].FindChildren('Medication') + except: + return None + + def GetResults(self): + """Helper for extracting Results/Labresults data from the CCR. + + Returns: + A list of ExtensionElements (one for each result found) or None if + no results where found in this CCR. + """ + try: + body = self.FindExtensions('Body')[0] + return body.FindChildren('Results')[0].FindChildren('Result') + except: + return None + + +class ProfileEntry(gdata.GDataEntry): + """The Google Health version of an Atom Entry.""" + + _tag = gdata.GDataEntry._tag + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}ContinuityOfCareRecord' % CCR_NAMESPACE] = ('ccr', Ccr) + + def __init__(self, ccr=None, author=None, category=None, content=None, + atom_id=None, link=None, published=None, title=None, + updated=None, text=None, extension_elements=None, + extension_attributes=None): + self.ccr = ccr + gdata.GDataEntry.__init__( + self, author=author, category=category, content=content, + atom_id=atom_id, link=link, published=published, title=title, + updated=updated, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class ProfileFeed(gdata.GDataFeed): + """A feed containing a list of Google Health profile entries.""" + + _tag = gdata.GDataFeed._tag + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ProfileEntry]) + + +class ProfileListEntry(gdata.GDataEntry): + """The Atom Entry in the Google Health profile list feed.""" + + _tag = gdata.GDataEntry._tag + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + + def GetProfileId(self): + return self.content.text + + def GetProfileName(self): + return self.title.text + + +class ProfileListFeed(gdata.GDataFeed): + """A feed containing a list of Google Health profile list entries.""" + + _tag = gdata.GDataFeed._tag + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ProfileListEntry]) + + +def ProfileEntryFromString(xml_string): + """Converts an XML string into a ProfileEntry object. + + Args: + xml_string: string The XML describing a Health profile feed entry. + + Returns: + A ProfileEntry object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(ProfileEntry, xml_string) + + +def ProfileListEntryFromString(xml_string): + """Converts an XML string into a ProfileListEntry object. + + Args: + xml_string: string The XML describing a Health profile list feed entry. + + Returns: + A ProfileListEntry object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(ProfileListEntry, xml_string) + + +def ProfileFeedFromString(xml_string): + """Converts an XML string into a ProfileFeed object. + + Args: + xml_string: string The XML describing a ProfileFeed feed. + + Returns: + A ProfileFeed object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(ProfileFeed, xml_string) + + +def ProfileListFeedFromString(xml_string): + """Converts an XML string into a ProfileListFeed object. + + Args: + xml_string: string The XML describing a ProfileListFeed feed. + + Returns: + A ProfileListFeed object corresponding to the given XML. + """ + return atom.CreateClassFromXMLString(ProfileListFeed, xml_string) diff --git a/patches/gdata/health/service.py b/patches/gdata/health/service.py new file mode 100644 index 0000000..3d38411 --- /dev/null +++ b/patches/gdata/health/service.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""HealthService extends GDataService to streamline Google Health API access. + + HealthService: Provides methods to interact with the profile, profile list, + and register/notices feeds. Extends GDataService. + + HealthProfileQuery: Queries the Google Health Profile feed. + + HealthProfileListQuery: Queries the Google Health Profile list feed. +""" + +__author__ = 'api.eric@google.com (Eric Bidelman)' + + +import atom +import gdata.health +import gdata.service + + +class HealthService(gdata.service.GDataService): + + """Client extension for the Google Health service Document List feed.""" + + def __init__(self, email=None, password=None, source=None, + use_h9_sandbox=False, server='www.google.com', + additional_headers=None, **kwargs): + """Creates a client for the Google Health service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + use_h9_sandbox: boolean (optional) True to issue requests against the + /h9 developer's sandbox. + server: string (optional) The name of the server to which a connection + will be opened. + additional_headers: dictionary (optional) Any additional headers which + should be included with CRUD operations. + kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + service = use_h9_sandbox and 'weaver' or 'health' + gdata.service.GDataService.__init__( + self, email=email, password=password, service=service, source=source, + server=server, additional_headers=additional_headers, **kwargs) + self.ssl = True + self.use_h9_sandbox = use_h9_sandbox + + def __get_service(self): + return self.use_h9_sandbox and 'h9' or 'health' + + def GetProfileFeed(self, query=None, profile_id=None): + """Fetches the users Google Health profile feed. + + Args: + query: HealthProfileQuery or string (optional) A query to use on the + profile feed. If None, a HealthProfileQuery is constructed. + profile_id: string (optional) The profile id to query the profile feed + with when using ClientLogin. Note: this parameter is ignored if + query is set. + + Returns: + A gdata.health.ProfileFeed object containing the user's Health profile. + """ + if query is None: + projection = profile_id and 'ui' or 'default' + uri = HealthProfileQuery( + service=self.__get_service(), projection=projection, + profile_id=profile_id).ToUri() + elif isinstance(query, HealthProfileQuery): + uri = query.ToUri() + else: + uri = query + + return self.GetFeed(uri, converter=gdata.health.ProfileFeedFromString) + + def GetProfileListFeed(self, query=None): + """Fetches the users Google Health profile feed. + + Args: + query: HealthProfileListQuery or string (optional) A query to use + on the profile list feed. If None, a HealthProfileListQuery is + constructed to /health/feeds/profile/list or /h9/feeds/profile/list. + + Returns: + A gdata.health.ProfileListFeed object containing the user's list + of profiles. + """ + if not query: + uri = HealthProfileListQuery(service=self.__get_service()).ToUri() + elif isinstance(query, HealthProfileListQuery): + uri = query.ToUri() + else: + uri = query + + return self.GetFeed(uri, converter=gdata.health.ProfileListFeedFromString) + + def SendNotice(self, subject, body=None, content_type='html', + ccr=None, profile_id=None): + """Sends (posts) a notice to the user's Google Health profile. + + Args: + subject: A string representing the message's subject line. + body: string (optional) The message body. + content_type: string (optional) The content type of the notice message + body. This parameter is only honored when a message body is + specified. + ccr: string (optional) The CCR XML document to reconcile into the + user's profile. + profile_id: string (optional) The profile id to work with when using + ClientLogin. Note: this parameter is ignored if query is set. + + Returns: + A gdata.health.ProfileEntry object of the posted entry. + """ + if body: + content = atom.Content(content_type=content_type, text=body) + else: + content = body + + entry = gdata.GDataEntry( + title=atom.Title(text=subject), content=content, + extension_elements=[atom.ExtensionElementFromString(ccr)]) + + projection = profile_id and 'ui' or 'default' + query = HealthRegisterQuery(service=self.__get_service(), + projection=projection, profile_id=profile_id) + return self.Post(entry, query.ToUri(), + converter=gdata.health.ProfileEntryFromString) + + +class HealthProfileQuery(gdata.service.Query): + + """Object used to construct a URI to query the Google Health profile feed.""" + + def __init__(self, service='health', feed='feeds/profile', + projection='default', profile_id=None, text_query=None, + params=None, categories=None): + """Constructor for Health profile feed query. + + Args: + service: string (optional) The service to query. Either 'health' or 'h9'. + feed: string (optional) The path for the feed. The default value is + 'feeds/profile'. + projection: string (optional) The visibility of the data. Possible values + are 'default' for AuthSub and 'ui' for ClientLogin. If this value + is set to 'ui', the profile_id parameter should also be set. + profile_id: string (optional) The profile id to query. This should only + be used when using ClientLogin. + text_query: str (optional) The contents of the q query parameter. The + contents of the text_query are URL escaped upon conversion to a URI. + Note: this parameter can only be used on the register feed using + ClientLogin. + params: dict (optional) Parameter value string pairs which become URL + params when translated to a URI. These parameters are added to + the query's items. + categories: list (optional) List of category strings which should be + included as query categories. See gdata.service.Query for + additional documentation. + """ + self.service = service + self.profile_id = profile_id + self.projection = projection + gdata.service.Query.__init__(self, feed=feed, text_query=text_query, + params=params, categories=categories) + + def ToUri(self): + """Generates a URI from the query parameters set in the object. + + Returns: + A string containing the URI used to retrieve entries from the Health + profile feed. + """ + old_feed = self.feed + self.feed = '/'.join([self.service, old_feed, self.projection]) + + if self.profile_id: + self.feed += '/' + self.profile_id + self.feed = '/%s' % (self.feed,) + + new_feed = gdata.service.Query.ToUri(self) + self.feed = old_feed + return new_feed + + +class HealthProfileListQuery(gdata.service.Query): + + """Object used to construct a URI to query a Health profile list feed.""" + + def __init__(self, service='health', feed='feeds/profile/list'): + """Constructor for Health profile list feed query. + + Args: + service: string (optional) The service to query. Either 'health' or 'h9'. + feed: string (optional) The path for the feed. The default value is + 'feeds/profile/list'. + """ + gdata.service.Query.__init__(self, feed) + self.service = service + + def ToUri(self): + """Generates a URI from the query parameters set in the object. + + Returns: + A string containing the URI used to retrieve entries from the + profile list feed. + """ + return '/%s' % ('/'.join([self.service, self.feed]),) + + +class HealthRegisterQuery(gdata.service.Query): + + """Object used to construct a URI to query a Health register/notice feed.""" + + def __init__(self, service='health', feed='feeds/register', + projection='default', profile_id=None): + """Constructor for Health profile list feed query. + + Args: + service: string (optional) The service to query. Either 'health' or 'h9'. + feed: string (optional) The path for the feed. The default value is + 'feeds/register'. + projection: string (optional) The visibility of the data. Possible values + are 'default' for AuthSub and 'ui' for ClientLogin. If this value + is set to 'ui', the profile_id parameter should also be set. + profile_id: string (optional) The profile id to query. This should only + be used when using ClientLogin. + """ + gdata.service.Query.__init__(self, feed) + self.service = service + self.projection = projection + self.profile_id = profile_id + + def ToUri(self): + """Generates a URI from the query parameters set in the object. + + Returns: + A string containing the URI needed to interact with the register feed. + """ + old_feed = self.feed + self.feed = '/'.join([self.service, old_feed, self.projection]) + new_feed = gdata.service.Query.ToUri(self) + self.feed = old_feed + + if self.profile_id: + new_feed += '/' + self.profile_id + return '/%s' % (new_feed,) diff --git a/patches/gdata/marketplace/__init__.py b/patches/gdata/marketplace/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/patches/gdata/marketplace/__init__.py @@ -0,0 +1 @@ + diff --git a/patches/gdata/marketplace/client.py b/patches/gdata/marketplace/client.py new file mode 100644 index 0000000..8ffc348 --- /dev/null +++ b/patches/gdata/marketplace/client.py @@ -0,0 +1,160 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""LicensingClient simplifies Google Apps Marketplace Licensing API calls. + +LicensingClient extends gdata.client.GDClient to ease interaction with +the Google Apps Marketplace Licensing API. These interactions include the ability +to retrieve License informations for an application in the Google Apps Marketplace. +""" + + +__author__ = 'Alexandre Vivien <alex@simplecode.fr>' + +import gdata.marketplace.data +import gdata.client +import urllib + + +# Feed URI template. This must end with a / +# The strings in this template are eventually replaced with the API version +# and Google Apps domain name, respectively. +LICENSE_ROOT_URL = 'http://feedserver-enterprise.googleusercontent.com' +LICENSE_FEED_TEMPLATE = '%s/license?bq=' % LICENSE_ROOT_URL +LICENSE_NOTIFICATIONS_FEED_TEMPLATE = '%s/licensenotification?bq=' % LICENSE_ROOT_URL + + +class LicensingClient(gdata.client.GDClient): + """Client extension for the Google Apps Marketplace Licensing API service. + + Attributes: + host: string The hostname for the Google Apps Marketplace Licensing API service. + api_version: string The version of the Google Apps Marketplace Licensing API. + """ + + api_version = '1.0' + auth_service = 'apps' + auth_scopes = gdata.gauth.AUTH_SCOPES['apps'] + ssl = False + + def __init__(self, domain, auth_token=None, **kwargs): + """Constructs a new client for the Google Apps Marketplace Licensing API. + + Args: + domain: string The Google Apps domain with the application installed. + auth_token: (optional) gdata.gauth.OAuthToken which authorizes this client to retrieve the License information. + kwargs: The other parameters to pass to the gdata.client.GDClient constructor. + """ + gdata.client.GDClient.__init__(self, auth_token=auth_token, **kwargs) + self.domain = domain + + def make_license_feed_uri(self, app_id=None, params=None): + """Creates a license feed URI for the Google Apps Marketplace Licensing API. + + Using this client's Google Apps domain, create a license feed URI for a particular application + in this domain. If params are provided, append them as GET params. + + Args: + app_id: string The ID of the application for which to make a license feed URI. + params: dict (optional) key -> value params to append as GET vars to the + URI. Example: params={'start': 'my-resource-id'} + Returns: + A string giving the URI for the application's license for this client's Google + Apps domain. + """ + parameters = '[appid=%s][domain=%s]' % (app_id, self.domain) + uri = LICENSE_FEED_TEMPLATE + urllib.quote_plus(parameters) + if params: + uri += '&' + urllib.urlencode(params) + return uri + + MakeLicenseFeedUri = make_license_feed_uri + + def make_license_notifications_feed_uri(self, app_id=None, startdatetime=None, max_results=None, params=None): + """Creates a license notifications feed URI for the Google Apps Marketplace Licensing API. + + Using this client's Google Apps domain, create a license notifications feed URI for a particular application. + If params are provided, append them as GET params. + + Args: + app_id: string The ID of the application for which to make a license feed URI. + startdatetime: Start date to retrieve the License notifications. + max_results: Number of results per page. Maximum is 100. + params: dict (optional) key -> value params to append as GET vars to the + URI. Example: params={'start': 'my-resource-id'} + Returns: + A string giving the URI for the application's license notifications for this client's Google + Apps domain. + """ + parameters = '[appid=%s]' % (app_id) + if startdatetime: + parameters += '[startdatetime=%s]' % startdatetime + else: + parameters += '[startdatetime=1970-01-01T00:00:00Z]' + if max_results: + parameters += '[max-results=%s]' % max_results + else: + parameters += '[max-results=100]' + uri = LICENSE_NOTIFICATIONS_FEED_TEMPLATE + urllib.quote_plus(parameters) + if params: + uri += '&' + urllib.urlencode(params) + return uri + + MakeLicenseNotificationsFeedUri = make_license_notifications_feed_uri + + def get_license(self, uri=None, app_id=None, **kwargs): + """Fetches the application's license by application ID. + + Args: + uri: string The base URI of the feed from which to fetch the license. + app_id: string The string ID of the application for which to fetch the license. + kwargs: The other parameters to pass to gdata.client.GDClient.get_entry(). + + Returns: + A License feed object representing the license with the given + base URI and application ID. + """ + + if uri is None: + uri = self.MakeLicenseFeedUri(app_id) + return self.get_feed(uri, + desired_class=gdata.marketplace.data.LicenseFeed, + **kwargs) + + GetLicense = get_license + + def get_license_notifications(self, uri=None, app_id=None, startdatetime=None, max_results=None, **kwargs): + """Fetches the application's license notifications by application ID. + + Args: + uri: string The base URI of the feed from which to fetch the license. + app_id: string The string ID of the application for which to fetch the license. + startdatetime: Start date to retrieve the License notifications. + max_results: Number of results per page. Maximum is 100. + kwargs: The other parameters to pass to gdata.client.GDClient.get_entry(). + + Returns: + A License feed object representing the license notifications with the given + base URI and application ID. + """ + + if uri is None: + uri = self.MakeLicenseNotificationsFeedUri(app_id, startdatetime, max_results) + return self.get_feed(uri, + desired_class=gdata.marketplace.data.LicenseFeed, + **kwargs) + + GetLicenseNotifications = get_license_notifications diff --git a/patches/gdata/marketplace/data.py b/patches/gdata/marketplace/data.py new file mode 100644 index 0000000..e8c76a2 --- /dev/null +++ b/patches/gdata/marketplace/data.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""Data model for parsing and generating XML for the Google Apps Marketplace Licensing API.""" + + +__author__ = 'Alexandre Vivien <alex@simplecode.fr>' + + +import atom.core +import gdata +import gdata.data + + +LICENSES_NAMESPACE = 'http://www.w3.org/2005/Atom' +LICENSES_TEMPLATE = '{%s}%%s' % LICENSES_NAMESPACE + + +class Enabled(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'enabled' + + +class Id(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'id' + + +class CustomerId(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'customerid' + + +class DomainName(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'domainname' + + +class InstallerEmail(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'installeremail' + + +class TosAcceptanceTime(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'tosacceptancetime' + + +class LastChangeTime(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'lastchangetime' + + +class ProductConfigId(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'productconfigid' + + +class State(atom.core.XmlElement): + """ """ + + _qname = LICENSES_TEMPLATE % 'state' + + +class Entity(atom.core.XmlElement): + """ The entity representing the License. """ + + _qname = LICENSES_TEMPLATE % 'entity' + + enabled = Enabled + id = Id + customer_id = CustomerId + domain_name = DomainName + installer_email = InstallerEmail + tos_acceptance_time = TosAcceptanceTime + last_change_time = LastChangeTime + product_config_id = ProductConfigId + state = State + + +class Content(atom.data.Content): + entity = Entity + +class LicenseEntry(gdata.data.GDEntry): + """ Represents a LicenseEntry object. """ + + content = Content + + +class LicenseFeed(gdata.data.GDFeed): + """ Represents a feed of LicenseEntry objects. """ + + # Override entry so that this feed knows how to type its list of entries. + entry = [LicenseEntry] diff --git a/patches/gdata/media/__init__.py b/patches/gdata/media/__init__.py new file mode 100644 index 0000000..e6af1ae --- /dev/null +++ b/patches/gdata/media/__init__.py @@ -0,0 +1,355 @@ +# -*-*- encoding: utf-8 -*-*- +# +# This is gdata.photos.media, implementing parts of the MediaRSS spec in gdata structures +# +# $Id: __init__.py 81 2007-10-03 14:41:42Z havard.gulldahl $ +# +# Copyright 2007 HÃ¥vard Gulldahl +# Portions copyright 2007 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. + + +"""Essential attributes of photos in Google Photos/Picasa Web Albums are +expressed using elements from the `media' namespace, defined in the +MediaRSS specification[1]. + +Due to copyright issues, the elements herein are documented sparingly, please +consult with the Google Photos API Reference Guide[2], alternatively the +official MediaRSS specification[1] for details. +(If there is a version conflict between the two sources, stick to the +Google Photos API). + +[1]: http://search.yahoo.com/mrss (version 1.1.1) +[2]: http://code.google.com/apis/picasaweb/reference.html#media_reference + +Keep in mind that Google Photos only uses a subset of the MediaRSS elements +(and some of the attributes are trimmed down, too): + +media:content +media:credit +media:description +media:group +media:keywords +media:thumbnail +media:title +""" + +__author__ = u'havard@gulldahl.no'# (HÃ¥vard Gulldahl)' #BUG: api chokes on non-ascii chars in __author__ +__license__ = 'Apache License v2' + + +import atom +import gdata + +MEDIA_NAMESPACE = 'http://search.yahoo.com/mrss/' +YOUTUBE_NAMESPACE = 'http://gdata.youtube.com/schemas/2007' + + +class MediaBaseElement(atom.AtomBase): + """Base class for elements in the MEDIA_NAMESPACE. + To add new elements, you only need to add the element tag name to self._tag + """ + + _tag = '' + _namespace = MEDIA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, name=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Content(MediaBaseElement): + """(attribute container) This element describes the original content, + e.g. an image or a video. There may be multiple Content elements + in a media:Group. + + For example, a video may have a + <media:content medium="image"> element that specifies a JPEG + representation of the video, and a <media:content medium="video"> + element that specifies the URL of the video itself. + + Attributes: + url: non-ambigous reference to online object + width: width of the object frame, in pixels + height: width of the object frame, in pixels + medium: one of `image' or `video', allowing the api user to quickly + determine the object's type + type: Internet media Type[1] (a.k.a. mime type) of the object -- a more + verbose way of determining the media type. To set the type member + in the contructor, use the content_type parameter. + (optional) fileSize: the size of the object, in bytes + + [1]: http://en.wikipedia.org/wiki/Internet_media_type + """ + + _tag = 'content' + _attributes = atom.AtomBase._attributes.copy() + _attributes['url'] = 'url' + _attributes['width'] = 'width' + _attributes['height'] = 'height' + _attributes['medium'] = 'medium' + _attributes['type'] = 'type' + _attributes['fileSize'] = 'fileSize' + + def __init__(self, url=None, width=None, height=None, + medium=None, content_type=None, fileSize=None, format=None, + extension_elements=None, extension_attributes=None, text=None): + MediaBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + self.url = url + self.width = width + self.height = height + self.medium = medium + self.type = content_type + self.fileSize = fileSize + + +def ContentFromString(xml_string): + return atom.CreateClassFromXMLString(Content, xml_string) + + +class Credit(MediaBaseElement): + """(string) Contains the nickname of the user who created the content, + e.g. `Liz Bennet'. + + This is a user-specified value that should be used when referring to + the user by name. + + Note that none of the attributes from the MediaRSS spec are supported. + """ + + _tag = 'credit' + + +def CreditFromString(xml_string): + return atom.CreateClassFromXMLString(Credit, xml_string) + + +class Description(MediaBaseElement): + """(string) A description of the media object. + Either plain unicode text, or entity-encoded html (look at the `type' + attribute). + + E.g `A set of photographs I took while vacationing in Italy.' + + For `api' projections, the description is in plain text; + for `base' projections, the description is in HTML. + + Attributes: + type: either `text' or `html'. To set the type member in the contructor, + use the description_type parameter. + """ + + _tag = 'description' + _attributes = atom.AtomBase._attributes.copy() + _attributes['type'] = 'type' + def __init__(self, description_type=None, + extension_elements=None, extension_attributes=None, text=None): + MediaBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + self.type = description_type + + +def DescriptionFromString(xml_string): + return atom.CreateClassFromXMLString(Description, xml_string) + + +class Keywords(MediaBaseElement): + """(string) Lists the tags associated with the entry, + e.g `italy, vacation, sunset'. + + Contains a comma-separated list of tags that have been added to the photo, or + all tags that have been added to photos in the album. + """ + + _tag = 'keywords' + + +def KeywordsFromString(xml_string): + return atom.CreateClassFromXMLString(Keywords, xml_string) + + +class Thumbnail(MediaBaseElement): + """(attributes) Contains the URL of a thumbnail of a photo or album cover. + + There can be multiple <media:thumbnail> elements for a given <media:group>; + for example, a given item may have multiple thumbnails at different sizes. + Photos generally have two thumbnails at different sizes; + albums generally have one cropped thumbnail. + + If the thumbsize parameter is set to the initial query, this element points + to thumbnails of the requested sizes; otherwise the thumbnails are the + default thumbnail size. + + This element must not be confused with the <gphoto:thumbnail> element. + + Attributes: + url: The URL of the thumbnail image. + height: The height of the thumbnail image, in pixels. + width: The width of the thumbnail image, in pixels. + """ + + _tag = 'thumbnail' + _attributes = atom.AtomBase._attributes.copy() + _attributes['url'] = 'url' + _attributes['width'] = 'width' + _attributes['height'] = 'height' + def __init__(self, url=None, width=None, height=None, + extension_attributes=None, text=None, extension_elements=None): + MediaBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + self.url = url + self.width = width + self.height = height + + +def ThumbnailFromString(xml_string): + return atom.CreateClassFromXMLString(Thumbnail, xml_string) + + +class Title(MediaBaseElement): + """(string) Contains the title of the entry's media content, in plain text. + + Attributes: + type: Always set to plain. To set the type member in the constructor, use + the title_type parameter. + """ + + _tag = 'title' + _attributes = atom.AtomBase._attributes.copy() + _attributes['type'] = 'type' + def __init__(self, title_type=None, + extension_attributes=None, text=None, extension_elements=None): + MediaBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + self.type = title_type + + +def TitleFromString(xml_string): + return atom.CreateClassFromXMLString(Title, xml_string) + + +class Player(MediaBaseElement): + """(string) Contains the embeddable player URL for the entry's media content + if the media is a video. + + Attributes: + url: Always set to plain + """ + + _tag = 'player' + _attributes = atom.AtomBase._attributes.copy() + _attributes['url'] = 'url' + + def __init__(self, player_url=None, + extension_attributes=None, extension_elements=None): + MediaBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.url= player_url + + +class Private(atom.AtomBase): + """The YouTube Private element""" + _tag = 'private' + _namespace = YOUTUBE_NAMESPACE + + +class Duration(atom.AtomBase): + """The YouTube Duration element""" + _tag = 'duration' + _namespace = YOUTUBE_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['seconds'] = 'seconds' + + +class Category(MediaBaseElement): + """The mediagroup:category element""" + + _tag = 'category' + _attributes = atom.AtomBase._attributes.copy() + _attributes['term'] = 'term' + _attributes['scheme'] = 'scheme' + _attributes['label'] = 'label' + + def __init__(self, term=None, scheme=None, label=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Category + + Args: + term: str + scheme: str + label: str + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.term = term + self.scheme = scheme + self.label = label + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Group(MediaBaseElement): + """Container element for all media elements. + The <media:group> element can appear as a child of an album, photo or + video entry.""" + + _tag = 'group' + _children = atom.AtomBase._children.copy() + _children['{%s}content' % MEDIA_NAMESPACE] = ('content', [Content,]) + _children['{%s}credit' % MEDIA_NAMESPACE] = ('credit', Credit) + _children['{%s}description' % MEDIA_NAMESPACE] = ('description', Description) + _children['{%s}keywords' % MEDIA_NAMESPACE] = ('keywords', Keywords) + _children['{%s}thumbnail' % MEDIA_NAMESPACE] = ('thumbnail', [Thumbnail,]) + _children['{%s}title' % MEDIA_NAMESPACE] = ('title', Title) + _children['{%s}category' % MEDIA_NAMESPACE] = ('category', [Category,]) + _children['{%s}duration' % YOUTUBE_NAMESPACE] = ('duration', Duration) + _children['{%s}private' % YOUTUBE_NAMESPACE] = ('private', Private) + _children['{%s}player' % MEDIA_NAMESPACE] = ('player', Player) + + def __init__(self, content=None, credit=None, description=None, keywords=None, + thumbnail=None, title=None, duration=None, private=None, + category=None, player=None, extension_elements=None, + extension_attributes=None, text=None): + + MediaBaseElement.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + self.content=content + self.credit=credit + self.description=description + self.keywords=keywords + self.thumbnail=thumbnail or [] + self.title=title + self.duration=duration + self.private=private + self.category=category or [] + self.player=player + + +def GroupFromString(xml_string): + return atom.CreateClassFromXMLString(Group, xml_string) diff --git a/patches/gdata/media/data.py b/patches/gdata/media/data.py new file mode 100644 index 0000000..bb5d2c8 --- /dev/null +++ b/patches/gdata/media/data.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Contains the data classes of the Yahoo! Media RSS Extension""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core + + +MEDIA_TEMPLATE = '{http://search.yahoo.com/mrss//}%s' + + +class MediaCategory(atom.core.XmlElement): + """Describes a media category.""" + _qname = MEDIA_TEMPLATE % 'category' + scheme = 'scheme' + label = 'label' + + +class MediaCopyright(atom.core.XmlElement): + """Describes a media copyright.""" + _qname = MEDIA_TEMPLATE % 'copyright' + url = 'url' + + +class MediaCredit(atom.core.XmlElement): + """Describes a media credit.""" + _qname = MEDIA_TEMPLATE % 'credit' + role = 'role' + scheme = 'scheme' + + +class MediaDescription(atom.core.XmlElement): + """Describes a media description.""" + _qname = MEDIA_TEMPLATE % 'description' + type = 'type' + + +class MediaHash(atom.core.XmlElement): + """Describes a media hash.""" + _qname = MEDIA_TEMPLATE % 'hash' + algo = 'algo' + + +class MediaKeywords(atom.core.XmlElement): + """Describes a media keywords.""" + _qname = MEDIA_TEMPLATE % 'keywords' + + +class MediaPlayer(atom.core.XmlElement): + """Describes a media player.""" + _qname = MEDIA_TEMPLATE % 'player' + height = 'height' + width = 'width' + url = 'url' + + +class MediaRating(atom.core.XmlElement): + """Describes a media rating.""" + _qname = MEDIA_TEMPLATE % 'rating' + scheme = 'scheme' + + +class MediaRestriction(atom.core.XmlElement): + """Describes a media restriction.""" + _qname = MEDIA_TEMPLATE % 'restriction' + relationship = 'relationship' + type = 'type' + + +class MediaText(atom.core.XmlElement): + """Describes a media text.""" + _qname = MEDIA_TEMPLATE % 'text' + end = 'end' + lang = 'lang' + type = 'type' + start = 'start' + + +class MediaThumbnail(atom.core.XmlElement): + """Describes a media thumbnail.""" + _qname = MEDIA_TEMPLATE % 'thumbnail' + time = 'time' + url = 'url' + width = 'width' + height = 'height' + + +class MediaTitle(atom.core.XmlElement): + """Describes a media title.""" + _qname = MEDIA_TEMPLATE % 'title' + type = 'type' + + +class MediaContent(atom.core.XmlElement): + """Describes a media content.""" + _qname = MEDIA_TEMPLATE % 'content' + bitrate = 'bitrate' + is_default = 'isDefault' + medium = 'medium' + height = 'height' + credit = [MediaCredit] + language = 'language' + hash = MediaHash + width = 'width' + player = MediaPlayer + url = 'url' + file_size = 'fileSize' + channels = 'channels' + expression = 'expression' + text = [MediaText] + samplingrate = 'samplingrate' + title = MediaTitle + category = [MediaCategory] + rating = [MediaRating] + type = 'type' + description = MediaDescription + framerate = 'framerate' + thumbnail = [MediaThumbnail] + duration = 'duration' + copyright = MediaCopyright + keywords = MediaKeywords + restriction = [MediaRestriction] + + +class MediaGroup(atom.core.XmlElement): + """Describes a media group.""" + _qname = MEDIA_TEMPLATE % 'group' + credit = [MediaCredit] + content = [MediaContent] + copyright = MediaCopyright + description = MediaDescription + category = [MediaCategory] + player = MediaPlayer + rating = [MediaRating] + hash = MediaHash + title = MediaTitle + keywords = MediaKeywords + restriction = [MediaRestriction] + thumbnail = [MediaThumbnail] + text = [MediaText] + + diff --git a/patches/gdata/notebook/__init__.py b/patches/gdata/notebook/__init__.py new file mode 100644 index 0000000..22071f7 --- /dev/null +++ b/patches/gdata/notebook/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# +# Copyright (C) 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. diff --git a/patches/gdata/notebook/data.py b/patches/gdata/notebook/data.py new file mode 100644 index 0000000..53405e0 --- /dev/null +++ b/patches/gdata/notebook/data.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""Contains the data classes of the Google Notebook Data API""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core +import atom.data +import gdata.data +import gdata.opensearch.data + + +NB_TEMPLATE = '{http://schemas.google.com/notes/2008/}%s' + + +class ComesAfter(atom.core.XmlElement): + """Preceding element.""" + _qname = NB_TEMPLATE % 'comesAfter' + id = 'id' + + +class NoteEntry(gdata.data.GDEntry): + """Describes a note entry in the feed of a user's notebook.""" + + +class NotebookFeed(gdata.data.GDFeed): + """Describes a notebook feed.""" + entry = [NoteEntry] + + +class NotebookListEntry(gdata.data.GDEntry): + """Describes a note list entry in the feed of a user's list of public notebooks.""" + + +class NotebookListFeed(gdata.data.GDFeed): + """Describes a notebook list feed.""" + entry = [NotebookListEntry] + + diff --git a/patches/gdata/oauth/CHANGES.txt b/patches/gdata/oauth/CHANGES.txt new file mode 100755 index 0000000..7c2b92c --- /dev/null +++ b/patches/gdata/oauth/CHANGES.txt @@ -0,0 +1,17 @@ +1. Moved oauth.py to __init__.py + +2. Refactored __init__.py for compatibility with python 2.2 (Issue 59) + +3. Refactored rsa.py for compatibility with python 2.2 (Issue 59) + +4. Refactored OAuthRequest.from_token_and_callback since the callback url was +getting double url-encoding the callback url in place of single. (Issue 43) + +5. Added build_signature_base_string method to rsa.py since it used the +implementation of this method from oauth.OAuthSignatureMethod_HMAC_SHA1 which +was incorrect since it enforced the presence of a consumer secret and a token +secret. Also, changed its super class from oauth.OAuthSignatureMethod_HMAC_SHA1 +to oauth.OAuthSignatureMethod (Issue 64) + +6. Refactored <OAuthRequest>.to_header method since it returned non-oauth params +as well which was incorrect. (Issue 31) \ No newline at end of file diff --git a/patches/gdata/oauth/__init__.py b/patches/gdata/oauth/__init__.py new file mode 100755 index 0000000..44d9c7a --- /dev/null +++ b/patches/gdata/oauth/__init__.py @@ -0,0 +1,529 @@ +import cgi +import urllib +import time +import random +import urlparse +import hmac +import binascii + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + +# Generic exception class +class OAuthError(RuntimeError): + def __init__(self, message='OAuth error occured.'): + self.message = message + +# optional WWW-Authenticate header (401 error) +def build_authenticate_header(realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + +# url escape +def escape(s): + # escape '/' too + return urllib.quote(s, safe='~') + +# util function: current timestamp +# seconds since epoch (UTC) +def generate_timestamp(): + return int(time.time()) + +# util function: nonce +# pseudorandom number +def generate_nonce(length=8): + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + +# OAuthConsumer is a data type that represents the identity of the Consumer +# via its shared secret with the Service Provider. +class OAuthConsumer(object): + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + +# OAuthToken is a data type that represents an End User via either an access +# or request token. +class OAuthToken(object): + # access tokens and request tokens + key = None + secret = None + + ''' + key = the token + secret = the token secret + ''' + def __init__(self, key, secret): + self.key = key + self.secret = secret + + def to_string(self): + return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + + # return a token from something like: + # oauth_token_secret=digg&oauth_token=digg + def from_string(s): + params = cgi.parse_qs(s, keep_blank_values=False) + key = params['oauth_token'][0] + secret = params['oauth_token_secret'][0] + return OAuthToken(key, secret) + from_string = staticmethod(from_string) + + def __str__(self): + return self.to_string() + +# OAuthRequest represents the request and can be serialized +class OAuthRequest(object): + ''' + OAuth parameters: + - oauth_consumer_key + - oauth_token + - oauth_signature_method + - oauth_signature + - oauth_timestamp + - oauth_nonce + - oauth_version + ... any additional parameters, as defined by the Service Provider. + ''' + parameters = None # oauth parameters + http_method = HTTP_METHOD + http_url = None + version = VERSION + + def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): + self.http_method = http_method + self.http_url = http_url + self.parameters = parameters or {} + + def set_parameter(self, parameter, value): + self.parameters[parameter] = value + + def get_parameter(self, parameter): + try: + return self.parameters[parameter] + except: + raise OAuthError('Parameter not found: %s' % parameter) + + def _get_timestamp_nonce(self): + return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') + + # get any non-oauth parameters + def get_nonoauth_parameters(self): + parameters = {} + for k, v in self.parameters.iteritems(): + # ignore oauth parameters + if k.find('oauth_') < 0: + parameters[k] = v + return parameters + + # serialize as a header for an HTTPAuth request + def to_header(self, realm=''): + auth_header = 'OAuth realm="%s"' % realm + # add the oauth parameters + if self.parameters: + for k, v in self.parameters.iteritems(): + if k[:6] == 'oauth_': + auth_header += ', %s="%s"' % (k, escape(str(v))) + return {'Authorization': auth_header} + + # serialize as post data for a POST request + def to_postdata(self): + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()]) + + # serialize as a url for a GET request + def to_url(self): + return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) + + # return a string that consists of all the parameters that need to be signed + def get_normalized_parameters(self): + params = self.parameters + try: + # exclude the signature if it exists + del params['oauth_signature'] + except: + pass + key_values = params.items() + # sort lexicographically, first after key, then after value + key_values.sort() + # combine key value pairs in string and escape + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values]) + + # just uppercases the http method + def get_normalized_http_method(self): + return self.http_method.upper() + + # parses the url and rebuilds it to be scheme://host/path + def get_normalized_http_url(self): + parts = urlparse.urlparse(self.http_url) + host = parts[1].lower() + if host.endswith(':80') or host.endswith(':443'): + host = host.split(':')[0] + url_string = '%s://%s%s' % (parts[0], host, parts[2]) # scheme, netloc, path + return url_string + + # set the signature parameter to the result of build_signature + def sign_request(self, signature_method, consumer, token): + # set the signature method + self.set_parameter('oauth_signature_method', signature_method.get_name()) + # set the signature + self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) + + def build_signature(self, signature_method, consumer, token): + # call the build signature method within the signature method + return signature_method.build_signature(self, consumer, token) + + def from_request(http_method, http_url, headers=None, parameters=None, query_string=None): + # combine multiple parameter sources + if parameters is None: + parameters = {} + + # headers + if headers and 'Authorization' in headers: + auth_header = headers['Authorization'] + # check that the authorization header is OAuth + if auth_header.index('OAuth') > -1: + try: + # get the parameters from the header + header_params = OAuthRequest._split_header(auth_header) + parameters.update(header_params) + except: + raise OAuthError('Unable to parse OAuth parameters from Authorization header.') + + # GET or POST query string + if query_string: + query_params = OAuthRequest._split_url_string(query_string) + parameters.update(query_params) + + # URL parameters + param_str = urlparse.urlparse(http_url)[4] # query + url_params = OAuthRequest._split_url_string(param_str) + parameters.update(url_params) + + if parameters: + return OAuthRequest(http_method, http_url, parameters) + + return None + from_request = staticmethod(from_request) + + def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': oauth_consumer.key, + 'oauth_timestamp': generate_timestamp(), + 'oauth_nonce': generate_nonce(), + 'oauth_version': OAuthRequest.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + + return OAuthRequest(http_method, http_url, parameters) + from_consumer_and_token = staticmethod(from_consumer_and_token) + + def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = callback + + return OAuthRequest(http_method, http_url, parameters) + from_token_and_callback = staticmethod(from_token_and_callback) + + # util function: turn Authorization: header into parameters, has to do some unescaping + def _split_header(header): + params = {} + parts = header[6:].split(',') + for param in parts: + # ignore realm parameter + if param.find('realm') > -1: + continue + # remove whitespace + param = param.strip() + # split key-value + param_parts = param.split('=', 1) + # remove quotes and unescape the value + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + _split_header = staticmethod(_split_header) + + # util function: turn url string into parameters, has to do some unescaping + # even empty values should be included + def _split_url_string(param_str): + parameters = cgi.parse_qs(param_str, keep_blank_values=True) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + _split_url_string = staticmethod(_split_url_string) + +# OAuthServer is a worker to check a requests validity against a data store +class OAuthServer(object): + timestamp_threshold = 300 # in seconds, five minutes + version = VERSION + signature_methods = None + data_store = None + + def __init__(self, data_store=None, signature_methods=None): + self.data_store = data_store + self.signature_methods = signature_methods or {} + + def set_data_store(self, oauth_data_store): + self.data_store = oauth_data_store + + def get_data_store(self): + return self.data_store + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.get_name()] = signature_method + return self.signature_methods + + # process a request_token request + # returns the request token on success + def fetch_request_token(self, oauth_request): + try: + # get the request token for authorization + token = self._get_token(oauth_request, 'request') + except OAuthError: + # no token required for the initial token request + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + self._check_signature(oauth_request, consumer, None) + # fetch a new token + token = self.data_store.fetch_request_token(consumer) + return token + + # process an access_token request + # returns the access token on success + def fetch_access_token(self, oauth_request): + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the request token + token = self._get_token(oauth_request, 'request') + self._check_signature(oauth_request, consumer, token) + new_token = self.data_store.fetch_access_token(consumer, token) + return new_token + + # verify an api call, checks all the parameters + def verify_request(self, oauth_request): + # -> consumer and token + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the access token + token = self._get_token(oauth_request, 'access') + self._check_signature(oauth_request, consumer, token) + parameters = oauth_request.get_nonoauth_parameters() + return consumer, token, parameters + + # authorize a request token + def authorize_token(self, token, user): + return self.data_store.authorize_request_token(token, user) + + # get the callback url + def get_callback(self, oauth_request): + return oauth_request.get_parameter('oauth_callback') + + # optional support for the authenticate header + def build_authenticate_header(self, realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + # verify the correct version request for this server + def _get_version(self, oauth_request): + try: + version = oauth_request.get_parameter('oauth_version') + except: + version = VERSION + if version and version != self.version: + raise OAuthError('OAuth version %s not supported.' % str(version)) + return version + + # figure out the signature with some defaults + def _get_signature_method(self, oauth_request): + try: + signature_method = oauth_request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + try: + # get the signature method object + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_consumer(self, oauth_request): + consumer_key = oauth_request.get_parameter('oauth_consumer_key') + if not consumer_key: + raise OAuthError('Invalid consumer key.') + consumer = self.data_store.lookup_consumer(consumer_key) + if not consumer: + raise OAuthError('Invalid consumer.') + return consumer + + # try to find the token for the provided request token key + def _get_token(self, oauth_request, token_type='access'): + token_field = oauth_request.get_parameter('oauth_token') + consumer = self._get_consumer(oauth_request) + token = self.data_store.lookup_token(consumer, token_type, token_field) + if not token: + raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) + return token + + def _check_signature(self, oauth_request, consumer, token): + timestamp, nonce = oauth_request._get_timestamp_nonce() + self._check_timestamp(timestamp) + self._check_nonce(consumer, token, nonce) + signature_method = self._get_signature_method(oauth_request) + try: + signature = oauth_request.get_parameter('oauth_signature') + except: + raise OAuthError('Missing signature.') + # validate the signature + valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) + if not valid_sig: + key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) + raise OAuthError('Invalid signature. Expected signature base string: %s' % base) + built = signature_method.build_signature(oauth_request, consumer, token) + + def _check_timestamp(self, timestamp): + # verify that timestamp is recentish + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) + + def _check_nonce(self, consumer, token, nonce): + # verify that the nonce is uniqueish + nonce = self.data_store.lookup_nonce(consumer, token, nonce) + if nonce: + raise OAuthError('Nonce already used: %s' % str(nonce)) + +# OAuthClient is a worker to attempt to execute a request +class OAuthClient(object): + consumer = None + token = None + + def __init__(self, oauth_consumer, oauth_token): + self.consumer = oauth_consumer + self.token = oauth_token + + def get_consumer(self): + return self.consumer + + def get_token(self): + return self.token + + def fetch_request_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def access_resource(self, oauth_request): + # -> some protected resource + raise NotImplementedError + +# OAuthDataStore is a database abstraction used to lookup consumers and tokens +class OAuthDataStore(object): + + def lookup_consumer(self, key): + # -> OAuthConsumer + raise NotImplementedError + + def lookup_token(self, oauth_consumer, token_type, token_token): + # -> OAuthToken + raise NotImplementedError + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): + # -> OAuthToken + raise NotImplementedError + + def fetch_request_token(self, oauth_consumer): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_consumer, oauth_token): + # -> OAuthToken + raise NotImplementedError + + def authorize_request_token(self, oauth_token, user): + # -> OAuthToken + raise NotImplementedError + +# OAuthSignatureMethod is a strategy class that implements a signature method +class OAuthSignatureMethod(object): + def get_name(self): + # -> str + raise NotImplementedError + + def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): + # -> str key, str raw + raise NotImplementedError + + def build_signature(self, oauth_request, oauth_consumer, oauth_token): + # -> str + raise NotImplementedError + + def check_signature(self, oauth_request, consumer, token, signature): + built = self.build_signature(oauth_request, consumer, token) + return built == signature + +class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): + + def get_name(self): + return 'HMAC-SHA1' + + def build_signature_base_string(self, oauth_request, consumer, token): + sig = ( + escape(oauth_request.get_normalized_http_method()), + escape(oauth_request.get_normalized_http_url()), + escape(oauth_request.get_normalized_parameters()), + ) + + key = '%s&' % escape(consumer.secret) + if token: + key += escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def build_signature(self, oauth_request, consumer, token): + # build the base signature string + key, raw = self.build_signature_base_string(oauth_request, consumer, token) + + # hmac object + try: + import hashlib # 2.5 + hashed = hmac.new(key, raw, hashlib.sha1) + except: + import sha # deprecated + hashed = hmac.new(key, raw, sha) + + # calculate the digest base 64 + return binascii.b2a_base64(hashed.digest())[:-1] + +class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): + + def get_name(self): + return 'PLAINTEXT' + + def build_signature_base_string(self, oauth_request, consumer, token): + # concatenate the consumer key and secret + sig = escape(consumer.secret) + '&' + if token: + sig = sig + escape(token.secret) + return sig + + def build_signature(self, oauth_request, consumer, token): + return self.build_signature_base_string(oauth_request, consumer, token) diff --git a/patches/gdata/oauth/rsa.py b/patches/gdata/oauth/rsa.py new file mode 100755 index 0000000..f8d9b85 --- /dev/null +++ b/patches/gdata/oauth/rsa.py @@ -0,0 +1,120 @@ +#!/usr/bin/python + +""" +requires tlslite - http://trevp.net/tlslite/ + +""" + +import binascii + +from gdata.tlslite.utils import keyfactory +from gdata.tlslite.utils import cryptomath + +# XXX andy: ugly local import due to module name, oauth.oauth +import gdata.oauth as oauth + +class OAuthSignatureMethod_RSA_SHA1(oauth.OAuthSignatureMethod): + def get_name(self): + return "RSA-SHA1" + + def _fetch_public_cert(self, oauth_request): + # not implemented yet, ideas are: + # (1) do a lookup in a table of trusted certs keyed off of consumer + # (2) fetch via http using a url provided by the requester + # (3) some sort of specific discovery code based on request + # + # either way should return a string representation of the certificate + raise NotImplementedError + + def _fetch_private_cert(self, oauth_request): + # not implemented yet, ideas are: + # (1) do a lookup in a table of trusted certs keyed off of consumer + # + # either way should return a string representation of the certificate + raise NotImplementedError + + def build_signature_base_string(self, oauth_request, consumer, token): + sig = ( + oauth.escape(oauth_request.get_normalized_http_method()), + oauth.escape(oauth_request.get_normalized_http_url()), + oauth.escape(oauth_request.get_normalized_parameters()), + ) + key = '' + raw = '&'.join(sig) + return key, raw + + def build_signature(self, oauth_request, consumer, token): + key, base_string = self.build_signature_base_string(oauth_request, + consumer, + token) + + # Fetch the private key cert based on the request + cert = self._fetch_private_cert(oauth_request) + + # Pull the private key from the certificate + privatekey = keyfactory.parsePrivateKey(cert) + + # Convert base_string to bytes + #base_string_bytes = cryptomath.createByteArraySequence(base_string) + + # Sign using the key + signed = privatekey.hashAndSign(base_string) + + return binascii.b2a_base64(signed)[:-1] + + def check_signature(self, oauth_request, consumer, token, signature): + decoded_sig = base64.b64decode(signature); + + key, base_string = self.build_signature_base_string(oauth_request, + consumer, + token) + + # Fetch the public key cert based on the request + cert = self._fetch_public_cert(oauth_request) + + # Pull the public key from the certificate + publickey = keyfactory.parsePEMKey(cert, public=True) + + # Check the signature + ok = publickey.hashAndVerify(decoded_sig, base_string) + + return ok + + +class TestOAuthSignatureMethod_RSA_SHA1(OAuthSignatureMethod_RSA_SHA1): + def _fetch_public_cert(self, oauth_request): + cert = """ +-----BEGIN CERTIFICATE----- +MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0 +IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV +BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY +zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb +mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3 +DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d +4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb +WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J +-----END CERTIFICATE----- +""" + return cert + + def _fetch_private_cert(self, oauth_request): + cert = """ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V +A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d +7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ +hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H +X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm +uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw +rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z +zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn +qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG +WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno +cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+ +3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8 +AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54 +Lw03eHTNQghS0A== +-----END PRIVATE KEY----- +""" + return cert diff --git a/patches/gdata/opensearch/__init__.py b/patches/gdata/opensearch/__init__.py new file mode 100644 index 0000000..22071f7 --- /dev/null +++ b/patches/gdata/opensearch/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# +# Copyright (C) 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. diff --git a/patches/gdata/opensearch/data.py b/patches/gdata/opensearch/data.py new file mode 100644 index 0000000..89d7a28 --- /dev/null +++ b/patches/gdata/opensearch/data.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""Contains the data classes of the OpenSearch Extension""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core + + +OPENSEARCH_TEMPLATE_V1 = '{http://a9.com/-/spec/opensearchrss/1.0//}%s' +OPENSEARCH_TEMPLATE_V2 = '{http://a9.com/-/spec/opensearch/1.1//}%s' + + +class ItemsPerPage(atom.core.XmlElement): + """Describes the number of items that will be returned per page for paged feeds""" + _qname = (OPENSEARCH_TEMPLATE_V1 % 'itemsPerPage', + OPENSEARCH_TEMPLATE_V2 % 'itemsPerPage') + + +class StartIndex(atom.core.XmlElement): + """Describes the starting index of the contained entries for paged feeds""" + _qname = (OPENSEARCH_TEMPLATE_V1 % 'startIndex', + OPENSEARCH_TEMPLATE_V2 % 'startIndex') + + +class TotalResults(atom.core.XmlElement): + """Describes the total number of results associated with this feed""" + _qname = (OPENSEARCH_TEMPLATE_V1 % 'totalResults', + OPENSEARCH_TEMPLATE_V2 % 'totalResults') + + diff --git a/patches/gdata/photos/__init__.py b/patches/gdata/photos/__init__.py new file mode 100644 index 0000000..1952135 --- /dev/null +++ b/patches/gdata/photos/__init__.py @@ -0,0 +1,1112 @@ +# -*-*- encoding: utf-8 -*-*- +# +# This is the base file for the PicasaWeb python client. +# It is used for lower level operations. +# +# $Id: __init__.py 148 2007-10-28 15:09:19Z havard.gulldahl $ +# +# Copyright 2007 HÃ¥vard Gulldahl +# Portions (C) 2006 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 provides a pythonic, gdata-centric interface to Google Photos +(a.k.a. Picasa Web Services. + +It is modelled after the gdata/* interfaces from the gdata-python-client +project[1] by Google. + +You'll find the user-friendly api in photos.service. Please see the +documentation or live help() system for available methods. + +[1]: http://gdata-python-client.googlecode.com/ + + """ + +__author__ = u'havard@gulldahl.no'# (HÃ¥vard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__ +__license__ = 'Apache License v2' +__version__ = '$Revision: 164 $'[11:-2] + +import re +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 +import gdata + +# importing google photo submodules +import gdata.media as Media, gdata.exif as Exif, gdata.geo as Geo + +# XML namespaces which are often used in Google Photo elements +PHOTOS_NAMESPACE = 'http://schemas.google.com/photos/2007' +MEDIA_NAMESPACE = 'http://search.yahoo.com/mrss/' +EXIF_NAMESPACE = 'http://schemas.google.com/photos/exif/2007' +OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/' +GEO_NAMESPACE = 'http://www.w3.org/2003/01/geo/wgs84_pos#' +GML_NAMESPACE = 'http://www.opengis.net/gml' +GEORSS_NAMESPACE = 'http://www.georss.org/georss' +PHEED_NAMESPACE = 'http://www.pheed.com/pheed/' +BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch' + + +class PhotosBaseElement(atom.AtomBase): + """Base class for elements in the PHOTO_NAMESPACE. To add new elements, + you only need to add the element tag name to self._tag + """ + + _tag = '' + _namespace = PHOTOS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, name=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + #def __str__(self): + #return str(self.text) + #def __unicode__(self): + #return unicode(self.text) + def __int__(self): + return int(self.text) + def bool(self): + return self.text == 'true' + +class GPhotosBaseFeed(gdata.GDataFeed, gdata.LinkFinder): + "Base class for all Feeds in gdata.photos" + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _attributes = gdata.GDataFeed._attributes.copy() + _children = gdata.GDataFeed._children.copy() + # We deal with Entry elements ourselves + del _children['{%s}entry' % atom.ATOM_NAMESPACE] + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None): + gdata.GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + def kind(self): + "(string) Returns the kind" + try: + return self.category[0].term.split('#')[1] + except IndexError: + return None + + def _feedUri(self, kind): + "Convenience method to return a uri to a feed of a special kind" + assert(kind in ('album', 'tag', 'photo', 'comment', 'user')) + here_href = self.GetSelfLink().href + if 'kind=%s' % kind in here_href: + return here_href + if not 'kind=' in here_href: + sep = '?' + if '?' in here_href: sep = '&' + return here_href + "%skind=%s" % (sep, kind) + rx = re.match('.*(kind=)(album|tag|photo|comment)', here_href) + return here_href[:rx.end(1)] + kind + here_href[rx.end(2):] + + def _ConvertElementTreeToMember(self, child_tree): + """Re-implementing the method from AtomBase, since we deal with + Entry elements specially""" + category = child_tree.find('{%s}category' % atom.ATOM_NAMESPACE) + if category is None: + return atom.AtomBase._ConvertElementTreeToMember(self, child_tree) + namespace, kind = category.get('term').split('#') + if namespace != PHOTOS_NAMESPACE: + return atom.AtomBase._ConvertElementTreeToMember(self, child_tree) + ## TODO: is it safe to use getattr on gdata.photos? + entry_class = getattr(gdata.photos, '%sEntry' % kind.title()) + if not hasattr(self, 'entry') or self.entry is None: + self.entry = [] + self.entry.append(atom._CreateClassFromElementTree( + entry_class, child_tree)) + +class GPhotosBaseEntry(gdata.GDataEntry, gdata.LinkFinder): + "Base class for all Entry elements in gdata.photos" + _tag = 'entry' + _kind = '' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, + updated=updated, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + self.category.append( + atom.Category(scheme='http://schemas.google.com/g/2005#kind', + term = 'http://schemas.google.com/photos/2007#%s' % self._kind)) + + def kind(self): + "(string) Returns the kind" + try: + return self.category[0].term.split('#')[1] + except IndexError: + return None + + def _feedUri(self, kind): + "Convenience method to get the uri to this entry's feed of the some kind" + try: + href = self.GetFeedLink().href + except AttributeError: + return None + sep = '?' + if '?' in href: sep = '&' + return '%s%skind=%s' % (href, sep, kind) + + +class PhotosBaseEntry(GPhotosBaseEntry): + pass + +class PhotosBaseFeed(GPhotosBaseFeed): + pass + +class GPhotosBaseData(object): + pass + +class Access(PhotosBaseElement): + """The Google Photo `Access' element. + + The album's access level. Valid values are `public' or `private'. + In documentation, access level is also referred to as `visibility.'""" + + _tag = 'access' +def AccessFromString(xml_string): + return atom.CreateClassFromXMLString(Access, xml_string) + +class Albumid(PhotosBaseElement): + "The Google Photo `Albumid' element" + + _tag = 'albumid' +def AlbumidFromString(xml_string): + return atom.CreateClassFromXMLString(Albumid, xml_string) + +class BytesUsed(PhotosBaseElement): + "The Google Photo `BytesUsed' element" + + _tag = 'bytesUsed' +def BytesUsedFromString(xml_string): + return atom.CreateClassFromXMLString(BytesUsed, xml_string) + +class Client(PhotosBaseElement): + "The Google Photo `Client' element" + + _tag = 'client' +def ClientFromString(xml_string): + return atom.CreateClassFromXMLString(Client, xml_string) + +class Checksum(PhotosBaseElement): + "The Google Photo `Checksum' element" + + _tag = 'checksum' +def ChecksumFromString(xml_string): + return atom.CreateClassFromXMLString(Checksum, xml_string) + +class CommentCount(PhotosBaseElement): + "The Google Photo `CommentCount' element" + + _tag = 'commentCount' +def CommentCountFromString(xml_string): + return atom.CreateClassFromXMLString(CommentCount, xml_string) + +class CommentingEnabled(PhotosBaseElement): + "The Google Photo `CommentingEnabled' element" + + _tag = 'commentingEnabled' +def CommentingEnabledFromString(xml_string): + return atom.CreateClassFromXMLString(CommentingEnabled, xml_string) + +class Height(PhotosBaseElement): + "The Google Photo `Height' element" + + _tag = 'height' +def HeightFromString(xml_string): + return atom.CreateClassFromXMLString(Height, xml_string) + +class Id(PhotosBaseElement): + "The Google Photo `Id' element" + + _tag = 'id' +def IdFromString(xml_string): + return atom.CreateClassFromXMLString(Id, xml_string) + +class Location(PhotosBaseElement): + "The Google Photo `Location' element" + + _tag = 'location' +def LocationFromString(xml_string): + return atom.CreateClassFromXMLString(Location, xml_string) + +class MaxPhotosPerAlbum(PhotosBaseElement): + "The Google Photo `MaxPhotosPerAlbum' element" + + _tag = 'maxPhotosPerAlbum' +def MaxPhotosPerAlbumFromString(xml_string): + return atom.CreateClassFromXMLString(MaxPhotosPerAlbum, xml_string) + +class Name(PhotosBaseElement): + "The Google Photo `Name' element" + + _tag = 'name' +def NameFromString(xml_string): + return atom.CreateClassFromXMLString(Name, xml_string) + +class Nickname(PhotosBaseElement): + "The Google Photo `Nickname' element" + + _tag = 'nickname' +def NicknameFromString(xml_string): + return atom.CreateClassFromXMLString(Nickname, xml_string) + +class Numphotos(PhotosBaseElement): + "The Google Photo `Numphotos' element" + + _tag = 'numphotos' +def NumphotosFromString(xml_string): + return atom.CreateClassFromXMLString(Numphotos, xml_string) + +class Numphotosremaining(PhotosBaseElement): + "The Google Photo `Numphotosremaining' element" + + _tag = 'numphotosremaining' +def NumphotosremainingFromString(xml_string): + return atom.CreateClassFromXMLString(Numphotosremaining, xml_string) + +class Position(PhotosBaseElement): + "The Google Photo `Position' element" + + _tag = 'position' +def PositionFromString(xml_string): + return atom.CreateClassFromXMLString(Position, xml_string) + +class Photoid(PhotosBaseElement): + "The Google Photo `Photoid' element" + + _tag = 'photoid' +def PhotoidFromString(xml_string): + return atom.CreateClassFromXMLString(Photoid, xml_string) + +class Quotacurrent(PhotosBaseElement): + "The Google Photo `Quotacurrent' element" + + _tag = 'quotacurrent' +def QuotacurrentFromString(xml_string): + return atom.CreateClassFromXMLString(Quotacurrent, xml_string) + +class Quotalimit(PhotosBaseElement): + "The Google Photo `Quotalimit' element" + + _tag = 'quotalimit' +def QuotalimitFromString(xml_string): + return atom.CreateClassFromXMLString(Quotalimit, xml_string) + +class Rotation(PhotosBaseElement): + "The Google Photo `Rotation' element" + + _tag = 'rotation' +def RotationFromString(xml_string): + return atom.CreateClassFromXMLString(Rotation, xml_string) + +class Size(PhotosBaseElement): + "The Google Photo `Size' element" + + _tag = 'size' +def SizeFromString(xml_string): + return atom.CreateClassFromXMLString(Size, xml_string) + +class Snippet(PhotosBaseElement): + """The Google Photo `snippet' element. + + When searching, the snippet element will contain a + string with the word you're looking for, highlighted in html markup + E.g. when your query is `hafjell', this element may contain: + `... here at <b>Hafjell</b>.' + + You'll find this element in searches -- that is, feeds that combine the + `kind=photo' and `q=yoursearch' parameters in the request. + + See also gphoto:truncated and gphoto:snippettype. + + """ + + _tag = 'snippet' +def SnippetFromString(xml_string): + return atom.CreateClassFromXMLString(Snippet, xml_string) + +class Snippettype(PhotosBaseElement): + """The Google Photo `Snippettype' element + + When searching, this element will tell you the type of element that matches. + + You'll find this element in searches -- that is, feeds that combine the + `kind=photo' and `q=yoursearch' parameters in the request. + + See also gphoto:snippet and gphoto:truncated. + + Possible values and their interpretation: + o ALBUM_TITLE - The album title matches + o PHOTO_TAGS - The match is a tag/keyword + o PHOTO_DESCRIPTION - The match is in the photo's description + + If you discover a value not listed here, please submit a patch to update this docstring. + + """ + + _tag = 'snippettype' +def SnippettypeFromString(xml_string): + return atom.CreateClassFromXMLString(Snippettype, xml_string) + +class Thumbnail(PhotosBaseElement): + """The Google Photo `Thumbnail' element + + Used to display user's photo thumbnail (hackergotchi). + + (Not to be confused with the <media:thumbnail> element, which gives you + small versions of the photo object.)""" + + _tag = 'thumbnail' +def ThumbnailFromString(xml_string): + return atom.CreateClassFromXMLString(Thumbnail, xml_string) + +class Timestamp(PhotosBaseElement): + """The Google Photo `Timestamp' element + Represented as the number of milliseconds since January 1st, 1970. + + + Take a look at the convenience methods .isoformat() and .datetime(): + + photo_epoch = Time.text # 1180294337000 + photo_isostring = Time.isoformat() # '2007-05-27T19:32:17.000Z' + + Alternatively: + photo_datetime = Time.datetime() # (requires python >= 2.3) + """ + + _tag = 'timestamp' + def isoformat(self): + """(string) Return the timestamp as a ISO 8601 formatted string, + e.g. '2007-05-27T19:32:17.000Z' + """ + import time + epoch = float(self.text)/1000 + return time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(epoch)) + + def datetime(self): + """(datetime.datetime) Return the timestamp as a datetime.datetime object + + Requires python 2.3 + """ + import datetime + epoch = float(self.text)/1000 + return datetime.datetime.fromtimestamp(epoch) +def TimestampFromString(xml_string): + return atom.CreateClassFromXMLString(Timestamp, xml_string) + +class Truncated(PhotosBaseElement): + """The Google Photo `Truncated' element + + You'll find this element in searches -- that is, feeds that combine the + `kind=photo' and `q=yoursearch' parameters in the request. + + See also gphoto:snippet and gphoto:snippettype. + + Possible values and their interpretation: + 0 -- unknown + """ + + _tag = 'Truncated' +def TruncatedFromString(xml_string): + return atom.CreateClassFromXMLString(Truncated, xml_string) + +class User(PhotosBaseElement): + "The Google Photo `User' element" + + _tag = 'user' +def UserFromString(xml_string): + return atom.CreateClassFromXMLString(User, xml_string) + +class Version(PhotosBaseElement): + "The Google Photo `Version' element" + + _tag = 'version' +def VersionFromString(xml_string): + return atom.CreateClassFromXMLString(Version, xml_string) + +class Width(PhotosBaseElement): + "The Google Photo `Width' element" + + _tag = 'width' +def WidthFromString(xml_string): + return atom.CreateClassFromXMLString(Width, xml_string) + +class Weight(PhotosBaseElement): + """The Google Photo `Weight' element. + + The weight of the tag is the number of times the tag + appears in the collection of tags currently being viewed. + The default weight is 1, in which case this tags is omitted.""" + _tag = 'weight' +def WeightFromString(xml_string): + return atom.CreateClassFromXMLString(Weight, xml_string) + +class CommentAuthor(atom.Author): + """The Atom `Author' element in CommentEntry entries is augmented to + contain elements from the PHOTOS_NAMESPACE + + http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38 + """ + _children = atom.Author._children.copy() + _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User) + _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname) + _children['{%s}thumbnail' % PHOTOS_NAMESPACE] = ('thumbnail', Thumbnail) +def CommentAuthorFromString(xml_string): + return atom.CreateClassFromXMLString(CommentAuthor, xml_string) + +########################## ################################ + +class AlbumData(object): + _children = {} + _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) + _children['{%s}name' % PHOTOS_NAMESPACE] = ('name', Name) + _children['{%s}location' % PHOTOS_NAMESPACE] = ('location', Location) + _children['{%s}access' % PHOTOS_NAMESPACE] = ('access', Access) + _children['{%s}bytesUsed' % PHOTOS_NAMESPACE] = ('bytesUsed', BytesUsed) + _children['{%s}timestamp' % PHOTOS_NAMESPACE] = ('timestamp', Timestamp) + _children['{%s}numphotos' % PHOTOS_NAMESPACE] = ('numphotos', Numphotos) + _children['{%s}numphotosremaining' % PHOTOS_NAMESPACE] = \ + ('numphotosremaining', Numphotosremaining) + _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User) + _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname) + _children['{%s}commentingEnabled' % PHOTOS_NAMESPACE] = \ + ('commentingEnabled', CommentingEnabled) + _children['{%s}commentCount' % PHOTOS_NAMESPACE] = \ + ('commentCount', CommentCount) + ## NOTE: storing media:group as self.media, to create a self-explaining api + gphoto_id = None + name = None + location = None + access = None + bytesUsed = None + timestamp = None + numphotos = None + numphotosremaining = None + user = None + nickname = None + commentingEnabled = None + commentCount = None + +class AlbumEntry(GPhotosBaseEntry, AlbumData): + """All metadata for a Google Photos Album + + Take a look at AlbumData for metadata accessible as attributes to this object. + + Notes: + To avoid name clashes, and to create a more sensible api, some + objects have names that differ from the original elements: + + o media:group -> self.media, + o geo:where -> self.geo, + o photo:id -> self.gphoto_id + """ + + _kind = 'album' + _children = GPhotosBaseEntry._children.copy() + _children.update(AlbumData._children.copy()) + # child tags only for Album entries, not feeds + _children['{%s}where' % GEORSS_NAMESPACE] = ('geo', Geo.Where) + _children['{%s}group' % MEDIA_NAMESPACE] = ('media', Media.Group) + media = Media.Group() + geo = Geo.Where() + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + #GPHOTO NAMESPACE: + gphoto_id=None, name=None, location=None, access=None, + timestamp=None, numphotos=None, user=None, nickname=None, + commentingEnabled=None, commentCount=None, thumbnail=None, + # MEDIA NAMESPACE: + media=None, + # GEORSS NAMESPACE: + geo=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + GPhotosBaseEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, + updated=updated, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + + ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id + self.gphoto_id = gphoto_id + self.name = name + self.location = location + self.access = access + self.timestamp = timestamp + self.numphotos = numphotos + self.user = user + self.nickname = nickname + self.commentingEnabled = commentingEnabled + self.commentCount = commentCount + self.thumbnail = thumbnail + self.extended_property = extended_property or [] + self.text = text + ## NOTE: storing media:group as self.media, and geo:where as geo, + ## to create a self-explaining api + self.media = media or Media.Group() + self.geo = geo or Geo.Where() + + def GetAlbumId(self): + "Return the id of this album" + + return self.GetFeedLink().href.split('/')[-1] + + def GetPhotosUri(self): + "(string) Return the uri to this albums feed of the PhotoEntry kind" + return self._feedUri('photo') + + def GetCommentsUri(self): + "(string) Return the uri to this albums feed of the CommentEntry kind" + return self._feedUri('comment') + + def GetTagsUri(self): + "(string) Return the uri to this albums feed of the TagEntry kind" + return self._feedUri('tag') + +def AlbumEntryFromString(xml_string): + return atom.CreateClassFromXMLString(AlbumEntry, xml_string) + +class AlbumFeed(GPhotosBaseFeed, AlbumData): + """All metadata for a Google Photos Album, including its sub-elements + + This feed represents an album as the container for other objects. + + A Album feed contains entries of + PhotoEntry, CommentEntry or TagEntry, + depending on the `kind' parameter in the original query. + + Take a look at AlbumData for accessible attributes. + + """ + + _children = GPhotosBaseFeed._children.copy() + _children.update(AlbumData._children.copy()) + + def GetPhotosUri(self): + "(string) Return the uri to the same feed, but of the PhotoEntry kind" + + return self._feedUri('photo') + + def GetTagsUri(self): + "(string) Return the uri to the same feed, but of the TagEntry kind" + + return self._feedUri('tag') + + def GetCommentsUri(self): + "(string) Return the uri to the same feed, but of the CommentEntry kind" + + return self._feedUri('comment') + +def AlbumFeedFromString(xml_string): + return atom.CreateClassFromXMLString(AlbumFeed, xml_string) + + +class PhotoData(object): + _children = {} + ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id + _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) + _children['{%s}albumid' % PHOTOS_NAMESPACE] = ('albumid', Albumid) + _children['{%s}checksum' % PHOTOS_NAMESPACE] = ('checksum', Checksum) + _children['{%s}client' % PHOTOS_NAMESPACE] = ('client', Client) + _children['{%s}height' % PHOTOS_NAMESPACE] = ('height', Height) + _children['{%s}position' % PHOTOS_NAMESPACE] = ('position', Position) + _children['{%s}rotation' % PHOTOS_NAMESPACE] = ('rotation', Rotation) + _children['{%s}size' % PHOTOS_NAMESPACE] = ('size', Size) + _children['{%s}timestamp' % PHOTOS_NAMESPACE] = ('timestamp', Timestamp) + _children['{%s}version' % PHOTOS_NAMESPACE] = ('version', Version) + _children['{%s}width' % PHOTOS_NAMESPACE] = ('width', Width) + _children['{%s}commentingEnabled' % PHOTOS_NAMESPACE] = \ + ('commentingEnabled', CommentingEnabled) + _children['{%s}commentCount' % PHOTOS_NAMESPACE] = \ + ('commentCount', CommentCount) + ## NOTE: storing media:group as self.media, exif:tags as self.exif, and + ## geo:where as self.geo, to create a self-explaining api + _children['{%s}tags' % EXIF_NAMESPACE] = ('exif', Exif.Tags) + _children['{%s}where' % GEORSS_NAMESPACE] = ('geo', Geo.Where) + _children['{%s}group' % MEDIA_NAMESPACE] = ('media', Media.Group) + # These elements show up in search feeds + _children['{%s}snippet' % PHOTOS_NAMESPACE] = ('snippet', Snippet) + _children['{%s}snippettype' % PHOTOS_NAMESPACE] = ('snippettype', Snippettype) + _children['{%s}truncated' % PHOTOS_NAMESPACE] = ('truncated', Truncated) + gphoto_id = None + albumid = None + checksum = None + client = None + height = None + position = None + rotation = None + size = None + timestamp = None + version = None + width = None + commentingEnabled = None + commentCount = None + snippet=None + snippettype=None + truncated=None + media = Media.Group() + geo = Geo.Where() + tags = Exif.Tags() + +class PhotoEntry(GPhotosBaseEntry, PhotoData): + """All metadata for a Google Photos Photo + + Take a look at PhotoData for metadata accessible as attributes to this object. + + Notes: + To avoid name clashes, and to create a more sensible api, some + objects have names that differ from the original elements: + + o media:group -> self.media, + o exif:tags -> self.exif, + o geo:where -> self.geo, + o photo:id -> self.gphoto_id + """ + + _kind = 'photo' + _children = GPhotosBaseEntry._children.copy() + _children.update(PhotoData._children.copy()) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, text=None, + # GPHOTO NAMESPACE: + gphoto_id=None, albumid=None, checksum=None, client=None, height=None, + position=None, rotation=None, size=None, timestamp=None, version=None, + width=None, commentCount=None, commentingEnabled=None, + # MEDIARSS NAMESPACE: + media=None, + # EXIF_NAMESPACE: + exif=None, + # GEORSS NAMESPACE: + geo=None, + extension_elements=None, extension_attributes=None): + GPhotosBaseEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + + + ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id + self.gphoto_id = gphoto_id + self.albumid = albumid + self.checksum = checksum + self.client = client + self.height = height + self.position = position + self.rotation = rotation + self.size = size + self.timestamp = timestamp + self.version = version + self.width = width + self.commentingEnabled = commentingEnabled + self.commentCount = commentCount + ## NOTE: storing media:group as self.media, to create a self-explaining api + self.media = media or Media.Group() + self.exif = exif or Exif.Tags() + self.geo = geo or Geo.Where() + + def GetPostLink(self): + "Return the uri to this photo's `POST' link (use it for updates of the object)" + + return self.GetFeedLink() + + def GetCommentsUri(self): + "Return the uri to this photo's feed of CommentEntry comments" + return self._feedUri('comment') + + def GetTagsUri(self): + "Return the uri to this photo's feed of TagEntry tags" + return self._feedUri('tag') + + def GetAlbumUri(self): + """Return the uri to the AlbumEntry containing this photo""" + + href = self.GetSelfLink().href + return href[:href.find('/photoid')] + +def PhotoEntryFromString(xml_string): + return atom.CreateClassFromXMLString(PhotoEntry, xml_string) + +class PhotoFeed(GPhotosBaseFeed, PhotoData): + """All metadata for a Google Photos Photo, including its sub-elements + + This feed represents a photo as the container for other objects. + + A Photo feed contains entries of + CommentEntry or TagEntry, + depending on the `kind' parameter in the original query. + + Take a look at PhotoData for metadata accessible as attributes to this object. + + """ + _children = GPhotosBaseFeed._children.copy() + _children.update(PhotoData._children.copy()) + + def GetTagsUri(self): + "(string) Return the uri to the same feed, but of the TagEntry kind" + + return self._feedUri('tag') + + def GetCommentsUri(self): + "(string) Return the uri to the same feed, but of the CommentEntry kind" + + return self._feedUri('comment') + +def PhotoFeedFromString(xml_string): + return atom.CreateClassFromXMLString(PhotoFeed, xml_string) + +class TagData(GPhotosBaseData): + _children = {} + _children['{%s}weight' % PHOTOS_NAMESPACE] = ('weight', Weight) + weight=None + +class TagEntry(GPhotosBaseEntry, TagData): + """All metadata for a Google Photos Tag + + The actual tag is stored in the .title.text attribute + + """ + + _kind = 'tag' + _children = GPhotosBaseEntry._children.copy() + _children.update(TagData._children.copy()) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + # GPHOTO NAMESPACE: + weight=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + GPhotosBaseEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated, text=text, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + + self.weight = weight + + def GetAlbumUri(self): + """Return the uri to the AlbumEntry containing this tag""" + + href = self.GetSelfLink().href + pos = href.find('/photoid') + if pos == -1: + return None + return href[:pos] + + def GetPhotoUri(self): + """Return the uri to the PhotoEntry containing this tag""" + + href = self.GetSelfLink().href + pos = href.find('/tag') + if pos == -1: + return None + return href[:pos] + +def TagEntryFromString(xml_string): + return atom.CreateClassFromXMLString(TagEntry, xml_string) + + +class TagFeed(GPhotosBaseFeed, TagData): + """All metadata for a Google Photos Tag, including its sub-elements""" + + _children = GPhotosBaseFeed._children.copy() + _children.update(TagData._children.copy()) + +def TagFeedFromString(xml_string): + return atom.CreateClassFromXMLString(TagFeed, xml_string) + +class CommentData(GPhotosBaseData): + _children = {} + ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id + _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) + _children['{%s}albumid' % PHOTOS_NAMESPACE] = ('albumid', Albumid) + _children['{%s}photoid' % PHOTOS_NAMESPACE] = ('photoid', Photoid) + _children['{%s}author' % atom.ATOM_NAMESPACE] = ('author', [CommentAuthor,]) + gphoto_id=None + albumid=None + photoid=None + author=None + +class CommentEntry(GPhotosBaseEntry, CommentData): + """All metadata for a Google Photos Comment + + The comment is stored in the .content.text attribute, + with a content type in .content.type. + + + """ + + _kind = 'comment' + _children = GPhotosBaseEntry._children.copy() + _children.update(CommentData._children.copy()) + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + # GPHOTO NAMESPACE: + gphoto_id=None, albumid=None, photoid=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + GPhotosBaseEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + self.gphoto_id = gphoto_id + self.albumid = albumid + self.photoid = photoid + + def GetCommentId(self): + """Return the globally unique id of this comment""" + return self.GetSelfLink().href.split('/')[-1] + + def GetAlbumUri(self): + """Return the uri to the AlbumEntry containing this comment""" + + href = self.GetSelfLink().href + return href[:href.find('/photoid')] + + def GetPhotoUri(self): + """Return the uri to the PhotoEntry containing this comment""" + + href = self.GetSelfLink().href + return href[:href.find('/commentid')] + +def CommentEntryFromString(xml_string): + return atom.CreateClassFromXMLString(CommentEntry, xml_string) + +class CommentFeed(GPhotosBaseFeed, CommentData): + """All metadata for a Google Photos Comment, including its sub-elements""" + + _children = GPhotosBaseFeed._children.copy() + _children.update(CommentData._children.copy()) + +def CommentFeedFromString(xml_string): + return atom.CreateClassFromXMLString(CommentFeed, xml_string) + +class UserData(GPhotosBaseData): + _children = {} + _children['{%s}maxPhotosPerAlbum' % PHOTOS_NAMESPACE] = ('maxPhotosPerAlbum', MaxPhotosPerAlbum) + _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname) + _children['{%s}quotalimit' % PHOTOS_NAMESPACE] = ('quotalimit', Quotalimit) + _children['{%s}quotacurrent' % PHOTOS_NAMESPACE] = ('quotacurrent', Quotacurrent) + _children['{%s}thumbnail' % PHOTOS_NAMESPACE] = ('thumbnail', Thumbnail) + _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User) + _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) + + maxPhotosPerAlbum=None + nickname=None + quotalimit=None + quotacurrent=None + thumbnail=None + user=None + gphoto_id=None + + +class UserEntry(GPhotosBaseEntry, UserData): + """All metadata for a Google Photos User + + This entry represents an album owner and all appropriate metadata. + + Take a look at at the attributes of the UserData for metadata available. + """ + _children = GPhotosBaseEntry._children.copy() + _children.update(UserData._children.copy()) + _kind = 'user' + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, + title=None, updated=None, + # GPHOTO NAMESPACE: + gphoto_id=None, maxPhotosPerAlbum=None, nickname=None, quotalimit=None, + quotacurrent=None, thumbnail=None, user=None, + extended_property=None, + extension_elements=None, extension_attributes=None, text=None): + + GPhotosBaseEntry.__init__(self, author=author, category=category, + content=content, + atom_id=atom_id, link=link, published=published, + title=title, updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + + self.gphoto_id=gphoto_id + self.maxPhotosPerAlbum=maxPhotosPerAlbum + self.nickname=nickname + self.quotalimit=quotalimit + self.quotacurrent=quotacurrent + self.thumbnail=thumbnail + self.user=user + + def GetAlbumsUri(self): + "(string) Return the uri to this user's feed of the AlbumEntry kind" + return self._feedUri('album') + + def GetPhotosUri(self): + "(string) Return the uri to this user's feed of the PhotoEntry kind" + return self._feedUri('photo') + + def GetCommentsUri(self): + "(string) Return the uri to this user's feed of the CommentEntry kind" + return self._feedUri('comment') + + def GetTagsUri(self): + "(string) Return the uri to this user's feed of the TagEntry kind" + return self._feedUri('tag') + +def UserEntryFromString(xml_string): + return atom.CreateClassFromXMLString(UserEntry, xml_string) + +class UserFeed(GPhotosBaseFeed, UserData): + """Feed for a User in the google photos api. + + This feed represents a user as the container for other objects. + + A User feed contains entries of + AlbumEntry, PhotoEntry, CommentEntry, UserEntry or TagEntry, + depending on the `kind' parameter in the original query. + + The user feed itself also contains all of the metadata available + as part of a UserData object.""" + _children = GPhotosBaseFeed._children.copy() + _children.update(UserData._children.copy()) + + def GetAlbumsUri(self): + """Get the uri to this feed, but with entries of the AlbumEntry kind.""" + return self._feedUri('album') + + def GetTagsUri(self): + """Get the uri to this feed, but with entries of the TagEntry kind.""" + return self._feedUri('tag') + + def GetPhotosUri(self): + """Get the uri to this feed, but with entries of the PhotosEntry kind.""" + return self._feedUri('photo') + + def GetCommentsUri(self): + """Get the uri to this feed, but with entries of the CommentsEntry kind.""" + return self._feedUri('comment') + +def UserFeedFromString(xml_string): + return atom.CreateClassFromXMLString(UserFeed, xml_string) + + + +def AnyFeedFromString(xml_string): + """Creates an instance of the appropriate feed class from the + xml string contents. + + Args: + xml_string: str A string which contains valid XML. The root element + of the XML string should match the tag and namespace of the desired + class. + + Returns: + An instance of the target class with members assigned according to the + contents of the XML - or a basic gdata.GDataFeed instance if it is + impossible to determine the appropriate class (look for extra elements + in GDataFeed's .FindExtensions() and extension_elements[] ). + """ + tree = ElementTree.fromstring(xml_string) + category = tree.find('{%s}category' % atom.ATOM_NAMESPACE) + if category is None: + # TODO: is this the best way to handle this? + return atom._CreateClassFromElementTree(GPhotosBaseFeed, tree) + namespace, kind = category.get('term').split('#') + if namespace != PHOTOS_NAMESPACE: + # TODO: is this the best way to handle this? + return atom._CreateClassFromElementTree(GPhotosBaseFeed, tree) + ## TODO: is getattr safe this way? + feed_class = getattr(gdata.photos, '%sFeed' % kind.title()) + return atom._CreateClassFromElementTree(feed_class, tree) + +def AnyEntryFromString(xml_string): + """Creates an instance of the appropriate entry class from the + xml string contents. + + Args: + xml_string: str A string which contains valid XML. The root element + of the XML string should match the tag and namespace of the desired + class. + + Returns: + An instance of the target class with members assigned according to the + contents of the XML - or a basic gdata.GDataEndry instance if it is + impossible to determine the appropriate class (look for extra elements + in GDataEntry's .FindExtensions() and extension_elements[] ). + """ + tree = ElementTree.fromstring(xml_string) + category = tree.find('{%s}category' % atom.ATOM_NAMESPACE) + if category is None: + # TODO: is this the best way to handle this? + return atom._CreateClassFromElementTree(GPhotosBaseEntry, tree) + namespace, kind = category.get('term').split('#') + if namespace != PHOTOS_NAMESPACE: + # TODO: is this the best way to handle this? + return atom._CreateClassFromElementTree(GPhotosBaseEntry, tree) + ## TODO: is getattr safe this way? + feed_class = getattr(gdata.photos, '%sEntry' % kind.title()) + return atom._CreateClassFromElementTree(feed_class, tree) + diff --git a/patches/gdata/photos/service.py b/patches/gdata/photos/service.py new file mode 100755 index 0000000..23c5feb --- /dev/null +++ b/patches/gdata/photos/service.py @@ -0,0 +1,681 @@ +#!/usr/bin/env python +# -*-*- encoding: utf-8 -*-*- +# +# This is the service file for the Google Photo python client. +# It is used for higher level operations. +# +# $Id: service.py 144 2007-10-25 21:03:34Z havard.gulldahl $ +# +# Copyright 2007 HÃ¥vard Gulldahl +# +# 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. + +"""Google PhotoService provides a human-friendly interface to +Google Photo (a.k.a Picasa Web) services[1]. + +It extends gdata.service.GDataService and as such hides all the +nasty details about authenticating, parsing and communicating with +Google Photos. + +[1]: http://code.google.com/apis/picasaweb/gdata.html + +Example: + import gdata.photos, gdata.photos.service + pws = gdata.photos.service.PhotosService() + pws.ClientLogin(username, password) + #Get all albums + albums = pws.GetUserFeed().entry + # Get all photos in second album + photos = pws.GetFeed(albums[1].GetPhotosUri()).entry + # Get all tags for photos in second album and print them + tags = pws.GetFeed(albums[1].GetTagsUri()).entry + print [ tag.summary.text for tag in tags ] + # Get all comments for the first photos in list and print them + comments = pws.GetCommentFeed(photos[0].GetCommentsUri()).entry + print [ c.summary.text for c in comments ] + + # Get a photo to work with + photo = photos[0] + # Update metadata + + # Attributes from the <gphoto:*> namespace + photo.summary.text = u'A nice view from my veranda' + photo.title.text = u'Verandaview.jpg' + + # Attributes from the <media:*> namespace + photo.media.keywords.text = u'Home, Long-exposure, Sunset' # Comma-separated + + # Adding attributes to media object + + # Rotate 90 degrees clockwise + photo.rotation = gdata.photos.Rotation(text='90') + + # Submit modified photo object + photo = pws.UpdatePhotoMetadata(photo) + + # Make sure you only modify the newly returned object, else you'll get + # versioning errors. See Optimistic-concurrency + + # Add comment to a picture + comment = pws.InsertComment(photo, u'I wish the water always was this warm') + + # Remove comment because it was silly + print "*blush*" + pws.Delete(comment.GetEditLink().href) + +""" + +__author__ = u'havard@gulldahl.no'# (HÃ¥vard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__ +__license__ = 'Apache License v2' +__version__ = '$Revision: 176 $'[11:-2] + + +import sys, os.path, StringIO +import time +import gdata.service +import gdata +import atom.service +import atom +import gdata.photos + +SUPPORTED_UPLOAD_TYPES = ('bmp', 'jpeg', 'jpg', 'gif', 'png') + +UNKOWN_ERROR=1000 +GPHOTOS_BAD_REQUEST=400 +GPHOTOS_CONFLICT=409 +GPHOTOS_INTERNAL_SERVER_ERROR=500 +GPHOTOS_INVALID_ARGUMENT=601 +GPHOTOS_INVALID_CONTENT_TYPE=602 +GPHOTOS_NOT_AN_IMAGE=603 +GPHOTOS_INVALID_KIND=604 + +class GooglePhotosException(Exception): + def __init__(self, response): + + self.error_code = response['status'] + self.reason = response['reason'].strip() + if '<html>' in str(response['body']): #general html message, discard it + response['body'] = "" + self.body = response['body'].strip() + self.message = "(%(status)s) %(body)s -- %(reason)s" % response + + #return explicit error codes + error_map = { '(12) Not an image':GPHOTOS_NOT_AN_IMAGE, + 'kind: That is not one of the acceptable values': + GPHOTOS_INVALID_KIND, + + } + for msg, code in error_map.iteritems(): + if self.body == msg: + self.error_code = code + break + self.args = [self.error_code, self.reason, self.body] + +class PhotosService(gdata.service.GDataService): + ssl = True + userUri = '/data/feed/api/user/%s' + + def __init__(self, email=None, password=None, source=None, + server='picasaweb.google.com', additional_headers=None, + **kwargs): + """Creates a client for the Google Photos service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'picasaweb.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + self.email = email + self.client = source + gdata.service.GDataService.__init__( + self, email=email, password=password, service='lh2', source=source, + server=server, additional_headers=additional_headers, **kwargs) + + def GetFeed(self, uri, limit=None, start_index=None): + """Get a feed. + + The results are ordered by the values of their `updated' elements, + with the most recently updated entry appearing first in the feed. + + Arguments: + uri: the uri to fetch + limit (optional): the maximum number of entries to return. Defaults to what + the server returns. + + Returns: + one of gdata.photos.AlbumFeed, + gdata.photos.UserFeed, + gdata.photos.PhotoFeed, + gdata.photos.CommentFeed, + gdata.photos.TagFeed, + depending on the results of the query. + Raises: + GooglePhotosException + + See: + http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual + """ + if limit is not None: + uri += '&max-results=%s' % limit + if start_index is not None: + uri += '&start-index=%s' % start_index + try: + return self.Get(uri, converter=gdata.photos.AnyFeedFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + def GetEntry(self, uri, limit=None, start_index=None): + """Get an Entry. + + Arguments: + uri: the uri to the entry + limit (optional): the maximum number of entries to return. Defaults to what + the server returns. + + Returns: + one of gdata.photos.AlbumEntry, + gdata.photos.UserEntry, + gdata.photos.PhotoEntry, + gdata.photos.CommentEntry, + gdata.photos.TagEntry, + depending on the results of the query. + Raises: + GooglePhotosException + """ + if limit is not None: + uri += '&max-results=%s' % limit + if start_index is not None: + uri += '&start-index=%s' % start_index + try: + return self.Get(uri, converter=gdata.photos.AnyEntryFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + def GetUserFeed(self, kind='album', user='default', limit=None): + """Get user-based feed, containing albums, photos, comments or tags; + defaults to albums. + + The entries are ordered by the values of their `updated' elements, + with the most recently updated entry appearing first in the feed. + + Arguments: + kind: the kind of entries to get, either `album', `photo', + `comment' or `tag', or a python list of these. Defaults to `album'. + user (optional): whose albums we're querying. Defaults to current user. + limit (optional): the maximum number of entries to return. + Defaults to everything the server returns. + + + Returns: + gdata.photos.UserFeed, containing appropriate Entry elements + + See: + http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual + http://googledataapis.blogspot.com/2007/07/picasa-web-albums-adds-new-api-features.html + """ + if isinstance(kind, (list, tuple) ): + kind = ",".join(kind) + + uri = '/data/feed/api/user/%s?kind=%s' % (user, kind) + return self.GetFeed(uri, limit=limit) + + def GetTaggedPhotos(self, tag, user='default', limit=None): + """Get all photos belonging to a specific user, tagged by the given keyword + + Arguments: + tag: The tag you're looking for, e.g. `dog' + user (optional): Whose images/videos you want to search, defaults + to current user + limit (optional): the maximum number of entries to return. + Defaults to everything the server returns. + + Returns: + gdata.photos.UserFeed containing PhotoEntry elements + """ + # Lower-casing because of + # http://code.google.com/p/gdata-issues/issues/detail?id=194 + uri = '/data/feed/api/user/%s?kind=photo&tag=%s' % (user, tag.lower()) + return self.GetFeed(uri, limit) + + def SearchUserPhotos(self, query, user='default', limit=100): + """Search through all photos for a specific user and return a feed. + This will look for matches in file names and image tags (a.k.a. keywords) + + Arguments: + query: The string you're looking for, e.g. `vacation' + user (optional): The username of whose photos you want to search, defaults + to current user. + limit (optional): Don't return more than `limit' hits, defaults to 100 + + Only public photos are searched, unless you are authenticated and + searching through your own photos. + + Returns: + gdata.photos.UserFeed with PhotoEntry elements + """ + uri = '/data/feed/api/user/%s?kind=photo&q=%s' % (user, query) + return self.GetFeed(uri, limit=limit) + + def SearchCommunityPhotos(self, query, limit=100): + """Search through all public photos and return a feed. + This will look for matches in file names and image tags (a.k.a. keywords) + + Arguments: + query: The string you're looking for, e.g. `vacation' + limit (optional): Don't return more than `limit' hits, defaults to 100 + + Returns: + gdata.GDataFeed with PhotoEntry elements + """ + uri='/data/feed/api/all?q=%s' % query + return self.GetFeed(uri, limit=limit) + + def GetContacts(self, user='default', limit=None): + """Retrieve a feed that contains a list of your contacts + + Arguments: + user: Username of the user whose contacts you want + + Returns + gdata.photos.UserFeed, with UserEntry entries + + See: + http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38 + """ + uri = '/data/feed/api/user/%s/contacts?kind=user' % user + return self.GetFeed(uri, limit=limit) + + def SearchContactsPhotos(self, user='default', search=None, limit=None): + """Search over your contacts' photos and return a feed + + Arguments: + user: Username of the user whose contacts you want + search (optional): What to search for (photo title, description and keywords) + + Returns + gdata.photos.UserFeed, with PhotoEntry elements + + See: + http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38 + """ + + uri = '/data/feed/api/user/%s/contacts?kind=photo&q=%s' % (user, search) + return self.GetFeed(uri, limit=limit) + + def InsertAlbum(self, title, summary, location=None, access='public', + commenting_enabled='true', timestamp=None): + """Add an album. + + Needs authentication, see self.ClientLogin() + + Arguments: + title: Album title + summary: Album summary / description + access (optional): `private' or `public'. Public albums are searchable + by everyone on the internet. Defaults to `public' + commenting_enabled (optional): `true' or `false'. Defaults to `true'. + timestamp (optional): A date and time for the album, in milliseconds since + Unix epoch[1] UTC. Defaults to now. + + Returns: + The newly created gdata.photos.AlbumEntry + + See: + http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed + + [1]: http://en.wikipedia.org/wiki/Unix_epoch + """ + album = gdata.photos.AlbumEntry() + album.title = atom.Title(text=title, title_type='text') + album.summary = atom.Summary(text=summary, summary_type='text') + if location is not None: + album.location = gdata.photos.Location(text=location) + album.access = gdata.photos.Access(text=access) + if commenting_enabled in ('true', 'false'): + album.commentingEnabled = gdata.photos.CommentingEnabled(text=commenting_enabled) + if timestamp is None: + timestamp = '%i' % int(time.time() * 1000) + album.timestamp = gdata.photos.Timestamp(text=timestamp) + try: + return self.Post(album, uri=self.userUri % self.email, + converter=gdata.photos.AlbumEntryFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + def InsertPhoto(self, album_or_uri, photo, filename_or_handle, + content_type='image/jpeg'): + """Add a PhotoEntry + + Needs authentication, see self.ClientLogin() + + Arguments: + album_or_uri: AlbumFeed or uri of the album where the photo should go + photo: PhotoEntry to add + filename_or_handle: A file-like object or file name where the image/video + will be read from + content_type (optional): Internet media type (a.k.a. mime type) of + media object. Currently Google Photos supports these types: + o image/bmp + o image/gif + o image/jpeg + o image/png + + Images will be converted to jpeg on upload. Defaults to `image/jpeg' + + """ + + try: + assert(isinstance(photo, gdata.photos.PhotoEntry)) + except AssertionError: + raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, + 'body':'`photo` must be a gdata.photos.PhotoEntry instance', + 'reason':'Found %s, not PhotoEntry' % type(photo) + }) + try: + majtype, mintype = content_type.split('/') + assert(mintype in SUPPORTED_UPLOAD_TYPES) + except (ValueError, AssertionError): + raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE, + 'body':'This is not a valid content type: %s' % content_type, + 'reason':'Accepted content types: %s' % \ + ['image/'+t for t in SUPPORTED_UPLOAD_TYPES] + }) + if isinstance(filename_or_handle, (str, unicode)) and \ + os.path.exists(filename_or_handle): # it's a file name + mediasource = gdata.MediaSource() + mediasource.setFile(filename_or_handle, content_type) + elif hasattr(filename_or_handle, 'read'):# it's a file-like resource + if hasattr(filename_or_handle, 'seek'): + filename_or_handle.seek(0) # rewind pointer to the start of the file + # gdata.MediaSource needs the content length, so read the whole image + file_handle = StringIO.StringIO(filename_or_handle.read()) + name = 'image' + if hasattr(filename_or_handle, 'name'): + name = filename_or_handle.name + mediasource = gdata.MediaSource(file_handle, content_type, + content_length=file_handle.len, file_name=name) + else: #filename_or_handle is not valid + raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, + 'body':'`filename_or_handle` must be a path name or a file-like object', + 'reason':'Found %s, not path name or object with a .read() method' % \ + filename_or_handle + }) + + if isinstance(album_or_uri, (str, unicode)): # it's a uri + feed_uri = album_or_uri + elif hasattr(album_or_uri, 'GetFeedLink'): # it's a AlbumFeed object + feed_uri = album_or_uri.GetFeedLink().href + + try: + return self.Post(photo, uri=feed_uri, media_source=mediasource, + converter=gdata.photos.PhotoEntryFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + def InsertPhotoSimple(self, album_or_uri, title, summary, filename_or_handle, + content_type='image/jpeg', keywords=None): + """Add a photo without constructing a PhotoEntry. + + Needs authentication, see self.ClientLogin() + + Arguments: + album_or_uri: AlbumFeed or uri of the album where the photo should go + title: Photo title + summary: Photo summary / description + filename_or_handle: A file-like object or file name where the image/video + will be read from + content_type (optional): Internet media type (a.k.a. mime type) of + media object. Currently Google Photos supports these types: + o image/bmp + o image/gif + o image/jpeg + o image/png + + Images will be converted to jpeg on upload. Defaults to `image/jpeg' + keywords (optional): a 1) comma separated string or 2) a python list() of + keywords (a.k.a. tags) to add to the image. + E.g. 1) `dog, vacation, happy' 2) ['dog', 'happy', 'vacation'] + + Returns: + The newly created gdata.photos.PhotoEntry or GooglePhotosException on errors + + See: + http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed + [1]: http://en.wikipedia.org/wiki/Unix_epoch + """ + + metadata = gdata.photos.PhotoEntry() + metadata.title=atom.Title(text=title) + metadata.summary = atom.Summary(text=summary, summary_type='text') + if keywords is not None: + if isinstance(keywords, list): + keywords = ','.join(keywords) + metadata.media.keywords = gdata.media.Keywords(text=keywords) + return self.InsertPhoto(album_or_uri, metadata, filename_or_handle, + content_type) + + def UpdatePhotoMetadata(self, photo): + """Update a photo's metadata. + + Needs authentication, see self.ClientLogin() + + You can update any or all of the following metadata properties: + * <title> + * <media:description> + * <gphoto:checksum> + * <gphoto:client> + * <gphoto:rotation> + * <gphoto:timestamp> + * <gphoto:commentingEnabled> + + Arguments: + photo: a gdata.photos.PhotoEntry object with updated elements + + Returns: + The modified gdata.photos.PhotoEntry + + Example: + p = GetFeed(uri).entry[0] + p.title.text = u'My new text' + p.commentingEnabled.text = 'false' + p = UpdatePhotoMetadata(p) + + It is important that you don't keep the old object around, once + it has been updated. See + http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency + """ + try: + return self.Put(data=photo, uri=photo.GetEditLink().href, + converter=gdata.photos.PhotoEntryFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + + def UpdatePhotoBlob(self, photo_or_uri, filename_or_handle, + content_type = 'image/jpeg'): + """Update a photo's binary data. + + Needs authentication, see self.ClientLogin() + + Arguments: + photo_or_uri: a gdata.photos.PhotoEntry that will be updated, or a + `edit-media' uri pointing to it + filename_or_handle: A file-like object or file name where the image/video + will be read from + content_type (optional): Internet media type (a.k.a. mime type) of + media object. Currently Google Photos supports these types: + o image/bmp + o image/gif + o image/jpeg + o image/png + Images will be converted to jpeg on upload. Defaults to `image/jpeg' + + Returns: + The modified gdata.photos.PhotoEntry + + Example: + p = GetFeed(PhotoUri) + p = UpdatePhotoBlob(p, '/tmp/newPic.jpg') + + It is important that you don't keep the old object around, once + it has been updated. See + http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency + """ + + try: + majtype, mintype = content_type.split('/') + assert(mintype in SUPPORTED_UPLOAD_TYPES) + except (ValueError, AssertionError): + raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE, + 'body':'This is not a valid content type: %s' % content_type, + 'reason':'Accepted content types: %s' % \ + ['image/'+t for t in SUPPORTED_UPLOAD_TYPES] + }) + + if isinstance(filename_or_handle, (str, unicode)) and \ + os.path.exists(filename_or_handle): # it's a file name + photoblob = gdata.MediaSource() + photoblob.setFile(filename_or_handle, content_type) + elif hasattr(filename_or_handle, 'read'):# it's a file-like resource + if hasattr(filename_or_handle, 'seek'): + filename_or_handle.seek(0) # rewind pointer to the start of the file + # gdata.MediaSource needs the content length, so read the whole image + file_handle = StringIO.StringIO(filename_or_handle.read()) + name = 'image' + if hasattr(filename_or_handle, 'name'): + name = filename_or_handle.name + mediasource = gdata.MediaSource(file_handle, content_type, + content_length=file_handle.len, file_name=name) + else: #filename_or_handle is not valid + raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, + 'body':'`filename_or_handle` must be a path name or a file-like object', + 'reason':'Found %s, not path name or an object with .read() method' % \ + type(filename_or_handle) + }) + + if isinstance(photo_or_uri, (str, unicode)): + entry_uri = photo_or_uri # it's a uri + elif hasattr(photo_or_uri, 'GetEditMediaLink'): + entry_uri = photo_or_uri.GetEditMediaLink().href + try: + return self.Put(photoblob, entry_uri, + converter=gdata.photos.PhotoEntryFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + def InsertTag(self, photo_or_uri, tag): + """Add a tag (a.k.a. keyword) to a photo. + + Needs authentication, see self.ClientLogin() + + Arguments: + photo_or_uri: a gdata.photos.PhotoEntry that will be tagged, or a + `post' uri pointing to it + (string) tag: The tag/keyword + + Returns: + The new gdata.photos.TagEntry + + Example: + p = GetFeed(PhotoUri) + tag = InsertTag(p, 'Beautiful sunsets') + + """ + tag = gdata.photos.TagEntry(title=atom.Title(text=tag)) + if isinstance(photo_or_uri, (str, unicode)): + post_uri = photo_or_uri # it's a uri + elif hasattr(photo_or_uri, 'GetEditMediaLink'): + post_uri = photo_or_uri.GetPostLink().href + try: + return self.Post(data=tag, uri=post_uri, + converter=gdata.photos.TagEntryFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + + def InsertComment(self, photo_or_uri, comment): + """Add a comment to a photo. + + Needs authentication, see self.ClientLogin() + + Arguments: + photo_or_uri: a gdata.photos.PhotoEntry that is about to be commented + , or a `post' uri pointing to it + (string) comment: The actual comment + + Returns: + The new gdata.photos.CommentEntry + + Example: + p = GetFeed(PhotoUri) + tag = InsertComment(p, 'OOOH! I would have loved to be there. + Who's that in the back?') + + """ + comment = gdata.photos.CommentEntry(content=atom.Content(text=comment)) + if isinstance(photo_or_uri, (str, unicode)): + post_uri = photo_or_uri # it's a uri + elif hasattr(photo_or_uri, 'GetEditMediaLink'): + post_uri = photo_or_uri.GetPostLink().href + try: + return self.Post(data=comment, uri=post_uri, + converter=gdata.photos.CommentEntryFromString) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + + def Delete(self, object_or_uri, *args, **kwargs): + """Delete an object. + + Re-implementing the GDataService.Delete method, to add some + convenience. + + Arguments: + object_or_uri: Any object that has a GetEditLink() method that + returns a link, or a uri to that object. + + Returns: + ? or GooglePhotosException on errors + """ + try: + uri = object_or_uri.GetEditLink().href + except AttributeError: + uri = object_or_uri + try: + return gdata.service.GDataService.Delete(self, uri, *args, **kwargs) + except gdata.service.RequestError, e: + raise GooglePhotosException(e.args[0]) + +def GetSmallestThumbnail(media_thumbnail_list): + """Helper function to get the smallest thumbnail of a list of + gdata.media.Thumbnail. + Returns gdata.media.Thumbnail """ + r = {} + for thumb in media_thumbnail_list: + r[int(thumb.width)*int(thumb.height)] = thumb + keys = r.keys() + keys.sort() + return r[keys[0]] + +def ConvertAtomTimestampToEpoch(timestamp): + """Helper function to convert a timestamp string, for instance + from atom:updated or atom:published, to milliseconds since Unix epoch + (a.k.a. POSIX time). + + `2007-07-22T00:45:10.000Z' -> """ + return time.mktime(time.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.000Z')) + ## TODO: Timezone aware diff --git a/patches/gdata/projecthosting/__init__.py b/patches/gdata/projecthosting/__init__.py new file mode 100755 index 0000000..8b13789 --- /dev/null +++ b/patches/gdata/projecthosting/__init__.py @@ -0,0 +1 @@ + diff --git a/patches/gdata/projecthosting/__init__.pyc b/patches/gdata/projecthosting/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d0d45b6c0949ff50e5c77af0db46b2b4e4eda67 GIT binary patch literal 152 zcmcckiI>arW0zkt0~9a<X$K%KW&si@3=F{<AQ3+eAi;n}6gvXN^m7w4^Yn|0lJ#>k zb1Dn+^HOwED@uwI^$QY9k~30^_0v-lOMtwh{H)aEl8pS~lFYnx{rLFIyv&mLc)fzk W5)Pm#Ho5sJr8%i~AiIizm;nHBQY3!> literal 0 HcmV?d00001 diff --git a/patches/gdata/projecthosting/client.py b/patches/gdata/projecthosting/client.py new file mode 100755 index 0000000..512eb32 --- /dev/null +++ b/patches/gdata/projecthosting/client.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python +# +# Copyright 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. + +import atom.data +import gdata.client +import gdata.gauth +import gdata.projecthosting.data + + +class ProjectHostingClient(gdata.client.GDClient): + """Client to interact with the Project Hosting GData API.""" + api_version = '1.0' + auth_service = 'code' + auth_scopes = gdata.gauth.AUTH_SCOPES['code'] + host = 'code.google.com' + ssl = True + + def get_issues(self, project_name, + desired_class=gdata.projecthosting.data.IssuesFeed, **kwargs): + """Get a feed of issues for a particular project. + + Args: + project_name str The name of the project. + query Query Set returned issues parameters. + + Returns: + data.IssuesFeed + """ + return self.get_feed(gdata.projecthosting.data.ISSUES_FULL_FEED % + project_name, desired_class=desired_class, **kwargs) + + def add_issue(self, project_name, title, content, author, + status=None, owner=None, labels=None, ccs=None, **kwargs): + """Create a new issue for the project. + + Args: + project_name str The name of the project. + title str The title of the new issue. + content str The summary of the new issue. + author str The authenticated user's username. + status str The status of the new issue, Accepted, etc. + owner str The username of new issue's owner. + labels [str] Labels to associate with the new issue. + ccs [str] usernames to Cc on the new issue. + Returns: + data.IssueEntry + """ + new_entry = gdata.projecthosting.data.IssueEntry( + title=atom.data.Title(text=title), + content=atom.data.Content(text=content), + author=[atom.data.Author(name=atom.data.Name(text=author))]) + + if status: + new_entry.status = gdata.projecthosting.data.Status(text=status) + + if owner: + owner = [gdata.projecthosting.data.Owner( + username=gdata.projecthosting.data.Username(text=owner))] + + if labels: + new_entry.label = [gdata.projecthosting.data.Label(text=label) + for label in labels] + if ccs: + new_entry.cc = [ + gdata.projecthosting.data.Cc( + username=gdata.projecthosting.data.Username(text=cc)) + for cc in ccs] + + return self.post( + new_entry, + gdata.projecthosting.data.ISSUES_FULL_FEED % project_name, + **kwargs) + + def update_issue(self, project_name, issue_id, author, comment=None, + summary=None, status=None, owner=None, labels=None, ccs=None, + **kwargs): + """Update or comment on one issue for the project. + + Args: + project_name str The name of the issue's project. + issue_id str The issue number needing updated. + author str The authenticated user's username. + comment str A comment to append to the issue + summary str Rewrite the summary of the issue. + status str A new status for the issue. + owner str The username of the new owner. + labels [str] Labels to set on the issue (prepend issue with - to remove a + label). + ccs [str] Ccs to set on th enew issue (prepend cc with - to remove a cc). + + Returns: + data.CommentEntry + """ + updates = gdata.projecthosting.data.Updates() + + if summary: + updates.summary = gdata.projecthosting.data.Summary(text=summary) + + if status: + updates.status = gdata.projecthosting.data.Status(text=status) + + if owner: + updates.ownerUpdate = gdata.projecthosting.data.OwnerUpdate(text=owner) + + if labels: + updates.label = [gdata.projecthosting.data.Label(text=label) + for label in labels] + if ccs: + updates.ccUpdate = [gdata.projecthosting.data.CcUpdate(text=cc) + for cc in ccs] + + update_entry = gdata.projecthosting.data.CommentEntry( + content=atom.data.Content(text=comment), + author=[atom.data.Author(name=atom.data.Name(text=author))], + updates=updates) + + return self.post( + update_entry, + gdata.projecthosting.data.COMMENTS_FULL_FEED % (project_name, issue_id), + **kwargs) + + def get_comments(self, project_name, issue_id, + desired_class=gdata.projecthosting.data.CommentsFeed, + **kwargs): + """Get a feed of all updates to an issue. + + Args: + project_name str The name of the issue's project. + issue_id str The issue number needing updated. + + Returns: + data.CommentsFeed + """ + return self.get_feed( + gdata.projecthosting.data.COMMENTS_FULL_FEED % (project_name, issue_id), + desired_class=desired_class, **kwargs) + + def update(self, entry, auth_token=None, force=False, **kwargs): + """Unsupported GData update method. + + Use update_*() instead. + """ + raise NotImplementedError( + 'GData Update operation unsupported, try update_*') + + def delete(self, entry_or_uri, auth_token=None, force=False, **kwargs): + """Unsupported GData delete method. + + Use update_issue(status='Closed') instead. + """ + raise NotImplementedError( + 'GData Delete API unsupported, try closing the issue instead.') + + +class Query(gdata.client.Query): + + def __init__(self, issue_id=None, label=None, canned_query=None, owner=None, + status=None, **kwargs): + """Constructs a Google Data Query to filter feed contents serverside. + Args: + issue_id: int or str The issue to return based on the issue id. + label: str A label returned issues must have. + canned_query: str Return issues based on a canned query identifier + owner: str Return issues based on the owner of the issue. For Gmail users, + this will be the part of the email preceding the '@' sign. + status: str Return issues based on the status of the issue. + """ + super(Query, self).__init__(**kwargs) + self.label = label + self.issue_id = issue_id + self.canned_query = canned_query + self.owner = owner + self.status = status + + def modify_request(self, http_request): + if self.issue_id: + gdata.client._add_query_param('id', self.issue_id, http_request) + if self.label: + gdata.client._add_query_param('label', self.label, http_request) + if self.canned_query: + gdata.client._add_query_param('can', self.canned_query, http_request) + if self.owner: + gdata.client._add_query_param('owner', self.owner, http_request) + if self.status: + gdata.client._add_query_param('status', self.status, http_request) + super(Query, self).modify_request(http_request) + + ModifyRequest = modify_request diff --git a/patches/gdata/projecthosting/client.pyc b/patches/gdata/projecthosting/client.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d558705f9696d2a99879137492d53643298325aa GIT binary patch literal 7240 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 N=Z0zWQ-AB1{{gww&KCdx literal 0 HcmV?d00001 diff --git a/patches/gdata/projecthosting/data.py b/patches/gdata/projecthosting/data.py new file mode 100755 index 0000000..b0af2f5 --- /dev/null +++ b/patches/gdata/projecthosting/data.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# +# Copyright 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 classes and constants for XML in the Google Project Hosting API. + +Canonical documentation for the raw XML which these classes represent can be +found here: http://code.google.com/p/support/wiki/IssueTrackerAPI +""" + + +__author__ = 'jlapenna@google.com (Joe LaPenna)' + +import atom.core +import gdata.data + + +ISSUES_TEMPLATE = '{http://schemas.google.com/projecthosting/issues/2009}%s' + + +ISSUES_FULL_FEED = '/feeds/issues/p/%s/issues/full' +COMMENTS_FULL_FEED = '/feeds/issues/p/%s/issues/%s/comments/full' + + +class Uri(atom.core.XmlElement): + """The issues:uri element.""" + _qname = ISSUES_TEMPLATE % 'uri' + + +class Username(atom.core.XmlElement): + """The issues:username element.""" + _qname = ISSUES_TEMPLATE % 'username' + + +class Cc(atom.core.XmlElement): + """The issues:cc element.""" + _qname = ISSUES_TEMPLATE % 'cc' + uri = Uri + username = Username + + +class Label(atom.core.XmlElement): + """The issues:label element.""" + _qname = ISSUES_TEMPLATE % 'label' + + +class Owner(atom.core.XmlElement): + """The issues:owner element.""" + _qname = ISSUES_TEMPLATE % 'owner' + uri = Uri + username = Username + + +class Stars(atom.core.XmlElement): + """The issues:stars element.""" + _qname = ISSUES_TEMPLATE % 'stars' + + +class State(atom.core.XmlElement): + """The issues:state element.""" + _qname = ISSUES_TEMPLATE % 'state' + + +class Status(atom.core.XmlElement): + """The issues:status element.""" + _qname = ISSUES_TEMPLATE % 'status' + + +class Summary(atom.core.XmlElement): + """The issues:summary element.""" + _qname = ISSUES_TEMPLATE % 'summary' + + +class OwnerUpdate(atom.core.XmlElement): + """The issues:ownerUpdate element.""" + _qname = ISSUES_TEMPLATE % 'ownerUpdate' + + +class CcUpdate(atom.core.XmlElement): + """The issues:ccUpdate element.""" + _qname = ISSUES_TEMPLATE % 'ccUpdate' + + +class Updates(atom.core.XmlElement): + """The issues:updates element.""" + _qname = ISSUES_TEMPLATE % 'updates' + summary = Summary + status = Status + ownerUpdate = OwnerUpdate + label = [Label] + ccUpdate = [CcUpdate] + + +class IssueEntry(gdata.data.GDEntry): + """Represents the information of one issue.""" + _qname = atom.data.ATOM_TEMPLATE % 'entry' + owner = Owner + cc = [Cc] + label = [Label] + stars = Stars + state = State + status = Status + + +class IssuesFeed(gdata.data.GDFeed): + """An Atom feed listing a project's issues.""" + entry = [IssueEntry] + + +class CommentEntry(gdata.data.GDEntry): + """An entry detailing one comment on an issue.""" + _qname = atom.data.ATOM_TEMPLATE % 'entry' + updates = Updates + + +class CommentsFeed(gdata.data.GDFeed): + """An Atom feed listing a project's issue's comments.""" + entry = [CommentEntry] diff --git a/patches/gdata/projecthosting/data.pyc b/patches/gdata/projecthosting/data.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfa2a6e8a31d5b1c9cd95a211a717826c9ecbb60 GIT binary patch literal 5533 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 U<kzO?Kiw&4$N%EmEVT-M0nF@@wEzGB literal 0 HcmV?d00001 diff --git a/patches/gdata/sample_util.py b/patches/gdata/sample_util.py new file mode 100644 index 0000000..aae866e --- /dev/null +++ b/patches/gdata/sample_util.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +"""Provides utility functions used with command line samples.""" + +# This module is used for version 2 of the Google Data APIs. + +import sys +import getpass +import urllib +import gdata.gauth + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +CLIENT_LOGIN = 1 +AUTHSUB = 2 +OAUTH = 3 + +HMAC = 1 +RSA = 2 + + +class SettingsUtil(object): + """Gather's user preferences from flags or command prompts. + + An instance of this object stores the choices made by the user. At some + point it might be useful to save the user's preferences so that they do + not need to always set flags or answer preference prompts. + """ + + def __init__(self, prefs=None): + self.prefs = prefs or {} + + def get_param(self, name, prompt='', secret=False, ask=True, reuse=False): + # First, check in this objects stored preferences. + if name in self.prefs: + return self.prefs[name] + # Second, check for a command line parameter. + value = None + for i in xrange(len(sys.argv)): + if sys.argv[i].startswith('--%s=' % name): + value = sys.argv[i].split('=')[1] + elif sys.argv[i] == '--%s' % name: + value = sys.argv[i + 1] + # Third, if it was not on the command line, ask the user to input the + # value. + if value is None and ask: + prompt = '%s: ' % prompt + if secret: + value = getpass.getpass(prompt) + else: + value = raw_input(prompt) + # If we want to save the preference for reuse in future requests, add it + # to this object's prefs. + if value is not None and reuse: + self.prefs[name] = value + return value + + def authorize_client(self, client, auth_type=None, service=None, + source=None, scopes=None, oauth_type=None, + consumer_key=None, consumer_secret=None): + """Uses command line arguments, or prompts user for token values.""" + if 'client_auth_token' in self.prefs: + return + if auth_type is None: + auth_type = int(self.get_param( + 'auth_type', 'Please choose the authorization mechanism you want' + ' to use.\n' + '1. to use your email address and password (ClientLogin)\n' + '2. to use a web browser to visit an auth web page (AuthSub)\n' + '3. if you have registed to use OAuth\n', reuse=True)) + + # Get the scopes for the services we want to access. + if auth_type == AUTHSUB or auth_type == OAUTH: + if scopes is None: + scopes = self.get_param( + 'scopes', 'Enter the URL prefixes (scopes) for the resources you ' + 'would like to access.\nFor multiple scope URLs, place a comma ' + 'between each URL.\n' + 'Example: http://www.google.com/calendar/feeds/,' + 'http://www.google.com/m8/feeds/\n', reuse=True).split(',') + elif isinstance(scopes, (str, unicode)): + scopes = scopes.split(',') + + if auth_type == CLIENT_LOGIN: + email = self.get_param('email', 'Please enter your username', + reuse=False) + password = self.get_param('password', 'Password', True, reuse=False) + if service is None: + service = self.get_param( + 'service', 'What is the name of the service you wish to access?' + '\n(See list:' + ' http://code.google.com/apis/gdata/faq.html#clientlogin)', + reuse=True) + if source is None: + source = self.get_param('source', ask=False, reuse=True) + client.client_login(email, password, source=source, service=service) + elif auth_type == AUTHSUB: + auth_sub_token = self.get_param('auth_sub_token', ask=False, reuse=True) + session_token = self.get_param('session_token', ask=False, reuse=True) + private_key = None + auth_url = None + single_use_token = None + rsa_private_key = self.get_param( + 'rsa_private_key', + 'If you want to use secure mode AuthSub, please provide the\n' + ' location of your RSA private key which corresponds to the\n' + ' certificate you have uploaded for your domain. If you do not\n' + ' have an RSA key, simply press enter', reuse=True) + + if rsa_private_key: + try: + private_key_file = open(rsa_private_key, 'rb') + private_key = private_key_file.read() + private_key_file.close() + except IOError: + print 'Unable to read private key from file' + + if private_key is not None: + if client.auth_token is None: + if session_token: + client.auth_token = gdata.gauth.SecureAuthSubToken( + session_token, private_key, scopes) + self.prefs['client_auth_token'] = gdata.gauth.token_to_blob( + client.auth_token) + return + elif auth_sub_token: + client.auth_token = gdata.gauth.SecureAuthSubToken( + auth_sub_token, private_key, scopes) + client.upgrade_token() + self.prefs['client_auth_token'] = gdata.gauth.token_to_blob( + client.auth_token) + return + + auth_url = gdata.gauth.generate_auth_sub_url( + 'http://gauthmachine.appspot.com/authsub', scopes, True) + print 'with a private key, get ready for this URL', auth_url + + else: + if client.auth_token is None: + if session_token: + client.auth_token = gdata.gauth.AuthSubToken(session_token, + scopes) + self.prefs['client_auth_token'] = gdata.gauth.token_to_blob( + client.auth_token) + return + elif auth_sub_token: + client.auth_token = gdata.gauth.AuthSubToken(auth_sub_token, + scopes) + client.upgrade_token() + self.prefs['client_auth_token'] = gdata.gauth.token_to_blob( + client.auth_token) + return + + auth_url = gdata.gauth.generate_auth_sub_url( + 'http://gauthmachine.appspot.com/authsub', scopes) + + print 'Visit the following URL in your browser to authorize this app:' + print str(auth_url) + print 'After agreeing to authorize the app, copy the token value from' + print ' the URL. Example: "www.google.com/?token=ab12" token value is' + print ' ab12' + token_value = raw_input('Please enter the token value: ') + if private_key is not None: + single_use_token = gdata.gauth.SecureAuthSubToken( + token_value, private_key, scopes) + else: + single_use_token = gdata.gauth.AuthSubToken(token_value, scopes) + client.auth_token = single_use_token + client.upgrade_token() + + elif auth_type == OAUTH: + if oauth_type is None: + oauth_type = int(self.get_param( + 'oauth_type', 'Please choose the authorization mechanism you want' + ' to use.\n' + '1. use an HMAC signature using your consumer key and secret\n' + '2. use RSA with your private key to sign requests\n', + reuse=True)) + + consumer_key = self.get_param( + 'consumer_key', 'Please enter your OAuth conumer key ' + 'which identifies your app', reuse=True) + + if oauth_type == HMAC: + consumer_secret = self.get_param( + 'consumer_secret', 'Please enter your OAuth conumer secret ' + 'which you share with the OAuth provider', True, reuse=False) + # Swap out this code once the client supports requesting an oauth + # token. + # Get a request token. + request_token = client.get_oauth_token( + scopes, 'http://gauthmachine.appspot.com/oauth', consumer_key, + consumer_secret=consumer_secret) + elif oauth_type == RSA: + rsa_private_key = self.get_param( + 'rsa_private_key', + 'Please provide the location of your RSA private key which\n' + ' corresponds to the certificate you have uploaded for your' + ' domain.', + reuse=True) + try: + private_key_file = open(rsa_private_key, 'rb') + private_key = private_key_file.read() + private_key_file.close() + except IOError: + print 'Unable to read private key from file' + + request_token = client.get_oauth_token( + scopes, 'http://gauthmachine.appspot.com/oauth', consumer_key, + rsa_private_key=private_key) + else: + print 'Invalid OAuth signature type' + return None + + # Authorize the request token in the browser. + print 'Visit the following URL in your browser to authorize this app:' + print str(request_token.generate_authorization_url()) + print 'After agreeing to authorize the app, copy URL from the browser\'s' + print ' address bar.' + url = raw_input('Please enter the url: ') + gdata.gauth.authorize_request_token(request_token, url) + # Exchange for an access token. + client.auth_token = client.get_access_token(request_token) + else: + print 'Invalid authorization type.' + return None + if client.auth_token: + self.prefs['client_auth_token'] = gdata.gauth.token_to_blob( + client.auth_token) + + +def get_param(name, prompt='', secret=False, ask=True): + settings = SettingsUtil() + return settings.get_param(name=name, prompt=prompt, secret=secret, ask=ask) + + +def authorize_client(client, auth_type=None, service=None, source=None, + scopes=None, oauth_type=None, consumer_key=None, + consumer_secret=None): + """Uses command line arguments, or prompts user for token values.""" + settings = SettingsUtil() + return settings.authorize_client(client=client, auth_type=auth_type, + service=service, source=source, + scopes=scopes, oauth_type=oauth_type, + consumer_key=consumer_key, + consumer_secret=consumer_secret) + + +def print_options(): + """Displays usage information, available command line params.""" + # TODO: fill in the usage description for authorizing the client. + print '' + diff --git a/patches/gdata/service.py b/patches/gdata/service.py new file mode 100755 index 0000000..df05d1f --- /dev/null +++ b/patches/gdata/service.py @@ -0,0 +1,1718 @@ +#!/usr/bin/python +# +# Copyright (C) 2006,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. + + +"""GDataService provides CRUD ops. and programmatic login for GData services. + + Error: A base exception class for all exceptions in the gdata_client + module. + + CaptchaRequired: This exception is thrown when a login attempt results in a + captcha challenge from the ClientLogin service. When this + exception is thrown, the captcha_token and captcha_url are + set to the values provided in the server's response. + + BadAuthentication: Raised when a login attempt is made with an incorrect + username or password. + + NotAuthenticated: Raised if an operation requiring authentication is called + before a user has authenticated. + + NonAuthSubToken: Raised if a method to modify an AuthSub token is used when + the user is either not authenticated or is authenticated + through another authentication mechanism. + + NonOAuthToken: Raised if a method to modify an OAuth token is used when the + user is either not authenticated or is authenticated through + another authentication mechanism. + + RequestError: Raised if a CRUD request returned a non-success code. + + UnexpectedReturnType: Raised if the response from the server was not of the + desired type. For example, this would be raised if the + server sent a feed when the client requested an entry. + + GDataService: Encapsulates user credentials needed to perform insert, update + and delete operations with the GData API. An instance can + perform user authentication, query, insertion, deletion, and + update. + + Query: Eases query URI creation by allowing URI parameters to be set as + dictionary attributes. For example a query with a feed of + '/base/feeds/snippets' and ['bq'] set to 'digital camera' will + produce '/base/feeds/snippets?bq=digital+camera' when .ToUri() is + called on it. +""" + + +__author__ = 'api.jscudder (Jeffrey Scudder)' + +import re +import urllib +import urlparse +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.service +import gdata +import atom +import atom.http_interface +import atom.token_store +import gdata.auth +import gdata.gauth + + +AUTH_SERVER_HOST = 'https://www.google.com' + + +# When requesting an AuthSub token, it is often helpful to track the scope +# which is being requested. One way to accomplish this is to add a URL +# parameter to the 'next' URL which contains the requested scope. This +# constant is the default name (AKA key) for the URL parameter. +SCOPE_URL_PARAM_NAME = 'authsub_token_scope' +# When requesting an OAuth access token or authorization of an existing OAuth +# request token, it is often helpful to track the scope(s) which is/are being +# requested. One way to accomplish this is to add a URL parameter to the +# 'callback' URL which contains the requested scope. This constant is the +# default name (AKA key) for the URL parameter. +OAUTH_SCOPE_URL_PARAM_NAME = 'oauth_token_scope' +# Maps the service names used in ClientLogin to scope URLs. +CLIENT_LOGIN_SCOPES = gdata.gauth.AUTH_SCOPES +# Default parameters for GDataService.GetWithRetries method +DEFAULT_NUM_RETRIES = 3 +DEFAULT_DELAY = 1 +DEFAULT_BACKOFF = 2 + + +def lookup_scopes(service_name): + """Finds the scope URLs for the desired service. + + In some cases, an unknown service may be used, and in those cases this + function will return None. + """ + if service_name in CLIENT_LOGIN_SCOPES: + return CLIENT_LOGIN_SCOPES[service_name] + return None + + +# Module level variable specifies which module should be used by GDataService +# objects to make HttpRequests. This setting can be overridden on each +# instance of GDataService. +# This module level variable is deprecated. Reassign the http_client member +# of a GDataService object instead. +http_request_handler = atom.service + + +class Error(Exception): + pass + + +class CaptchaRequired(Error): + pass + + +class BadAuthentication(Error): + pass + + +class NotAuthenticated(Error): + pass + + +class NonAuthSubToken(Error): + pass + + +class NonOAuthToken(Error): + pass + + +class RequestError(Error): + pass + + +class UnexpectedReturnType(Error): + pass + + +class BadAuthenticationServiceURL(Error): + pass + + +class FetchingOAuthRequestTokenFailed(RequestError): + pass + + +class TokenUpgradeFailed(RequestError): + pass + + +class RevokingOAuthTokenFailed(RequestError): + pass + + +class AuthorizationRequired(Error): + pass + + +class TokenHadNoScope(Error): + pass + + +class RanOutOfTries(Error): + pass + + +class GDataService(atom.service.AtomService): + """Contains elements needed for GData login and CRUD request headers. + + Maintains additional headers (tokens for example) needed for the GData + services to allow a user to perform inserts, updates, and deletes. + """ + # The hander member is deprecated, use http_client instead. + handler = None + # The auth_token member is deprecated, use the token_store instead. + auth_token = None + # The tokens dict is deprecated in favor of the token_store. + tokens = None + + def __init__(self, email=None, password=None, account_type='HOSTED_OR_GOOGLE', + service=None, auth_service_url=None, source=None, server=None, + additional_headers=None, handler=None, tokens=None, + http_client=None, token_store=None): + """Creates an object of type GDataService. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + account_type: string (optional) The type of account to use. Use + 'GOOGLE' for regular Google accounts or 'HOSTED' for Google + Apps accounts, or 'HOSTED_OR_GOOGLE' to try finding a HOSTED + account first and, if it doesn't exist, try finding a regular + GOOGLE account. Default value: 'HOSTED_OR_GOOGLE'. + service: string (optional) The desired service for which credentials + will be obtained. + auth_service_url: string (optional) User-defined auth token request URL + allows users to explicitly specify where to send auth token requests. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'base.google.com'. + additional_headers: dictionary (optional) Any additional headers which + should be included with CRUD operations. + handler: module (optional) This parameter is deprecated and has been + replaced by http_client. + tokens: This parameter is deprecated, calls should be made to + token_store instead. + 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. + """ + atom.service.AtomService.__init__(self, http_client=http_client, + token_store=token_store) + self.email = email + self.password = password + self.account_type = account_type + self.service = service + self.auth_service_url = auth_service_url + self.server = server + self.additional_headers = additional_headers or {} + self._oauth_input_params = None + self.__SetSource(source) + self.__captcha_token = None + self.__captcha_url = None + self.__gsessionid = None + + if http_request_handler.__name__ == 'gdata.urlfetch': + import gdata.alt.appengine + self.http_client = gdata.alt.appengine.AppEngineHttpClient() + + def _SetSessionId(self, session_id): + """Used in unit tests to simulate a 302 which sets a gsessionid.""" + self.__gsessionid = session_id + + # Define properties for GDataService + def _SetAuthSubToken(self, auth_token, scopes=None): + """Deprecated, use SetAuthSubToken instead.""" + self.SetAuthSubToken(auth_token, scopes=scopes) + + def __SetAuthSubToken(self, auth_token, scopes=None): + """Deprecated, use SetAuthSubToken instead.""" + self._SetAuthSubToken(auth_token, scopes=scopes) + + def _GetAuthToken(self): + """Returns the auth token used for authenticating requests. + + Returns: + string + """ + current_scopes = lookup_scopes(self.service) + if current_scopes: + token = self.token_store.find_token(current_scopes[0]) + if hasattr(token, 'auth_header'): + return token.auth_header + return None + + def _GetCaptchaToken(self): + """Returns a captcha token if the most recent login attempt generated one. + + The captcha token is only set if the Programmatic Login attempt failed + because the Google service issued a captcha challenge. + + Returns: + string + """ + return self.__captcha_token + + def __GetCaptchaToken(self): + return self._GetCaptchaToken() + + captcha_token = property(__GetCaptchaToken, + doc="""Get the captcha token for a login request.""") + + def _GetCaptchaURL(self): + """Returns the URL of the captcha image if a login attempt generated one. + + The captcha URL is only set if the Programmatic Login attempt failed + because the Google service issued a captcha challenge. + + Returns: + string + """ + return self.__captcha_url + + def __GetCaptchaURL(self): + return self._GetCaptchaURL() + + captcha_url = property(__GetCaptchaURL, + doc="""Get the captcha URL for a login request.""") + + def GetGeneratorFromLinkFinder(self, link_finder, func, + num_retries=DEFAULT_NUM_RETRIES, + delay=DEFAULT_DELAY, + backoff=DEFAULT_BACKOFF): + """returns a generator for pagination""" + yield link_finder + next = link_finder.GetNextLink() + while next is not None: + next_feed = func(str(self.GetWithRetries( + next.href, num_retries=num_retries, delay=delay, backoff=backoff))) + yield next_feed + next = next_feed.GetNextLink() + + def _GetElementGeneratorFromLinkFinder(self, link_finder, func, + num_retries=DEFAULT_NUM_RETRIES, + delay=DEFAULT_DELAY, + backoff=DEFAULT_BACKOFF): + for element in self.GetGeneratorFromLinkFinder(link_finder, func, + num_retries=num_retries, + delay=delay, + backoff=backoff).entry: + yield element + + def GetOAuthInputParameters(self): + return self._oauth_input_params + + def SetOAuthInputParameters(self, signature_method, consumer_key, + consumer_secret=None, rsa_key=None, + two_legged_oauth=False, requestor_id=None): + """Sets parameters required for using OAuth authentication mechanism. + + NOTE: Though consumer_secret and rsa_key are optional, either of the two + is required depending on the value of the signature_method. + + Args: + signature_method: class which provides implementation for strategy class + oauth.oauth.OAuthSignatureMethod. Signature method to be used for + signing each request. Valid implementations are provided as the + constants defined by gdata.auth.OAuthSignatureMethod. Currently + they are gdata.auth.OAuthSignatureMethod.RSA_SHA1 and + gdata.auth.OAuthSignatureMethod.HMAC_SHA1 + consumer_key: string Domain identifying third_party web application. + consumer_secret: string (optional) Secret generated during registration. + Required only for HMAC_SHA1 signature method. + rsa_key: string (optional) Private key required for RSA_SHA1 signature + method. + two_legged_oauth: boolean (optional) Enables two-legged OAuth process. + requestor_id: string (optional) User email adress to make requests on + their behalf. This parameter should only be set when two_legged_oauth + is True. + """ + self._oauth_input_params = gdata.auth.OAuthInputParams( + signature_method, consumer_key, consumer_secret=consumer_secret, + rsa_key=rsa_key, requestor_id=requestor_id) + if two_legged_oauth: + oauth_token = gdata.auth.OAuthToken( + oauth_input_params=self._oauth_input_params) + self.SetOAuthToken(oauth_token) + + def FetchOAuthRequestToken(self, scopes=None, extra_parameters=None, + request_url='%s/accounts/OAuthGetRequestToken' % \ + AUTH_SERVER_HOST, oauth_callback=None): + """Fetches and sets the OAuth request token and returns it. + + Args: + scopes: string or list of string base URL(s) of the service(s) to be + accessed. If None, then this method tries to determine the + scope(s) from the current service. + extra_parameters: dict (optional) key-value pairs as any additional + parameters to be included in the URL and signature while making a + request for fetching an OAuth request token. All the OAuth parameters + are added by default. But if provided through this argument, any + default parameters will be overwritten. For e.g. a default parameter + oauth_version 1.0 can be overwritten if + extra_parameters = {'oauth_version': '2.0'} + request_url: Request token URL. The default is + 'https://www.google.com/accounts/OAuthGetRequestToken'. + oauth_callback: str (optional) If set, it is assume the client is using + the OAuth v1.0a protocol where the callback url is sent in the + request token step. If the oauth_callback is also set in + extra_params, this value will override that one. + + Returns: + The fetched request token as a gdata.auth.OAuthToken object. + + Raises: + FetchingOAuthRequestTokenFailed if the server responded to the request + with an error. + """ + if scopes is None: + scopes = lookup_scopes(self.service) + if not isinstance(scopes, (list, tuple)): + scopes = [scopes,] + if oauth_callback: + if extra_parameters is not None: + extra_parameters['oauth_callback'] = oauth_callback + else: + extra_parameters = {'oauth_callback': oauth_callback} + request_token_url = gdata.auth.GenerateOAuthRequestTokenUrl( + self._oauth_input_params, scopes, + request_token_url=request_url, + extra_parameters=extra_parameters) + response = self.http_client.request('GET', str(request_token_url)) + if response.status == 200: + token = gdata.auth.OAuthToken() + token.set_token_string(response.read()) + token.scopes = scopes + token.oauth_input_params = self._oauth_input_params + self.SetOAuthToken(token) + return token + error = { + 'status': response.status, + 'reason': 'Non 200 response on fetch request token', + 'body': response.read() + } + raise FetchingOAuthRequestTokenFailed(error) + + def SetOAuthToken(self, oauth_token): + """Attempts to set the current token and add it to the token store. + + The oauth_token can be any OAuth token i.e. unauthorized request token, + authorized request token or access token. + This method also attempts to add the token to the token store. + Use this method any time you want the current token to point to the + oauth_token passed. For e.g. call this method with the request token + you receive from FetchOAuthRequestToken. + + Args: + request_token: gdata.auth.OAuthToken OAuth request token. + """ + if self.auto_set_current_token: + self.current_token = oauth_token + if self.auto_store_tokens: + self.token_store.add_token(oauth_token) + + def GenerateOAuthAuthorizationURL( + self, request_token=None, callback_url=None, extra_params=None, + include_scopes_in_callback=False, + scopes_param_prefix=OAUTH_SCOPE_URL_PARAM_NAME, + request_url='%s/accounts/OAuthAuthorizeToken' % AUTH_SERVER_HOST): + """Generates URL at which user will login to authorize the request token. + + Args: + request_token: gdata.auth.OAuthToken (optional) OAuth request token. + If not specified, then the current token will be used if it is of + type <gdata.auth.OAuthToken>, else it is found by looking in the + token_store by looking for a token for the current scope. + callback_url: string (optional) The URL user will be sent to after + logging in and granting access. + extra_params: dict (optional) Additional parameters to be sent. + include_scopes_in_callback: Boolean (default=False) if set to True, and + if 'callback_url' is present, the 'callback_url' will be modified to + include the scope(s) from the request token as a URL parameter. The + key for the 'callback' URL's scope parameter will be + OAUTH_SCOPE_URL_PARAM_NAME. The benefit of including the scope URL as + a parameter to the 'callback' URL, is that the page which receives + the OAuth token will be able to tell which URLs the token grants + access to. + scopes_param_prefix: string (default='oauth_token_scope') The URL + parameter key which maps to the list of valid scopes for the token. + This URL parameter will be included in the callback URL along with + the scopes of the token as value if include_scopes_in_callback=True. + request_url: Authorization URL. The default is + 'https://www.google.com/accounts/OAuthAuthorizeToken'. + Returns: + A string URL at which the user is required to login. + + Raises: + NonOAuthToken if the user's request token is not an OAuth token or if a + request token was not available. + """ + if request_token and not isinstance(request_token, gdata.auth.OAuthToken): + raise NonOAuthToken + if not request_token: + if isinstance(self.current_token, gdata.auth.OAuthToken): + request_token = self.current_token + else: + current_scopes = lookup_scopes(self.service) + if current_scopes: + token = self.token_store.find_token(current_scopes[0]) + if isinstance(token, gdata.auth.OAuthToken): + request_token = token + if not request_token: + raise NonOAuthToken + return str(gdata.auth.GenerateOAuthAuthorizationUrl( + request_token, + authorization_url=request_url, + callback_url=callback_url, extra_params=extra_params, + include_scopes_in_callback=include_scopes_in_callback, + scopes_param_prefix=scopes_param_prefix)) + + def UpgradeToOAuthAccessToken(self, authorized_request_token=None, + request_url='%s/accounts/OAuthGetAccessToken' \ + % AUTH_SERVER_HOST, oauth_version='1.0', + oauth_verifier=None): + """Upgrades the authorized request token to an access token and returns it + + Args: + authorized_request_token: gdata.auth.OAuthToken (optional) OAuth request + token. If not specified, then the current token will be used if it is + of type <gdata.auth.OAuthToken>, else it is found by looking in the + token_store by looking for a token for the current scope. + request_url: Access token URL. The default is + 'https://www.google.com/accounts/OAuthGetAccessToken'. + oauth_version: str (default='1.0') oauth_version parameter. All other + 'oauth_' parameters are added by default. This parameter too, is + added by default but here you can override it's value. + oauth_verifier: str (optional) If present, it is assumed that the client + will use the OAuth v1.0a protocol which includes passing the + oauth_verifier (as returned by the SP) in the access token step. + + Returns: + Access token + + Raises: + NonOAuthToken if the user's authorized request token is not an OAuth + token or if an authorized request token was not available. + TokenUpgradeFailed if the server responded to the request with an + error. + """ + if (authorized_request_token and + not isinstance(authorized_request_token, gdata.auth.OAuthToken)): + raise NonOAuthToken + if not authorized_request_token: + if isinstance(self.current_token, gdata.auth.OAuthToken): + authorized_request_token = self.current_token + else: + current_scopes = lookup_scopes(self.service) + if current_scopes: + token = self.token_store.find_token(current_scopes[0]) + if isinstance(token, gdata.auth.OAuthToken): + authorized_request_token = token + if not authorized_request_token: + raise NonOAuthToken + access_token_url = gdata.auth.GenerateOAuthAccessTokenUrl( + authorized_request_token, + self._oauth_input_params, + access_token_url=request_url, + oauth_version=oauth_version, + oauth_verifier=oauth_verifier) + response = self.http_client.request('GET', str(access_token_url)) + if response.status == 200: + token = gdata.auth.OAuthTokenFromHttpBody(response.read()) + token.scopes = authorized_request_token.scopes + token.oauth_input_params = authorized_request_token.oauth_input_params + self.SetOAuthToken(token) + return token + else: + raise TokenUpgradeFailed({'status': response.status, + 'reason': 'Non 200 response on upgrade', + 'body': response.read()}) + + def RevokeOAuthToken(self, request_url='%s/accounts/AuthSubRevokeToken' % \ + AUTH_SERVER_HOST): + """Revokes an existing OAuth token. + + request_url: Token revoke URL. The default is + 'https://www.google.com/accounts/AuthSubRevokeToken'. + Raises: + NonOAuthToken if the user's auth token is not an OAuth token. + RevokingOAuthTokenFailed if request for revoking an OAuth token failed. + """ + scopes = lookup_scopes(self.service) + token = self.token_store.find_token(scopes[0]) + if not isinstance(token, gdata.auth.OAuthToken): + raise NonOAuthToken + + response = token.perform_request(self.http_client, 'GET', request_url, + headers={'Content-Type':'application/x-www-form-urlencoded'}) + if response.status == 200: + self.token_store.remove_token(token) + else: + raise RevokingOAuthTokenFailed + + def GetAuthSubToken(self): + """Returns the AuthSub token as a string. + + If the token is an gdta.auth.AuthSubToken, the Authorization Label + ("AuthSub token") is removed. + + This method examines the current_token to see if it is an AuthSubToken + or SecureAuthSubToken. If not, it searches the token_store for a token + which matches the current scope. + + The current scope is determined by the service name string member. + + Returns: + If the current_token is set to an AuthSubToken/SecureAuthSubToken, + return the token string. If there is no current_token, a token string + for a token which matches the service object's default scope is returned. + If there are no tokens valid for the scope, returns None. + """ + if isinstance(self.current_token, gdata.auth.AuthSubToken): + return self.current_token.get_token_string() + current_scopes = lookup_scopes(self.service) + if current_scopes: + token = self.token_store.find_token(current_scopes[0]) + if isinstance(token, gdata.auth.AuthSubToken): + return token.get_token_string() + else: + token = self.token_store.find_token(atom.token_store.SCOPE_ALL) + if isinstance(token, gdata.auth.ClientLoginToken): + return token.get_token_string() + return None + + def SetAuthSubToken(self, token, scopes=None, rsa_key=None): + """Sets the token sent in requests to an AuthSub token. + + Sets the current_token and attempts to add the token to the token_store. + + Only use this method if you have received a token from the AuthSub + service. The auth token is set automatically when UpgradeToSessionToken() + is used. See documentation for Google AuthSub here: + http://code.google.com/apis/accounts/AuthForWebApps.html + + Args: + token: gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken or string + The token returned by the AuthSub service. If the token is an + AuthSubToken or SecureAuthSubToken, the scope information stored in + the token is used. If the token is a string, the scopes parameter is + used to determine the valid scopes. + scopes: list of URLs for which the token is valid. This is only used + if the token parameter is a string. + rsa_key: string (optional) Private key required for RSA_SHA1 signature + method. This parameter is necessary if the token is a string + representing a secure token. + """ + if not isinstance(token, gdata.auth.AuthSubToken): + token_string = token + if rsa_key: + token = gdata.auth.SecureAuthSubToken(rsa_key) + else: + token = gdata.auth.AuthSubToken() + + token.set_token_string(token_string) + + # If no scopes were set for the token, use the scopes passed in, or + # try to determine the scopes based on the current service name. If + # all else fails, set the token to match all requests. + if not token.scopes: + if scopes is None: + scopes = lookup_scopes(self.service) + if scopes is None: + scopes = [atom.token_store.SCOPE_ALL] + token.scopes = scopes + if self.auto_set_current_token: + self.current_token = token + if self.auto_store_tokens: + self.token_store.add_token(token) + + def GetClientLoginToken(self): + """Returns the token string for the current token or a token matching the + service scope. + + If the current_token is a ClientLoginToken, the token string for + the current token is returned. If the current_token is not set, this method + searches for a token in the token_store which is valid for the service + object's current scope. + + The current scope is determined by the service name string member. + The token string is the end of the Authorization header, it doesn not + include the ClientLogin label. + """ + if isinstance(self.current_token, gdata.auth.ClientLoginToken): + return self.current_token.get_token_string() + current_scopes = lookup_scopes(self.service) + if current_scopes: + token = self.token_store.find_token(current_scopes[0]) + if isinstance(token, gdata.auth.ClientLoginToken): + return token.get_token_string() + else: + token = self.token_store.find_token(atom.token_store.SCOPE_ALL) + if isinstance(token, gdata.auth.ClientLoginToken): + return token.get_token_string() + return None + + def SetClientLoginToken(self, token, scopes=None): + """Sets the token sent in requests to a ClientLogin token. + + This method sets the current_token to a new ClientLoginToken and it + also attempts to add the ClientLoginToken to the token_store. + + Only use this method if you have received a token from the ClientLogin + service. The auth_token is set automatically when ProgrammaticLogin() + is used. See documentation for Google ClientLogin here: + http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html + + Args: + token: string or instance of a ClientLoginToken. + """ + if not isinstance(token, gdata.auth.ClientLoginToken): + token_string = token + token = gdata.auth.ClientLoginToken() + token.set_token_string(token_string) + + if not token.scopes: + if scopes is None: + scopes = lookup_scopes(self.service) + if scopes is None: + scopes = [atom.token_store.SCOPE_ALL] + token.scopes = scopes + if self.auto_set_current_token: + self.current_token = token + if self.auto_store_tokens: + self.token_store.add_token(token) + + # Private methods to create the source property. + def __GetSource(self): + return self.__source + + def __SetSource(self, new_source): + self.__source = new_source + # Update the UserAgent header to include the new application name. + self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % ( + self.__source,) + + source = property(__GetSource, __SetSource, + doc="""The source is the name of the application making the request. + It should be in the form company_id-app_name-app_version""") + + # Authentication operations + + def ProgrammaticLogin(self, captcha_token=None, captcha_response=None): + """Authenticates the user and sets the GData Auth token. + + Login retreives a temporary auth token which must be used with all + requests to GData services. The auth token is stored in the GData client + object. + + Login is also used to respond to a captcha challenge. If the user's login + attempt failed with a CaptchaRequired error, the user can respond by + calling Login with the captcha token and the answer to the challenge. + + Args: + captcha_token: string (optional) The identifier for the captcha challenge + which was presented to the user. + captcha_response: string (optional) The user's answer to the captch + challenge. + + Raises: + CaptchaRequired if the login service will require a captcha response + BadAuthentication if the login service rejected the username or password + Error if the login service responded with a 403 different from the above + """ + request_body = gdata.auth.generate_client_login_request_body(self.email, + self.password, self.service, self.source, self.account_type, + captcha_token, captcha_response) + + # If the user has defined their own authentication service URL, + # send the ClientLogin requests to this URL: + if not self.auth_service_url: + auth_request_url = AUTH_SERVER_HOST + '/accounts/ClientLogin' + else: + auth_request_url = self.auth_service_url + + auth_response = self.http_client.request('POST', auth_request_url, + data=request_body, + headers={'Content-Type':'application/x-www-form-urlencoded'}) + response_body = auth_response.read() + + if auth_response.status == 200: + # TODO: insert the token into the token_store directly. + self.SetClientLoginToken( + gdata.auth.get_client_login_token(response_body)) + self.__captcha_token = None + self.__captcha_url = None + + elif auth_response.status == 403: + # Examine each line to find the error type and the captcha token and + # captch URL if they are present. + captcha_parameters = gdata.auth.get_captcha_challenge(response_body, + captcha_base_url='%s/accounts/' % AUTH_SERVER_HOST) + if captcha_parameters: + self.__captcha_token = captcha_parameters['token'] + self.__captcha_url = captcha_parameters['url'] + raise CaptchaRequired, 'Captcha Required' + elif response_body.splitlines()[0] == 'Error=BadAuthentication': + self.__captcha_token = None + self.__captcha_url = None + raise BadAuthentication, 'Incorrect username or password' + else: + self.__captcha_token = None + self.__captcha_url = None + raise Error, 'Server responded with a 403 code' + elif auth_response.status == 302: + self.__captcha_token = None + self.__captcha_url = None + # 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 BadAuthenticationServiceURL, 'Server responded with a 302 code.' + + def ClientLogin(self, username, password, account_type=None, service=None, + auth_service_url=None, source=None, captcha_token=None, + captcha_response=None): + """Convenience method for authenticating using ProgrammaticLogin. + + Sets values for email, password, and other optional members. + + Args: + username: + password: + account_type: string (optional) + service: string (optional) + auth_service_url: string (optional) + captcha_token: string (optional) + captcha_response: string (optional) + """ + self.email = username + self.password = password + + if account_type: + self.account_type = account_type + if service: + self.service = service + if source: + self.source = source + if auth_service_url: + self.auth_service_url = auth_service_url + + self.ProgrammaticLogin(captcha_token, captcha_response) + + def GenerateAuthSubURL(self, next, scope, secure=False, session=True, + domain='default'): + """Generate a URL at which the user will login and be redirected back. + + Users enter their credentials on a Google login page and a token is sent + to the URL specified in next. See documentation for AuthSub login at: + http://code.google.com/apis/accounts/docs/AuthSub.html + + Args: + next: string The URL user will be sent to after logging in. + scope: string or list of strings. The URLs of the services to be + accessed. + secure: boolean (optional) Determines whether or not the issued token + is a secure token. + session: boolean (optional) Determines whether or not the issued token + can be upgraded to a session token. + """ + if not isinstance(scope, (list, tuple)): + scope = (scope,) + return gdata.auth.generate_auth_sub_url(next, scope, secure=secure, + session=session, + request_url='%s/accounts/AuthSubRequest' % AUTH_SERVER_HOST, + domain=domain) + + def UpgradeToSessionToken(self, token=None): + """Upgrades a single use AuthSub token to a session token. + + Args: + token: A gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken + (optional) which is good for a single use but can be upgraded + to a session token. If no token is passed in, the token + is found by looking in the token_store by looking for a token + for the current scope. + + Raises: + NonAuthSubToken if the user's auth token is not an AuthSub token + TokenUpgradeFailed if the server responded to the request with an + error. + """ + if token is None: + scopes = lookup_scopes(self.service) + if scopes: + token = self.token_store.find_token(scopes[0]) + else: + token = self.token_store.find_token(atom.token_store.SCOPE_ALL) + if not isinstance(token, gdata.auth.AuthSubToken): + raise NonAuthSubToken + + self.SetAuthSubToken(self.upgrade_to_session_token(token)) + + def upgrade_to_session_token(self, token): + """Upgrades a single use AuthSub token to a session token. + + Args: + token: A gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken + which is good for a single use but can be upgraded to a + session token. + + Returns: + The upgraded token as a gdata.auth.AuthSubToken object. + + Raises: + TokenUpgradeFailed if the server responded to the request with an + error. + """ + response = token.perform_request(self.http_client, 'GET', + AUTH_SERVER_HOST + '/accounts/AuthSubSessionToken', + headers={'Content-Type':'application/x-www-form-urlencoded'}) + response_body = response.read() + if response.status == 200: + token.set_token_string( + gdata.auth.token_from_http_body(response_body)) + return token + else: + raise TokenUpgradeFailed({'status': response.status, + 'reason': 'Non 200 response on upgrade', + 'body': response_body}) + + def RevokeAuthSubToken(self): + """Revokes an existing AuthSub token. + + Raises: + NonAuthSubToken if the user's auth token is not an AuthSub token + """ + scopes = lookup_scopes(self.service) + token = self.token_store.find_token(scopes[0]) + if not isinstance(token, gdata.auth.AuthSubToken): + raise NonAuthSubToken + + response = token.perform_request(self.http_client, 'GET', + AUTH_SERVER_HOST + '/accounts/AuthSubRevokeToken', + headers={'Content-Type':'application/x-www-form-urlencoded'}) + if response.status == 200: + self.token_store.remove_token(token) + + def AuthSubTokenInfo(self): + """Fetches the AuthSub token's metadata from the server. + + Raises: + NonAuthSubToken if the user's auth token is not an AuthSub token + """ + scopes = lookup_scopes(self.service) + token = self.token_store.find_token(scopes[0]) + if not isinstance(token, gdata.auth.AuthSubToken): + raise NonAuthSubToken + + response = token.perform_request(self.http_client, 'GET', + AUTH_SERVER_HOST + '/accounts/AuthSubTokenInfo', + headers={'Content-Type':'application/x-www-form-urlencoded'}) + result_body = response.read() + if response.status == 200: + return result_body + else: + raise RequestError, {'status': response.status, + 'body': result_body} + + def GetWithRetries(self, uri, extra_headers=None, redirects_remaining=4, + encoding='UTF-8', converter=None, num_retries=DEFAULT_NUM_RETRIES, + delay=DEFAULT_DELAY, backoff=DEFAULT_BACKOFF, logger=None): + """This is a wrapper method for Get with retrying capability. + + To avoid various errors while retrieving bulk entities by retrying + specified times. + + Note this method relies on the time module and so may not be usable + by default in Python2.2. + + Args: + num_retries: Integer; the retry count. + delay: Integer; the initial delay for retrying. + backoff: Integer; how much the delay should lengthen after each failure. + logger: An object which has a debug(str) method to receive logging + messages. Recommended that you pass in the logging module. + Raises: + ValueError if any of the parameters has an invalid value. + RanOutOfTries on failure after number of retries. + """ + # Moved import for time module inside this method since time is not a + # default module in Python2.2. This method will not be usable in + # Python2.2. + import time + if backoff <= 1: + raise ValueError("backoff must be greater than 1") + num_retries = int(num_retries) + + if num_retries < 0: + raise ValueError("num_retries must be 0 or greater") + + if delay <= 0: + raise ValueError("delay must be greater than 0") + + # Let's start + mtries, mdelay = num_retries, delay + while mtries > 0: + if mtries != num_retries: + if logger: + logger.debug("Retrying: %s" % uri) + try: + rv = self.Get(uri, extra_headers=extra_headers, + redirects_remaining=redirects_remaining, + encoding=encoding, converter=converter) + except SystemExit: + # Allow this error + raise + except RequestError, e: + # Error 500 is 'internal server error' and warrants a retry + # Error 503 is 'service unavailable' and warrants a retry + if e[0]['status'] not in [500, 503]: + raise e + # Else, fall through to the retry code... + except Exception, e: + if logger: + logger.debug(e) + # Fall through to the retry code... + else: + # This is the right path. + return rv + mtries -= 1 + time.sleep(mdelay) + mdelay *= backoff + raise RanOutOfTries('Ran out of tries.') + + # CRUD operations + def Get(self, uri, extra_headers=None, redirects_remaining=4, + encoding='UTF-8', converter=None): + """Query the GData API with the given URI + + The uri is the portion of the URI after the server value + (ex: www.google.com). + + 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: dictionary (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. + redirects_remaining: int (optional) Tracks the number of additional + redirects this method will allow. If the service object receives + a redirect and remaining is 0, it will not follow the redirect. + This was added to avoid infinite redirect loops. + encoding: string (optional) The character encoding for the server's + response. Default is UTF-8 + converter: func (optional) A function which will transform + the server's results before it is returned. Example: use + GDataFeedFromString to parse the server response as if it + were a GDataFeed. + + Returns: + If there is no ResultsTransformer specified in the call, a GDataFeed + or GDataEntry depending on which is sent from the server. If the + response is niether a feed or entry and there is no ResultsTransformer, + return a string. If there is a ResultsTransformer, the returned value + will be that of the ResultsTransformer function. + """ + + if extra_headers is None: + extra_headers = {} + + if self.__gsessionid is not None: + if uri.find('gsessionid=') < 0: + if uri.find('?') > -1: + uri += '&gsessionid=%s' % (self.__gsessionid,) + else: + uri += '?gsessionid=%s' % (self.__gsessionid,) + + server_response = self.request('GET', uri, + headers=extra_headers) + result_body = server_response.read() + + if server_response.status == 200: + if converter: + return converter(result_body) + # There was no ResultsTransformer specified, so try to convert the + # server's response into a GDataFeed. + feed = gdata.GDataFeedFromString(result_body) + if not feed: + # If conversion to a GDataFeed failed, try to convert the server's + # response to a GDataEntry. + entry = gdata.GDataEntryFromString(result_body) + if not entry: + # The server's response wasn't a feed, or an entry, so return the + # response body as a string. + return result_body + return entry + return feed + elif server_response.status == 302: + if redirects_remaining > 0: + location = (server_response.getheader('Location') + or server_response.getheader('location')) + if location is not None: + m = re.compile('[\?\&]gsessionid=(\w*\-)').search(location) + if m is not None: + self.__gsessionid = m.group(1) + return GDataService.Get(self, location, extra_headers, redirects_remaining - 1, + encoding=encoding, converter=converter) + else: + raise RequestError, {'status': server_response.status, + 'reason': '302 received without Location header', + 'body': result_body} + else: + raise RequestError, {'status': server_response.status, + 'reason': 'Redirect received, but redirects_remaining <= 0', + 'body': result_body} + else: + raise RequestError, {'status': server_response.status, + 'reason': server_response.reason, 'body': result_body} + + def GetMedia(self, uri, extra_headers=None): + """Returns a MediaSource containing media and its metadata from the given + URI string. + """ + response_handle = self.request('GET', uri, + headers=extra_headers) + return gdata.MediaSource(response_handle, response_handle.getheader( + 'Content-Type'), + response_handle.getheader('Content-Length')) + + def GetEntry(self, uri, extra_headers=None): + """Query the GData API with the given URI and receive an Entry. + + See also documentation for gdata.service.Get + + Args: + uri: string The query in the form of a URI. Example: + '/base/feeds/snippets?bq=digital+camera'. + extra_headers: dictionary (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. + + Returns: + A GDataEntry built from the XML in the server's response. + """ + + result = GDataService.Get(self, uri, extra_headers, + converter=atom.EntryFromString) + if isinstance(result, atom.Entry): + return result + else: + raise UnexpectedReturnType, 'Server did not send an entry' + + def GetFeed(self, uri, extra_headers=None, + converter=gdata.GDataFeedFromString): + """Query the GData API with the given URI and receive a Feed. + + See also documentation for gdata.service.Get + + Args: + uri: string The query in the form of a URI. Example: + '/base/feeds/snippets?bq=digital+camera'. + extra_headers: dictionary (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. + + Returns: + A GDataFeed built from the XML in the server's response. + """ + + result = GDataService.Get(self, uri, extra_headers, converter=converter) + if isinstance(result, atom.Feed): + return result + else: + raise UnexpectedReturnType, 'Server did not send a feed' + + def GetNext(self, feed): + """Requests the next 'page' of results in the feed. + + This method uses the feed's next link to request an additional feed + and uses the class of the feed to convert the results of the GET request. + + Args: + feed: atom.Feed or a subclass. The feed should contain a next link and + the type of the feed will be applied to the results from the + server. The new feed which is returned will be of the same class + as this feed which was passed in. + + Returns: + A new feed representing the next set of results in the server's feed. + The type of this feed will match that of the feed argument. + """ + next_link = feed.GetNextLink() + # Create a closure which will convert an XML string to the class of + # the feed object passed in. + def ConvertToFeedClass(xml_string): + return atom.CreateClassFromXMLString(feed.__class__, xml_string) + # Make a GET request on the next link and use the above closure for the + # converted which processes the XML string from the server. + if next_link and next_link.href: + return GDataService.Get(self, next_link.href, + converter=ConvertToFeedClass) + else: + return None + + def Post(self, data, uri, extra_headers=None, url_params=None, + escape_params=True, redirects_remaining=4, media_source=None, + converter=None): + """Insert or update data into a GData service at the given URI. + + Args: + data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry 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. + media_source: MediaSource (optional) Container for the media to be sent + along with the entry, if provided. + converter: func (optional) A function which will be executed on the + server's response. Often this is a function like + GDataEntryFromString which will parse the body of the server's + response and return a GDataEntry. + + Returns: + If the post succeeded, this method will return a GDataFeed, GDataEntry, + or the results of running converter on the server's result body (if + converter was specified). + """ + return GDataService.PostOrPut(self, 'POST', data, uri, + extra_headers=extra_headers, url_params=url_params, + escape_params=escape_params, redirects_remaining=redirects_remaining, + media_source=media_source, converter=converter) + + def PostOrPut(self, verb, data, uri, extra_headers=None, url_params=None, + escape_params=True, redirects_remaining=4, media_source=None, + converter=None): + """Insert data into a GData service at the given URI. + + Args: + verb: string, either 'POST' or 'PUT' + data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry 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. + media_source: MediaSource (optional) Container for the media to be sent + along with the entry, if provided. + converter: func (optional) A function which will be executed on the + server's response. Often this is a function like + GDataEntryFromString which will parse the body of the server's + response and return a GDataEntry. + + Returns: + If the post succeeded, this method will return a GDataFeed, GDataEntry, + or the results of running converter on the server's result body (if + converter was specified). + """ + if extra_headers is None: + extra_headers = {} + + if self.__gsessionid is not None: + if uri.find('gsessionid=') < 0: + if url_params is None: + url_params = {} + url_params['gsessionid'] = self.__gsessionid + + if data and media_source: + if ElementTree.iselement(data): + data_str = ElementTree.tostring(data) + else: + data_str = str(data) + + multipart = [] + multipart.append('Media multipart posting\r\n--END_OF_PART\r\n' + \ + 'Content-Type: application/atom+xml\r\n\r\n') + multipart.append('\r\n--END_OF_PART\r\nContent-Type: ' + \ + media_source.content_type+'\r\n\r\n') + multipart.append('\r\n--END_OF_PART--\r\n') + + extra_headers['MIME-version'] = '1.0' + extra_headers['Content-Length'] = str(len(multipart[0]) + + len(multipart[1]) + len(multipart[2]) + + len(data_str) + media_source.content_length) + + extra_headers['Content-Type'] = 'multipart/related; boundary=END_OF_PART' + server_response = self.request(verb, uri, + data=[multipart[0], data_str, multipart[1], media_source.file_handle, + multipart[2]], headers=extra_headers, url_params=url_params) + result_body = server_response.read() + + elif media_source or isinstance(data, gdata.MediaSource): + if isinstance(data, gdata.MediaSource): + media_source = data + extra_headers['Content-Length'] = str(media_source.content_length) + extra_headers['Content-Type'] = media_source.content_type + server_response = self.request(verb, uri, + data=media_source.file_handle, headers=extra_headers, + url_params=url_params) + result_body = server_response.read() + + else: + http_data = data + if 'Content-Type' not in extra_headers: + content_type = 'application/atom+xml' + extra_headers['Content-Type'] = content_type + server_response = self.request(verb, uri, data=http_data, + headers=extra_headers, url_params=url_params) + result_body = server_response.read() + + # Server returns 201 for most post requests, but when performing a batch + # request the server responds with a 200 on success. + if server_response.status == 201 or server_response.status == 200: + if converter: + return converter(result_body) + feed = gdata.GDataFeedFromString(result_body) + if not feed: + entry = gdata.GDataEntryFromString(result_body) + if not entry: + return result_body + return entry + return feed + elif server_response.status == 302: + if redirects_remaining > 0: + location = (server_response.getheader('Location') + or server_response.getheader('location')) + if location is not None: + m = re.compile('[\?\&]gsessionid=(\w*\-)').search(location) + if m is not None: + self.__gsessionid = m.group(1) + return GDataService.PostOrPut(self, verb, data, location, + extra_headers, url_params, escape_params, + redirects_remaining - 1, media_source, converter=converter) + else: + raise RequestError, {'status': server_response.status, + 'reason': '302 received without Location header', + 'body': result_body} + else: + raise RequestError, {'status': server_response.status, + 'reason': 'Redirect received, but redirects_remaining <= 0', + 'body': result_body} + else: + raise RequestError, {'status': server_response.status, + 'reason': server_response.reason, 'body': result_body} + + def Put(self, data, uri, extra_headers=None, url_params=None, + escape_params=True, redirects_remaining=3, media_source=None, + converter=None): + """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. + converter: func (optional) A function which will be executed on the + server's response. Often this is a function like + GDataEntryFromString which will parse the body of the server's + response and return a GDataEntry. + + Returns: + If the put succeeded, this method will return a GDataFeed, GDataEntry, + or the results of running converter on the server's result body (if + converter was specified). + """ + return GDataService.PostOrPut(self, 'PUT', data, uri, + extra_headers=extra_headers, url_params=url_params, + escape_params=escape_params, redirects_remaining=redirects_remaining, + media_source=media_source, converter=converter) + + def Delete(self, uri, extra_headers=None, url_params=None, + escape_params=True, redirects_remaining=4): + """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: + True if the entry was deleted. + """ + if extra_headers is None: + extra_headers = {} + + if self.__gsessionid is not None: + if uri.find('gsessionid=') < 0: + if url_params is None: + url_params = {} + url_params['gsessionid'] = self.__gsessionid + + server_response = self.request('DELETE', uri, + headers=extra_headers, url_params=url_params) + result_body = server_response.read() + + if server_response.status == 200: + return True + elif server_response.status == 302: + if redirects_remaining > 0: + location = (server_response.getheader('Location') + or server_response.getheader('location')) + if location is not None: + m = re.compile('[\?\&]gsessionid=(\w*\-)').search(location) + if m is not None: + self.__gsessionid = m.group(1) + return GDataService.Delete(self, location, extra_headers, + url_params, escape_params, redirects_remaining - 1) + else: + raise RequestError, {'status': server_response.status, + 'reason': '302 received without Location header', + 'body': result_body} + else: + raise RequestError, {'status': server_response.status, + 'reason': 'Redirect received, but redirects_remaining <= 0', + 'body': result_body} + else: + raise RequestError, {'status': server_response.status, + 'reason': server_response.reason, 'body': result_body} + + +def ExtractToken(url, scopes_included_in_next=True): + """Gets the AuthSub token from the current page's URL. + + Designed to be used on the URL that the browser is sent to after the user + authorizes this application at the page given by GenerateAuthSubRequestUrl. + + Args: + url: The current page's URL. It should contain the token as a URL + parameter. Example: 'http://example.com/?...&token=abcd435' + scopes_included_in_next: If True, this function looks for a scope value + associated with the token. The scope is a URL parameter with the + key set to SCOPE_URL_PARAM_NAME. This parameter should be present + if the AuthSub request URL was generated using + GenerateAuthSubRequestUrl with include_scope_in_next set to True. + + Returns: + A tuple containing the token string and a list of scope strings for which + this token should be valid. If the scope was not included in the URL, the + tuple will contain (token, None). + """ + parsed = urlparse.urlparse(url) + token = gdata.auth.AuthSubTokenFromUrl(parsed[4]) + scopes = '' + if scopes_included_in_next: + for pair in parsed[4].split('&'): + if pair.startswith('%s=' % SCOPE_URL_PARAM_NAME): + scopes = urllib.unquote_plus(pair.split('=')[1]) + return (token, scopes.split(' ')) + + +def GenerateAuthSubRequestUrl(next, scopes, hd='default', secure=False, + session=True, request_url='https://www.google.com/accounts/AuthSubRequest', + include_scopes_in_next=True): + """Creates a URL to request an AuthSub token to access Google services. + + For more details on AuthSub, see the documentation here: + http://code.google.com/apis/accounts/docs/AuthSub.html + + Args: + next: The URL where the browser should be sent after the user authorizes + the application. This page is responsible for receiving the token + which is embeded in the URL as a parameter. + scopes: The base URL to which access will be granted. Example: + 'http://www.google.com/calendar/feeds' will grant access to all + URLs in the Google Calendar data API. If you would like a token for + multiple scopes, pass in a list of URL strings. + hd: The domain to which the user's account belongs. This is set to the + domain name if you are using Google Apps. Example: 'example.org' + Defaults to 'default' + secure: If set to True, all requests should be signed. The default is + False. + session: If set to True, the token received by the 'next' URL can be + upgraded to a multiuse session token. If session is set to False, the + token may only be used once and cannot be upgraded. Default is True. + request_url: The base of the URL to which the user will be sent to + authorize this application to access their data. The default is + 'https://www.google.com/accounts/AuthSubRequest'. + include_scopes_in_next: Boolean if set to true, the 'next' parameter will + be modified to include the requested scope as a URL parameter. The + key for the next's scope parameter will be SCOPE_URL_PARAM_NAME. The + benefit of including the scope URL as a parameter to the next URL, is + that the page which receives the AuthSub token will be able to tell + which URLs the token grants access to. + + Returns: + A URL string to which the browser should be sent. + """ + if isinstance(scopes, list): + scope = ' '.join(scopes) + else: + scope = scopes + if include_scopes_in_next: + if next.find('?') > -1: + next += '&%s' % urllib.urlencode({SCOPE_URL_PARAM_NAME:scope}) + else: + next += '?%s' % urllib.urlencode({SCOPE_URL_PARAM_NAME:scope}) + return gdata.auth.GenerateAuthSubUrl(next=next, scope=scope, secure=secure, + session=session, request_url=request_url, domain=hd) + + +class Query(dict): + """Constructs a query URL to be used in GET requests + + Url parameters are created by adding key-value pairs to this object as a + dict. For example, to add &max-results=25 to the URL do + my_query['max-results'] = 25 + + Category queries are created by adding category strings to the categories + member. All items in the categories list will be concatenated with the / + symbol (symbolizing a category x AND y restriction). If you would like to OR + 2 categories, append them as one string with a | between the categories. + For example, do query.categories.append('Fritz|Laurie') to create a query + like this feed/-/Fritz%7CLaurie . This query will look for results in both + categories. + """ + + def __init__(self, feed=None, text_query=None, params=None, + categories=None): + """Constructor for Query + + Args: + feed: str (optional) The path for the feed (Examples: + '/base/feeds/snippets' or 'calendar/feeds/jo@gmail.com/private/full' + text_query: str (optional) The contents of the q query parameter. The + contents of the text_query are URL escaped upon conversion to a URI. + params: dict (optional) Parameter value string pairs which become URL + params when translated to a URI. These parameters are added to the + query's items (key-value pairs). + categories: list (optional) List of category strings which should be + included as query categories. See + http://code.google.com/apis/gdata/reference.html#Queries for + details. If you want to get results from category A or B (both + categories), specify a single list item 'A|B'. + """ + + self.feed = feed + self.categories = [] + if text_query: + self.text_query = text_query + if isinstance(params, dict): + for param in params: + self[param] = params[param] + if isinstance(categories, list): + for category in categories: + self.categories.append(category) + + def _GetTextQuery(self): + if 'q' in self.keys(): + return self['q'] + else: + return None + + def _SetTextQuery(self, query): + self['q'] = query + + text_query = property(_GetTextQuery, _SetTextQuery, + doc="""The feed query's q parameter""") + + def _GetAuthor(self): + if 'author' in self.keys(): + return self['author'] + else: + return None + + def _SetAuthor(self, query): + self['author'] = query + + author = property(_GetAuthor, _SetAuthor, + doc="""The feed query's author parameter""") + + def _GetAlt(self): + if 'alt' in self.keys(): + return self['alt'] + else: + return None + + def _SetAlt(self, query): + self['alt'] = query + + alt = property(_GetAlt, _SetAlt, + doc="""The feed query's alt parameter""") + + def _GetUpdatedMin(self): + if 'updated-min' in self.keys(): + return self['updated-min'] + else: + return None + + def _SetUpdatedMin(self, query): + self['updated-min'] = query + + updated_min = property(_GetUpdatedMin, _SetUpdatedMin, + doc="""The feed query's updated-min parameter""") + + def _GetUpdatedMax(self): + if 'updated-max' in self.keys(): + return self['updated-max'] + else: + return None + + def _SetUpdatedMax(self, query): + self['updated-max'] = query + + updated_max = property(_GetUpdatedMax, _SetUpdatedMax, + doc="""The feed query's updated-max parameter""") + + def _GetPublishedMin(self): + if 'published-min' in self.keys(): + return self['published-min'] + else: + return None + + def _SetPublishedMin(self, query): + self['published-min'] = query + + published_min = property(_GetPublishedMin, _SetPublishedMin, + doc="""The feed query's published-min parameter""") + + def _GetPublishedMax(self): + if 'published-max' in self.keys(): + return self['published-max'] + else: + return None + + def _SetPublishedMax(self, query): + self['published-max'] = query + + published_max = property(_GetPublishedMax, _SetPublishedMax, + doc="""The feed query's published-max parameter""") + + def _GetStartIndex(self): + if 'start-index' in self.keys(): + return self['start-index'] + else: + return None + + def _SetStartIndex(self, query): + if not isinstance(query, str): + query = str(query) + self['start-index'] = query + + start_index = property(_GetStartIndex, _SetStartIndex, + doc="""The feed query's start-index parameter""") + + def _GetMaxResults(self): + if 'max-results' in self.keys(): + return self['max-results'] + else: + return None + + def _SetMaxResults(self, query): + if not isinstance(query, str): + query = str(query) + self['max-results'] = query + + max_results = property(_GetMaxResults, _SetMaxResults, + doc="""The feed query's max-results parameter""") + + def _GetOrderBy(self): + if 'orderby' in self.keys(): + return self['orderby'] + else: + return None + + def _SetOrderBy(self, query): + self['orderby'] = query + + orderby = property(_GetOrderBy, _SetOrderBy, + doc="""The feed query's orderby parameter""") + + def ToUri(self): + q_feed = self.feed or '' + category_string = '/'.join( + [urllib.quote_plus(c) for c in self.categories]) + # Add categories to the feed if there are any. + if len(self.categories) > 0: + q_feed = q_feed + '/-/' + category_string + return atom.service.BuildUri(q_feed, self) + + def __str__(self): + return self.ToUri() diff --git a/patches/gdata/sites/__init__.py b/patches/gdata/sites/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/patches/gdata/sites/client.py b/patches/gdata/sites/client.py new file mode 100755 index 0000000..2915fc5 --- /dev/null +++ b/patches/gdata/sites/client.py @@ -0,0 +1,462 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""SitesClient extends gdata.client.GDClient to streamline Sites API calls.""" + + +__author__ = 'e.bidelman (Eric Bidelman)' + +import atom.data +import gdata.client +import gdata.sites.data +import gdata.gauth + + +# Feed URI templates +CONTENT_FEED_TEMPLATE = '/feeds/content/%s/%s/' +REVISION_FEED_TEMPLATE = '/feeds/revision/%s/%s/' +ACTIVITY_FEED_TEMPLATE = '/feeds/activity/%s/%s/' +SITE_FEED_TEMPLATE = '/feeds/site/%s/' +ACL_FEED_TEMPLATE = '/feeds/acl/site/%s/%s/' + + +class SitesClient(gdata.client.GDClient): + + """Client extension for the Google Sites API service.""" + + host = 'sites.google.com' # default server for the API + domain = 'site' # default site domain name + api_version = '1.1' # default major version for the service. + auth_service = 'jotspot' + auth_scopes = gdata.gauth.AUTH_SCOPES['jotspot'] + ssl = True + + def __init__(self, site=None, domain=None, auth_token=None, **kwargs): + """Constructs a new client for the Sites API. + + Args: + site: string (optional) Name (webspace) of the Google Site + domain: string (optional) Domain of the (Google Apps hosted) Site. + If no domain is given, the Site is assumed to be a consumer Google + Site, in which case the value 'site' is used. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: The other parameters to pass to gdata.client.GDClient + constructor. + """ + gdata.client.GDClient.__init__(self, auth_token=auth_token, **kwargs) + self.site = site + if domain is not None: + self.domain = domain + + def __make_kind_category(self, label): + if label is None: + return None + return atom.data.Category( + scheme=gdata.sites.data.SITES_KIND_SCHEME, + term='%s#%s' % (gdata.sites.data.SITES_NAMESPACE, label), label=label) + + __MakeKindCategory = __make_kind_category + + def __upload(self, entry, media_source, auth_token=None, **kwargs): + """Uploads an attachment file to the Sites API. + + Args: + entry: gdata.sites.data.ContentEntry The Atom XML to include. + media_source: gdata.data.MediaSource The file payload to be uploaded. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to gdata.client.post(). + + Returns: + The created entry. + """ + uri = self.make_content_feed_uri() + return self.post(entry, uri, media_source=media_source, + auth_token=auth_token, **kwargs) + + def _get_file_content(self, uri): + """Fetches the file content from the specified URI. + + Args: + uri: string The full URL to fetch the file contents from. + + Returns: + The binary file content. + + Raises: + gdata.client.RequestError: on error response from server. + """ + server_response = self.request('GET', uri) + if server_response.status != 200: + raise gdata.client.RequestError, {'status': server_response.status, + 'reason': server_response.reason, + 'body': server_response.read()} + return server_response.read() + + _GetFileContent = _get_file_content + + def make_content_feed_uri(self): + return CONTENT_FEED_TEMPLATE % (self.domain, self.site) + + MakeContentFeedUri = make_content_feed_uri + + def make_revision_feed_uri(self): + return REVISION_FEED_TEMPLATE % (self.domain, self.site) + + MakeRevisionFeedUri = make_revision_feed_uri + + def make_activity_feed_uri(self): + return ACTIVITY_FEED_TEMPLATE % (self.domain, self.site) + + MakeActivityFeedUri = make_activity_feed_uri + + def make_site_feed_uri(self, site_name=None): + if site_name is not None: + return (SITE_FEED_TEMPLATE % self.domain) + site_name + else: + return SITE_FEED_TEMPLATE % self.domain + + MakeSiteFeedUri = make_site_feed_uri + + def make_acl_feed_uri(self): + return ACL_FEED_TEMPLATE % (self.domain, self.site) + + MakeAclFeedUri = make_acl_feed_uri + + def get_content_feed(self, uri=None, auth_token=None, **kwargs): + """Retrieves the content feed containing the current state of site. + + Args: + uri: string (optional) A full URI to query the Content feed with. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + gdata.sites.data.ContentFeed + """ + if uri is None: + uri = self.make_content_feed_uri() + return self.get_feed(uri, desired_class=gdata.sites.data.ContentFeed, + auth_token=auth_token, **kwargs) + + GetContentFeed = get_content_feed + + def get_revision_feed(self, entry_or_uri_or_id, auth_token=None, **kwargs): + """Retrieves the revision feed containing the revision history for a node. + + Args: + entry_or_uri_or_id: string or gdata.sites.data.ContentEntry A full URI, + content entry node ID, or a content entry object of the entry to + retrieve revision information for. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + gdata.sites.data.RevisionFeed + """ + uri = self.make_revision_feed_uri() + if isinstance(entry_or_uri_or_id, gdata.sites.data.ContentEntry): + uri = entry_or_uri_or_id.FindRevisionLink() + elif entry_or_uri_or_id.find('/') == -1: + uri += entry_or_uri_or_id + else: + uri = entry_or_uri_or_id + return self.get_feed(uri, desired_class=gdata.sites.data.RevisionFeed, + auth_token=auth_token, **kwargs) + + GetRevisionFeed = get_revision_feed + + def get_activity_feed(self, uri=None, auth_token=None, **kwargs): + """Retrieves the activity feed containing recent Site activity. + + Args: + uri: string (optional) A full URI to query the Activity feed. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + gdata.sites.data.ActivityFeed + """ + if uri is None: + uri = self.make_activity_feed_uri() + return self.get_feed(uri, desired_class=gdata.sites.data.ActivityFeed, + auth_token=auth_token, **kwargs) + + GetActivityFeed = get_activity_feed + + def get_site_feed(self, uri=None, auth_token=None, **kwargs): + """Retrieves the site feed containing a list of sites a user has access to. + + Args: + uri: string (optional) A full URI to query the site feed. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + gdata.sites.data.SiteFeed + """ + if uri is None: + uri = self.make_site_feed_uri() + return self.get_feed(uri, desired_class=gdata.sites.data.SiteFeed, + auth_token=auth_token, **kwargs) + + GetSiteFeed = get_site_feed + + def get_acl_feed(self, uri=None, auth_token=None, **kwargs): + """Retrieves the acl feed containing a site's sharing permissions. + + Args: + uri: string (optional) A full URI to query the acl feed. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.get_feed(). + + Returns: + gdata.sites.data.AclFeed + """ + if uri is None: + uri = self.make_acl_feed_uri() + return self.get_feed(uri, desired_class=gdata.sites.data.AclFeed, + auth_token=auth_token, **kwargs) + + GetAclFeed = get_acl_feed + + def create_site(self, title, description=None, source_site=None, + theme=None, uri=None, auth_token=None, **kwargs): + """Creates a new Google Site. + + Note: This feature is only available to Google Apps domains. + + Args: + title: string Title for the site. + description: string (optional) A description/summary for the site. + source_site: string (optional) The site feed URI of the site to copy. + This parameter should only be specified when copying a site. + theme: string (optional) The name of the theme to create the site with. + uri: string (optional) A full site feed URI to override where the site + is created/copied. By default, the site will be created under + the currently set domain (e.g. self.domain). + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to gdata.client.post(). + + Returns: + gdata.sites.data.SiteEntry of the created site. + """ + new_entry = gdata.sites.data.SiteEntry(title=atom.data.Title(text=title)) + + if description is not None: + new_entry.summary = gdata.sites.data.Summary(text=description) + + # Add the source link if we're making a copy of a site. + if source_site is not None: + source_link = atom.data.Link(rel=gdata.sites.data.SITES_SOURCE_LINK_REL, + type='application/atom+xml', + href=source_site) + new_entry.link.append(source_link) + + if theme is not None: + new_entry.theme = gdata.sites.data.Theme(text=theme) + + if uri is None: + uri = self.make_site_feed_uri() + + return self.post(new_entry, uri, auth_token=auth_token, **kwargs) + + CreateSite = create_site + + def create_page(self, kind, title, html='', page_name=None, parent=None, + auth_token=None, **kwargs): + """Creates a new page (specified by kind) on a Google Site. + + Args: + kind: string The type of page/item to create. For example, webpage, + listpage, comment, announcementspage, filecabinet, etc. The full list + of supported kinds can be found in gdata.sites.gdata.SUPPORT_KINDS. + title: string Title for the page. + html: string (optional) XHTML for the page's content body. + page_name: string (optional) The URL page name to set. If not set, the + title will be normalized and used as the page's URL path. + parent: string or gdata.sites.data.ContentEntry (optional) The parent + entry or parent link url to create the page under. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to gdata.client.post(). + + Returns: + gdata.sites.data.ContentEntry of the created page. + """ + new_entry = gdata.sites.data.ContentEntry( + title=atom.data.Title(text=title), kind=kind, + content=gdata.sites.data.Content(text=html)) + + if page_name is not None: + new_entry.page_name = gdata.sites.data.PageName(text=page_name) + + # Add parent link to entry if it should be uploaded as a subpage. + if isinstance(parent, gdata.sites.data.ContentEntry): + parent_link = atom.data.Link(rel=gdata.sites.data.SITES_PARENT_LINK_REL, + type='application/atom+xml', + href=parent.GetSelfLink().href) + new_entry.link.append(parent_link) + elif parent is not None: + parent_link = atom.data.Link(rel=gdata.sites.data.SITES_PARENT_LINK_REL, + type='application/atom+xml', + href=parent) + new_entry.link.append(parent_link) + + return self.post(new_entry, self.make_content_feed_uri(), + auth_token=auth_token, **kwargs) + + CreatePage = create_page + + def create_webattachment(self, src, content_type, title, parent, + description=None, auth_token=None, **kwargs): + """Creates a new webattachment within a filecabinet. + + Args: + src: string The url of the web attachment. + content_type: string The MIME type of the web attachment. + title: string The title to name the web attachment. + parent: string or gdata.sites.data.ContentEntry (optional) The + parent entry or url of the filecabinet to create the attachment under. + description: string (optional) A summary/description for the attachment. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to gdata.client.post(). + + Returns: + gdata.sites.data.ContentEntry of the created page. + """ + new_entry = gdata.sites.data.ContentEntry( + title=atom.data.Title(text=title), kind='webattachment', + content=gdata.sites.data.Content(src=src, type=content_type)) + + if isinstance(parent, gdata.sites.data.ContentEntry): + link = atom.data.Link(rel=gdata.sites.data.SITES_PARENT_LINK_REL, + type='application/atom+xml', + href=parent.GetSelfLink().href) + elif parent is not None: + link = atom.data.Link(rel=gdata.sites.data.SITES_PARENT_LINK_REL, + type='application/atom+xml', href=parent) + + new_entry.link.append(link) + + # Add file decription if it was specified + if description is not None: + new_entry.summary = gdata.sites.data.Summary(type='text', + text=description) + + return self.post(new_entry, self.make_content_feed_uri(), + auth_token=auth_token, **kwargs) + + CreateWebAttachment = create_webattachment + + def upload_attachment(self, file_handle, parent, content_type=None, + title=None, description=None, folder_name=None, + auth_token=None, **kwargs): + """Uploads an attachment to a parent page. + + Args: + file_handle: MediaSource or string A gdata.data.MediaSource object + containing the file to be uploaded or the full path name to the + file on disk. + parent: gdata.sites.data.ContentEntry or string The parent page to + upload the file to or the full URI of the entry's self link. + content_type: string (optional) The MIME type of the file + (e.g 'application/pdf'). This should be provided if file is not a + MediaSource object. + title: string (optional) The title to name the attachment. If not + included, the filepath or media source's filename is used. + description: string (optional) A summary/description for the attachment. + folder_name: string (optional) The name of an existing folder to upload + the attachment to. This only applies when the parent parameter points + to a filecabinet entry. + auth_token: (optional) gdata.gauth.ClientLoginToken, AuthSubToken, or + OAuthToken which authorizes this client to edit the user's data. + kwargs: Other parameters to pass to self.__upload(). + + Returns: + A gdata.sites.data.ContentEntry containing information about the created + attachment. + """ + if isinstance(parent, gdata.sites.data.ContentEntry): + link = atom.data.Link(rel=gdata.sites.data.SITES_PARENT_LINK_REL, + type='application/atom+xml', + href=parent.GetSelfLink().href) + else: + link = atom.data.Link(rel=gdata.sites.data.SITES_PARENT_LINK_REL, + type='application/atom+xml', + href=parent) + + if not isinstance(file_handle, gdata.data.MediaSource): + ms = gdata.data.MediaSource(file_path=file_handle, + content_type=content_type) + else: + ms = file_handle + + # If no title specified, use the file name + if title is None: + title = ms.file_name + + new_entry = gdata.sites.data.ContentEntry(kind='attachment') + new_entry.title = atom.data.Title(text=title) + new_entry.link.append(link) + + # Add file decription if it was specified + if description is not None: + new_entry.summary = gdata.sites.data.Summary(type='text', + text=description) + + # Upload the attachment to a filecabinet folder? + if parent.Kind() == 'filecabinet' and folder_name is not None: + folder_category = atom.data.Category( + scheme=gdata.sites.data.FOLDER_KIND_TERM, term=folder_name) + new_entry.category.append(folder_category) + + return self.__upload(new_entry, ms, auth_token=auth_token, **kwargs) + + UploadAttachment = upload_attachment + + def download_attachment(self, uri_or_entry, file_path): + """Downloads an attachment file to disk. + + Args: + uri_or_entry: string The full URL to download the file from. + file_path: string The full path to save the file to. + + Raises: + gdata.client.RequestError: on error response from server. + """ + uri = uri_or_entry + if isinstance(uri_or_entry, gdata.sites.data.ContentEntry): + uri = uri_or_entry.content.src + + f = open(file_path, 'wb') + try: + f.write(self._get_file_content(uri)) + except gdata.client.RequestError, e: + f.close() + raise e + f.flush() + f.close() + + DownloadAttachment = download_attachment diff --git a/patches/gdata/sites/data.py b/patches/gdata/sites/data.py new file mode 100644 index 0000000..dc8dfb2 --- /dev/null +++ b/patches/gdata/sites/data.py @@ -0,0 +1,376 @@ +#!/usr/bin/python +# +# Copyright 2009 Google Inc. All Rights Reserved. +# +# 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. + +"""Data model classes for parsing and generating XML for the Sites Data API.""" + +__author__ = 'e.bidelman (Eric Bidelman)' + + +import atom.core +import atom.data +import gdata.acl.data +import gdata.data + +# XML Namespaces used in Google Sites entities. +SITES_NAMESPACE = 'http://schemas.google.com/sites/2008' +SITES_TEMPLATE = '{http://schemas.google.com/sites/2008}%s' +SPREADSHEETS_NAMESPACE = 'http://schemas.google.com/spreadsheets/2006' +SPREADSHEETS_TEMPLATE = '{http://schemas.google.com/spreadsheets/2006}%s' +DC_TERMS_TEMPLATE = '{http://purl.org/dc/terms}%s' +THR_TERMS_TEMPLATE = '{http://purl.org/syndication/thread/1.0}%s' +XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml' +XHTML_TEMPLATE = '{http://www.w3.org/1999/xhtml}%s' + +SITES_PARENT_LINK_REL = SITES_NAMESPACE + '#parent' +SITES_REVISION_LINK_REL = SITES_NAMESPACE + '#revision' +SITES_SOURCE_LINK_REL = SITES_NAMESPACE + '#source' + +SITES_KIND_SCHEME = 'http://schemas.google.com/g/2005#kind' +ANNOUNCEMENT_KIND_TERM = SITES_NAMESPACE + '#announcement' +ANNOUNCEMENT_PAGE_KIND_TERM = SITES_NAMESPACE + '#announcementspage' +ATTACHMENT_KIND_TERM = SITES_NAMESPACE + '#attachment' +COMMENT_KIND_TERM = SITES_NAMESPACE + '#comment' +FILECABINET_KIND_TERM = SITES_NAMESPACE + '#filecabinet' +LISTITEM_KIND_TERM = SITES_NAMESPACE + '#listitem' +LISTPAGE_KIND_TERM = SITES_NAMESPACE + '#listpage' +WEBPAGE_KIND_TERM = SITES_NAMESPACE + '#webpage' +WEBATTACHMENT_KIND_TERM = SITES_NAMESPACE + '#webattachment' +FOLDER_KIND_TERM = SITES_NAMESPACE + '#folder' + +SUPPORT_KINDS = [ + 'announcement', 'announcementspage', 'attachment', 'comment', 'filecabinet', + 'listitem', 'listpage', 'webpage', 'webattachment' + ] + + +class Revision(atom.core.XmlElement): + """Google Sites <sites:revision>.""" + _qname = SITES_TEMPLATE % 'revision' + + +class PageName(atom.core.XmlElement): + """Google Sites <sites:pageName>.""" + _qname = SITES_TEMPLATE % 'pageName' + + +class SiteName(atom.core.XmlElement): + """Google Sites <sites:siteName>.""" + _qname = SITES_TEMPLATE % 'siteName' + + +class Theme(atom.core.XmlElement): + """Google Sites <sites:theme>.""" + _qname = SITES_TEMPLATE % 'theme' + + +class Deleted(atom.core.XmlElement): + """Google Sites <gd:deleted>.""" + _qname = gdata.data.GDATA_TEMPLATE % 'deleted' + + +class Publisher(atom.core.XmlElement): + """Google Sites <dc:pulisher>.""" + _qname = DC_TERMS_TEMPLATE % 'publisher' + + +class Worksheet(atom.core.XmlElement): + """Google Sites List Page <gs:worksheet>.""" + + _qname = SPREADSHEETS_TEMPLATE % 'worksheet' + name = 'name' + + +class Header(atom.core.XmlElement): + """Google Sites List Page <gs:header>.""" + + _qname = SPREADSHEETS_TEMPLATE % 'header' + row = 'row' + + +class Column(atom.core.XmlElement): + """Google Sites List Page <gs:column>.""" + + _qname = SPREADSHEETS_TEMPLATE % 'column' + index = 'index' + name = 'name' + + +class Data(atom.core.XmlElement): + """Google Sites List Page <gs:data>.""" + + _qname = SPREADSHEETS_TEMPLATE % 'data' + startRow = 'startRow' + column = [Column] + + +class Field(atom.core.XmlElement): + """Google Sites List Item <gs:field>.""" + + _qname = SPREADSHEETS_TEMPLATE % 'field' + index = 'index' + name = 'name' + + +class InReplyTo(atom.core.XmlElement): + """Google Sites List Item <thr:in-reply-to>.""" + + _qname = THR_TERMS_TEMPLATE % 'in-reply-to' + href = 'href' + ref = 'ref' + source = 'source' + type = 'type' + + +class Content(atom.data.Content): + """Google Sites version of <atom:content> that encapsulates XHTML.""" + + def __init__(self, html=None, type=None, **kwargs): + if type is None and html: + type = 'xhtml' + super(Content, self).__init__(type=type, **kwargs) + if html is not None: + self.html = html + + def _get_html(self): + if self.children: + return self.children[0] + else: + return '' + + def _set_html(self, html): + if not html: + self.children = [] + return + + if type(html) == str: + html = atom.core.parse(html) + if not html.namespace: + html.namespace = XHTML_NAMESPACE + + self.children = [html] + + html = property(_get_html, _set_html) + + +class Summary(atom.data.Summary): + """Google Sites version of <atom:summary>.""" + + def __init__(self, html=None, type=None, text=None, **kwargs): + if type is None and html: + type = 'xhtml' + + super(Summary, self).__init__(type=type, text=text, **kwargs) + if html is not None: + self.html = html + + def _get_html(self): + if self.children: + return self.children[0] + else: + return '' + + def _set_html(self, html): + if not html: + self.children = [] + return + + if type(html) == str: + html = atom.core.parse(html) + if not html.namespace: + html.namespace = XHTML_NAMESPACE + + self.children = [html] + + html = property(_get_html, _set_html) + + +class BaseSiteEntry(gdata.data.GDEntry): + """Google Sites Entry.""" + + def __init__(self, kind=None, **kwargs): + super(BaseSiteEntry, self).__init__(**kwargs) + if kind is not None: + self.category.append( + atom.data.Category(scheme=SITES_KIND_SCHEME, + term='%s#%s' % (SITES_NAMESPACE, kind), + label=kind)) + + def __find_category_scheme(self, scheme): + for category in self.category: + if category.scheme == scheme: + return category + return None + + def kind(self): + kind = self.__find_category_scheme(SITES_KIND_SCHEME) + if kind is not None: + return kind.term[len(SITES_NAMESPACE) + 1:] + else: + return None + + Kind = kind + + def get_node_id(self): + return self.id.text[self.id.text.rfind('/') + 1:] + + GetNodeId = get_node_id + + def find_parent_link(self): + return self.find_url(SITES_PARENT_LINK_REL) + + FindParentLink = find_parent_link + + def is_deleted(self): + return self.deleted is not None + + IsDeleted = is_deleted + + +class ContentEntry(BaseSiteEntry): + """Google Sites Content Entry.""" + content = Content + deleted = Deleted + publisher = Publisher + in_reply_to = InReplyTo + worksheet = Worksheet + header = Header + data = Data + field = [Field] + revision = Revision + page_name = PageName + feed_link = gdata.data.FeedLink + + def find_revison_link(self): + return self.find_url(SITES_REVISION_LINK_REL) + + FindRevisionLink = find_revison_link + + +class ContentFeed(gdata.data.GDFeed): + """Google Sites Content Feed. + + The Content feed is a feed containing the current, editable site content. + """ + entry = [ContentEntry] + + def __get_entry_type(self, kind): + matches = [] + for entry in self.entry: + if entry.Kind() == kind: + matches.append(entry) + return matches + + def get_announcements(self): + return self.__get_entry_type('announcement') + + GetAnnouncements = get_announcements + + def get_announcement_pages(self): + return self.__get_entry_type('announcementspage') + + GetAnnouncementPages = get_announcement_pages + + def get_attachments(self): + return self.__get_entry_type('attachment') + + GetAttachments = get_attachments + + def get_comments(self): + return self.__get_entry_type('comment') + + GetComments = get_comments + + def get_file_cabinets(self): + return self.__get_entry_type('filecabinet') + + GetFileCabinets = get_file_cabinets + + def get_list_items(self): + return self.__get_entry_type('listitem') + + GetListItems = get_list_items + + def get_list_pages(self): + return self.__get_entry_type('listpage') + + GetListPages = get_list_pages + + def get_webpages(self): + return self.__get_entry_type('webpage') + + GetWebpages = get_webpages + + def get_webattachments(self): + return self.__get_entry_type('webattachment') + + GetWebattachments = get_webattachments + + +class ActivityEntry(BaseSiteEntry): + """Google Sites Activity Entry.""" + summary = Summary + + +class ActivityFeed(gdata.data.GDFeed): + """Google Sites Activity Feed. + + The Activity feed is a feed containing recent Site activity. + """ + entry = [ActivityEntry] + + +class RevisionEntry(BaseSiteEntry): + """Google Sites Revision Entry.""" + content = Content + + +class RevisionFeed(gdata.data.GDFeed): + """Google Sites Revision Feed. + + The Activity feed is a feed containing recent Site activity. + """ + entry = [RevisionEntry] + + +class SiteEntry(gdata.data.GDEntry): + """Google Sites Site Feed Entry.""" + site_name = SiteName + theme = Theme + + def find_source_link(self): + return self.find_url(SITES_SOURCE_LINK_REL) + + FindSourceLink = find_source_link + + +class SiteFeed(gdata.data.GDFeed): + """Google Sites Site Feed. + + The Site feed can be used to list a user's sites and create new sites. + """ + entry = [SiteEntry] + + +class AclEntry(gdata.acl.data.AclEntry): + """Google Sites ACL Entry.""" + + +class AclFeed(gdata.acl.data.AclFeed): + """Google Sites ACL Feed. + + The ACL feed can be used to modify the sharing permissions of a Site. + """ + entry = [AclEntry] diff --git a/patches/gdata/spreadsheet/__init__.py b/patches/gdata/spreadsheet/__init__.py new file mode 100755 index 0000000..e9a0fb3 --- /dev/null +++ b/patches/gdata/spreadsheet/__init__.py @@ -0,0 +1,474 @@ +#!/usr/bin/python +# +# Copyright (C) 2007 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. + +"""Contains extensions to Atom objects used with Google Spreadsheets. +""" + +__author__ = 'api.laurabeth@gmail.com (Laura Beth Lincoln)' + + +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 +import gdata +import re +import string + + +# XML namespaces which are often used in Google Spreadsheets entities. +GSPREADSHEETS_NAMESPACE = 'http://schemas.google.com/spreadsheets/2006' +GSPREADSHEETS_TEMPLATE = '{http://schemas.google.com/spreadsheets/2006}%s' + +GSPREADSHEETS_EXTENDED_NAMESPACE = ('http://schemas.google.com/spreadsheets' + '/2006/extended') +GSPREADSHEETS_EXTENDED_TEMPLATE = ('{http://schemas.google.com/spreadsheets' + '/2006/extended}%s') + + +class ColCount(atom.AtomBase): + """The Google Spreadsheets colCount element """ + + _tag = 'colCount' + _namespace = GSPREADSHEETS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ColCountFromString(xml_string): + return atom.CreateClassFromXMLString(ColCount, xml_string) + + +class RowCount(atom.AtomBase): + """The Google Spreadsheets rowCount element """ + + _tag = 'rowCount' + _namespace = GSPREADSHEETS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +def RowCountFromString(xml_string): + return atom.CreateClassFromXMLString(RowCount, xml_string) + + +class Cell(atom.AtomBase): + """The Google Spreadsheets cell element """ + + _tag = 'cell' + _namespace = GSPREADSHEETS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['row'] = 'row' + _attributes['col'] = 'col' + _attributes['inputValue'] = 'inputValue' + _attributes['numericValue'] = 'numericValue' + + def __init__(self, text=None, row=None, col=None, inputValue=None, + numericValue=None, extension_elements=None, extension_attributes=None): + self.text = text + self.row = row + self.col = col + self.inputValue = inputValue + self.numericValue = numericValue + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def CellFromString(xml_string): + return atom.CreateClassFromXMLString(Cell, xml_string) + + +class Custom(atom.AtomBase): + """The Google Spreadsheets custom element""" + + _namespace = GSPREADSHEETS_EXTENDED_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, column=None, text=None, extension_elements=None, + extension_attributes=None): + self.column = column # The name of the column + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + def _BecomeChildElement(self, tree): + new_child = ElementTree.Element('') + tree.append(new_child) + new_child.tag = '{%s}%s' % (self.__class__._namespace, + self.column) + self._AddMembersToElementTree(new_child) + + def _ToElementTree(self): + new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace, + self.column)) + self._AddMembersToElementTree(new_tree) + return new_tree + + def _HarvestElementTree(self, tree): + namespace_uri, local_tag = string.split(tree.tag[1:], "}", 1) + self.column = local_tag + # Fill in the instance members from the contents of the XML tree. + for child in tree: + self._ConvertElementTreeToMember(child) + for attribute, value in tree.attrib.iteritems(): + self._ConvertElementAttributeToMember(attribute, value) + self.text = tree.text + + +def CustomFromString(xml_string): + element_tree = ElementTree.fromstring(xml_string) + return _CustomFromElementTree(element_tree) + + +def _CustomFromElementTree(element_tree): + namespace_uri, local_tag = string.split(element_tree.tag[1:], "}", 1) + if namespace_uri == GSPREADSHEETS_EXTENDED_NAMESPACE: + new_custom = Custom() + new_custom._HarvestElementTree(element_tree) + new_custom.column = local_tag + return new_custom + return None + + + + + +class SpreadsheetsSpreadsheet(gdata.GDataEntry): + """A Google Spreadsheets flavor of a Spreadsheet Atom Entry """ + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, title=None, control=None, updated=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.control = control + self.title = title + self.updated = updated + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SpreadsheetsSpreadsheetFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsSpreadsheet, + xml_string) + + +class SpreadsheetsWorksheet(gdata.GDataEntry): + """A Google Spreadsheets flavor of a Worksheet Atom Entry """ + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}rowCount' % GSPREADSHEETS_NAMESPACE] = ('row_count', + RowCount) + _children['{%s}colCount' % GSPREADSHEETS_NAMESPACE] = ('col_count', + ColCount) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, title=None, control=None, updated=None, + row_count=None, col_count=None, text=None, extension_elements=None, + extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.control = control + self.title = title + self.updated = updated + self.row_count = row_count + self.col_count = col_count + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SpreadsheetsWorksheetFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsWorksheet, + xml_string) + + +class SpreadsheetsCell(gdata.BatchEntry): + """A Google Spreadsheets flavor of a Cell Atom Entry """ + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.BatchEntry._children.copy() + _attributes = gdata.BatchEntry._attributes.copy() + _children['{%s}cell' % GSPREADSHEETS_NAMESPACE] = ('cell', Cell) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, title=None, control=None, updated=None, + cell=None, batch_operation=None, batch_id=None, batch_status=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.control = control + self.title = title + self.batch_operation = batch_operation + self.batch_id = batch_id + self.batch_status = batch_status + self.updated = updated + self.cell = cell + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SpreadsheetsCellFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsCell, + xml_string) + + +class SpreadsheetsList(gdata.GDataEntry): + """A Google Spreadsheets flavor of a List Atom Entry """ + + _tag = 'entry' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, title=None, control=None, updated=None, + custom=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.control = control + self.title = title + self.updated = updated + self.custom = custom or {} + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + # We need to overwrite _ConvertElementTreeToMember to add special logic to + # convert custom attributes to members + def _ConvertElementTreeToMember(self, child_tree): + # Find the element's tag in this class's list of child members + if self.__class__._children.has_key(child_tree.tag): + member_name = self.__class__._children[child_tree.tag][0] + member_class = self.__class__._children[child_tree.tag][1] + # If the class member is supposed to contain a list, make sure the + # matching member is set to a list, then append the new member + # instance to the list. + if isinstance(member_class, list): + if getattr(self, member_name) is None: + setattr(self, member_name, []) + getattr(self, member_name).append(atom._CreateClassFromElementTree( + member_class[0], child_tree)) + else: + setattr(self, member_name, + atom._CreateClassFromElementTree(member_class, child_tree)) + elif child_tree.tag.find('{%s}' % GSPREADSHEETS_EXTENDED_NAMESPACE) == 0: + # If this is in the custom namespace, make add it to the custom dict. + name = child_tree.tag[child_tree.tag.index('}')+1:] + custom = _CustomFromElementTree(child_tree) + if custom: + self.custom[name] = custom + else: + atom.ExtensionContainer._ConvertElementTreeToMember(self, child_tree) + + # We need to overwtite _AddMembersToElementTree to add special logic to + # convert custom members to XML nodes. + def _AddMembersToElementTree(self, tree): + # Convert the members of this class which are XML child nodes. + # This uses the class's _children dictionary to find the members which + # should become XML child nodes. + member_node_names = [values[0] for tag, values in + self.__class__._children.iteritems()] + for member_name in member_node_names: + member = getattr(self, member_name) + if member is None: + pass + elif isinstance(member, list): + for instance in member: + instance._BecomeChildElement(tree) + else: + member._BecomeChildElement(tree) + # Convert the members of this class which are XML attributes. + for xml_attribute, member_name in self.__class__._attributes.iteritems(): + member = getattr(self, member_name) + if member is not None: + tree.attrib[xml_attribute] = member + # Convert all special custom item attributes to nodes + for name, custom in self.custom.iteritems(): + custom._BecomeChildElement(tree) + # Lastly, call the ExtensionContainers's _AddMembersToElementTree to + # convert any extension attributes. + atom.ExtensionContainer._AddMembersToElementTree(self, tree) + + +def SpreadsheetsListFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsList, + xml_string) + element_tree = ElementTree.fromstring(xml_string) + return _SpreadsheetsListFromElementTree(element_tree) + + +class SpreadsheetsSpreadsheetsFeed(gdata.GDataFeed): + """A feed containing Google Spreadsheets Spreadsheets""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [SpreadsheetsSpreadsheet]) + + +def SpreadsheetsSpreadsheetsFeedFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsSpreadsheetsFeed, + xml_string) + + +class SpreadsheetsWorksheetsFeed(gdata.GDataFeed): + """A feed containing Google Spreadsheets Spreadsheets""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [SpreadsheetsWorksheet]) + + +def SpreadsheetsWorksheetsFeedFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsWorksheetsFeed, + xml_string) + + +class SpreadsheetsCellsFeed(gdata.BatchFeed): + """A feed containing Google Spreadsheets Cells""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.BatchFeed._children.copy() + _attributes = gdata.BatchFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [SpreadsheetsCell]) + _children['{%s}rowCount' % GSPREADSHEETS_NAMESPACE] = ('row_count', + RowCount) + _children['{%s}colCount' % GSPREADSHEETS_NAMESPACE] = ('col_count', + ColCount) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, + entry=None, total_results=None, start_index=None, + items_per_page=None, extension_elements=None, + extension_attributes=None, text=None, row_count=None, + col_count=None, interrupted=None): + gdata.BatchFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, + start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text, interrupted=interrupted) + self.row_count = row_count + self.col_count = col_count + + def GetBatchLink(self): + for link in self.link: + if link.rel == 'http://schemas.google.com/g/2005#batch': + return link + return None + + +def SpreadsheetsCellsFeedFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsCellsFeed, + xml_string) + + +class SpreadsheetsListFeed(gdata.GDataFeed): + """A feed containing Google Spreadsheets Spreadsheets""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [SpreadsheetsList]) + + +def SpreadsheetsListFeedFromString(xml_string): + return atom.CreateClassFromXMLString(SpreadsheetsListFeed, + xml_string) diff --git a/patches/gdata/spreadsheet/service.py b/patches/gdata/spreadsheet/service.py new file mode 100755 index 0000000..66c82ce --- /dev/null +++ b/patches/gdata/spreadsheet/service.py @@ -0,0 +1,484 @@ +#!/usr/bin/python +# +# Copyright (C) 2007 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. + +"""SpreadsheetsService extends the GDataService to streamline Google +Spreadsheets operations. + + SpreadsheetService: Provides methods to query feeds and manipulate items. + Extends GDataService. + + DictionaryToParamList: Function which converts a dictionary into a list of + URL arguments (represented as strings). This is a + utility function used in CRUD operations. +""" + +__author__ = 'api.laurabeth@gmail.com (Laura Beth Lincoln)' + + +import gdata +import atom.service +import gdata.service +import gdata.spreadsheet +import atom + + +class Error(Exception): + """Base class for exceptions in this module.""" + pass + + +class RequestError(Error): + pass + + +class SpreadsheetsService(gdata.service.GDataService): + """Client for the Google Spreadsheets service.""" + + def __init__(self, email=None, password=None, source=None, + server='spreadsheets.google.com', additional_headers=None, + **kwargs): + """Creates a client for the Google Spreadsheets service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'spreadsheets.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='wise', source=source, + server=server, additional_headers=additional_headers, **kwargs) + + def GetSpreadsheetsFeed(self, key=None, query=None, visibility='private', + projection='full'): + """Gets a spreadsheets feed or a specific entry if a key is defined + Args: + key: string (optional) The spreadsheet key defined in /ccc?key= + query: DocumentQuery (optional) Query parameters + + Returns: + If there is no key, then a SpreadsheetsSpreadsheetsFeed. + If there is a key, then a SpreadsheetsSpreadsheet. + """ + + uri = ('https://%s/feeds/spreadsheets/%s/%s' + % (self.server, visibility, projection)) + + if key is not None: + uri = '%s/%s' % (uri, key) + + if query != None: + query.feed = uri + uri = query.ToUri() + + if key: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsSpreadsheetFromString) + else: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsSpreadsheetsFeedFromString) + + def GetWorksheetsFeed(self, key, wksht_id=None, query=None, + visibility='private', projection='full'): + """Gets a worksheets feed or a specific entry if a wksht is defined + Args: + key: string The spreadsheet key defined in /ccc?key= + wksht_id: string (optional) The id for a specific worksheet entry + query: DocumentQuery (optional) Query parameters + + Returns: + If there is no wksht_id, then a SpreadsheetsWorksheetsFeed. + If there is a wksht_id, then a SpreadsheetsWorksheet. + """ + + uri = ('https://%s/feeds/worksheets/%s/%s/%s' + % (self.server, key, visibility, projection)) + + if wksht_id != None: + uri = '%s/%s' % (uri, wksht_id) + + if query != None: + query.feed = uri + uri = query.ToUri() + + if wksht_id: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsWorksheetFromString) + else: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsWorksheetsFeedFromString) + + def AddWorksheet(self, title, row_count, col_count, key): + """Creates a new worksheet in the desired spreadsheet. + + The new worksheet is appended to the end of the list of worksheets. The + new worksheet will only have the available number of columns and cells + specified. + + Args: + title: str The title which will be displayed in the list of worksheets. + row_count: int or str The number of rows in the new worksheet. + col_count: int or str The number of columns in the new worksheet. + key: str The spreadsheet key to the spreadsheet to which the new + worksheet should be added. + + Returns: + A SpreadsheetsWorksheet if the new worksheet was created succesfully. + """ + new_worksheet = gdata.spreadsheet.SpreadsheetsWorksheet( + title=atom.Title(text=title), + row_count=gdata.spreadsheet.RowCount(text=str(row_count)), + col_count=gdata.spreadsheet.ColCount(text=str(col_count))) + return self.Post(new_worksheet, + 'https://%s/feeds/worksheets/%s/private/full' % (self.server, key), + converter=gdata.spreadsheet.SpreadsheetsWorksheetFromString) + + def UpdateWorksheet(self, worksheet_entry, url=None): + """Changes the size and/or title of the desired worksheet. + + Args: + worksheet_entry: SpreadsheetWorksheet The new contents of the + worksheet. + url: str (optional) The URL to which the edited worksheet entry should + be sent. If the url is None, the edit URL from the worksheet will + be used. + + Returns: + A SpreadsheetsWorksheet with the new information about the worksheet. + """ + target_url = url or worksheet_entry.GetEditLink().href + return self.Put(worksheet_entry, target_url, + converter=gdata.spreadsheet.SpreadsheetsWorksheetFromString) + + def DeleteWorksheet(self, worksheet_entry=None, url=None): + """Removes the desired worksheet from the spreadsheet + + Args: + worksheet_entry: SpreadsheetWorksheet (optional) The worksheet to + be deleted. If this is none, then the DELETE reqest is sent to + the url specified in the url parameter. + url: str (optaional) The URL to which the DELETE request should be + sent. If left as None, the worksheet's edit URL is used. + + Returns: + True if the worksheet was deleted successfully. + """ + if url: + target_url = url + else: + target_url = worksheet_entry.GetEditLink().href + return self.Delete(target_url) + + def GetCellsFeed(self, key, wksht_id='default', cell=None, query=None, + visibility='private', projection='full'): + """Gets a cells feed or a specific entry if a cell is defined + Args: + key: string The spreadsheet key defined in /ccc?key= + wksht_id: string The id for a specific worksheet entry + cell: string (optional) The R1C1 address of the cell + query: DocumentQuery (optional) Query parameters + + Returns: + If there is no cell, then a SpreadsheetsCellsFeed. + If there is a cell, then a SpreadsheetsCell. + """ + + uri = ('https://%s/feeds/cells/%s/%s/%s/%s' + % (self.server, key, wksht_id, visibility, projection)) + + if cell != None: + uri = '%s/%s' % (uri, cell) + + if query != None: + query.feed = uri + uri = query.ToUri() + + if cell: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsCellFromString) + else: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsCellsFeedFromString) + + def GetListFeed(self, key, wksht_id='default', row_id=None, query=None, + visibility='private', projection='full'): + """Gets a list feed or a specific entry if a row_id is defined + Args: + key: string The spreadsheet key defined in /ccc?key= + wksht_id: string The id for a specific worksheet entry + row_id: string (optional) The row_id of a row in the list + query: DocumentQuery (optional) Query parameters + + Returns: + If there is no row_id, then a SpreadsheetsListFeed. + If there is a row_id, then a SpreadsheetsList. + """ + + uri = ('https://%s/feeds/list/%s/%s/%s/%s' + % (self.server, key, wksht_id, visibility, projection)) + + if row_id is not None: + uri = '%s/%s' % (uri, row_id) + + if query is not None: + query.feed = uri + uri = query.ToUri() + + if row_id: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsListFromString) + else: + return self.Get(uri, + converter=gdata.spreadsheet.SpreadsheetsListFeedFromString) + + def UpdateCell(self, row, col, inputValue, key, wksht_id='default'): + """Updates an existing cell. + + Args: + row: int The row the cell to be editted is in + col: int The column the cell to be editted is in + inputValue: str the new value of the cell + key: str The key of the spreadsheet in which this cell resides. + wksht_id: str The ID of the worksheet which holds this cell. + + Returns: + The updated cell entry + """ + row = str(row) + col = str(col) + # make the new cell + new_cell = gdata.spreadsheet.Cell(row=row, col=col, inputValue=inputValue) + # get the edit uri and PUT + cell = 'R%sC%s' % (row, col) + entry = self.GetCellsFeed(key, wksht_id, cell) + for a_link in entry.link: + if a_link.rel == 'edit': + entry.cell = new_cell + return self.Put(entry, a_link.href, + converter=gdata.spreadsheet.SpreadsheetsCellFromString) + + def _GenerateCellsBatchUrl(self, spreadsheet_key, worksheet_id): + return ('https://spreadsheets.google.com/feeds/cells/%s/%s/' + 'private/full/batch' % (spreadsheet_key, worksheet_id)) + + def ExecuteBatch(self, batch_feed, url=None, spreadsheet_key=None, + worksheet_id=None, + converter=gdata.spreadsheet.SpreadsheetsCellsFeedFromString): + """Sends a batch request feed to the server. + + The batch request needs to be sent to the batch URL for a particular + worksheet. You can specify the worksheet by providing the spreadsheet_key + and worksheet_id, or by sending the URL from the cells feed's batch link. + + Args: + batch_feed: gdata.spreadsheet.SpreadsheetsCellFeed A feed containing + BatchEntry elements which contain the desired CRUD operation and + any necessary data to modify a cell. + url: str (optional) The batch URL for the cells feed to which these + changes should be applied. This can be found by calling + cells_feed.GetBatchLink().href. + spreadsheet_key: str (optional) Used to generate the batch request URL + if the url argument is None. If using the spreadsheet key to + generate the URL, the worksheet id is also required. + worksheet_id: str (optional) Used if the url is not provided, it is + oart of the batch feed target URL. This is used with the spreadsheet + key. + converter: Function (optional) Function to be executed on the server's + response. This function should take one string as a parameter. The + default value is SpreadsheetsCellsFeedFromString which will turn the result + into a gdata.spreadsheet.SpreadsheetsCellsFeed object. + + Returns: + A gdata.BatchFeed containing the results. + """ + + if url is None: + url = self._GenerateCellsBatchUrl(spreadsheet_key, worksheet_id) + return self.Post(batch_feed, url, converter=converter) + + def InsertRow(self, row_data, key, wksht_id='default'): + """Inserts a new row with the provided data + + Args: + uri: string The post uri of the list feed + row_data: dict A dictionary of column header to row data + + Returns: + The inserted row + """ + new_entry = gdata.spreadsheet.SpreadsheetsList() + for k, v in row_data.iteritems(): + new_custom = gdata.spreadsheet.Custom() + new_custom.column = k + new_custom.text = v + new_entry.custom[new_custom.column] = new_custom + # Generate the post URL for the worksheet which will receive the new entry. + post_url = 'https://spreadsheets.google.com/feeds/list/%s/%s/private/full'%( + key, wksht_id) + return self.Post(new_entry, post_url, + converter=gdata.spreadsheet.SpreadsheetsListFromString) + + def UpdateRow(self, entry, new_row_data): + """Updates a row with the provided data + + If you want to add additional information to a row, it is often + easier to change the values in entry.custom, then use the Put + method instead of UpdateRow. This UpdateRow method will replace + the contents of the row with new_row_data - it will change all columns + not just the columns specified in the new_row_data dict. + + Args: + entry: gdata.spreadsheet.SpreadsheetsList The entry to be updated + new_row_data: dict A dictionary of column header to row data + + Returns: + The updated row + """ + entry.custom = {} + for k, v in new_row_data.iteritems(): + new_custom = gdata.spreadsheet.Custom() + new_custom.column = k + new_custom.text = v + entry.custom[k] = new_custom + for a_link in entry.link: + if a_link.rel == 'edit': + return self.Put(entry, a_link.href, + converter=gdata.spreadsheet.SpreadsheetsListFromString) + + def DeleteRow(self, entry): + """Deletes a row, the provided entry + + Args: + entry: gdata.spreadsheet.SpreadsheetsList The row to be deleted + + Returns: + The delete response + """ + for a_link in entry.link: + if a_link.rel == 'edit': + return self.Delete(a_link.href) + + +class DocumentQuery(gdata.service.Query): + + def _GetTitleQuery(self): + return self['title'] + + def _SetTitleQuery(self, document_query): + self['title'] = document_query + + title = property(_GetTitleQuery, _SetTitleQuery, + doc="""The title query parameter""") + + def _GetTitleExactQuery(self): + return self['title-exact'] + + def _SetTitleExactQuery(self, document_query): + self['title-exact'] = document_query + + title_exact = property(_GetTitleExactQuery, _SetTitleExactQuery, + doc="""The title-exact query parameter""") + + +class CellQuery(gdata.service.Query): + + def _GetMinRowQuery(self): + return self['min-row'] + + def _SetMinRowQuery(self, cell_query): + self['min-row'] = cell_query + + min_row = property(_GetMinRowQuery, _SetMinRowQuery, + doc="""The min-row query parameter""") + + def _GetMaxRowQuery(self): + return self['max-row'] + + def _SetMaxRowQuery(self, cell_query): + self['max-row'] = cell_query + + max_row = property(_GetMaxRowQuery, _SetMaxRowQuery, + doc="""The max-row query parameter""") + + def _GetMinColQuery(self): + return self['min-col'] + + def _SetMinColQuery(self, cell_query): + self['min-col'] = cell_query + + min_col = property(_GetMinColQuery, _SetMinColQuery, + doc="""The min-col query parameter""") + + def _GetMaxColQuery(self): + return self['max-col'] + + def _SetMaxColQuery(self, cell_query): + self['max-col'] = cell_query + + max_col = property(_GetMaxColQuery, _SetMaxColQuery, + doc="""The max-col query parameter""") + + def _GetRangeQuery(self): + return self['range'] + + def _SetRangeQuery(self, cell_query): + self['range'] = cell_query + + range = property(_GetRangeQuery, _SetRangeQuery, + doc="""The range query parameter""") + + def _GetReturnEmptyQuery(self): + return self['return-empty'] + + def _SetReturnEmptyQuery(self, cell_query): + self['return-empty'] = cell_query + + return_empty = property(_GetReturnEmptyQuery, _SetReturnEmptyQuery, + doc="""The return-empty query parameter""") + + +class ListQuery(gdata.service.Query): + + def _GetSpreadsheetQuery(self): + return self['sq'] + + def _SetSpreadsheetQuery(self, list_query): + self['sq'] = list_query + + sq = property(_GetSpreadsheetQuery, _SetSpreadsheetQuery, + doc="""The sq query parameter""") + + def _GetOrderByQuery(self): + return self['orderby'] + + def _SetOrderByQuery(self, list_query): + self['orderby'] = list_query + + orderby = property(_GetOrderByQuery, _SetOrderByQuery, + doc="""The orderby query parameter""") + + def _GetReverseQuery(self): + return self['reverse'] + + def _SetReverseQuery(self, list_query): + self['reverse'] = list_query + + reverse = property(_GetReverseQuery, _SetReverseQuery, + doc="""The reverse query parameter""") diff --git a/patches/gdata/spreadsheet/text_db.py b/patches/gdata/spreadsheet/text_db.py new file mode 100644 index 0000000..a8de546 --- /dev/null +++ b/patches/gdata/spreadsheet/text_db.py @@ -0,0 +1,559 @@ +#!/usr/bin/python +# +# Copyright Google 2007-2008, all rights reserved. +# +# 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. + + +import StringIO +import gdata +import gdata.service +import gdata.spreadsheet +import gdata.spreadsheet.service +import gdata.docs +import gdata.docs.service + + +"""Make the Google Documents API feel more like using a database. + +This module contains a client and other classes which make working with the +Google Documents List Data API and the Google Spreadsheets Data API look a +bit more like working with a heirarchical database. Using the DatabaseClient, +you can create or find spreadsheets and use them like a database, with +worksheets representing tables and rows representing records. + +Example Usage: +# Create a new database, a new table, and add records. +client = gdata.spreadsheet.text_db.DatabaseClient(username='jo@example.com', + password='12345') +database = client.CreateDatabase('My Text Database') +table = database.CreateTable('addresses', ['name','email', + 'phonenumber', 'mailingaddress']) +record = table.AddRecord({'name':'Bob', 'email':'bob@example.com', + 'phonenumber':'555-555-1234', 'mailingaddress':'900 Imaginary St.'}) + +# Edit a record +record.content['email'] = 'bob2@example.com' +record.Push() + +# Delete a table +table.Delete + +Warnings: +Care should be exercised when using this module on spreadsheets +which contain formulas. This module treats all rows as containing text and +updating a row will overwrite any formula with the output of the formula. +The intended use case is to allow easy storage of text data in a spreadsheet. + + Error: Domain specific extension of Exception. + BadCredentials: Error raised is username or password was incorrect. + CaptchaRequired: Raised if a login attempt failed and a CAPTCHA challenge + was issued. + DatabaseClient: Communicates with Google Docs APIs servers. + Database: Represents a spreadsheet and interacts with tables. + Table: Represents a worksheet and interacts with records. + RecordResultSet: A list of records in a table. + Record: Represents a row in a worksheet allows manipulation of text data. +""" + + +__author__ = 'api.jscudder (Jeffrey Scudder)' + + +class Error(Exception): + pass + + +class BadCredentials(Error): + pass + + +class CaptchaRequired(Error): + pass + + +class DatabaseClient(object): + """Allows creation and finding of Google Spreadsheets databases. + + The DatabaseClient simplifies the process of creating and finding Google + Spreadsheets and will talk to both the Google Spreadsheets API and the + Google Documents List API. + """ + + def __init__(self, username=None, password=None): + """Constructor for a Database Client. + + If the username and password are present, the constructor will contact + the Google servers to authenticate. + + Args: + username: str (optional) Example: jo@example.com + password: str (optional) + """ + self.__docs_client = gdata.docs.service.DocsService() + self.__spreadsheets_client = ( + gdata.spreadsheet.service.SpreadsheetsService()) + self.SetCredentials(username, password) + + def SetCredentials(self, username, password): + """Attempts to log in to Google APIs using the provided credentials. + + If the username or password are None, the client will not request auth + tokens. + + Args: + username: str (optional) Example: jo@example.com + password: str (optional) + """ + self.__docs_client.email = username + self.__docs_client.password = password + self.__spreadsheets_client.email = username + self.__spreadsheets_client.password = password + if username and password: + try: + self.__docs_client.ProgrammaticLogin() + self.__spreadsheets_client.ProgrammaticLogin() + except gdata.service.CaptchaRequired: + raise CaptchaRequired('Please visit https://www.google.com/accounts/' + 'DisplayUnlockCaptcha to unlock your account.') + except gdata.service.BadAuthentication: + raise BadCredentials('Username or password incorrect.') + + def CreateDatabase(self, name): + """Creates a new Google Spreadsheet with the desired name. + + Args: + name: str The title for the spreadsheet. + + Returns: + A Database instance representing the new spreadsheet. + """ + # Create a Google Spreadsheet to form the foundation of this database. + # Spreadsheet is created by uploading a file to the Google Documents + # List API. + virtual_csv_file = StringIO.StringIO(',,,') + virtual_media_source = gdata.MediaSource(file_handle=virtual_csv_file, content_type='text/csv', content_length=3) + db_entry = self.__docs_client.UploadSpreadsheet(virtual_media_source, name) + return Database(spreadsheet_entry=db_entry, database_client=self) + + def GetDatabases(self, spreadsheet_key=None, name=None): + """Finds spreadsheets which have the unique key or title. + + If querying on the spreadsheet_key there will be at most one result, but + searching by name could yield multiple results. + + Args: + spreadsheet_key: str The unique key for the spreadsheet, this + usually in the the form 'pk23...We' or 'o23...423.12,,,3'. + name: str The title of the spreadsheets. + + Returns: + A list of Database objects representing the desired spreadsheets. + """ + if spreadsheet_key: + db_entry = self.__docs_client.GetDocumentListEntry( + r'/feeds/documents/private/full/spreadsheet%3A' + spreadsheet_key) + return [Database(spreadsheet_entry=db_entry, database_client=self)] + else: + title_query = gdata.docs.service.DocumentQuery() + title_query['title'] = name + db_feed = self.__docs_client.QueryDocumentListFeed(title_query.ToUri()) + matching_databases = [] + for entry in db_feed.entry: + matching_databases.append(Database(spreadsheet_entry=entry, + database_client=self)) + return matching_databases + + def _GetDocsClient(self): + return self.__docs_client + + def _GetSpreadsheetsClient(self): + return self.__spreadsheets_client + + +class Database(object): + """Provides interface to find and create tables. + + The database represents a Google Spreadsheet. + """ + + def __init__(self, spreadsheet_entry=None, database_client=None): + """Constructor for a database object. + + Args: + spreadsheet_entry: gdata.docs.DocumentListEntry The + Atom entry which represents the Google Spreadsheet. The + spreadsheet's key is extracted from the entry and stored as a + member. + database_client: DatabaseClient A client which can talk to the + Google Spreadsheets servers to perform operations on worksheets + within this spreadsheet. + """ + self.entry = spreadsheet_entry + if self.entry: + id_parts = spreadsheet_entry.id.text.split('/') + self.spreadsheet_key = id_parts[-1].replace('spreadsheet%3A', '') + self.client = database_client + + def CreateTable(self, name, fields=None): + """Add a new worksheet to this spreadsheet and fill in column names. + + Args: + name: str The title of the new worksheet. + fields: list of strings The column names which are placed in the + first row of this worksheet. These names are converted into XML + tags by the server. To avoid changes during the translation + process I recommend using all lowercase alphabetic names. For + example ['somelongname', 'theothername'] + + Returns: + Table representing the newly created worksheet. + """ + worksheet = self.client._GetSpreadsheetsClient().AddWorksheet(title=name, + row_count=1, col_count=len(fields), key=self.spreadsheet_key) + return Table(name=name, worksheet_entry=worksheet, + database_client=self.client, + spreadsheet_key=self.spreadsheet_key, fields=fields) + + def GetTables(self, worksheet_id=None, name=None): + """Searches for a worksheet with the specified ID or name. + + The list of results should have one table at most, or no results + if the id or name were not found. + + Args: + worksheet_id: str The ID of the worksheet, example: 'od6' + name: str The title of the worksheet. + + Returns: + A list of length 0 or 1 containing the desired Table. A list is returned + to make this method feel like GetDatabases and GetRecords. + """ + if worksheet_id: + worksheet_entry = self.client._GetSpreadsheetsClient().GetWorksheetsFeed( + self.spreadsheet_key, wksht_id=worksheet_id) + return [Table(name=worksheet_entry.title.text, + worksheet_entry=worksheet_entry, database_client=self.client, + spreadsheet_key=self.spreadsheet_key)] + else: + matching_tables = [] + query = None + if name: + query = gdata.spreadsheet.service.DocumentQuery() + query.title = name + + worksheet_feed = self.client._GetSpreadsheetsClient().GetWorksheetsFeed( + self.spreadsheet_key, query=query) + for entry in worksheet_feed.entry: + matching_tables.append(Table(name=entry.title.text, + worksheet_entry=entry, database_client=self.client, + spreadsheet_key=self.spreadsheet_key)) + return matching_tables + + def Delete(self): + """Deletes the entire database spreadsheet from Google Spreadsheets.""" + entry = self.client._GetDocsClient().Get( + r'http://docs.google.com/feeds/documents/private/full/spreadsheet%3A' + + self.spreadsheet_key) + self.client._GetDocsClient().Delete(entry.GetEditLink().href) + + +class Table(object): + + def __init__(self, name=None, worksheet_entry=None, database_client=None, + spreadsheet_key=None, fields=None): + self.name = name + self.entry = worksheet_entry + id_parts = worksheet_entry.id.text.split('/') + self.worksheet_id = id_parts[-1] + self.spreadsheet_key = spreadsheet_key + self.client = database_client + self.fields = fields or [] + if fields: + self.SetFields(fields) + + def LookupFields(self): + """Queries to find the column names in the first row of the worksheet. + + Useful when you have retrieved the table from the server and you don't + know the column names. + """ + if self.entry: + first_row_contents = [] + query = gdata.spreadsheet.service.CellQuery() + query.max_row = '1' + query.min_row = '1' + feed = self.client._GetSpreadsheetsClient().GetCellsFeed( + self.spreadsheet_key, wksht_id=self.worksheet_id, query=query) + for entry in feed.entry: + first_row_contents.append(entry.content.text) + # Get the next set of cells if needed. + next_link = feed.GetNextLink() + while next_link: + feed = self.client._GetSpreadsheetsClient().Get(next_link.href, + converter=gdata.spreadsheet.SpreadsheetsCellsFeedFromString) + for entry in feed.entry: + first_row_contents.append(entry.content.text) + next_link = feed.GetNextLink() + # Convert the contents of the cells to valid headers. + self.fields = ConvertStringsToColumnHeaders(first_row_contents) + + def SetFields(self, fields): + """Changes the contents of the cells in the first row of this worksheet. + + Args: + fields: list of strings The names in the list comprise the + first row of the worksheet. These names are converted into XML + tags by the server. To avoid changes during the translation + process I recommend using all lowercase alphabetic names. For + example ['somelongname', 'theothername'] + """ + # TODO: If the table already had fields, we might want to clear out the, + # current column headers. + self.fields = fields + i = 0 + for column_name in fields: + i = i + 1 + # TODO: speed this up by using a batch request to update cells. + self.client._GetSpreadsheetsClient().UpdateCell(1, i, column_name, + self.spreadsheet_key, self.worksheet_id) + + def Delete(self): + """Deletes this worksheet from the spreadsheet.""" + worksheet = self.client._GetSpreadsheetsClient().GetWorksheetsFeed( + self.spreadsheet_key, wksht_id=self.worksheet_id) + self.client._GetSpreadsheetsClient().DeleteWorksheet( + worksheet_entry=worksheet) + + def AddRecord(self, data): + """Adds a new row to this worksheet. + + Args: + data: dict of strings Mapping of string values to column names. + + Returns: + Record which represents this row of the spreadsheet. + """ + new_row = self.client._GetSpreadsheetsClient().InsertRow(data, + self.spreadsheet_key, wksht_id=self.worksheet_id) + return Record(content=data, row_entry=new_row, + spreadsheet_key=self.spreadsheet_key, worksheet_id=self.worksheet_id, + database_client=self.client) + + def GetRecord(self, row_id=None, row_number=None): + """Gets a single record from the worksheet based on row ID or number. + + Args: + row_id: The ID for the individual row. + row_number: str or int The position of the desired row. Numbering + begins at 1, which refers to the second row in the worksheet since + the first row is used for column names. + + Returns: + Record for the desired row. + """ + if row_id: + row_entry = self.client._GetSpreadsheetsClient().GetListFeed( + self.spreadsheet_key, wksht_id=self.worksheet_id, row_id=row_id) + return Record(content=None, row_entry=row_entry, + spreadsheet_key=self.spreadsheet_key, + worksheet_id=self.worksheet_id, database_client=self.client) + else: + row_query = gdata.spreadsheet.service.ListQuery() + row_query.start_index = str(row_number) + row_query.max_results = '1' + row_feed = self.client._GetSpreadsheetsClient().GetListFeed( + self.spreadsheet_key, wksht_id=self.worksheet_id, query=row_query) + if len(row_feed.entry) >= 1: + return Record(content=None, row_entry=row_feed.entry[0], + spreadsheet_key=self.spreadsheet_key, + worksheet_id=self.worksheet_id, database_client=self.client) + else: + return None + + def GetRecords(self, start_row, end_row): + """Gets all rows between the start and end row numbers inclusive. + + Args: + start_row: str or int + end_row: str or int + + Returns: + RecordResultSet for the desired rows. + """ + start_row = int(start_row) + end_row = int(end_row) + max_rows = end_row - start_row + 1 + row_query = gdata.spreadsheet.service.ListQuery() + row_query.start_index = str(start_row) + row_query.max_results = str(max_rows) + rows_feed = self.client._GetSpreadsheetsClient().GetListFeed( + self.spreadsheet_key, wksht_id=self.worksheet_id, query=row_query) + return RecordResultSet(rows_feed, self.client, self.spreadsheet_key, + self.worksheet_id) + + def FindRecords(self, query_string): + """Performs a query against the worksheet to find rows which match. + + For details on query string syntax see the section on sq under + http://code.google.com/apis/spreadsheets/reference.html#list_Parameters + + Args: + query_string: str Examples: 'name == john' to find all rows with john + in the name column, '(cost < 19.50 and name != toy) or cost > 500' + + Returns: + RecordResultSet with the first group of matches. + """ + row_query = gdata.spreadsheet.service.ListQuery() + row_query.sq = query_string + matching_feed = self.client._GetSpreadsheetsClient().GetListFeed( + self.spreadsheet_key, wksht_id=self.worksheet_id, query=row_query) + return RecordResultSet(matching_feed, self.client, + self.spreadsheet_key, self.worksheet_id) + + +class RecordResultSet(list): + """A collection of rows which allows fetching of the next set of results. + + The server may not send all rows in the requested range because there are + too many. Using this result set you can access the first set of results + as if it is a list, then get the next batch (if there are more results) by + calling GetNext(). + """ + + def __init__(self, feed, client, spreadsheet_key, worksheet_id): + self.client = client + self.spreadsheet_key = spreadsheet_key + self.worksheet_id = worksheet_id + self.feed = feed + list(self) + for entry in self.feed.entry: + self.append(Record(content=None, row_entry=entry, + spreadsheet_key=spreadsheet_key, worksheet_id=worksheet_id, + database_client=client)) + + def GetNext(self): + """Fetches the next batch of rows in the result set. + + Returns: + A new RecordResultSet. + """ + next_link = self.feed.GetNextLink() + if next_link and next_link.href: + new_feed = self.client._GetSpreadsheetsClient().Get(next_link.href, + converter=gdata.spreadsheet.SpreadsheetsListFeedFromString) + return RecordResultSet(new_feed, self.client, self.spreadsheet_key, + self.worksheet_id) + + +class Record(object): + """Represents one row in a worksheet and provides a dictionary of values. + + Attributes: + custom: dict Represents the contents of the row with cell values mapped + to column headers. + """ + + def __init__(self, content=None, row_entry=None, spreadsheet_key=None, + worksheet_id=None, database_client=None): + """Constructor for a record. + + Args: + content: dict of strings Mapping of string values to column names. + row_entry: gdata.spreadsheet.SpreadsheetsList The Atom entry + representing this row in the worksheet. + spreadsheet_key: str The ID of the spreadsheet in which this row + belongs. + worksheet_id: str The ID of the worksheet in which this row belongs. + database_client: DatabaseClient The client which can be used to talk + the Google Spreadsheets server to edit this row. + """ + self.entry = row_entry + self.spreadsheet_key = spreadsheet_key + self.worksheet_id = worksheet_id + if row_entry: + self.row_id = row_entry.id.text.split('/')[-1] + else: + self.row_id = None + self.client = database_client + self.content = content or {} + if not content: + self.ExtractContentFromEntry(row_entry) + + def ExtractContentFromEntry(self, entry): + """Populates the content and row_id based on content of the entry. + + This method is used in the Record's contructor. + + Args: + entry: gdata.spreadsheet.SpreadsheetsList The Atom entry + representing this row in the worksheet. + """ + self.content = {} + if entry: + self.row_id = entry.id.text.split('/')[-1] + for label, custom in entry.custom.iteritems(): + self.content[label] = custom.text + + def Push(self): + """Send the content of the record to spreadsheets to edit the row. + + All items in the content dictionary will be sent. Items which have been + removed from the content may remain in the row. The content member + of the record will not be modified so additional fields in the row + might be absent from this local copy. + """ + self.entry = self.client._GetSpreadsheetsClient().UpdateRow(self.entry, self.content) + + def Pull(self): + """Query Google Spreadsheets to get the latest data from the server. + + Fetches the entry for this row and repopulates the content dictionary + with the data found in the row. + """ + if self.row_id: + self.entry = self.client._GetSpreadsheetsClient().GetListFeed( + self.spreadsheet_key, wksht_id=self.worksheet_id, row_id=self.row_id) + self.ExtractContentFromEntry(self.entry) + + def Delete(self): + self.client._GetSpreadsheetsClient().DeleteRow(self.entry) + + +def ConvertStringsToColumnHeaders(proposed_headers): + """Converts a list of strings to column names which spreadsheets accepts. + + When setting values in a record, the keys which represent column names must + fit certain rules. They are all lower case, contain no spaces or special + characters. If two columns have the same name after being sanitized, the + columns further to the right have _2, _3 _4, etc. appended to them. + + If there are column names which consist of all special characters, or if + the column header is blank, an obfuscated value will be used for a column + name. This method does not handle blank column names or column names with + only special characters. + """ + headers = [] + for input_string in proposed_headers: + # TODO: probably a more efficient way to do this. Perhaps regex. + sanitized = input_string.lower().replace('_', '').replace( + ':', '').replace(' ', '') + # When the same sanitized header appears multiple times in the first row + # of a spreadsheet, _n is appended to the name to make it unique. + header_count = headers.count(sanitized) + if header_count > 0: + headers.append('%s_%i' % (sanitized, header_count+1)) + else: + headers.append(sanitized) + return headers diff --git a/patches/gdata/spreadsheets/__init__.py b/patches/gdata/spreadsheets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patches/gdata/spreadsheets/client.py b/patches/gdata/spreadsheets/client.py new file mode 100644 index 0000000..e270c3d --- /dev/null +++ b/patches/gdata/spreadsheets/client.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + + +"""Contains a client to communicate with the Google Spreadsheets servers. + +For documentation on the Spreadsheets API, see: +http://code.google.com/apis/spreadsheets/ +""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import gdata.client +import gdata.gauth +import gdata.spreadsheets.data +import atom.data +import atom.http_core + + +SPREADSHEETS_URL = ('https://spreadsheets.google.com/feeds/spreadsheets' + '/private/full') +WORKSHEETS_URL = ('https://spreadsheets.google.com/feeds/worksheets/' + '%s/private/full') +WORKSHEET_URL = ('https://spreadsheets.google.com/feeds/worksheets/' + '%s/private/full/%s') +TABLES_URL = 'https://spreadsheets.google.com/feeds/%s/tables' +RECORDS_URL = 'https://spreadsheets.google.com/feeds/%s/records/%s' +RECORD_URL = 'https://spreadsheets.google.com/feeds/%s/records/%s/%s' + + +class SpreadsheetsClient(gdata.client.GDClient): + api_version = '3' + auth_service = 'wise' + auth_scopes = gdata.gauth.AUTH_SCOPES['wise'] + ssl = True + + def get_spreadsheets(self, auth_token=None, + desired_class=gdata.spreadsheets.data.SpreadsheetsFeed, + **kwargs): + """Obtains a feed with the spreadsheets belonging to the current user. + + Args: + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + 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. Defaults to + gdata.spreadsheets.data.SpreadsheetsFeed. + """ + return self.get_feed(SPREADSHEETS_URL, auth_token=auth_token, + desired_class=desired_class, **kwargs) + + GetSpreadsheets = get_spreadsheets + + def get_worksheets(self, spreadsheet_key, auth_token=None, + desired_class=gdata.spreadsheets.data.WorksheetsFeed, + **kwargs): + """Finds the worksheets within a given spreadsheet. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + 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. Defaults to + gdata.spreadsheets.data.WorksheetsFeed. + """ + return self.get_feed(WORKSHEETS_URL % spreadsheet_key, + auth_token=auth_token, desired_class=desired_class, + **kwargs) + + GetWorksheets = get_worksheets + + def add_worksheet(self, spreadsheet_key, title, rows, cols, + auth_token=None, **kwargs): + """Creates a new worksheet entry in the spreadsheet. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + title: str, The title to be used in for the worksheet. + rows: str or int, The number of rows this worksheet should start with. + cols: str or int, The number of columns this worksheet should start with. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + new_worksheet = gdata.spreadsheets.data.WorksheetEntry( + title=atom.data.Title(text=title), + row_count=gdata.spreadsheets.data.RowCount(text=str(rows)), + col_count=gdata.spreadsheets.data.ColCount(text=str(cols))) + return self.post(new_worksheet, WORKSHEETS_URL % spreadsheet_key, + auth_token=auth_token, **kwargs) + + AddWorksheet = add_worksheet + + def get_worksheet(self, spreadsheet_key, worksheet_id, + desired_class=gdata.spreadsheets.data.WorksheetEntry, + auth_token=None, **kwargs): + """Retrieves a single worksheet. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + worksheet_id: str, The unique ID for the worksheet withing the desired + spreadsheet. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + 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. Defaults to + gdata.spreadsheets.data.WorksheetEntry. + + """ + return self.get_entry(WORKSHEET_URL % (spreadsheet_key, worksheet_id,), + auth_token=auth_token, desired_class=desired_class, + **kwargs) + + GetWorksheet = get_worksheet + + def add_table(self, spreadsheet_key, title, summary, worksheet_name, + header_row, num_rows, start_row, insertion_mode, + column_headers, auth_token=None, **kwargs): + """Creates a new table within the worksheet. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + title: str, The title for the new table within a worksheet. + summary: str, A description of the table. + worksheet_name: str The name of the worksheet in which this table + should live. + header_row: int or str, The number of the row in the worksheet which + will contain the column names for the data in this table. + num_rows: int or str, The number of adjacent rows in this table. + start_row: int or str, The number of the row at which the data begins. + insertion_mode: str + column_headers: dict of strings, maps the column letters (A, B, C) to + the desired name which will be viewable in the + worksheet. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + data = gdata.spreadsheets.data.Data( + insertion_mode=insertion_mode, num_rows=str(num_rows), + start_row=str(start_row)) + for index, name in column_headers.iteritems(): + data.column.append(gdata.spreadsheets.data.Column( + index=index, name=name)) + new_table = gdata.spreadsheets.data.Table( + title=atom.data.Title(text=title), summary=atom.data.Summary(summary), + worksheet=gdata.spreadsheets.data.Worksheet(name=worksheet_name), + header=gdata.spreadsheets.data.Header(row=str(header_row)), data=data) + return self.post(new_table, TABLES_URL % spreadsheet_key, + auth_token=auth_token, **kwargs) + + AddTable = add_table + + def get_tables(self, spreadsheet_key, + desired_class=gdata.spreadsheets.data.TablesFeed, + auth_token=None, **kwargs): + """Retrieves a feed listing the tables in this spreadsheet. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + 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. Defaults to + gdata.spreadsheets.data.TablesFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.get_feed(TABLES_URL % spreadsheet_key, + desired_class=desired_class, auth_token=auth_token, + **kwargs) + + GetTables = get_tables + + def add_record(self, spreadsheet_key, table_id, fields, + title=None, auth_token=None, **kwargs): + """Adds a new row to the table. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + table_id: str, The ID of the table within the worksheet which should + receive this new record. The table ID can be found using the + get_table_id method of a gdata.spreadsheets.data.Table. + fields: dict of strings mapping column names to values. + title: str, optional The title for this row. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + new_record = gdata.spreadsheets.data.Record() + if title is not None: + new_record.title = atom.data.Title(text=title) + for name, value in fields.iteritems(): + new_record.field.append(gdata.spreadsheets.data.Field( + name=name, text=value)) + return self.post(new_record, RECORDS_URL % (spreadsheet_key, table_id), + auth_token=auth_token, **kwargs) + + AddRecord = add_record + + def get_records(self, spreadsheet_key, table_id, + desired_class=gdata.spreadsheets.data.RecordsFeed, + auth_token=None, **kwargs): + """Retrieves the records in a table. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + table_id: str, The ID of the table within the worksheet whose records + we would like to fetch. The table ID can be found using the + get_table_id method of a gdata.spreadsheets.data.Table. + 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. Defaults to + gdata.spreadsheets.data.RecordsFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient. + """ + return self.get_feed(RECORDS_URL % (spreadsheet_key, table_id), + desired_class=desired_class, auth_token=auth_token, + **kwargs) + + GetRecords = get_records + + def get_record(self, spreadsheet_key, table_id, record_id, + desired_class=gdata.spreadsheets.data.Record, + auth_token=None, **kwargs): + """Retrieves a single record from the table. + + Args: + spreadsheet_key: str, The unique ID of this containing spreadsheet. This + can be the ID from the URL or as provided in a + Spreadsheet entry. + table_id: str, The ID of the table within the worksheet whose records + we would like to fetch. The table ID can be found using the + get_table_id method of a gdata.spreadsheets.data.Table. + record_id: str, The ID of the record within this table which we want to + fetch. You can find the record ID using get_record_id() on + an instance of the gdata.spreadsheets.data.Record class. + 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. Defaults to + gdata.spreadsheets.data.RecordsFeed. + 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. Represents the current user. Defaults to None + and if None, this method will look for a value in the + auth_token member of SpreadsheetsClient.""" + return self.get_entry(RECORD_URL % (spreadsheet_key, table_id, record_id), + desired_class=desired_class, auth_token=auth_token, + **kwargs) + + GetRecord = get_record + + +class SpreadsheetQuery(gdata.client.Query): + + def __init__(self, title=None, title_exact=None, **kwargs): + """Adds Spreadsheets feed query parameters to a request. + + Args: + title: str Specifies the search terms for the title of a document. + This parameter used without title-exact will only submit partial + queries, not exact queries. + title_exact: str Specifies whether the title query should be taken as an + exact string. Meaningless without title. Possible values are + 'true' and 'false'. + """ + gdata.client.Query.__init__(self, **kwargs) + self.title = title + self.title_exact = title_exact + + def modify_request(self, http_request): + gdata.client._add_query_param('title', self.title, http_request) + gdata.client._add_query_param('title-exact', self.title_exact, + http_request) + gdata.client.Query.modify_request(self, http_request) + + ModifyRequest = modify_request + + +class WorksheetQuery(SpreadsheetQuery): + pass + + +class ListQuery(gdata.client.Query): + + def __init__(self, order_by=None, reverse=None, sq=None, **kwargs): + """Adds List-feed specific query parameters to a request. + + Args: + order_by: str Specifies what column to use in ordering the entries in + the feed. By position (the default): 'position' returns + rows in the order in which they appear in the GUI. Row 1, then + row 2, then row 3, and so on. By column: + 'column:columnName' sorts rows in ascending order based on the + values in the column with the given columnName, where + columnName is the value in the header row for that column. + reverse: str Specifies whether to sort in descending or ascending order. + Reverses default sort order: 'true' results in a descending + sort; 'false' (the default) results in an ascending sort. + sq: str Structured query on the full text in the worksheet. + [columnName][binaryOperator][value] + Supported binaryOperators are: + - (), for overriding order of operations + - = or ==, for strict equality + - <> or !=, for strict inequality + - and or &&, for boolean and + - or or ||, for boolean or + """ + gdata.client.Query.__init__(self, **kwargs) + self.order_by = order_by + self.reverse = reverse + self.sq = sq + + def modify_request(self, http_request): + gdata.client._add_query_param('orderby', self.order_by, http_request) + gdata.client._add_query_param('reverse', self.reverse, http_request) + gdata.client._add_query_param('sq', self.sq, http_request) + gdata.client.Query.modify_request(self, http_request) + + ModifyRequest = modify_request + + +class TableQuery(ListQuery): + pass + + +class CellQuery(gdata.client.Query): + + def __init__(self, min_row=None, max_row=None, min_col=None, max_col=None, + range=None, return_empty=None, **kwargs): + """Adds Cells-feed specific query parameters to a request. + + Args: + min_row: str or int Positional number of minimum row returned in query. + max_row: str or int Positional number of maximum row returned in query. + min_col: str or int Positional number of minimum column returned in query. + max_col: str or int Positional number of maximum column returned in query. + range: str A single cell or a range of cells. Use standard spreadsheet + cell-range notations, using a colon to separate start and end of + range. Examples: + - 'A1' and 'R1C1' both specify only cell A1. + - 'D1:F3' and 'R1C4:R3C6' both specify the rectangle of cells with + corners at D1 and F3. + return_empty: str If 'true' then empty cells will be returned in the feed. + If omitted, the default is 'false'. + """ + gdata.client.Query.__init__(self, **kwargs) + self.min_row = min_row + self.max_row = max_row + self.min_col = min_col + self.max_col = max_col + self.range = range + self.return_empty = return_empty + + def modify_request(self, http_request): + gdata.client._add_query_param('min-row', self.min_row, http_request) + gdata.client._add_query_param('max-row', self.max_row, http_request) + gdata.client._add_query_param('min-col', self.min_col, http_request) + gdata.client._add_query_param('max-col', self.max_col, http_request) + gdata.client._add_query_param('range', self.range, http_request) + gdata.client._add_query_param('return-empty', self.return_empty, + http_request) + gdata.client.Query.modify_request(self, http_request) + + ModifyRequest = modify_request diff --git a/patches/gdata/spreadsheets/data.py b/patches/gdata/spreadsheets/data.py new file mode 100644 index 0000000..efb729f --- /dev/null +++ b/patches/gdata/spreadsheets/data.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +# +# Copyright (C) 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 classes and constants for the XML in the Google Spreadsheets API. + +Documentation for the raw XML which these classes represent can be found here: +http://code.google.com/apis/spreadsheets/docs/3.0/reference.html#Elements +""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core +import gdata.data + + +GS_TEMPLATE = '{http://schemas.google.com/spreadsheets/2006}%s' +GSX_NAMESPACE = 'http://schemas.google.com/spreadsheets/2006/extended' + + +INSERT_MODE = 'insert' +OVERWRITE_MODE = 'overwrite' + + +WORKSHEETS_REL = 'http://schemas.google.com/spreadsheets/2006#worksheetsfeed' + + +class Error(Exception): + pass + + +class FieldMissing(Exception): + pass + + +class HeaderNotSet(Error): + """The desired column header had no value for the row in the list feed.""" + + +class Cell(atom.core.XmlElement): + """The gs:cell element. + + A cell in the worksheet. The <gs:cell> element can appear only as a child + of <atom:entry>. + """ + _qname = GS_TEMPLATE % 'cell' + col = 'col' + input_value = 'inputValue' + numeric_value = 'numericValue' + row = 'row' + + +class ColCount(atom.core.XmlElement): + """The gs:colCount element. + + Indicates the number of columns in the worksheet, including columns that + contain only empty cells. The <gs:colCount> element can appear as a child + of <atom:entry> or <atom:feed> + """ + _qname = GS_TEMPLATE % 'colCount' + + +class Field(atom.core.XmlElement): + """The gs:field element. + + A field single cell within a record. Contained in an <atom:entry>. + """ + _qname = GS_TEMPLATE % 'field' + index = 'index' + name = 'name' + + +class Column(Field): + """The gs:column element.""" + _qname = GS_TEMPLATE % 'column' + + +class Data(atom.core.XmlElement): + """The gs:data element. + + A data region of a table. Contained in an <atom:entry> element. + """ + _qname = GS_TEMPLATE % 'data' + column = [Column] + insertion_mode = 'insertionMode' + num_rows = 'numRows' + start_row = 'startRow' + + +class Header(atom.core.XmlElement): + """The gs:header element. + + Indicates which row is the header row. Contained in an <atom:entry>. + """ + _qname = GS_TEMPLATE % 'header' + row = 'row' + + +class RowCount(atom.core.XmlElement): + """The gs:rowCount element. + + Indicates the number of total rows in the worksheet, including rows that + contain only empty cells. The <gs:rowCount> element can appear as a + child of <atom:entry> or <atom:feed>. + """ + _qname = GS_TEMPLATE % 'rowCount' + + +class Worksheet(atom.core.XmlElement): + """The gs:worksheet element. + + The worksheet where the table lives.Contained in an <atom:entry>. + """ + _qname = GS_TEMPLATE % 'worksheet' + name = 'name' + + +class Spreadsheet(gdata.data.GDEntry): + """An Atom entry which represents a Google Spreadsheet.""" + + def find_worksheets_feed(self): + return self.find_url(WORKSHEETS_REL) + + FindWorksheetsFeed = find_worksheets_feed + + +class SpreadsheetsFeed(gdata.data.GDFeed): + """An Atom feed listing a user's Google Spreadsheets.""" + entry = [Spreadsheet] + + +class WorksheetEntry(gdata.data.GDEntry): + """An Atom entry representing a single worksheet in a spreadsheet.""" + row_count = RowCount + col_count = ColCount + + +class WorksheetsFeed(gdata.data.GDFeed): + """A feed containing the worksheets in a single spreadsheet.""" + entry = [WorksheetEntry] + + +class Table(gdata.data.GDEntry): + """An Atom entry that represents a subsection of a worksheet. + + A table allows you to treat part or all of a worksheet somewhat like a + table in a database that is, as a set of structured data items. Tables + don't exist until you explicitly create them before you can use a table + feed, you have to explicitly define where the table data comes from. + """ + data = Data + header = Header + worksheet = Worksheet + + def get_table_id(self): + if self.id.text: + return self.id.text.split('/')[-1] + return None + + GetTableId = get_table_id + + +class TablesFeed(gdata.data.GDFeed): + """An Atom feed containing the tables defined within a worksheet.""" + entry = [Table] + + +class Record(gdata.data.GDEntry): + """An Atom entry representing a single record in a table. + + Note that the order of items in each record is the same as the order of + columns in the table definition, which may not match the order of + columns in the GUI. + """ + field = [Field] + + def value_for_index(self, column_index): + for field in self.field: + if field.index == column_index: + return field.text + raise FieldMissing('There is no field for %s' % column_index) + + ValueForIndex = value_for_index + + def value_for_name(self, name): + for field in self.field: + if field.name == name: + return field.text + raise FieldMissing('There is no field for %s' % name) + + ValueForName = value_for_name + + def get_record_id(self): + if self.id.text: + return self.id.text.split('/')[-1] + return None + + +class RecordsFeed(gdata.data.GDFeed): + """An Atom feed containing the individuals records in a table.""" + entry = [Record] + + +class ListRow(atom.core.XmlElement): + """A gsx column value within a row. + + The local tag in the _qname is blank and must be set to the column + name. For example, when adding to a ListEntry, do: + col_value = ListRow(text='something') + col_value._qname = col_value._qname % 'mycolumnname' + """ + _qname = '{http://schemas.google.com/spreadsheets/2006/extended}%s' + + +class ListEntry(gdata.data.GDEntry): + """An Atom entry representing a worksheet row in the list feed. + + The values for a particular column can be get and set using + x.get_value('columnheader') and x.set_value('columnheader', 'value'). + See also the explanation of column names in the ListFeed class. + """ + + def get_value(self, column_name): + """Returns the displayed text for the desired column in this row. + + The formula or input which generated the displayed value is not accessible + through the list feed, to see the user's input, use the cells feed. + + If a column is not present in this spreadsheet, or there is no value + for a column in this row, this method will return None. + """ + values = self.get_elements(column_name, GSX_NAMESPACE) + if len(values) == 0: + return None + return values[0].text + + def set_value(self, column_name, value): + """Changes the value of cell in this row under the desired column name. + + Warning: if the cell contained a formula, it will be wiped out by setting + the value using the list feed since the list feed only works with + displayed values. + + No client side checking is performed on the column_name, you need to + ensure that the column_name is the local tag name in the gsx tag for the + column. For example, the column_name will not contain special characters, + spaces, uppercase letters, etc. + """ + # Try to find the column in this row to change an existing value. + values = self.get_elements(column_name, GSX_NAMESPACE) + if len(values) > 0: + values[0].text = value + else: + # There is no value in this row for the desired column, so add a new + # gsx:column_name element. + new_value = ListRow(text=value) + new_value._qname = new_value._qname % (column_name,) + self._other_elements.append(new_value) + + +class ListsFeed(gdata.data.GDFeed): + """An Atom feed in which each entry represents a row in a worksheet. + + The first row in the worksheet is used as the column names for the values + in each row. If a header cell is empty, then a unique column ID is used + for the gsx element name. + + Spaces in a column name are removed from the name of the corresponding + gsx element. + + Caution: The columnNames are case-insensitive. For example, if you see + a <gsx:e-mail> element in a feed, you can't know whether the column + heading in the original worksheet was "e-mail" or "E-Mail". + + Note: If two or more columns have the same name, then subsequent columns + of the same name have _n appended to the columnName. For example, if the + first column name is "e-mail", followed by columns named "E-Mail" and + "E-mail", then the columnNames will be gsx:e-mail, gsx:e-mail_2, and + gsx:e-mail_3 respectively. + """ + entry = [ListEntry] + + +class CellEntry(gdata.data.BatchEntry): + """An Atom entry representing a single cell in a worksheet.""" + cell = Cell + + +class CellsFeed(gdata.data.BatchFeed): + """An Atom feed contains one entry per cell in a worksheet. + + The cell feed supports batch operations, you can send multiple cell + operations in one HTTP request. + """ + entry = [CellEntry] + + def batch_set_cell(row, col, input): + pass + diff --git a/patches/gdata/test_config.py b/patches/gdata/test_config.py new file mode 100644 index 0000000..5e597eb --- /dev/null +++ b/patches/gdata/test_config.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python + +# Copyright (C) 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. + + +import sys +import unittest +import getpass +import inspect +import atom.mock_http_core +import gdata.gauth + + +"""Loads configuration for tests which connect to Google servers. + +Settings used in tests are stored in a ConfigCollection instance in this +module called options. If your test needs to get a test related setting, +use + +import gdata.test_config +option_value = gdata.test_config.options.get_value('x') + +The above will check the command line for an '--x' argument, and if not +found will either use the default value for 'x' or prompt the user to enter +one. + +Your test can override the value specified by the user by performing: + +gdata.test_config.options.set_value('x', 'y') + +If your test uses a new option which you would like to allow the user to +specify on the command line or via a prompt, you can use the register_option +method as follows: + +gdata.test_config.options.register( + 'option_name', 'Prompt shown to the user', secret=False #As for password. + 'This is the description of the option, shown when help is requested.', + 'default value, provide only if you do not want the user to be prompted') +""" + + +class Option(object): + + def __init__(self, name, prompt, secret=False, description=None, default=None): + self.name = name + self.prompt = prompt + self.secret = secret + self.description = description + self.default = default + + def get(self): + value = self.default + # Check for a command line parameter. + for i in xrange(len(sys.argv)): + if sys.argv[i].startswith('--%s=' % self.name): + value = sys.argv[i].split('=')[1] + elif sys.argv[i] == '--%s' % self.name: + value = sys.argv[i + 1] + # If the param was not on the command line, ask the user to input the + # value. + # In order for this to prompt the user, the default value for the option + # must be None. + if value is None: + prompt = '%s: ' % self.prompt + if self.secret: + value = getpass.getpass(prompt) + else: + print 'You can specify this on the command line using --%s' % self.name + value = raw_input(prompt) + return value + + +class ConfigCollection(object): + + def __init__(self, options=None): + self.options = options or {} + self.values = {} + + def register_option(self, option): + self.options[option.name] = option + + def register(self, *args, **kwargs): + self.register_option(Option(*args, **kwargs)) + + def get_value(self, option_name): + if option_name in self.values: + return self.values[option_name] + value = self.options[option_name].get() + if value is not None: + self.values[option_name] = value + return value + + def set_value(self, option_name, value): + self.values[option_name] = value + + def render_usage(self): + message_parts = [] + for opt_name, option in self.options.iteritems(): + message_parts.append('--%s: %s' % (opt_name, option.description)) + return '\n'.join(message_parts) + + +options = ConfigCollection() + + +# Register the default options. +options.register( + 'username', + 'Please enter the email address of your test account', + description=('The email address you want to sign in with. ' + 'Make sure this is a test account as these tests may edit' + ' or delete data.')) +options.register( + 'password', + 'Please enter the password for your test account', + secret=True, description='The test account password.') +options.register( + 'clearcache', + 'Delete cached data? (enter true or false)', + description=('If set to true, any temporary files which cache test' + ' requests and responses will be deleted.'), + default='true') +options.register( + 'savecache', + 'Save requests and responses in a temporary file? (enter true or false)', + description=('If set to true, requests to the server and responses will' + ' be saved in temporary files.'), + default='false') +options.register( + 'runlive', + 'Run the live tests which contact the server? (enter true or false)', + description=('If set to true, the tests will make real HTTP requests to' + ' the servers. This slows down test execution and may' + ' modify the users data, be sure to use a test account.'), + default='true') +options.register( + 'ssl', + 'Run the live tests over SSL (enter true or false)', + description='If set to true, all tests will be performed over HTTPS (SSL)', + default='false') +options.register( + 'appsusername', + 'Please enter the email address of your test Apps domain account', + description=('The email address you want to sign in with. ' + 'Make sure this is a test account on your Apps domain as ' + 'these tests may edit or delete data.')) +options.register( + 'appspassword', + 'Please enter the password for your test Apps domain account', + secret=True, description='The test Apps account password.') + +# Other options which may be used if needed. +BLOG_ID_OPTION = Option( + 'blogid', + 'Please enter the ID of your test blog', + description=('The blog ID for the blog which should have test posts added' + ' to it. Example 7682659670455539811')) +TEST_IMAGE_LOCATION_OPTION = Option( + 'imgpath', + 'Please enter the full path to a test image to upload', + description=('This test image will be uploaded to a service which' + ' accepts a media file, it must be a jpeg.')) +SPREADSHEET_ID_OPTION = Option( + 'spreadsheetid', + 'Please enter the ID of a spreadsheet to use in these tests', + description=('The spreadsheet ID for the spreadsheet which should be' + ' modified by theses tests.')) +APPS_DOMAIN_OPTION = Option( + 'appsdomain', + 'Please enter your Google Apps domain', + description=('The domain the Google Apps is hosted on or leave blank' + ' if n/a')) +SITES_NAME_OPTION = Option( + 'sitename', + 'Please enter name of your Google Site', + description='The webspace name of the Site found in its URL.') +PROJECT_NAME_OPTION = Option( + 'project_name', + 'Please enter the name of your project hosting project', + description=('The name of the project which should have test issues added' + ' to it. Example gdata-python-client')) +ISSUE_ASSIGNEE_OPTION = Option( + 'issue_assignee', + 'Enter the email address of the target owner of the updated issue.', + description=('The email address of the user a created issue\'s owner will ' + ' become. Example testuser2@gmail.com')) +GA_TABLE_ID = Option( + 'table_id', + 'Enter the Table ID of the Google Analytics profile to test', + description=('The Table ID of the Google Analytics profile to test.' + ' Example ga:1174')) +TARGET_USERNAME_OPTION = Option( + 'targetusername', + 'Please enter the username (without domain) of the user which will be' + ' affected by the tests', + description=('The username of the user to be tested')) +YT_DEVELOPER_KEY_OPTION = Option( + 'developerkey', + 'Please enter your YouTube developer key', + description=('The YouTube developer key for your account')) +YT_CLIENT_ID_OPTION = Option( + 'clientid', + 'Please enter your YouTube client ID', + description=('The YouTube client ID for your account')) +YT_VIDEO_ID_OPTION= Option( + 'videoid', + 'Please enter the ID of a YouTube video you uploaded', + description=('The video ID of a YouTube video uploaded to your account')) + + +# Functions to inject a cachable HTTP client into a service client. +def configure_client(client, case_name, service_name, use_apps_auth=False): + """Sets up a mock client which will reuse a saved session. + + Should be called during setUp of each unit test. + + Handles authentication to allow the GDClient to make requests which + require an auth header. + + Args: + client: a gdata.GDClient whose http_client member should be replaced + with a atom.mock_http_core.MockHttpClient so that repeated + executions can used cached responses instead of contacting + the server. + case_name: str The name of the test case class. Examples: 'BloggerTest', + 'ContactsTest'. Used to save a session + for the ClientLogin auth token request, so the case_name + should be reused if and only if the same username, password, + and service are being used. + service_name: str The service name as used for ClientLogin to identify + the Google Data API being accessed. Example: 'blogger', + 'wise', etc. + use_apps_auth: bool (optional) If set to True, use appsusername and + appspassword command-line args instead of username and + password respectively. + """ + # Use a mock HTTP client which will record and replay the HTTP traffic + # from these tests. + client.http_client = atom.mock_http_core.MockHttpClient() + client.http_client.cache_case_name = case_name + # Getting the auth token only needs to be done once in the course of test + # runs. + auth_token_key = '%s_auth_token' % service_name + if (auth_token_key not in options.values + and options.get_value('runlive') == 'true'): + client.http_client.cache_test_name = 'client_login' + cache_name = client.http_client.get_cache_file_name() + if options.get_value('clearcache') == 'true': + client.http_client.delete_session(cache_name) + client.http_client.use_cached_session(cache_name) + if not use_apps_auth: + username = options.get_value('username') + password = options.get_value('password') + else: + username = options.get_value('appsusername') + password = options.get_value('appspassword') + auth_token = client.request_client_login_token(username, password, + case_name, service=service_name) + options.values[auth_token_key] = gdata.gauth.token_to_blob(auth_token) + client.http_client.close_session() + # Allow a config auth_token of False to prevent the client's auth header + # from being modified. + if auth_token_key in options.values: + client.auth_token = gdata.gauth.token_from_blob( + options.values[auth_token_key]) + + +def configure_cache(client, test_name): + """Loads or begins a cached session to record HTTP traffic. + + Should be called at the beginning of each test method. + + Args: + client: a gdata.GDClient whose http_client member has been replaced + with a atom.mock_http_core.MockHttpClient so that repeated + executions can used cached responses instead of contacting + the server. + test_name: str The name of this test method. Examples: + 'TestClass.test_x_works', 'TestClass.test_crud_operations'. + This is used to name the recording of the HTTP requests and + responses, so it should be unique to each test method in the + test case. + """ + # Auth token is obtained in configure_client which is called as part of + # setUp. + client.http_client.cache_test_name = test_name + cache_name = client.http_client.get_cache_file_name() + if options.get_value('clearcache') == 'true': + client.http_client.delete_session(cache_name) + client.http_client.use_cached_session(cache_name) + + +def close_client(client): + """Saves the recoded responses to a temp file if the config file allows. + + This should be called in the unit test's tearDown method. + + Checks to see if the 'savecache' option is set to 'true', to make sure we + only save sessions to repeat if the user desires. + """ + if client and options.get_value('savecache') == 'true': + # If this was a live request, save the recording. + client.http_client.close_session() + + +def configure_service(service, case_name, service_name): + """Sets up a mock GDataService v1 client to reuse recorded sessions. + + Should be called during setUp of each unit test. This is a duplicate of + configure_client, modified to handle old v1 service classes. + """ + service.http_client.v2_http_client = atom.mock_http_core.MockHttpClient() + service.http_client.v2_http_client.cache_case_name = case_name + # Getting the auth token only needs to be done once in the course of test + # runs. + auth_token_key = 'service_%s_auth_token' % service_name + if (auth_token_key not in options.values + and options.get_value('runlive') == 'true'): + service.http_client.v2_http_client.cache_test_name = 'client_login' + cache_name = service.http_client.v2_http_client.get_cache_file_name() + if options.get_value('clearcache') == 'true': + service.http_client.v2_http_client.delete_session(cache_name) + service.http_client.v2_http_client.use_cached_session(cache_name) + service.ClientLogin(options.get_value('username'), + options.get_value('password'), + service=service_name, source=case_name) + options.values[auth_token_key] = service.GetClientLoginToken() + service.http_client.v2_http_client.close_session() + if auth_token_key in options.values: + service.SetClientLoginToken(options.values[auth_token_key]) + + +def configure_service_cache(service, test_name): + """Loads or starts a session recording for a v1 Service object. + + Duplicates the behavior of configure_cache, but the target for this + function is a v1 Service object instead of a v2 Client. + """ + service.http_client.v2_http_client.cache_test_name = test_name + cache_name = service.http_client.v2_http_client.get_cache_file_name() + if options.get_value('clearcache') == 'true': + service.http_client.v2_http_client.delete_session(cache_name) + service.http_client.v2_http_client.use_cached_session(cache_name) + + +def close_service(service): + if service and options.get_value('savecache') == 'true': + # If this was a live request, save the recording. + service.http_client.v2_http_client.close_session() + + +def build_suite(classes): + """Creates a TestSuite for all unit test classes in the list. + + Assumes that each of the classes in the list has unit test methods which + begin with 'test'. Calls unittest.makeSuite. + + Returns: + A new unittest.TestSuite containing a test suite for all classes. + """ + suites = [unittest.makeSuite(a_class, 'test') for a_class in classes] + return unittest.TestSuite(suites) + + +def check_data_classes(test, classes): + import inspect + for data_class in classes: + test.assert_(data_class.__doc__ is not None, + 'The class %s should have a docstring' % data_class) + if hasattr(data_class, '_qname'): + qname_versions = None + if isinstance(data_class._qname, tuple): + qname_versions = data_class._qname + else: + qname_versions = (data_class._qname,) + for versioned_qname in qname_versions: + test.assert_(isinstance(versioned_qname, str), + 'The class %s has a non-string _qname' % data_class) + test.assert_(not versioned_qname.endswith('}'), + 'The _qname for class %s is only a namespace' % ( + data_class)) + + for attribute_name, value in data_class.__dict__.iteritems(): + # Ignore all elements that start with _ (private members) + if not attribute_name.startswith('_'): + try: + if not (isinstance(value, str) or inspect.isfunction(value) + or (isinstance(value, list) + and issubclass(value[0], atom.core.XmlElement)) + or type(value) == property # Allow properties. + or inspect.ismethod(value) # Allow methods. + or issubclass(value, atom.core.XmlElement)): + test.fail( + 'XmlElement member should have an attribute, XML class,' + ' or list of XML classes as attributes.') + + except TypeError: + test.fail('Element %s in %s was of type %s' % ( + attribute_name, data_class._qname, type(value))) + + +def check_clients_with_auth(test, classes): + for client_class in classes: + test.assert_(hasattr(client_class, 'api_version')) + test.assert_(isinstance(client_class.auth_service, (str, unicode, int))) + test.assert_(hasattr(client_class, 'auth_service')) + test.assert_(isinstance(client_class.auth_service, (str, unicode))) + test.assert_(hasattr(client_class, 'auth_scopes')) + test.assert_(isinstance(client_class.auth_scopes, (list, tuple))) diff --git a/patches/gdata/test_data.py b/patches/gdata/test_data.py new file mode 100755 index 0000000..f6f3e91 --- /dev/null +++ b/patches/gdata/test_data.py @@ -0,0 +1,5504 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 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. + + + +XML_ENTRY_1 = """<?xml version='1.0'?> +<entry xmlns='http://www.w3.org/2005/Atom' + xmlns:g='http://base.google.com/ns/1.0'> + <category scheme="http://base.google.com/categories/itemtypes" + term="products"/> + <id> http://www.google.com/test/id/url </id> + <title type='text'>Testing 2000 series laptop + +

A Testing Laptop
+ + + + Computer + Laptop + testing laptop + products +""" + + +TEST_BASE_ENTRY = """ + + + Testing 2000 series laptop + +
A Testing Laptop
+
+ + yes + + + + Computer + Laptop + testing laptop + products +
""" + + +BIG_FEED = """ + + dive into mark + + A <em>lot</em> of effort + went into making this effortless + + 2005-07-31T12:29:29Z + tag:example.org,2003:3 + + + Copyright (c) 2003, Mark Pilgrim + + Example Toolkit + + + Atom draft-07 snapshot + + + tag:example.org,2003:3.2397 + 2005-07-31T12:29:29Z + 2003-12-13T08:29:29-04:00 + + Mark Pilgrim + http://example.org/ + f8dy@example.com + + + Sam Ruby + + + Joe Gregorio + + +
+

[Update: The Atom draft is finished.]

+
+
+
+
+""" + +SMALL_FEED = """ + + Example Feed + + 2003-12-13T18:30:02Z + + John Doe + + urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 + + Atom-Powered Robots Run Amok + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2003-12-13T18:30:02Z + Some text. + + +""" + +GBASE_FEED = """ + +http://www.google.com/base/feeds/snippets +2007-02-08T23:18:21.935Z +Items matching query: digital camera + + + + + + + + +GoogleBase +2171885 +1 +25 + +http://www.google.com/base/feeds/snippets/13246453826751927533 +2007-02-08T13:23:27.000Z +2007-02-08T16:40:57.000Z + + +Digital Camera Battery Notebook Computer 12v DC Power Cable - 5.5mm x 2.5mm (Center +) Camera Connecting Cables +Notebook Computer 12v DC Power Cable - 5.5mm x 2.1mm (Center +) This connection cable will allow any Digital Pursuits battery pack to power portable computers that operate with 12v power and have a 2.1mm power connector (center +) Digital ... + + + + + +B&H Photo-Video +anon-szot0wdsq0at@base.google.com + +PayPal & Bill Me Later credit available online only. +new +420 9th Ave. 10001 +305668-REG +Products +Digital Camera Battery +2007-03-10T13:23:27.000Z +1172711 +34.95 usd +Digital Photography>Camera Connecting Cables +EN +DCB5092 +US +1.0 +http://base.google.com/base_image?q=http%3A%2F%2Fwww.bhphotovideo.com%2Fimages%2Fitems%2F305668.jpg&dhm=ffffffff84c9a95e&size=6 + + +http://www.google.com/base/feeds/snippets/10145771037331858608 +2007-02-08T13:23:27.000Z +2007-02-08T16:40:57.000Z + + +Digital Camera Battery Electronic Device 5v DC Power Cable - 5.5mm x 2.5mm (Center +) Camera Connecting Cables +Electronic Device 5v DC Power Cable - 5.5mm x 2.5mm (Center +) This connection cable will allow any Digital Pursuits battery pack to power any electronic device that operates with 5v power and has a 2.5mm power connector (center +) Digital ... + + + + + +B&H Photo-Video +anon-szot0wdsq0at@base.google.com + +420 9th Ave. 10001 +new +0.18 +US +Digital Photography>Camera Connecting Cables +PayPal & Bill Me Later credit available online only. +305656-REG +http://base.google.com/base_image?q=http%3A%2F%2Fwww.bhphotovideo.com%2Fimages%2Fitems%2F305656.jpg&dhm=7315bdc8&size=6 +DCB5108 +838098005108 +34.95 usd +EN +Digital Camera Battery +1172711 +Products +2007-03-10T13:23:27.000Z + + +http://www.google.com/base/feeds/snippets/3128608193804768644 +2007-02-08T02:21:27.000Z +2007-02-08T15:40:13.000Z + + +Digital Camera Battery Power Cable for Kodak 645 Pro-Back ProBack & DCS-300 Series Camera Connecting Cables +Camera Connection Cable - to Power Kodak 645 Pro-Back DCS-300 Series Digital Cameras This connection cable will allow any Digital Pursuits battery pack to power the following digital cameras: Kodak DCS Pro Back 645 DCS-300 series Digital Photography ... + + + + + +B&H Photo-Video +anon-szot0wdsq0at@base.google.com + +0.3 +DCB6006 +http://base.google.com/base_image?q=http%3A%2F%2Fwww.bhphotovideo.com%2Fimages%2Fitems%2F305685.jpg&dhm=72f0ca0a&size=6 +420 9th Ave. 10001 +PayPal & Bill Me Later credit available online only. +Products +US +digital kodak camera +Digital Camera Battery +2007-03-10T02:21:27.000Z +EN +new +34.95 usd +1172711 +Digital Photography>Camera Connecting Cables +305685-REG + +""" + +EXTENSION_TREE = """ + + + John Doe + Bar + + + +""" + +TEST_AUTHOR = """ + + John Doe + johndoes@someemailadress.com + http://www.google.com + +""" + +TEST_LINK = """ + +""" + +TEST_GBASE_ATTRIBUTE = """ + Digital Camera Battery +""" + + +CALENDAR_FEED = """ + + http://www.google.com/calendar/feeds/default + 2007-03-20T22:48:57.833Z + GData Ops Demo's Calendar List + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + Google Calendar + 1 + + + http://www.google.com/calendar/feeds/default/gdata.ops.demo%40gmail.com + 2007-03-20T22:48:57.837Z + 2007-03-20T22:48:52.000Z + GData Ops Demo + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + http://www.google.com/calendar/feeds/default/jnh21ovnjgfph21h32gvms2758%40group.calendar.google.com + 2007-03-20T22:48:57.837Z + 2007-03-20T22:48:53.000Z + GData Ops Demo Secondary Calendar + + + + + + + GData Ops Demo Secondary Calendar + + + + + + + + +""" + +CALENDAR_FULL_EVENT_FEED = """ + + + http://www.google.com/calendar/feeds/default/private/full + 2007-03-20T21:29:57.000Z + + GData Ops Demo + GData Ops Demo + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + Google Calendar + 10 + 1 + 25 + + + + http://www.google.com/calendar/feeds/default/private/full/o99flmgmkfkfrr8u745ghr3100 + 2007-03-20T21:29:52.000Z + 2007-03-20T21:29:57.000Z + + test deleted + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/2qt3ao5hbaq7m9igr5ak9esjo0 + 2007-03-20T21:26:04.000Z + 2007-03-20T21:28:46.000Z + + Afternoon at Dolores Park with Kim + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/uvsqhg7klnae40v50vihr1pvos + 2007-03-20T21:28:37.000Z + 2007-03-20T21:28:37.000Z + + Team meeting + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + DTSTART;TZID=America/Los_Angeles:20070323T090000 + DTEND;TZID=America/Los_Angeles:20070323T100000 + RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20070817T160000Z;WKST=SU + BEGIN:VTIMEZONE TZID:America/Los_Angeles + X-LIC-LOCATION:America/Los_Angeles BEGIN:STANDARD + TZOFFSETFROM:-0700 TZOFFSETTO:-0800 TZNAME:PST + DTSTART:19701025T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU + END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 + TZNAME:PDT DTSTART:19700405T020000 + RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU END:DAYLIGHT + END:VTIMEZONE + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/st4vk9kiffs6rasrl32e4a7alo + 2007-03-20T21:25:46.000Z + 2007-03-20T21:25:46.000Z + + Movie with Kim and danah + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/ofl1e45ubtsoh6gtu127cls2oo + 2007-03-20T21:24:43.000Z + 2007-03-20T21:25:08.000Z + + Dinner with Kim and Sarah + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/b69s2avfi2joigsclecvjlc91g + 2007-03-20T21:24:19.000Z + 2007-03-20T21:25:05.000Z + + Dinner with Jane and John + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/u9p66kkiotn8bqh9k7j4rcnjjc + 2007-03-20T21:24:33.000Z + 2007-03-20T21:24:33.000Z + + Tennis with Elizabeth + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/76oj2kceidob3s708tvfnuaq3c + 2007-03-20T21:24:00.000Z + 2007-03-20T21:24:00.000Z + + Lunch with Jenn + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/5np9ec8m7uoauk1vedh5mhodco + 2007-03-20T07:50:02.000Z + 2007-03-20T20:39:26.000Z + + test entry + test desc + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/fu6sl0rqakf3o0a13oo1i1a1mg + 2007-02-14T23:23:37.000Z + 2007-02-14T23:25:30.000Z + + test + + + + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + + + + + + + + + http://www.google.com/calendar/feeds/default/private/full/h7a0haa4da8sil3rr19ia6luvc + 2007-07-16T22:13:28.000Z + 2007-07-16T22:13:29.000Z + + + + + + + + + + + + + GData Ops Demo + gdata.ops.demo@gmail.com + + + + + + + + + + + + + +""" + +CALENDAR_BATCH_REQUEST = """ + + + + 1 + + + Event inserted via batch + + + 2 + + http://www.google.com/calendar/feeds/default/private/full/glcs0kv2qqa0gf52qi1jo018gc + + Event queried via batch + + + 3 + + http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs + + Event updated via batch + + + + + + 4 + + http://www.google.com/calendar/feeds/default/private/full/d8qbg9egk1n6lhsgq1sjbqffqc + + Event deleted via batch + + + + + +""" + +CALENDAR_BATCH_RESPONSE = """ + + http://www.google.com/calendar/feeds/default/private/full + 2007-09-21T23:01:00.380Z + + Batch Feed + + + + + 1 + + + http://www.google.com/calendar/feeds/default/private/full/n9ug78gd9tv53ppn4hdjvk68ek + + Event inserted via batch + + + + + + 2 + + + http://www.google.com/calendar/feeds/default/private/full/glsc0kv2aqa0ff52qi1jo018gc + + Event queried via batch + + + + + + 3 + + + http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs + + Event updated via batch + + + + 3 + + + + + 4 + + + http://www.google.com/calendar/feeds/default/private/full/d8qbg9egk1n6lhsgq1sjbqffqc + + Event deleted via batch + Deleted + + +""" + +GBASE_ATTRIBUTE_FEED = """ + + http://www.google.com/base/feeds/attributes + 2006-11-01T20:35:59.578Z + + + Attribute histogram for query: [item type:jobs] + + + + GoogleBase + 16 + 1 + 16 + + http://www.google.com/base/feeds/attributes/job+industry%28text%29N%5Bitem+type%3Ajobs%5D + 2006-11-01T20:36:00.100Z + job industry(text) + Attribute"job industry" of type text. + + + + it internet + healthcare + information technology + accounting + clerical and administrative + other + sales and sales management + information systems + engineering and architecture + sales + + + +""" + + +GBASE_ATTRIBUTE_ENTRY = """ + + http://www.google.com/base/feeds/attributes/job+industry%28text%29N%5Bitem+type%3Ajobs%5D + 2006-11-01T20:36:00.100Z + job industry(text) + Attribute"job industry" of type text. + + + + it internet + healthcare + information technology + accounting + clerical and administrative + other + sales and sales management + information systems + engineering and architecture + sales + + +""" + +GBASE_LOCALES_FEED = """ + + http://www.google.com/base/feeds/locales/ + 2006-06-13T18:11:40.120Z + Locales + + + + + Google Inc. + base@google.com + + GoogleBase + 3 + 25 + + + http://www.google.com/base/feeds/locales/en_US + 2006-03-27T22:27:36.658Z + + + en_US + en_US + + + + + + http://www.google.com/base/feeds/locales/en_GB + 2006-06-13T18:14:18.601Z + + en_GB + en_GB + + + + + http://www.google.com/base/feeds/locales/de_DE + 2006-06-13T18:14:18.601Z + + de_DE + de_DE + + + +""" + +GBASE_STRING_ENCODING_ENTRY = """ + + http://www.google.com/base/feeds/snippets/17495780256183230088 + 2007-12-09T03:13:07.000Z + 2008-01-07T03:26:46.000Z + + Digital Camera Cord Fits SONY Cybershot DSC-R1 S40 + SONY \xC2\xB7 Cybershot Digital Camera Usb Cable DESCRIPTION + This is a 2.5 USB 2.0 A to Mini B (5 Pin) high quality digital camera + cable used for connecting your Sony Digital Cameras and Camcoders. Backward + Compatible with USB 2.0, 1.0 and 1.1. Fully ... + + + + eBay + + Products + EN + US + 0.99 usd + http://thumbs.ebaystatic.com/pict/270195049057_1.jpg + Cameras & Photo>Digital Camera Accessories>Cables + Cords & Connectors>USB Cables>For Other Brands + 11729 + 270195049057 + 2008-02-06T03:26:46Z +""" + + +RECURRENCE_EXCEPTION_ENTRY = """ + + http://www.google.com/calendar/feeds/default/private/composite/i7lgfj69mjqjgnodklif3vbm7g + 2007-04-05T21:51:49.000Z + 2007-04-05T21:51:49.000Z + + testDavid + + + + + + gdata ops + gdata.ops.test@gmail.com + + + + + + + + + + DTSTART;TZID=America/Anchorage:20070403T100000 + DTEND;TZID=America/Anchorage:20070403T110000 + RRULE:FREQ=DAILY;UNTIL=20070408T180000Z;WKST=SU + EXDATE;TZID=America/Anchorage:20070407T100000 + EXDATE;TZID=America/Anchorage:20070405T100000 + EXDATE;TZID=America/Anchorage:20070404T100000 BEGIN:VTIMEZONE + TZID:America/Anchorage X-LIC-LOCATION:America/Anchorage + BEGIN:STANDARD TZOFFSETFROM:-0800 TZOFFSETTO:-0900 TZNAME:AKST + DTSTART:19701025T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU + END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:-0900 TZOFFSETTO:-0800 + TZNAME:AKDT DTSTART:19700405T020000 + RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU END:DAYLIGHT + END:VTIMEZONE + + + + + + i7lgfj69mjqjgnodklif3vbm7g_20070407T180000Z + 2007-04-05T21:51:49.000Z + 2007-04-05T21:52:58.000Z + + testDavid + + + + gdata ops + gdata.ops.test@gmail.com + + + + + + + + + + + + + + + + + + + 2007-04-05T21:54:09.285Z + + + Comments for: testDavid + + + + + + + + + + + + """ + +NICK_ENTRY = """ + + https://apps-apis.google.com/a/feeds/example.com/nickname/2.0/Foo + 1970-01-01T00:00:00.000Z + + Foo + + + + +""" + +NICK_FEED = """ + + + http://apps-apis.google.com/a/feeds/example.com/nickname/2.0 + + 1970-01-01T00:00:00.000Z + + Nicknames for user SusanJones + + + + 1 + 2 + + + http://apps-apis.google.com/a/feeds/example.com/nickname/2.0/Foo + + + Foo + + + + + + + + http://apps-apis.google.com/a/feeds/example.com/nickname/2.0/suse + + + suse + + + + + +""" + +USER_ENTRY = """ + + https://apps-apis.google.com/a/feeds/example.com/user/2.0/TestUser + 1970-01-01T00:00:00.000Z + + TestUser + + + + + + + +""" + +USER_FEED = """ + + + http://apps-apis.google.com/a/feeds/example.com/user/2.0 + + 1970-01-01T00:00:00.000Z + + Users + """ + +EMAIL_LIST_ENTRY = """ + + + https://apps-apis.google.com/a/feeds/example.com/emailList/2.0/testlist + + 1970-01-01T00:00:00.000Z + + testlist + + + + +""" + +EMAIL_LIST_FEED = """ + + + http://apps-apis.google.com/a/feeds/example.com/emailList/2.0 + + 1970-01-01T00:00:00.000Z + + EmailLists + """ + +EMAIL_LIST_RECIPIENT_ENTRY = """ + + https://apps-apis.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/TestUser%40example.com + 1970-01-01T00:00:00.000Z + + TestUser + + + +""" + +EMAIL_LIST_RECIPIENT_FEED = """ + + + http://apps-apis.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient + + 1970-01-01T00:00:00.000Z + + Recipients for email list us-sales + """ + +ACL_FEED = """ + + http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full + 2007-04-21T00:52:04.000Z + Elizabeth Bennet's access control list + + + + + + + + + Google Calendar + 2 + 1 + + http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com + 2007-04-21T00:52:04.000Z + + + owner + + + + + + + Elizabeth Bennet + liz@gmail.com + + + + + + + http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/default + 2007-04-21T00:52:04.000Z + + + read + + + + + + + Elizabeth Bennet + liz@gmail.com + + + + + + """ + +ACL_ENTRY = """ + + http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com + 2007-04-21T00:52:04.000Z + + + owner + + + + + + + Elizabeth Bennet + liz@gmail.com + + + + + """ + +DOCUMENT_LIST_FEED = """ +21test.usertest.user@gmail.comhttps://docs.google.com/feeds/documents/private/full/spreadsheet%3AsupercalifragilisticexpeadociousTest Spreadsheet2007-07-03T18:03:32.045Z + +document:dfrkj84g_3348jbxpxcd + + test.user + test.user@gmail.com + +2009-03-05T07:48:21.493Z + +test.usertest.user@gmail.comhttp://docs.google.com/feeds/documents/private/full/document%3Agr00vyTest Document2007-07-03T18:02:50.338Z + + + test.user + test.user@gmail.com + + + 2009-03-05T07:48:21.493Z +http://docs.google.com/feeds/documents/private/fullAvailable +Documents - +test.user@gmail.com2007-07-09T23:07:21.898Z + +""" + +DOCUMENT_LIST_ENTRY = """ + +test.usertest.user@gmail.com +https://docs.google.com/feeds/documents/private/full/spreadsheet%3Asupercalifragilisticexpealidocious + +Test Spreadsheet2007-07-03T18:03:32.045Z +spreadsheet:supercalifragilisticexpealidocious + + test.user + test.user@gmail.com + +2009-03-05T07:48:21.493Z + + +""" + +DOCUMENT_LIST_ENTRY_V3 = """ + +test.usertest.user@gmail.com +https://docs.google.com/feeds/documents/private/full/spreadsheet%3Asupercalifragilisticexpealidocious + + +Test Spreadsheet2007-07-03T18:03:32.045Z +spreadsheet:supercalifragilisticexpealidocious + + test.user + test.user@gmail.com + +2009-03-05T07:48:21.493Z + +1000 + + + +""" + +DOCUMENT_LIST_ACL_ENTRY = """ + + + + +""" + +DOCUMENT_LIST_ACL_WITHKEY_ENTRY = """ + + + + +""" + +DOCUMENT_LIST_ACL_FEED = """ + +http://docs.google.com/feeds/acl/private/full/spreadsheet%3ApFrmMi8feTQYCgZpwUQ +2009-02-22T03:48:25.895Z + +Document Permissions + + + + +2 +1 + + http://docs.google.com/feeds/acl/private/full/spreadsheet%3ApFrmMi8feTQp4pwUwUQ/user%3Auser%40gmail.com + 2009-02-22T03:48:25.896Z + + Document Permission - user@gmail.com + + + + + + + http://docs.google.com/feeds/acl/private/full/spreadsheet%3ApFrmMi8fCgZp4pwUwUQ/user%3Auser2%40google.com + 2009-02-22T03:48:26.257Z + + Document Permission - user2@google.com + + + + + +""" + +DOCUMENT_LIST_REVISION_FEED = """ + +https://docs.google.com/feeds/default/private/full/resource_id/revisions +2009-08-17T04:22:10.378Z +Document Revisions + + + +6 +1 + + https://docs.google.com/feeds/id/resource_id/revisions/2 + 2009-08-17T04:22:10.440Z + 2009-08-14T07:11:34.197Z + Revision 2 + + + + + + another_user + another_user@gmail.com + + + + + + +""" + +BATCH_ENTRY = """ + + http://www.google.com/base/feeds/items/2173859253842813008 + 2006-07-11T14:51:43.560Z + 2006-07-11T14:51: 43.560Z + title + content + + + recipes + + itemB + +""" + +BATCH_FEED_REQUEST = """ + + My Batch Feed + + http://www.google.com/base/feeds/items/13308004346459454600 + + + + http://www.google.com/base/feeds/items/17437536661927313949 + + + + ... + ... + itemA + + recipes + + + ... + ... + itemB + + recipes + +""" + +BATCH_FEED_RESULT = """ + + http://www.google.com/base/feeds/items + 2006-07-11T14:51:42.894Z + My Batch + + + + + http://www.google.com/base/feeds/items/2173859253842813008 + 2006-07-11T14:51:43.560Z + 2006-07-11T14:51: 43.560Z + ... + ... + + + recipes + + itemB + + + + http://www.google.com/base/feeds/items/11974645606383737963 + 2006-07-11T14:51:43.247Z + 2006-07-11T14:51: 43.247Z + ... + ... + + + recipes + + itemA + + + + http://www.google.com/base/feeds/items/13308004346459454600 + 2006-07-11T14:51:42.894Z + Error + Bad request + + + + + + + + http://www.google.com/base/feeds/items/17437536661927313949 + 2006-07-11T14:51:43.246Z + Deleted + + + +""" + +ALBUM_FEED = """ + + http://picasaweb.google.com/data/feed/api/user/sample.user/albumid/1 + 2007-09-21T18:23:05.000Z + + Test + + public + http://lh6.google.com/sample.user/Rt8WNoDZEJE/AAAAAAAAABk/HQGlDhpIgWo/s160-c/Test.jpg + + + + + + sample + http://picasaweb.google.com/sample.user + + Picasaweb 4 + 1 + 500 + 1 + Test + + public 1188975600000 + 2 + sample.user + sample + true + 0 + http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/photoid/2 + 2007-09-05T20:49:23.000Z + 2007-09-21T18:23:05.000Z + + Aqua Blue.jpg + Blue + + + + 2 + 1190398985145172 + 0.0 + 1 2560 + 1600 + 883405 + + + 1189025362000 + true + c041ce17aaa637eb656c81d9cf526c24 + + true + 1 + + Aqua Blue.jpg Blue + tag, test + + + + + sample + + + + http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/photoid/3 + 2007-09-05T20:49:24.000Z + 2007-09-21T18:19:38.000Z + + Aqua Graphite.jpg + Gray + + + + + 3 + 1190398778006402 + 1.0 + 1 + 2560 + 1600 + 798334 + + + 1189025363000 + + true + a5ce2e36b9df7d3cb081511c72e73926 + + true + 0 + + Aqua Graphite.jpg + Gray + + + + + + sample + + + + http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/tag/tag + 2007-09-05T20:49:24.000Z + + tag + tag + + + + sample + http://picasaweb.google.com/sample.user + + + + http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/tag/test + 2007-09-05T20:49:24.000Z + + test + test + + + + sample + http://picasaweb.google.com/sample.user + + +""" + +CODE_SEARCH_FEED = """ + +http://www.google.com/codesearch/feeds/search?q=malloc +2007-12-19T16:08:04Z +Google Code Search +Google Code Search +2530000 +1 + +Google Code Search + +http://www.google.com/codesearch + + + + + +http://www.google.com/codesearch?hl=en&q=+malloc+show:LDjwp-Iqc7U:84hEYaYsZk8:xDGReDhvNi0&sa=N&ct=rx&cd=1&cs_p=http://www.gnu.org&cs_f=software/autoconf/manual/autoconf-2.60/autoconf.html-002&cs_p=http://www.gnu.org&cs_f=software/autoconf/manual/autoconf-2.60/autoconf.html-002#first2007-12-19T16:08:04ZCode owned by external author.software/autoconf/manual/autoconf-2.60/autoconf.html<pre> 8: void *<b>malloc</b> (); + + +</pre><pre> #undef <b>malloc</b> +</pre><pre> void *<b>malloc</b> (); + +</pre><pre> rpl_<b>malloc</b> (size_t n) +</pre><pre> return <b>malloc</b> (n); + +</pre> +http://www.google.com/codesearch?hl=en&q=+malloc+show:h4hfh-fV-jI:niBq_bwWZNs:H0OhClf0HWQ&sa=N&ct=rx&cd=2&cs_p=ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz&cs_f=guile-1.6.8/libguile/mallocs.c&cs_p=ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz&cs_f=guile-1.6.8/libguile/mallocs.c#first2007-12-19T16:08:04ZCode owned by external author.guile-1.6.8/libguile/mallocs.c<pre> 86: { + scm_t_bits mem = n ? (scm_t_bits) <b>malloc</b> (n) : 0; + if (n &amp;&amp; !mem) + +</pre><pre>#include &lt;<b>malloc</b>.h&gt; +</pre><pre>scm_t_bits scm_tc16_<b>malloc</b>; + +</pre><pre><b>malloc</b>_free (SCM ptr) +</pre><pre><b>malloc</b>_print (SCM exp, SCM port, scm_print_state *pstate SCM_UNUSED) + +</pre><pre> scm_puts(&quot;#&lt;<b>malloc</b> &quot;, port); +</pre><pre> scm_t_bits mem = n ? (scm_t_bits) <b>malloc</b> (n) : 0; + +</pre><pre> SCM_RETURN_NEWSMOB (scm_tc16_<b>malloc</b>, mem); +</pre><pre> scm_tc16_<b>malloc</b> = scm_make_smob_type (&quot;<b>malloc</b>&quot;, 0); + +</pre><pre> scm_set_smob_free (scm_tc16_<b>malloc</b>, <b>malloc</b>_free); +</pre>GPL + +http://www.google.com/codesearch?hl=en&q=+malloc+show:9wyZUG-N_30:7_dFxoC1ZrY:C0_iYbFj90M&sa=N&ct=rx&cd=3&cs_p=http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz&cs_f=bash-3.0/lib/malloc/alloca.c&cs_p=http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz&cs_f=bash-3.0/lib/malloc/alloca.c#first2007-12-19T16:08:04ZCode owned by external author.bash-3.0/lib/malloc/alloca.c<pre> 78: #ifndef emacs + #define <b>malloc</b> x<b>malloc</b> + extern pointer x<b>malloc</b> (); + +</pre><pre> <b>malloc</b>. The Emacs executable needs alloca to call x<b>malloc</b>, because +</pre><pre> ordinary <b>malloc</b> isn&#39;t protected from input signals. On the other + +</pre><pre> hand, the utilities in lib-src need alloca to call <b>malloc</b>; some of +</pre><pre> them are very simple, and don&#39;t have an x<b>malloc</b> routine. + +</pre><pre> Callers below should use <b>malloc</b>. */ +</pre><pre>#define <b>malloc</b> x<b>malloc</b> + +</pre><pre>extern pointer x<b>malloc</b> (); +</pre><pre> It is very important that sizeof(header) agree with <b>malloc</b> + +</pre><pre> register pointer new = <b>malloc</b> (sizeof (header) + size); +</pre>GPL +http://www.google.com/codesearch?hl=en&q=+malloc+show:uhVCKyPcT6k:8juMxxzmUJw:H7_IDsTB2L4&sa=N&ct=rx&cd=4&cs_p=http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2&cs_f=mozilla/xpcom/build/malloc.c&cs_p=http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2&cs_f=mozilla/xpcom/build/malloc.c#first2007-12-19T16:08:04ZCode owned by external author.mozilla/xpcom/build/malloc.c<pre> 54: http://gee.cs.oswego.edu/dl/html/<b>malloc</b>.html + + You may already by default be using a c library containing a <b>malloc</b> + +</pre><pre>/* ---------- To make a <b>malloc</b>.h, start cutting here ------------ */ +</pre><pre> Note: There may be an updated version of this <b>malloc</b> obtainable at + +</pre><pre> ftp://gee.cs.oswego.edu/pub/misc/<b>malloc</b>.c +</pre><pre>* Why use this <b>malloc</b>? + +</pre><pre> most tunable <b>malloc</b> ever written. However it is among the fastest +</pre><pre> allocator for <b>malloc</b>-intensive programs. + +</pre><pre> http://gee.cs.oswego.edu/dl/html/<b>malloc</b>.html +</pre><pre> You may already by default be using a c library containing a <b>malloc</b> + +</pre><pre> that is somehow based on some version of this <b>malloc</b> (for example in +</pre>Mozilla +http://www.google.com/codesearch?hl=en&q=+malloc+show:4n1P2HVOISs:Ybbpph0wR2M:OhIN_sDrG0U&sa=N&ct=rx&cd=5&cs_p=http://regexps.srparish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz&cs_f=hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh&cs_p=http://regexps.srparish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz&cs_f=hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh#first2007-12-19T16:08:04ZCode owned by external author.hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh<pre> 11: echo ================ unit-must-<b>malloc</b> tests ================ + ./unit-must-<b>malloc</b> + echo ...passed + +</pre><pre># tag: Tom Lord Tue Dec 4 14:54:29 2001 (mem-tests/unit-must-<b>malloc</b>.sh) +</pre><pre>echo ================ unit-must-<b>malloc</b> tests ================ + +</pre><pre>./unit-must-<b>malloc</b> +</pre>GPL +http://www.google.com/codesearch?hl=en&q=+malloc+show:GzkwiWG266M:ykuz3bG00ws:2sTvVSif08g&sa=N&ct=rx&cd=6&cs_p=http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2&cs_f=tar-1.14/lib/malloc.c&cs_p=http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2&cs_f=tar-1.14/lib/malloc.c#first2007-12-19T16:08:04ZCode owned by external author.tar-1.14/lib/malloc.c<pre> 22: #endif + #undef <b>malloc</b> + + +</pre><pre>/* Work around bug on some systems where <b>malloc</b> (0) fails. +</pre><pre>#undef <b>malloc</b> + +</pre><pre>rpl_<b>malloc</b> (size_t n) +</pre><pre> return <b>malloc</b> (n); + +</pre>GPL +http://www.google.com/codesearch?hl=en&q=+malloc+show:o_TFIeBY6dY:ktI_dt8wPao:AI03BD1Dz0Y&sa=N&ct=rx&cd=7&cs_p=http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz&cs_f=tar-1.16.1/lib/malloc.c&cs_p=http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz&cs_f=tar-1.16.1/lib/malloc.c#first2007-12-19T16:08:04ZCode owned by external author.tar-1.16.1/lib/malloc.c<pre> 21: #include &lt;config.h&gt; + #undef <b>malloc</b> + + +</pre><pre>/* <b>malloc</b>() function that is glibc compatible. +</pre><pre>#undef <b>malloc</b> + +</pre><pre>rpl_<b>malloc</b> (size_t n) +</pre><pre> return <b>malloc</b> (n); + +</pre>GPL +http://www.google.com/codesearch?hl=en&q=+malloc+show:_ibw-VLkMoI:jBOtIJSmFd4:-0NUEVeCwfY&sa=N&ct=rx&cd=8&cs_p=http://freshmeat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2&cs_f=uClibc-0.9.29/include/malloc.h&cs_p=http://freshmeat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2&cs_f=uClibc-0.9.29/include/malloc.h#first2007-12-19T16:08:04ZCode owned by external author.uClibc-0.9.29/include/malloc.h<pre> 1: /* Prototypes and definition for <b>malloc</b> implementation. + Copyright (C) 1996, 1997, 1999, 2000 Free Software Foundation, Inc. + +</pre><pre>/* Prototypes and definition for <b>malloc</b> implementation. +</pre><pre> `pt<b>malloc</b>&#39;, a <b>malloc</b> implementation for multiple threads without + +</pre><pre> See the files `pt<b>malloc</b>.c&#39; or `COPYRIGHT&#39; for copying conditions. +</pre><pre> This work is mainly derived from <b>malloc</b>-2.6.4 by Doug Lea + +</pre><pre> ftp://g.oswego.edu/pub/misc/<b>malloc</b>.c +</pre><pre> `pt<b>malloc</b>.c&#39;. + +</pre><pre># define __<b>malloc</b>_ptr_t void * +</pre><pre># define __<b>malloc</b>_ptr_t char * + +</pre><pre># define __<b>malloc</b>_size_t size_t +</pre>LGPL +http://www.google.com/codesearch?hl=en&q=+malloc+show:F6qHcZ9vefo:bTX7o9gKfks:hECF4r_eKC0&sa=N&ct=rx&cd=9&cs_p=http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz&cs_f=glibc-2.0.1/hurd/hurdmalloc.h&cs_p=http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz&cs_f=glibc-2.0.1/hurd/hurdmalloc.h#first2007-12-19T16:08:04ZCode owned by external author.glibc-2.0.1/hurd/hurdmalloc.h<pre> 15: #define <b>malloc</b> _hurd_<b>malloc</b> + #define realloc _hurd_realloc + +</pre><pre> All hurd-internal code which uses <b>malloc</b> et al includes this file so it +</pre><pre> will use the internal <b>malloc</b> routines _hurd_{<b>malloc</b>,realloc,free} + +</pre><pre> of <b>malloc</b> et al is the unixoid one using sbrk. +</pre><pre>extern void *_hurd_<b>malloc</b> (size_t); + +</pre><pre>#define <b>malloc</b> _hurd_<b>malloc</b> +</pre>GPL + +http://www.google.com/codesearch?hl=en&q=+malloc+show:CHUvHYzyLc8:pdcAfzDA6lY:wjofHuNLTHg&sa=N&ct=rx&cd=10&cs_p=ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2&cs_f=httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h&cs_p=ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2&cs_f=httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h#first2007-12-19T16:08:04ZCode owned by external author.httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h<pre> 173: #undef <b>malloc</b> + #define <b>malloc</b>(x) library_<b>malloc</b>(gLibHandle,x) + +</pre><pre>/* Redefine <b>malloc</b> to use the library <b>malloc</b> call so +</pre><pre>#undef <b>malloc</b> + +</pre><pre>#define <b>malloc</b>(x) library_<b>malloc</b>(gLibHandle,x) +</pre>Apache + +""" + +YOUTUBE_VIDEO_FEED = """http://gdata.youtube.com/feeds/api/standardfeeds/top_rated2008-05-14T02:24:07.000-07:00Top Ratedhttp://www.youtube.com/img/pic_youtubelogo_123x63.gifYouTubehttp://www.youtube.com/YouTube data API100125 +http://gdata.youtube.com/feeds/api/videos/C71ypXYGho82008-03-20T10:17:27.000-07:002008-05-14T04:26:37.000-07:00Me odeio por te amar - KARYN GARCIAhttp://www.karyngarcia.com.brTvKarynGarciahttp://gdata.youtube.com/feeds/api/users/tvkaryngarciaMe odeio por te amar - KARYN GARCIAhttp://www.karyngarcia.com.bramar, boyfriend, garcia, karyn, me, odeio, por, teMusictest111test222 +http://gdata.youtube.com/feeds/api/videos/gsVaTyb1tBw2008-02-15T04:31:45.000-08:002008-05-14T05:09:42.000-07:00extreme helmet cam Kani, Keil and Patotrimmedperaltamagichttp://gdata.youtube.com/feeds/api/users/peraltamagicextreme helmet cam Kani, Keil and Patotrimmedalcala, cam, campillo, dirt, extreme, helmet, kani, patoSports +""" + +YOUTUBE_ENTRY_PRIVATE = """ + + http://gdata.youtube.com/feeds/videos/UMFI1hdm96E + 2007-01-07T01:50:15.000Z + 2007-01-07T01:50:15.000Z + + + + + + + + + "Crazy (Gnarles Barkley)" - Acoustic Cover + <div style="color: #000000;font-family: + Arial, Helvetica, sans-serif; font-size:12px; font-size: 12px; + width: 555px;"><table cellspacing="0" cellpadding="0" + border="0"><tbody><tr><td width="140" + valign="top" rowspan="2"><div style="border: 1px solid + #999999; margin: 0px 10px 5px 0px;"><a + href="http://www.youtube.com/watch?v=UMFI1hdm96E"><img + alt="" + src="http://img.youtube.com/vi/UMFI1hdm96E/2.jpg"></a></div></td> + <td width="256" valign="top"><div style="font-size: + 12px; font-weight: bold;"><a style="font-size: 15px; + font-weight: bold; font-decoration: none;" + href="http://www.youtube.com/watch?v=UMFI1hdm96E">&quot;Crazy + (Gnarles Barkley)&quot; - Acoustic Cover</a> + <br></div> <div style="font-size: 12px; margin: + 3px 0px;"><span>Gnarles Barkley acoustic cover + http://www.myspace.com/davidchoimusic</span></div></td> + <td style="font-size: 11px; line-height: 1.4em; padding-left: + 20px; padding-top: 1px;" width="146" + valign="top"><div><span style="color: #666666; + font-size: 11px;">From:</span> <a + href="http://www.youtube.com/profile?user=davidchoimusic">davidchoimusic</a></div> + <div><span style="color: #666666; font-size: + 11px;">Views:</span> 113321</div> <div + style="white-space: nowrap;text-align: left"><img + style="border: 0px none; margin: 0px; padding: 0px; + vertical-align: middle; font-size: 11px;" align="top" alt="" + src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"> + <img style="border: 0px none; margin: 0px; padding: 0px; + vertical-align: middle; font-size: 11px;" align="top" alt="" + src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"> + <img style="border: 0px none; margin: 0px; padding: 0px; + vertical-align: middle; font-size: 11px;" align="top" alt="" + src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"> + <img style="border: 0px none; margin: 0px; padding: 0px; + vertical-align: middle; font-size: 11px;" align="top" alt="" + src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"> + <img style="border: 0px none; margin: 0px; padding: 0px; + vertical-align: middle; font-size: 11px;" align="top" alt="" + src="http://gdata.youtube.com/static/images/icn_star_half_11x11.gif"></div> + <div style="font-size: 11px;">1005 <span style="color: + #666666; font-size: + 11px;">ratings</span></div></td></tr> + <tr><td><span style="color: #666666; font-size: + 11px;">Time:</span> <span style="color: #000000; + font-size: 11px; font-weight: + bold;">04:15</span></td> <td style="font-size: + 11px; padding-left: 20px;"><span style="color: #666666; + font-size: 11px;">More in</span> <a + href="http://www.youtube.com/categories_portal?c=10">Music</a></td></tr></tbody></table></div> + + + + + + davidchoimusic + http://gdata.youtube.com/feeds/users/davidchoimusic + + + "Crazy (Gnarles Barkley)" - Acoustic Cover + Gnarles Barkley acoustic cover http://www.myspace.com/davidchoimusic + music, singing, gnarls, barkley, acoustic, cover + + + Music + + DeveloperTag1 + + + + + + + + + + + + + 37.398529052734375 -122.0635986328125 + + + + + + + + yes + + The content of this video may violate the terms of use. + +""" + +YOUTUBE_COMMENT_FEED = """ +http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments2008-05-19T21:45:45.261ZCommentshttp://www.youtube.com/img/pic_youtubelogo_123x63.gifYouTubehttp://www.youtube.com/YouTube data API0125 + + http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/91F809A3DE2EB81B + 2008-02-22T15:27:15.000-08:002008-02-22T15:27:15.000-08:00 + + test66 + test66 + + + + apitestjhartmannhttp://gdata.youtube.com/feeds/users/apitestjhartmann + + + http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/A261AEEFD23674AA + 2008-02-22T15:27:01.000-08:002008-02-22T15:27:01.000-08:00 + + test333 + test333 + + + + apitestjhartmannhttp://gdata.youtube.com/feeds/users/apitestjhartmann + + + http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/0DCF1E3531B3FF85 + 2008-02-22T15:11:06.000-08:002008-02-22T15:11:06.000-08:00 + + test2 + test2 + + + + apitestjhartmannhttp://gdata.youtube.com/feeds/users/apitestjhartmann + +""" + +YOUTUBE_PLAYLIST_FEED = """ + + http://gdata.youtube.com/feeds/users/andyland74/playlists?start-index=1&max-results=25 + 2008-02-26T00:26:15.635Z + + andyland74's Playlists + http://www.youtube.com/img/pic_youtubelogo_123x63.gif + + + + + + andyland74 + http://gdata.youtube.com/feeds/users/andyland74 + + YouTube data API + 1 + 1 + 25 + + My new playlist Description + + http://gdata.youtube.com/feeds/users/andyland74/playlists/8BCDD04DE8F771B2 + 2007-11-04T17:30:27.000-08:00 + 2008-02-22T09:55:14.000-08:00 + + My New Playlist Title + My new playlist Description + + + + + andyland74 + http://gdata.youtube.com/feeds/users/andyland74 + + +""" + +YOUTUBE_PLAYLIST_VIDEO_FEED = """http://gdata.youtube.com/feeds/api/playlists/BCB3BB96DF51B5052008-05-16T12:03:17.000-07:00Test PlaylistTest playlist 1http://www.youtube.com/img/pic_youtubelogo_123x63.gifgdpythonhttp://gdata.youtube.com/feeds/api/users/gdpythonYouTube data API1125Test PlaylistTest playlist 1http://gdata.youtube.com/feeds/api/playlists/BCB3BB96DF51B505/B0F29389E537F8882008-05-16T20:54:08.520ZUploading YouTube Videos with the PHP Client LibraryJochen Hartmann demonstrates the basics of how to use the PHP Client Library with the YouTube Data API. + +PHP Developer's Guide: +http://code.google.com/apis/youtube/developers_guide_php.html + +Other documentation: +http://code.google.com/apis/youtube/GoogleDevelopershttp://gdata.youtube.com/feeds/api/users/googledevelopersUploading YouTube Videos with the PHP Client LibraryJochen Hartmann demonstrates the basics of how to use the PHP Client Library with the YouTube Data API. + +PHP Developer's Guide: +http://code.google.com/apis/youtube/developers_guide_php.html + +Other documentation: +http://code.google.com/apis/youtube/api, data, demo, php, screencast, tutorial, uploading, walkthrough, youtubeEducationundefined1""" + +YOUTUBE_SUBSCRIPTION_FEED = """ + + http://gdata.youtube.com/feeds/users/andyland74/subscriptions?start-index=1&max-results=25 + 2008-02-26T00:26:15.635Z + + andyland74's Subscriptions + http://www.youtube.com/img/pic_youtubelogo_123x63.gif + + + + + + andyland74 + http://gdata.youtube.com/feeds/users/andyland74 + + YouTube data API + 1 + 1 + 25 + + http://gdata.youtube.com/feeds/users/andyland74/subscriptions/d411759045e2ad8c + 2007-11-04T17:30:27.000-08:00 + 2008-02-22T09:55:14.000-08:00 + + + Videos published by : NBC + + + + + andyland74 + http://gdata.youtube.com/feeds/users/andyland74 + + NBC + + +""" + +YOUTUBE_VIDEO_RESPONSE_FEED = """ + + http://gdata.youtube.com/feeds/videos/2c3q9K4cHzY/responses2008-05-19T22:37:34.076ZVideos responses to 'Giant NES controller coffee table'http://www.youtube.com/img/pic_youtubelogo_123x63.gifYouTubehttp://www.youtube.com/YouTube data API8125 + + http://gdata.youtube.com/feeds/videos/7b9EnRI9VbY2008-03-11T19:08:53.000-07:002008-05-18T21:33:10.000-07:00 + + + + + + + + + + + + Catnip Partysnipped + + + + + PismoBeachhttp://gdata.youtube.com/feeds/users/pismobeach + + Catnip Party + Uncle, Hillary, Hankette, and B4 all but overdose on the patioBrattman, cat, catmint, catnip, cats, chat, drug, gato, gatto, kat, kato, katt, Katze, kedi, kissa, OD, overdose, party, sex, Uncle + + Animals + + + + + + + + + + + + + + + + +""" + + +YOUTUBE_PROFILE = """ + + http://gdata.youtube.com/feeds/users/andyland74 + 2006-10-16T00:09:45.000-07:00 + 2008-02-26T11:48:21.000-08:00 + + + andyland74 Channel + + + + andyland74 + http://gdata.youtube.com/feeds/users/andyland74 + + 33 + andyland74 + andy + example + Catch-22 + m + Google + Testing YouTube APIs + Somewhere + US + Aqua Teen Hungerforce + Elliott Smith + Technical Writer + University of North Carolina + + + + + + + + +""" + +YOUTUBE_CONTACTS_FEED = """ + http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts2008-05-16T19:24:34.916Zapitestjhartmann's Contactshttp://www.youtube.com/img/pic_youtubelogo_123x63.gifapitestjhartmannhttp://gdata.youtube.com/feeds/users/apitestjhartmannYouTube data API2125 + + http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/test898990902008-02-04T11:27:54.000-08:002008-05-16T19:24:34.916Ztest89899090apitestjhartmannhttp://gdata.youtube.com/feeds/users/apitestjhartmanntest89899090requested + + http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/testjfisher2008-02-26T14:13:03.000-08:002008-05-16T19:24:34.916Ztestjfisherapitestjhartmannhttp://gdata.youtube.com/feeds/users/apitestjhartmanntestjfisherpending +""" + +NEW_CONTACT = """ + + http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/8411573 + 2008-02-28T18:47:02.303Z + + Fitzgerald + Notes + + + + + (206)555-1212 + 456-123-2133 + (206)555-1213 + + + + + + + 1600 Amphitheatre Pkwy Mountain View +""" + +CONTACTS_FEED = """ + + http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base + 2008-03-05T12:36:38.836Z + + Contacts + + + + + + Elizabeth Bennet + liz@gmail.com + + + Contacts + + 1 + 1 + 25 + + + http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/c9012de + + 2008-03-05T12:36:38.835Z + + Fitzgerald + + + + + + 456 + + + + +""" + + +CONTACT_GROUPS_FEED = """ + + jo@gmail.com + 2008-05-21T21:11:25.237Z + + Jo's Contact Groups + + + + + + + Jo Brown + jo@gmail.com + + Contacts + 3 + 1 + 25 + + http://google.com/m8/feeds/groups/jo%40gmail.com/base/270f + 2008-05-14T13:10:19.070Z + + joggers + joggers + + + +""" + +CONTACT_GROUP_ENTRY = """ + + + http://www.google.com/feeds/groups/jo%40gmail.com/base/1234 + 2005-01-18T21:00:00Z + 2006-01-01T00:00:00Z + Salsa group + Salsa group + + + + Very nice people. + +""" + +CALENDAR_RESOURCE_ENTRY = """ + + + + + +""" + +CALENDAR_RESOURCES_FEED = """ + + https://apps-apis.google.com/a/feeds/calendar/resource/2.0/yourdomain.com + 2008-10-17T15:29:21.064Z + + + + + 1 + + https://apps-apis.google.com/a/feeds/calendar/resource/2.0/yourdomain.com/CR-NYC-14-12-BR + 2008-10-17T15:29:21.064Z + + + + + + + + + + https://apps-apis.google.com/a/feeds/calendar/resource/2.0/yourdomain.com/?start=(Bike)-London-43-Lobby-Bike-1 + 2008-10-17T15:29:21.064Z + + + + + + + + +""" + +BLOG_ENTRY = """ + tag:blogger.com,1999:blog-blogID.post-postID + 2006-08-02T18:44:43.089-07:00 + 2006-11-08T18:10:23.020-08:00 + Lizzy's Diary + Being the journal of Elizabeth Bennet + + + + + + + + + + + + Elizabeth Bennet + liz@gmail.com + +""" + +BLOG_POST = """ + Marriage! + +
+

Mr. Darcy has proposed marriage to me!

+

He is the last man on earth I would ever desire to marry.

+

Whatever shall I do?

+
+
+ + Elizabeth Bennet + liz@gmail.com + +
""" + +BLOG_POSTS_FEED = """ + tag:blogger.com,1999:blog-blogID + 2006-11-08T18:10:23.020-08:00 + Lizzy's Diary + + + + + + + + Elizabeth Bennet + liz@gmail.com + + Blogger + + tag:blogger.com,1999:blog-blogID.post-postID + 2006-11-08T18:10:00.000-08:00 + 2006-11-08T18:10:14.954-08:00 + Quite disagreeable + <p>I met Mr. Bingley's friend Mr. Darcy + this evening. I found him quite disagreeable.</p> + + + + + + + + Elizabeth Bennet + liz@gmail.com + + +""" + +BLOG_COMMENTS_FEED = """ + tag:blogger.com,1999:blog-blogID.postpostID..comments + 2007-04-04T21:56:29.803-07:00 + My Blog : Time to relax + + + + + Blog Author name + + Blogger + 1 + 1 + + tag:blogger.com,1999:blog-blogID.post-commentID + 2007-04-04T21:56:00.000-07:00 + 2007-04-04T21:56:29.803-07:00 + This is my first comment + This is my first comment + + + + + Blog Author name + + + +""" + + +SITES_FEED = """ + https://www.google.com/webmasters/tools/feeds/sites + Sites + 1 + + + + + 2008-10-02T07:26:51.833Z + + http://www.example.com + http://www.example.com + + + + 2007-11-17T18:27:32.543Z + + + + true + 2008-09-14T08:59:28.000 + US + none + normal + true + false + + + 456456-google.html + +""" + + +SITEMAPS_FEED = """ + http://www.example.com + http://www.example.com/ + 2006-11-17T18:27:32.543Z + + + + HTML + WAP + + + Value1 + Value2 + Value3 + + + http://www.example.com/sitemap-index.xml + http://www.example.com/sitemap-index.xml + + 2006-11-17T18:27:32.543Z + WEB + StatusValue + 2006-11-18T19:27:32.543Z + 102 + + + http://www.example.com/mobile/sitemap-index.xml + http://www.example.com/mobile/sitemap-index.xml + + 2006-11-17T18:27:32.543Z + StatusValue + 2006-11-18T19:27:32.543Z + 102 + HTML + + + http://www.example.com/news/sitemap-index.xml + http://www.example.com/news/sitemap-index.xml + + 2006-11-17T18:27:32.543Z + StatusValue + 2006-11-18T19:27:32.543Z + 102 + LabelValue + +""" + +HEALTH_CCR_NOTICE_PAYLOAD = """ + + + + + Start date + 2007-04-04T07:00:00Z + + + Aortic valve disorders + + 410.10 + ICD9 + 2004 + + + Active + + + +""" + +HEALTH_PROFILE_ENTRY_DIGEST = """ + + https://www.google.com/health/feeds/profile/default/vneCn5qdEIY_digest + 2008-09-29T07:52:17.176Z + + + + + + vneCn5qdEIY + + English + + en + ISO-639-1 + + + V1.0 + + 2008-09-29T07:52:17.176Z + + + Google Health Profile + + + + + + Pregnancy status + + + Not pregnant + + + + + user@google.com + + Patient + + + + + + + Breastfeeding status + + + Not breastfeeding + + + + + user@gmail.com + + Patient + + + + + + + + Hn0FE0IlcY-FMFFgSTxkvA/CONDITION/0 + + + Start date + + 2007-04-04T07:00:00Z + + + Aortic valve disorders + + 410.10 + ICD9 + 2004 + + + + Active + + + + example.com + + Information Provider + + + + + + + + Malaria + + 136.9 + ICD9_Broader + + + 084.6 + ICD9 + + + + ACTIVE + + + + user@gmail.com + + Patient + + + + + + + + + + + + Race + + S15814 + HL7 + + + + White + + + + + user@gmail.com + + Patient + + + + + + + + + + + + + + Allergy + + + A-Fil + + + ACTIVE + + + + user@gmail.com + + Patient + + + + + + + Severe + + + + + + Allergy + + + A.E.R Traveler + + + ACTIVE + + + + user@gmail.com + + Patient + + + + + + + Severe + + + + + + + + + + ACTIVE + + + + user@gmail.com + + Patient + + + + + + A& D + + + + 0 + + + + + + + + + + 0 + + + + To skin + + C38305 + FDA + + 0 + + + + + + + + + + + ACTIVE + + + + user@gmail.com + + Patient + + + + + + A-Fil + + + + 0 + + + + + + + + + + 0 + + + + To skin + + C38305 + FDA + + 0 + + + + + + + + + + + ACTIVE + + + + user@gmail.com + + Patient + + + + + + Lipitor + + + + 0 + + + + + + + + + + 0 + + + + By mouth + + C38288 + FDA + + 0 + + + + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + Chickenpox Vaccine + + 21 + HL7 + + + + + + + + + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + + + Height + + + + 0 + + 70 + + inches + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + + + Weight + + + + 0 + + 2480 + + ounces + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + + + Blood Type + + + + 0 + + O+ + + + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + + + Collection start date + + 2008-09-03 + + + + Acetaldehyde - Blood + + + + 0 + + + + + + + + + + + + Abdominal Ultrasound + + + + + user@gmail.com + + Patient + + + + + + + + Abdominoplasty + + + + + user@gmail.com + + Patient + + + + + + + + + Google Health Profile + + + + + + + + 1984-07-22 + + + Male + + + + + + user@gmail.com + + Patient + + + + + + +""" + +HEALTH_PROFILE_FEED = """ +https://www.google.com/health/feeds/profile/default +2008-09-30T01:07:17.888Z + +Profile Feed + + + + +1 + + https://www.google.com/health/feeds/profile/default/DysasdfARnFAao + 2008-09-29T03:12:50.850Z + 2008-09-29T03:12:50.850Z + + + + + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/MEDICATION/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DA%26+D"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/DysasdfARnFAao"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/DysasdfARnFAao"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>hiD9sEigSzdk8nNT0evR4g</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Medications> + <Medication> + <Type/> + <Description/> + <Status> + <Text>ACTIVE</Text> + </Status> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <Product> + <ProductName> + <Text>A& D</Text> + </ProductName> + <Strength> + <Units/> + <StrengthSequencePosition>0</StrengthSequencePosition> + <VariableStrengthModifier/> + </Strength> + </Product> + <Directions> + <Direction> + <Description/> + <DeliveryMethod/> + <Dose> + <Units/> + <DoseSequencePosition>0</DoseSequencePosition> + <VariableDoseModifier/> + </Dose> + <Route> + <Text>To skin</Text> + <Code> + <Value>C38305</Value> + <CodingSystem>FDA</CodingSystem> + </Code> + <RouteSequencePosition>0</RouteSequencePosition> + <MultipleRouteModifier/> + </Route> + </Direction> + </Directions> + <Refills/> + </Medication> + </Medications> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/7I1WQzZrgp4</id> + <published>2008-09-29T03:27:14.909Z</published> + <updated>2008-09-29T03:27:14.909Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category scheme="http://schemas.google.com/health/item" term="A-Fil"/> + <category term="ALLERGY"/> + <title type="text"/> + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DA-Fil/ALLERGY"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/7I1WQzZrgp4"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/7I1WQzZrgp4"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>YOyHDxQUiECCPgnsjV8SlQ</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Alerts> + <Alert> + <Type> + <Text>Allergy</Text> + </Type> + <Description> + <Text>A-Fil</Text> + </Description> + <Status> + <Text>ACTIVE</Text> + </Status> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <Reaction> + <Description/> + <Severity> + <Text>Severe</Text> + </Severity> + </Reaction> + </Alert> + </Alerts> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/Dz9wV83sKFg</id> + <published>2008-09-29T03:12:52.166Z</published> + <updated>2008-09-29T03:12:52.167Z</updated> + <category term="MEDICATION"/> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category scheme="http://schemas.google.com/health/item" term="A-Fil"/> + <title type="text"/> + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/MEDICATION/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DA-Fil"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/Dz9wV83sKFg"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/Dz9wV83sKFg"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>7w.XFEPeuIYN3Rn32pUiUw</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Medications> + <Medication> + <Type/> + <Description/> + <Status> + <Text>ACTIVE</Text> + </Status> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <Product> + <ProductName> + <Text>A-Fil</Text> + </ProductName> + <Strength> + <Units/> + <StrengthSequencePosition>0</StrengthSequencePosition> + <VariableStrengthModifier/> + </Strength> + </Product> + <Directions> + <Direction> + <Description/> + <DeliveryMethod/> + <Dose> + <Units/> + <DoseSequencePosition>0</DoseSequencePosition> + <VariableDoseModifier/> + </Dose> + <Route> + <Text>To skin</Text> + <Code> + <Value>C38305</Value> + <CodingSystem>FDA</CodingSystem> + </Code> + <RouteSequencePosition>0</RouteSequencePosition> + <MultipleRouteModifier/> + </Route> + </Direction> + </Directions> + <Refills/> + </Medication> + </Medications> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/lzsxVzqZUyw</id> + <published>2008-09-29T03:13:07.496Z</published> + <updated>2008-09-29T03:13:07.497Z</updated> + <category scheme="http://schemas.google.com/health/item" term="A.E.R Traveler"/> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category term="ALLERGY"/> + <title type="text"/> + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DA.E.R+Traveler/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/ALLERGY"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/lzsxVzqZUyw"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/lzsxVzqZUyw"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>5efFB0J2WgEHNUvk2z3A1A</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Alerts> + <Alert> + <Type> + <Text>Allergy</Text> + </Type> + <Description> + <Text>A.E.R Traveler</Text> + </Description> + <Status> + <Text>ACTIVE</Text> + </Status> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <Reaction> + <Description/> + <Severity> + <Text>Severe</Text> + </Severity> + </Reaction> + </Alert> + </Alerts> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/6PvhfKAXyYw</id> + <published>2008-09-29T03:13:02.123Z</published> + <updated>2008-09-29T03:13:02.124Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category term="PROCEDURE"/> + <category scheme="http://schemas.google.com/health/item" term="Abdominal Ultrasound"/> + <title type="text"/> + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/PROCEDURE/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DAbdominal+Ultrasound"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/6PvhfKAXyYw"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/6PvhfKAXyYw"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>W3Wbvx_QHwG5pxVchpuF1A</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Procedures> + <Procedure> + <Type/> + <Description> + <Text>Abdominal Ultrasound</Text> + </Description> + <Status/> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + </Procedure> + </Procedures> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/r2zGPGewCeU</id> + <published>2008-09-29T03:13:03.434Z</published> + <updated>2008-09-29T03:13:03.435Z</updated> + <category scheme="http://schemas.google.com/health/item" term="Abdominoplasty"/> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category term="PROCEDURE"/> + <title type="text"/> + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DAbdominoplasty/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/PROCEDURE"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/r2zGPGewCeU"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/r2zGPGewCeU"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>OUKgj5X0KMnbkC5sDL.yHA</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Procedures> + <Procedure> + <Type/> + <Description> + <Text>Abdominoplasty</Text> + </Description> + <Status/> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + </Procedure> + </Procedures> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/_cCCbQ0O3ug</id> + <published>2008-09-29T03:13:29.041Z</published> + <updated>2008-09-29T03:13:29.042Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category scheme="http://schemas.google.com/health/item" term="Acetaldehyde - Blood"/> + <category term="LABTEST"/> + <title type="text"/> + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DAcetaldehyde+-+Blood/LABTEST"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/_cCCbQ0O3ug"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/_cCCbQ0O3ug"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>YWtomFb8aG.DueZ7z7fyug</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Results> + <Result> + <Type/> + <Description/> + <Status/> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <Substance/> + <Test> + <DateTime> + <Type> + <Text>Collection start date</Text> + </Type> + <ExactDateTime>2008-09-03</ExactDateTime> + </DateTime> + <Type/> + <Description> + <Text>Acetaldehyde - Blood</Text> + </Description> + <Status/> + <TestResult> + <ResultSequencePosition>0</ResultSequencePosition> + <VariableResultModifier/> + <Units/> + </TestResult> + <ConfidenceValue/> + </Test> + </Result> + </Results> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/BdyA3iJZyCc</id> + <published>2008-09-29T03:00:45.915Z</published> + <updated>2008-09-29T03:00:45.915Z</updated> + <category scheme="http://schemas.google.com/health/item" term="Aortic valve disorders"/> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category term="CONDITION"/> + <title type="text">Aortic valve disorders + + + + + example.com + example.com + + + h1ljpoeKJ85li.1FHsG9Gw + + + + Hn0FE0IlcY-FMFFgSTxkvA/CONDITION/0 + + + Start date + + 2007-04-04T07:00:00Z + + + Aortic valve disorders + + 410.10 + ICD9 + 2004 + + + + Active + + + + example.com + + Information Provider + + + + + + + + + + https://www.google.com/health/feeds/profile/default/Cl.aMWIH5VA + 2008-09-29T03:13:34.996Z + 2008-09-29T03:13:34.997Z + + + + + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DChickenpox+Vaccine/IMMUNIZATION"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/Cl.aMWIH5VA"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/Cl.aMWIH5VA"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>KlhUqfftgELIitpKbqYalw</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Immunizations> + <Immunization> + <Type/> + <Description/> + <Status/> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <Product> + <ProductName> + <Text>Chickenpox Vaccine</Text> + <Code> + <Value>21</Value> + <CodingSystem>HL7</CodingSystem> + </Code> + </ProductName> + </Product> + <Directions> + <Direction> + <Description/> + <DeliveryMethod/> + </Direction> + </Directions> + <Refills/> + </Immunization> + </Immunizations> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/l0a7.FlX3_0</id> + <published>2008-09-29T03:14:47.461Z</published> + <updated>2008-09-29T03:14:47.461Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category term="DEMOGRAPHICS"/> + <category scheme="http://schemas.google.com/health/item" term="Demographics"/> + <title type="text">Demographics + + + + + + User Name + user@gmail.com + + + U5GDAVOxFbexQw3iyvqPYg + + + + + + + + + + + + + + + + 1984-07-22 + + + Male + + + + + + user@gmail.com + + Patient + + + + + + + + + https://www.google.com/health/feeds/profile/default/oIBDdgwFLyo + 2008-09-29T03:14:47.690Z + 2008-09-29T03:14:47.691Z + + + + FunctionalStatus + + + + + + User Name + user@gmail.com + + + W.EJcnhxb7W5M4eR4Tr1YA + + + + + + + + + + Pregnancy status + + + Not pregnant + + + + + user@gmail.com + + Patient + + + + + + + Breastfeeding status + + + Not breastfeeding + + + + + user@gmail.com + + Patient + + + + + + + + + + https://www.google.com/health/feeds/profile/default/wwljIlXuTVg + 2008-09-29T03:26:10.080Z + 2008-09-29T03:26:10.081Z + + + + + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/MEDICATION/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DLipitor"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/wwljIlXuTVg"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/wwljIlXuTVg"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>OrpghzvvbG_YaO5koqT2ug</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Medications> + <Medication> + <Type/> + <Description/> + <Status> + <Text>ACTIVE</Text> + </Status> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <Product> + <ProductName> + <Text>Lipitor</Text> + </ProductName> + <Strength> + <Units/> + <StrengthSequencePosition>0</StrengthSequencePosition> + <VariableStrengthModifier/> + </Strength> + </Product> + <Directions> + <Direction> + <Description/> + <DeliveryMethod/> + <Dose> + <Units/> + <DoseSequencePosition>0</DoseSequencePosition> + <VariableDoseModifier/> + </Dose> + <Route> + <Text>By mouth</Text> + <Code> + <Value>C38288</Value> + <CodingSystem>FDA</CodingSystem> + </Code> + <RouteSequencePosition>0</RouteSequencePosition> + <MultipleRouteModifier/> + </Route> + </Direction> + </Directions> + <Refills/> + </Medication> + </Medications> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/dd09TR12SiY</id> + <published>2008-09-29T07:52:17.175Z</published> + <updated>2008-09-29T07:52:17.176Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category scheme="http://schemas.google.com/health/item" term="Malaria"/> + <category term="CONDITION"/> + <title type="text"/> + <content type="html"/> + <link rel="http://schemas.google.com/health/data#complete" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/-/%7Bhttp%3A%2F%2Fschemas.google.com%2Fg%2F2005%23kind%7Dhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fkinds%23profile/%7Bhttp%3A%2F%2Fschemas.google.com%2Fhealth%2Fitem%7DMalaria/CONDITION"/> + <link rel="self" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/dd09TR12SiY"/> + <link rel="edit" type="application/atom+xml" href="https://www.google.com/health/feeds/profile/default/dd09TR12SiY"/> + <author> + <name>User Name</name> + <email>user@gmail.com</email> + </author> + <ContinuityOfCareRecord xmlns="urn:astm-org:CCR"> + <CCRDocumentObjectID>XF99N6X4lpy.jfPUPLMMSQ</CCRDocumentObjectID> + <Language/> + <DateTime> + <Type/> + </DateTime> + <Patient/> + <Body> + <Problems> + <Problem> + <Type/> + <Description> + <Text>Malaria</Text> + <Code> + <Value>136.9</Value> + <CodingSystem>ICD9_Broader</CodingSystem> + </Code> + <Code> + <Value>084.6</Value> + <CodingSystem>ICD9</CodingSystem> + </Code> + </Description> + <Status> + <Text>ACTIVE</Text> + </Status> + <Source> + <Actor> + <ActorID>user@gmail.com</ActorID> + <ActorRole> + <Text>Patient</Text> + </ActorRole> + </Actor> + </Source> + <HealthStatus> + <Description/> + </HealthStatus> + </Problem> + </Problems> + </Body> + </ContinuityOfCareRecord> +</entry> +<entry> + <id>https://www.google.com/health/feeds/profile/default/aS0Cf964DPs</id> + <published>2008-09-29T03:14:47.463Z</published> + <updated>2008-09-29T03:14:47.463Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/health/kinds#profile"/> + <category term="DEMOGRAPHICS"/> + <category scheme="http://schemas.google.com/health/item" term="SocialHistory (Drinking, Smoking)"/> + <title type="text">SocialHistory (Drinking, Smoking) + + + + + + User Name + user@gmail.com + + + kXylGU5YXLBzriv61xPGZQ + + + + + + + + + + Race + + S15814 + HL7 + + + + White + + + + + user@gmail.com + + Patient + + + + + + + + + + + + + + + https://www.google.com/health/feeds/profile/default/s5lII5xfj_g + 2008-09-29T03:14:47.544Z + 2008-09-29T03:14:47.545Z + + + + VitalSigns + + + + + + User Name + user@gmail.com + + + FTTIiY0TVVj35kZqFFjPjQ + + + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + + + Height + + + + 0 + + 70 + + inches + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + + + Weight + + + + 0 + + 2480 + + ounces + + + + + + + + + + + + user@gmail.com + + Patient + + + + + + + + Blood Type + + + + 0 + + O+ + + + + + + + + + +""" + +HEALTH_PROFILE_LIST_ENTRY = """ + + https://www.google.com/health/feeds/profile/list/vndCn5sdfwdEIY + 1970-01-01T00:00:00.000Z + profile name + vndCn5sdfwdEIY + + + + user@gmail.com + +""" + +BOOK_ENTRY = """"""\ + """"""\ + """http://www.google.com/books/feeds/volumes/b7GZr5Btp30C"""\ + """2009-04-24T23:35:16.000Z"""\ + """"""\ + """A theory of justice"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """John Rawls"""\ + """1999"""\ + """p Since it appeared in 1971, John Rawls's i A Theory of Justice /i has become a classic. The author has now revised the original edition to clear up a number of difficulties he and others have found in the original book. /p p Rawls aims to express an essential part of the common core of the democratic tradition--justice as fairness--and to provide an alternative to utilitarianism, which had dominated the Anglo-Saxon tradition of political thought since the nineteenth century. Rawls substitutes the ideal of the social contract as a more satisfactory account of the basic rights and liberties of citizens as free and equal persons. "Each person," writes Rawls, "possesses an inviolability founded on justice that even the welfare of society as a whole cannot override." Advancing the ideas of Rousseau, Kant, Emerson, and Lincoln, Rawls's theory is as powerful today as it was when first published. /p"""\ + """538 pages"""\ + """b7GZr5Btp30C"""\ + """ISBN:0198250541"""\ + """ISBN:9780198250548"""\ + """en"""\ + """Oxford University Press"""\ + """A theory of justice"""\ +"""""" + +BOOK_FEED = """"""\ + """"""\ + """http://www.google.com/books/feeds/volumes"""\ + """2009-04-24T23:39:47.000Z"""\ + """"""\ + """Search results for 9780198250548"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """Google Books Search"""\ + """http://www.google.com"""\ + """"""\ + """Google Book Search data API"""\ + """1"""\ + """1"""\ + """20"""\ + """"""\ + """http://www.google.com/books/feeds/volumes/b7GZr5Btp30C"""\ + """2009-04-24T23:39:47.000Z"""\ + """"""\ + """A theory of justice"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """"""\ + """John Rawls"""\ + """1999"""\ + """... 9780198250548 ..."""\ + """538 pages"""\ + """b7GZr5Btp30C"""\ + """ISBN:0198250541"""\ + """ISBN:9780198250548"""\ + """Law"""\ + """A theory of justice"""\ + """"""\ +"""""" + +MAP_FEED = """ + + http://maps.google.com/maps/feeds/maps/208825816854482607313 + 2009-07-27T18:48:29.631Z + + My maps + + + + + + + Roman + + 1 + 1 + 1 + + http://maps.google.com/maps/feeds/maps/208825816854482607313/00046fb45f88fa910bcea + 2009-07-27T18:46:34.451Z + 2009-07-27T18:48:29.631Z + 2009-07-27T18:48:29.631Z + + yes + + + Untitled +
+ + + + + + Roman + + + +""" + +MAP_ENTRY = """ + + http://maps.google.com/maps/feeds/maps/208825816854482607313/00046fb45f88fa910bcea + 2009-07-27T18:46:34.451Z + 2009-07-27T18:48:29.631Z + 2009-07-27T18:48:29.631Z + + yes + + + Untitled + + + + + + + Roman + + +""" + +MAP_FEATURE_FEED = """ + + http://maps.google.com/maps/feeds/features/208825816854482607313/00046fb45f88fa910bcea + 2009-07-27T18:48:29.631Z + + Untitled + + + + + 4 + 1 + 4 + + http://maps.google.com/maps/feeds/features/208825816854482607313/00046fb45f88fa910bcea/00046fb4632573b19e0b7 + 2009-07-27T18:47:35.037Z + 2009-07-27T18:47:35.037Z + 2009-07-27T18:47:35.037Z + + Some feature title + + + Some feature title + Some feature content]]> + + + -113.818359,41.442726,0.0 + + + + + + + Roman + + + Roman + + + + http://maps.google.com/maps/feeds/features/208825816854482607313/00046fb45f88fa910bcea/00046fb46325e839a11e6 + 2009-07-27T18:47:35.067Z + 2009-07-27T18:48:22.184Z + 2009-07-27T18:48:22.184Z + + A cool poly! + + + A cool poly! + And a description]]> + + + + + 1 + -109.775391,47.457809,0.0 -99.755859,51.508742,0.0 -92.900391,48.04871,0.0 -92.8125,44.339565,0.0 -95.273437,44.402392,0.0 -97.207031,46.619261,0.0 -100.898437,46.073231,0.0 -102.480469,43.068888,0.0 -110.742187,45.274886,0.0 -109.775391,47.457809,0.0 + + + + + + + + + Roman + + + Roman + + + + http://maps.google.com/maps/feeds/features/208825816854482607313/00046fb45f88fa910bcea/00046fb465f5002e56b7a + 2009-07-27T18:48:22.194Z + 2009-07-27T18:48:22.194Z + 2009-07-27T18:48:22.194Z + + New Mexico + + + New Mexico + Word.]]> + + + 1 + -110.039062,37.788081,0.0 -103.183594,37.926868,0.0 -103.183594,32.472695,0.0 -108.896484,32.026706,0.0 -109.863281,31.203405,0.0 -110.039062,37.788081,0.0 + + + + + + + Roman + + + Roman + + + +""" + +MAP_FEATURE_ENTRY = """ + + http://maps.google.com/maps/feeds/features/208825816854482607313/00046fb45f88fa910bcea/00046fb4632573b19e0b7 + 2009-07-27T18:47:35.037Z + 2009-07-27T18:47:35.037Z + 2009-07-27T18:47:35.037Z + + Some feature title + + + Some feature title + Some feature content]]> + + + -113.818359,41.442726,0.0 + + + + + + + Roman + + + Roman + + +""" + +MAP_FEATURE_KML = """ + Some feature title + Some feature content]]> + + + -113.818359,41.442726,0.0 + + +""" + +SITES_LISTPAGE_ENTRY = ''' + + http:///sites.google.com/feeds/content/site/gdatatestsite/1712987567114738703 + 2009-06-16T00:37:37.393Z + + ListPagesTitle + +
+ +
stuff go here
asdf
+
sdf
+
+
+
+
+
+
+
+ + + + Test User + test@gmail.com + + + + + + + + + + + +
''' + +SITES_COMMENT_ENTRY = ''' + + http://sites.google.com/feeds/content/site/gdatatestsite/abc123 + 2009-06-15T18:40:22.407Z + + + <content type="xhtml"> + <div xmlns="http://www.w3.org/1999/xhtml">first comment</div> + </content> + <link rel="http://schemas.google.com/sites/2008#parent" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123parent"/> + <link rel="self" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <link rel="edit" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <author> + <name>Test User</name> + <email>test@gmail.com</email> + </author> + <thr:in-reply-to xmlns:thr="http://purl.org/syndication/thread/1.0" href="http://sites.google.com/site/gdatatestsite/annoucment/testpost" ref="http://sites.google.com/feeds/content/site/gdatatestsite/abc123" source="http://sites.google.com/feeds/content/site/gdatatestsite" type="text/html"/> +</entry>''' + +SITES_LISTITEM_ENTRY = '''<?xml version="1.0" encoding="UTF-8"?> +<entry xmlns="http://www.w3.org/2005/Atom"> + <id>http://sites.google.com/feeds/content/site/gdatatestsite/abc123</id> + <updated>2009-06-16T00:34:55.633Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/sites/2008#listitem"/> + <title type="text"/> + <link rel="http://schemas.google.com/sites/2008#parent" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123def"/> + <link rel="self" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <link rel="edit" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <author> + <name>Test User</name> + <email>test@gmail.com</email> + </author> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="A" name="Owner">test value</gs:field> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="B" name="Description">test</gs:field> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="C" name="Resolution">90</gs:field> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="D" name="Complete"/> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="E" name="MyCo">2009-05-31</gs:field> +</entry>''' + +SITES_CONTENT_FEED = '''<?xml version="1.0" encoding="UTF-8"?> +<feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" +xmlns:sites="http://schemas.google.com/sites/2008" xmlns:gs="http://schemas.google.com/spreadsheets/2006" +xmlns:dc="http://purl.org/dc/terms" xmlns:batch="http://schemas.google.com/gdata/batch" +xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr="http://purl.org/syndication/thread/1.0"> +<id>http://sites.google.com/feeds/content/site/gdatatestsite</id> +<updated>2009-06-15T21:35:43.282Z</updated> +<link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite"/> +<link rel="http://schemas.google.com/g/2005#post" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite"/> +<link rel="self" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite"/> +<generator version="1" uri="http://sites.google.com">Google Sites</generator> +<openSearch:startIndex>1</openSearch:startIndex> +<entry> + <id>http:///sites.google.com/feeds/content/site/gdatatestsite/1712987567114738703</id> + <updated>2009-06-16T00:37:37.393Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/sites/2008#listpage"/> + <title type="text">ListPagesTitle + +
+ +
stuff go here
asdf
+
sdf
+
+
+
+
+
+
+
+ + + + + + Test User + test@gmail.com + + + + + + + + + + + 2 + + home + +
+ + http://sites.google.com/feeds/content/site/gdatatestsite/abc123 + 2009-06-17T00:40:37.082Z + + filecabinet + +
+ +
sdf
+
+
+
+ + + + + + Test User + test@gmail.com + + +
+ + http://sites.google.com/feeds/content/site/gdatatestsite/abc123 + 2009-06-16T00:34:55.633Z + + + <link rel="http://schemas.google.com/sites/2008#parent" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123def"/> + <link rel="self" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <link rel="edit" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <link rel="http://schemas.google.com/sites/2008#revision" type="application/atom+xml" href="http:///sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <author> + <name>Test User</name> + <email>test@gmail.com</email> + </author> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="A" name="Owner">test value</gs:field> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="B" name="Description">test</gs:field> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="C" name="Resolution">90</gs:field> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="D" name="Complete"/> + <gs:field xmlns:gs="http://schemas.google.com/spreadsheets/2006" index="E" name="MyCo">2009-05-31</gs:field> +</entry> +<entry> + <id>http://sites.google.com/feeds/content/site/gdatatestsite/abc123</id> + <updated>2009-06-15T18:40:32.922Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/sites/2008#attachment"/> + <title type="text">testFile.ods + + + + + + + Test User + test@gmail.com + + + something else + + + http://sites.google.com/feeds/content/site/gdatatestsite/abc123 + 2009-06-15T18:40:22.407Z + + + <content type="xhtml"> + <div xmlns="http://www.w3.org/1999/xhtml">first comment</div> + </content> + <link rel="http://schemas.google.com/sites/2008#parent" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <link rel="self" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <link rel="edit" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <link rel="http://schemas.google.com/sites/2008#revision" type="application/atom+xml" href="http:///sites.google.com/feeds/content/site/gdatatestsite/abc123"/> + <author> + <name>Test User</name> + <email>test@gmail.com</email> + </author> + <thr:in-reply-to xmlns:thr="http://purl.org/syndication/thread/1.0" href="http://sites.google.com/site/gdatatestsite/annoucment/testpost" ref="http://sites.google.com/feeds/content/site/gdatatestsite/abc123" source="http://sites.google.com/feeds/content/site/gdatatestsite" type="text/html"/> +</entry> +<entry> + <id>http://sites.google.com/feeds/content/site/gdatatestsite/abc123</id> + <updated>2009-06-15T18:40:16.388Z</updated> + <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/sites/2008#announcement"/> + <title type="text">TestPost + +
+ +
content goes here
+
+
+
+ + + + + + Test User + test@gmail.com + +
+ + http://sites.google.com/feeds/content/site/gdatatestsite/abc123 + 2009-06-12T23:37:59.417Z + + Home + +
+ +
Some Content goes here
+
+
+
+ +
+
+
+
+
+
+ + + + + Test User + test@gmail.com + +
+ + http://sites.google.com/feeds/content/site/gdatatestsite/2639323850129333500 + 2009-06-12T23:32:09.191Z + + annoucment + +
+
+
+ + + + + Test User + test@gmail.com + + +
+''' + +SITES_ACTIVITY_FEED = ''' + +http://sites.google.com/feeds/activity/site/siteName +2009-08-19T05:46:01.503Z +Activity + + + +Google Sites +1 + +http://sites.google.com/feeds/activity/site/siteName/197441951793148343 +2009-08-17T00:08:19.387Z + +NewWebpage3 +
+ + + + + + + User + user@gmail.com + + + +http://sites.google.com/feeds/activity/site/siteName/7299542210274956360 +2009-08-17T00:08:03.711Z + +NewWebpage3 + +
User edited NewWebpage3 +
+
+ + + + + User + user@gmail.com + +
+''' + +SITES_REVISION_FEED = ''' + +http://sites.google.com/feeds/revision/site/siteName/2947510322163358574 +2009-08-19T06:20:18.151Z +Revisions + + +Google Sites +1 + +http://sites.google.com/feeds/revision/site/siteName/2947510322163358574/1 +2009-08-19T04:33:14.856Z + + +<content type="xhtml"> + <div xmlns="http://www.w3.org/1999/xhtml"> + <table cellspacing="0" class="sites-layout-name-one-column sites-layout-hbox"> + <tbody> + <tr> + <td class="sites-layout-tile sites-tile-name-content-1">testcomment</td> + </tr> + </tbody> + </table> +</div> +</content> +<link rel="http://schemas.google.com/sites/2008#parent" type="application/atom+xml" href="http://sites.google.com/feeds/content/site/siteName/54395424125706119"/> +<link rel="alternate" type="text" href="http://sites.google.com/site/system/app/pages/admin/compare?wuid=wuid%3Agx%3A28e7a9057c581b6e&rev1=1"/> +<link rel="self" type="application/atom+xml" href="http://sites.google.com/feeds/revision/site/siteName/2947510322163358574/1"/> +<author> + <name>User</name> + <email>user@gmail.com</email> +</author> +<thr:in-reply-to href="http://sites.google.com/site/siteName/code/js" ref="http://sites.google.com/feeds/content/site/siteName/54395424125706119" source="http://sites.google.com/feeds/content/google.com/siteName" type="text/html;charset=UTF-8"/> +<sites:revision>1</sites:revision> +</entry> +</feed>''' + +SITES_SITE_FEED = ''' +<feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:gAcl="http://schemas.google.com/acl/2007" xmlns:sites="http://schemas.google.com/sites/2008" xmlns:gs="http://schemas.google.com/spreadsheets/2006" xmlns:dc="http://purl.org/dc/terms" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr="http://purl.org/syndication/thread/1.0"> +<id>https://sites.google.com/feeds/site/example.com</id> +<updated>2009-12-09T01:05:54.631Z</updated> +<title>Site + + + +Google Sites +1 + +https://sites.google.com/feeds/site/example.com/new-test-site +2009-12-02T22:55:31.040Z +2009-12-02T22:55:31.040Z +New Test Site +A new site to hold memories + + + + + +new-test-site +iceberg + + +https://sites.google.com/feeds/site/example.com/newautosite2 +2009-12-05T00:28:01.077Z +2009-12-05T00:28:01.077Z +newAutoSite3 +A new site to hold memories2 + + + + +newautosite2 +default + +''' + +SITES_ACL_FEED = ''' + +https://sites.google.comsites.google.com/feeds/acl/site/example.com/new-test-site +2009-12-09T01:24:59.080Z + +Acl + + + +Google Sites +1 + + https://sites.google.com/feeds/acl/site/google.com/new-test-site/user%3Auser%40example.com + 2009-12-09T01:24:59.080Z + 2009-12-09T01:24:59.080Z + + + + + + +''' + +ANALYTICS_ACCOUNT_FEED_old = ''' + +http://www.google.com/analytics/feeds/accounts/abc@test.com +2009-06-25T03:55:22.000-07:00 +Profile list for abc@test.com + + +Google Analytics + +Google Analytics +12 +1 +12 + +http://www.google.com/analytics/feeds/accounts/ga:1174 +2009-06-25T03:55:22.000-07:00 +www.googlestore.com + +ga:1174 + + + + + + + +''' + +ANALYTICS_ACCOUNT_FEED = ''' + + http://www.google.com/analytics/feeds/accounts/api.nickm@google.com + 2009-10-14T09:14:25.000-07:00 + Profile list for abc@test.com + + + Google Analytics + + Google Analytics + 37 + 1 + 37 + + ga:operatingSystem==iPhone + + + http://www.google.com/analytics/feeds/accounts/ga:1174 + 2009-10-14T09:14:25.000-07:00 + www.googlestore.com + + + + + + + + + + + + + + + + + + + + + + ga:1174 + +''' + +ANALYTICS_DATA_FEED = ''' + + http://www.google.com/analytics/feeds/data?ids=ga:1174&dimensions=ga:medium,ga:source&metrics=ga:bounces,ga:visits&filters=ga:medium%3D%3Dreferral&start-date=2008-10-01&end-date=2008-10-31 + 2008-10-31T16:59:59.999-07:00 + Google Analytics Data for Profile 1174 + + + + Google Analytics + + Google Analytics + 6451 + 1 + 2 + 2008-10-01 + 2008-10-31 + + ga:operatingSystem==iPhone + + + + + + true + + ga:1174 + www.googlestore.com + + + + + + http://www.google.com/analytics/feeds/data?ids=ga:1174&ga:medium=referral&ga:source=blogger.com&filters=ga:medium%3D%3Dreferral&start-date=2008-10-01&end-date=2008-10-31 + 2008-10-30T17:00:00.001-07:00 + ga:source=blogger.com | ga:medium=referral + + + + + + +''' + + +ANALYTICS_MGMT_PROFILE_FEED = ''' + + https://www.google.com/analytics/feeds/datasources/ga/accounts/~all/webproperties/~all/profiles + 2010-06-14T22:18:48.676Z + Google Analytics Profiles for superman@gmail.com + + + Google Analytics + + Google Analytics + 1 + 1 + 1000 + + https://www.google.com/analytics/feeds/datasources/ga/accounts/30481/webproperties/UA-30481-1/profiles/1174 + 2010-06-09T05:58:15.436-07:00 + Google Analytics Profile www.googlestore.com + + + + + + + + + + + + +''' + +ANALYTICS_MGMT_GOAL_FEED = ''' + + https://www.google.com/analytics/feeds/datasources/ga/accounts/~all/webproperties/~all/profiles/~all/goals + 2010-06-14T22:21:18.485Z + Google Analytics Goals for superman@gmail.com + + + Google Analytics + + Google Analytics + 3 + 1 + 1000 + + https://www.google.com/analytics/feeds/datasources/ga/accounts/30481/webproperties/UA-30481-1/profiles/1174/goals/1 + 2010-02-07T13:12:43.377-08:00 + Google Analytics Goal 1 + + + + + + + + + + + https://www.google.com/analytics/feeds/datasources/ga/accounts/30481/webproperties/UA-30481-1/profiles/1174/goals/2 + 2010-02-07T13:12:43.376-08:00 + Google Analytics Goal 2 + + + + + + + + +''' + +ANALYTICS_MGMT_ADV_SEGMENT_FEED = ''' + + https://www.google.com/analytics/feeds/datasources/ga/segments + 2010-06-14T22:22:02.728Z + Google Analytics Advanced Segments for superman@gmail.com + + + Google Analytics + + Google Analytics + 2 + 1 + 1000 + + https://www.google.com/analytics/feeds/datasources/ga/segments/gaid::0 + 2009-10-26T13:00:44.915-07:00 + Google Analytics Advanced Segment Sources Form Google + + + ga:source=~^\Qgoogle\E + + + +''' diff --git a/patches/gdata/tlslite/BaseDB.py b/patches/gdata/tlslite/BaseDB.py new file mode 100755 index 0000000..ca8dff6 --- /dev/null +++ b/patches/gdata/tlslite/BaseDB.py @@ -0,0 +1,120 @@ +"""Base class for SharedKeyDB and VerifierDB.""" + +import anydbm +import thread + +class BaseDB: + def __init__(self, filename, type): + self.type = type + self.filename = filename + if self.filename: + self.db = None + else: + self.db = {} + self.lock = thread.allocate_lock() + + def create(self): + """Create a new on-disk database. + + @raise anydbm.error: If there's a problem creating the database. + """ + if self.filename: + self.db = anydbm.open(self.filename, "n") #raises anydbm.error + self.db["--Reserved--type"] = self.type + self.db.sync() + else: + self.db = {} + + def open(self): + """Open a pre-existing on-disk database. + + @raise anydbm.error: If there's a problem opening the database. + @raise ValueError: If the database is not of the right type. + """ + if not self.filename: + raise ValueError("Can only open on-disk databases") + self.db = anydbm.open(self.filename, "w") #raises anydbm.error + try: + if self.db["--Reserved--type"] != self.type: + raise ValueError("Not a %s database" % self.type) + except KeyError: + raise ValueError("Not a recognized database") + + def __getitem__(self, username): + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + valueStr = self.db[username] + finally: + self.lock.release() + + return self._getItem(username, valueStr) + + def __setitem__(self, username, value): + if self.db == None: + raise AssertionError("DB not open") + + valueStr = self._setItem(username, value) + + self.lock.acquire() + try: + self.db[username] = valueStr + if self.filename: + self.db.sync() + finally: + self.lock.release() + + def __delitem__(self, username): + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + del(self.db[username]) + if self.filename: + self.db.sync() + finally: + self.lock.release() + + def __contains__(self, username): + """Check if the database contains the specified username. + + @type username: str + @param username: The username to check for. + + @rtype: bool + @return: True if the database contains the username, False + otherwise. + + """ + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + return self.db.has_key(username) + finally: + self.lock.release() + + def check(self, username, param): + value = self.__getitem__(username) + return self._checkItem(value, username, param) + + def keys(self): + """Return a list of usernames in the database. + + @rtype: list + @return: The usernames in the database. + """ + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + usernames = self.db.keys() + finally: + self.lock.release() + usernames = [u for u in usernames if not u.startswith("--Reserved--")] + return usernames \ No newline at end of file diff --git a/patches/gdata/tlslite/Checker.py b/patches/gdata/tlslite/Checker.py new file mode 100755 index 0000000..f978697 --- /dev/null +++ b/patches/gdata/tlslite/Checker.py @@ -0,0 +1,146 @@ +"""Class for post-handshake certificate checking.""" + +from utils.cryptomath import hashAndBase64 +from X509 import X509 +from X509CertChain import X509CertChain +from errors import * + + +class Checker: + """This class is passed to a handshake function to check the other + party's certificate chain. + + If a handshake function completes successfully, but the Checker + judges the other party's certificate chain to be missing or + inadequate, a subclass of + L{tlslite.errors.TLSAuthenticationError} will be raised. + + Currently, the Checker can check either an X.509 or a cryptoID + chain (for the latter, cryptoIDlib must be installed). + """ + + def __init__(self, cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + checkResumedSession=False): + """Create a new Checker instance. + + You must pass in one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + @type cryptoID: str + @param cryptoID: A cryptoID which the other party's certificate + chain must match. The cryptoIDlib module must be installed. + Mutually exclusive with all of the 'x509...' arguments. + + @type protocol: str + @param protocol: A cryptoID protocol URI which the other + party's certificate chain must match. Requires the 'cryptoID' + argument. + + @type x509Fingerprint: str + @param x509Fingerprint: A hex-encoded X.509 end-entity + fingerprint which the other party's end-entity certificate must + match. Mutually exclusive with the 'cryptoID' and + 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed. Mutually exclusive with the 'cryptoID' and + 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type checkResumedSession: bool + @param checkResumedSession: If resumed sessions should be + checked. This defaults to False, on the theory that if the + session was checked once, we don't need to bother + re-checking it. + """ + + if cryptoID and (x509Fingerprint or x509TrustList): + raise ValueError() + if x509Fingerprint and x509TrustList: + raise ValueError() + if x509CommonName and not x509TrustList: + raise ValueError() + if protocol and not cryptoID: + raise ValueError() + if cryptoID: + import cryptoIDlib #So we raise an error here + if x509TrustList: + import cryptlib_py #So we raise an error here + self.cryptoID = cryptoID + self.protocol = protocol + self.x509Fingerprint = x509Fingerprint + self.x509TrustList = x509TrustList + self.x509CommonName = x509CommonName + self.checkResumedSession = checkResumedSession + + def __call__(self, connection): + """Check a TLSConnection. + + When a Checker is passed to a handshake function, this will + be called at the end of the function. + + @type connection: L{tlslite.TLSConnection.TLSConnection} + @param connection: The TLSConnection to examine. + + @raise tlslite.errors.TLSAuthenticationError: If the other + party's certificate chain is missing or bad. + """ + if not self.checkResumedSession and connection.resumed: + return + + if self.cryptoID or self.x509Fingerprint or self.x509TrustList: + if connection._client: + chain = connection.session.serverCertChain + else: + chain = connection.session.clientCertChain + + if self.x509Fingerprint or self.x509TrustList: + if isinstance(chain, X509CertChain): + if self.x509Fingerprint: + if chain.getFingerprint() != self.x509Fingerprint: + raise TLSFingerprintError(\ + "X.509 fingerprint mismatch: %s, %s" % \ + (chain.getFingerprint(), self.x509Fingerprint)) + else: #self.x509TrustList + if not chain.validate(self.x509TrustList): + raise TLSValidationError("X.509 validation failure") + if self.x509CommonName and \ + (chain.getCommonName() != self.x509CommonName): + raise TLSAuthorizationError(\ + "X.509 Common Name mismatch: %s, %s" % \ + (chain.getCommonName(), self.x509CommonName)) + elif chain: + raise TLSAuthenticationTypeError() + else: + raise TLSNoAuthenticationError() + elif self.cryptoID: + import cryptoIDlib.CertChain + if isinstance(chain, cryptoIDlib.CertChain.CertChain): + if chain.cryptoID != self.cryptoID: + raise TLSFingerprintError(\ + "cryptoID mismatch: %s, %s" % \ + (chain.cryptoID, self.cryptoID)) + if self.protocol: + if not chain.checkProtocol(self.protocol): + raise TLSAuthorizationError(\ + "cryptoID protocol mismatch") + if not chain.validate(): + raise TLSValidationError("cryptoID validation failure") + elif chain: + raise TLSAuthenticationTypeError() + else: + raise TLSNoAuthenticationError() + diff --git a/patches/gdata/tlslite/FileObject.py b/patches/gdata/tlslite/FileObject.py new file mode 100755 index 0000000..6ee02b2 --- /dev/null +++ b/patches/gdata/tlslite/FileObject.py @@ -0,0 +1,220 @@ +"""Class returned by TLSConnection.makefile().""" + +class FileObject: + """This class provides a file object interface to a + L{tlslite.TLSConnection.TLSConnection}. + + Call makefile() on a TLSConnection to create a FileObject instance. + + This class was copied, with minor modifications, from the + _fileobject class in socket.py. Note that fileno() is not + implemented.""" + + default_bufsize = 16384 #TREV: changed from 8192 + + def __init__(self, sock, mode='rb', bufsize=-1): + self._sock = sock + self.mode = mode # Not actually used in this version + if bufsize < 0: + bufsize = self.default_bufsize + self.bufsize = bufsize + self.softspace = False + if bufsize == 0: + self._rbufsize = 1 + elif bufsize == 1: + self._rbufsize = self.default_bufsize + else: + self._rbufsize = bufsize + self._wbufsize = bufsize + self._rbuf = "" # A string + self._wbuf = [] # A list of strings + + def _getclosed(self): + return self._sock is not None + closed = property(_getclosed, doc="True if the file is closed") + + def close(self): + try: + if self._sock: + for result in self._sock._decrefAsync(): #TREV + pass + finally: + self._sock = None + + def __del__(self): + try: + self.close() + except: + # close() may fail if __init__ didn't complete + pass + + def flush(self): + if self._wbuf: + buffer = "".join(self._wbuf) + self._wbuf = [] + self._sock.sendall(buffer) + + #def fileno(self): + # raise NotImplementedError() #TREV + + def write(self, data): + data = str(data) # XXX Should really reject non-string non-buffers + if not data: + return + self._wbuf.append(data) + if (self._wbufsize == 0 or + self._wbufsize == 1 and '\n' in data or + self._get_wbuf_len() >= self._wbufsize): + self.flush() + + def writelines(self, list): + # XXX We could do better here for very long lists + # XXX Should really reject non-string non-buffers + self._wbuf.extend(filter(None, map(str, list))) + if (self._wbufsize <= 1 or + self._get_wbuf_len() >= self._wbufsize): + self.flush() + + def _get_wbuf_len(self): + buf_len = 0 + for x in self._wbuf: + buf_len += len(x) + return buf_len + + def read(self, size=-1): + data = self._rbuf + if size < 0: + # Read until EOF + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + if self._rbufsize <= 1: + recv_size = self.default_bufsize + else: + recv_size = self._rbufsize + while True: + data = self._sock.recv(recv_size) + if not data: + break + buffers.append(data) + return "".join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + left = size - buf_len + recv_size = max(self._rbufsize, left) + data = self._sock.recv(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readline(self, size=-1): + data = self._rbuf + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + assert data == "" + buffers = [] + recv = self._sock.recv + while data != "\n": + data = recv(1) + if not data: + break + buffers.append(data) + return "".join(buffers) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self._sock.recv(self._rbufsize) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + return "".join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self._sock.recv(self._rbufsize) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readlines(self, sizehint=0): + total = 0 + list = [] + while True: + line = self.readline() + if not line: + break + list.append(line) + total += len(line) + if sizehint and total >= sizehint: + break + return list + + # Iterator protocols + + def __iter__(self): + return self + + def next(self): + line = self.readline() + if not line: + raise StopIteration + return line diff --git a/patches/gdata/tlslite/HandshakeSettings.py b/patches/gdata/tlslite/HandshakeSettings.py new file mode 100755 index 0000000..c7c3223 --- /dev/null +++ b/patches/gdata/tlslite/HandshakeSettings.py @@ -0,0 +1,159 @@ +"""Class for setting handshake parameters.""" + +from constants import CertificateType +from utils import cryptomath +from utils import cipherfactory + +class HandshakeSettings: + """This class encapsulates various parameters that can be used with + a TLS handshake. + @sort: minKeySize, maxKeySize, cipherNames, certificateTypes, + minVersion, maxVersion + + @type minKeySize: int + @ivar minKeySize: The minimum bit length for asymmetric keys. + + If the other party tries to use SRP, RSA, or Diffie-Hellman + parameters smaller than this length, an alert will be + signalled. The default is 1023. + + @type maxKeySize: int + @ivar maxKeySize: The maximum bit length for asymmetric keys. + + If the other party tries to use SRP, RSA, or Diffie-Hellman + parameters larger than this length, an alert will be signalled. + The default is 8193. + + @type cipherNames: list + @ivar cipherNames: The allowed ciphers, in order of preference. + + The allowed values in this list are 'aes256', 'aes128', '3des', and + 'rc4'. If these settings are used with a client handshake, they + determine the order of the ciphersuites offered in the ClientHello + message. + + If these settings are used with a server handshake, the server will + choose whichever ciphersuite matches the earliest entry in this + list. + + NOTE: If '3des' is used in this list, but TLS Lite can't find an + add-on library that supports 3DES, then '3des' will be silently + removed. + + The default value is ['aes256', 'aes128', '3des', 'rc4']. + + @type certificateTypes: list + @ivar certificateTypes: The allowed certificate types, in order of + preference. + + The allowed values in this list are 'x509' and 'cryptoID'. This + list is only used with a client handshake. The client will + advertise to the server which certificate types are supported, and + will check that the server uses one of the appropriate types. + + NOTE: If 'cryptoID' is used in this list, but cryptoIDlib is not + installed, then 'cryptoID' will be silently removed. + + @type minVersion: tuple + @ivar minVersion: The minimum allowed SSL/TLS version. + + This variable can be set to (3,0) for SSL 3.0, (3,1) for + TLS 1.0, or (3,2) for TLS 1.1. If the other party wishes to + use a lower version, a protocol_version alert will be signalled. + The default is (3,0). + + @type maxVersion: tuple + @ivar maxVersion: The maximum allowed SSL/TLS version. + + This variable can be set to (3,0) for SSL 3.0, (3,1) for + TLS 1.0, or (3,2) for TLS 1.1. If the other party wishes to + use a higher version, a protocol_version alert will be signalled. + The default is (3,2). (WARNING: Some servers may (improperly) + reject clients which offer support for TLS 1.1. In this case, + try lowering maxVersion to (3,1)). + """ + def __init__(self): + self.minKeySize = 1023 + self.maxKeySize = 8193 + self.cipherNames = ["aes256", "aes128", "3des", "rc4"] + self.cipherImplementations = ["cryptlib", "openssl", "pycrypto", + "python"] + self.certificateTypes = ["x509", "cryptoID"] + self.minVersion = (3,0) + self.maxVersion = (3,2) + + #Filters out options that are not supported + def _filter(self): + other = HandshakeSettings() + other.minKeySize = self.minKeySize + other.maxKeySize = self.maxKeySize + other.cipherNames = self.cipherNames + other.cipherImplementations = self.cipherImplementations + other.certificateTypes = self.certificateTypes + other.minVersion = self.minVersion + other.maxVersion = self.maxVersion + + if not cipherfactory.tripleDESPresent: + other.cipherNames = [e for e in self.cipherNames if e != "3des"] + if len(other.cipherNames)==0: + raise ValueError("No supported ciphers") + + try: + import cryptoIDlib + except ImportError: + other.certificateTypes = [e for e in self.certificateTypes \ + if e != "cryptoID"] + if len(other.certificateTypes)==0: + raise ValueError("No supported certificate types") + + if not cryptomath.cryptlibpyLoaded: + other.cipherImplementations = [e for e in \ + self.cipherImplementations if e != "cryptlib"] + if not cryptomath.m2cryptoLoaded: + other.cipherImplementations = [e for e in \ + other.cipherImplementations if e != "openssl"] + if not cryptomath.pycryptoLoaded: + other.cipherImplementations = [e for e in \ + other.cipherImplementations if e != "pycrypto"] + if len(other.cipherImplementations)==0: + raise ValueError("No supported cipher implementations") + + if other.minKeySize<512: + raise ValueError("minKeySize too small") + if other.minKeySize>16384: + raise ValueError("minKeySize too large") + if other.maxKeySize<512: + raise ValueError("maxKeySize too small") + if other.maxKeySize>16384: + raise ValueError("maxKeySize too large") + for s in other.cipherNames: + if s not in ("aes256", "aes128", "rc4", "3des"): + raise ValueError("Unknown cipher name: '%s'" % s) + for s in other.cipherImplementations: + if s not in ("cryptlib", "openssl", "python", "pycrypto"): + raise ValueError("Unknown cipher implementation: '%s'" % s) + for s in other.certificateTypes: + if s not in ("x509", "cryptoID"): + raise ValueError("Unknown certificate type: '%s'" % s) + + if other.minVersion > other.maxVersion: + raise ValueError("Versions set incorrectly") + + if not other.minVersion in ((3,0), (3,1), (3,2)): + raise ValueError("minVersion set incorrectly") + + if not other.maxVersion in ((3,0), (3,1), (3,2)): + raise ValueError("maxVersion set incorrectly") + + return other + + def _getCertificateTypes(self): + l = [] + for ct in self.certificateTypes: + if ct == "x509": + l.append(CertificateType.x509) + elif ct == "cryptoID": + l.append(CertificateType.cryptoID) + else: + raise AssertionError() + return l diff --git a/patches/gdata/tlslite/Session.py b/patches/gdata/tlslite/Session.py new file mode 100755 index 0000000..a951f45 --- /dev/null +++ b/patches/gdata/tlslite/Session.py @@ -0,0 +1,131 @@ +"""Class representing a TLS session.""" + +from utils.compat import * +from mathtls import * +from constants import * + +class Session: + """ + This class represents a TLS session. + + TLS distinguishes between connections and sessions. A new + handshake creates both a connection and a session. Data is + transmitted over the connection. + + The session contains a more permanent record of the handshake. The + session can be inspected to determine handshake results. The + session can also be used to create a new connection through + "session resumption". If the client and server both support this, + they can create a new connection based on an old session without + the overhead of a full handshake. + + The session for a L{tlslite.TLSConnection.TLSConnection} can be + retrieved from the connection's 'session' attribute. + + @type srpUsername: str + @ivar srpUsername: The client's SRP username (or None). + + @type sharedKeyUsername: str + @ivar sharedKeyUsername: The client's shared-key username (or + None). + + @type clientCertChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @ivar clientCertChain: The client's certificate chain (or None). + + @type serverCertChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @ivar serverCertChain: The server's certificate chain (or None). + """ + + def __init__(self): + self.masterSecret = createByteArraySequence([]) + self.sessionID = createByteArraySequence([]) + self.cipherSuite = 0 + self.srpUsername = None + self.sharedKeyUsername = None + self.clientCertChain = None + self.serverCertChain = None + self.resumable = False + self.sharedKey = False + + def _clone(self): + other = Session() + other.masterSecret = self.masterSecret + other.sessionID = self.sessionID + other.cipherSuite = self.cipherSuite + other.srpUsername = self.srpUsername + other.sharedKeyUsername = self.sharedKeyUsername + other.clientCertChain = self.clientCertChain + other.serverCertChain = self.serverCertChain + other.resumable = self.resumable + other.sharedKey = self.sharedKey + return other + + def _calcMasterSecret(self, version, premasterSecret, clientRandom, + serverRandom): + if version == (3,0): + self.masterSecret = PRF_SSL(premasterSecret, + concatArrays(clientRandom, serverRandom), 48) + elif version in ((3,1), (3,2)): + self.masterSecret = PRF(premasterSecret, "master secret", + concatArrays(clientRandom, serverRandom), 48) + else: + raise AssertionError() + + def valid(self): + """If this session can be used for session resumption. + + @rtype: bool + @return: If this session can be used for session resumption. + """ + return self.resumable or self.sharedKey + + def _setResumable(self, boolean): + #Only let it be set if this isn't a shared key + if not self.sharedKey: + #Only let it be set to True if the sessionID is non-null + if (not boolean) or (boolean and self.sessionID): + self.resumable = boolean + + def getCipherName(self): + """Get the name of the cipher used with this connection. + + @rtype: str + @return: The name of the cipher used with this connection. + Either 'aes128', 'aes256', 'rc4', or '3des'. + """ + if self.cipherSuite in CipherSuite.aes128Suites: + return "aes128" + elif self.cipherSuite in CipherSuite.aes256Suites: + return "aes256" + elif self.cipherSuite in CipherSuite.rc4Suites: + return "rc4" + elif self.cipherSuite in CipherSuite.tripleDESSuites: + return "3des" + else: + return None + + def _createSharedKey(self, sharedKeyUsername, sharedKey): + if len(sharedKeyUsername)>16: + raise ValueError() + if len(sharedKey)>47: + raise ValueError() + + self.sharedKeyUsername = sharedKeyUsername + + self.sessionID = createByteArrayZeros(16) + for x in range(len(sharedKeyUsername)): + self.sessionID[x] = ord(sharedKeyUsername[x]) + + premasterSecret = createByteArrayZeros(48) + sharedKey = chr(len(sharedKey)) + sharedKey + for x in range(48): + premasterSecret[x] = ord(sharedKey[x % len(sharedKey)]) + + self.masterSecret = PRF(premasterSecret, "shared secret", + createByteArraySequence([]), 48) + self.sharedKey = True + return self + + diff --git a/patches/gdata/tlslite/SessionCache.py b/patches/gdata/tlslite/SessionCache.py new file mode 100755 index 0000000..34cf0b0 --- /dev/null +++ b/patches/gdata/tlslite/SessionCache.py @@ -0,0 +1,103 @@ +"""Class for caching TLS sessions.""" + +import thread +import time + +class SessionCache: + """This class is used by the server to cache TLS sessions. + + Caching sessions allows the client to use TLS session resumption + and avoid the expense of a full handshake. To use this class, + simply pass a SessionCache instance into the server handshake + function. + + This class is thread-safe. + """ + + #References to these instances + #are also held by the caller, who may change the 'resumable' + #flag, so the SessionCache must return the same instances + #it was passed in. + + def __init__(self, maxEntries=10000, maxAge=14400): + """Create a new SessionCache. + + @type maxEntries: int + @param maxEntries: The maximum size of the cache. When this + limit is reached, the oldest sessions will be deleted as + necessary to make room for new ones. The default is 10000. + + @type maxAge: int + @param maxAge: The number of seconds before a session expires + from the cache. The default is 14400 (i.e. 4 hours).""" + + self.lock = thread.allocate_lock() + + # Maps sessionIDs to sessions + self.entriesDict = {} + + #Circular list of (sessionID, timestamp) pairs + self.entriesList = [(None,None)] * maxEntries + + self.firstIndex = 0 + self.lastIndex = 0 + self.maxAge = maxAge + + def __getitem__(self, sessionID): + self.lock.acquire() + try: + self._purge() #Delete old items, so we're assured of a new one + session = self.entriesDict[sessionID] + + #When we add sessions they're resumable, but it's possible + #for the session to be invalidated later on (if a fatal alert + #is returned), so we have to check for resumability before + #returning the session. + + if session.valid(): + return session + else: + raise KeyError() + finally: + self.lock.release() + + + def __setitem__(self, sessionID, session): + self.lock.acquire() + try: + #Add the new element + self.entriesDict[sessionID] = session + self.entriesList[self.lastIndex] = (sessionID, time.time()) + self.lastIndex = (self.lastIndex+1) % len(self.entriesList) + + #If the cache is full, we delete the oldest element to make an + #empty space + if self.lastIndex == self.firstIndex: + del(self.entriesDict[self.entriesList[self.firstIndex][0]]) + self.firstIndex = (self.firstIndex+1) % len(self.entriesList) + finally: + self.lock.release() + + #Delete expired items + def _purge(self): + currentTime = time.time() + + #Search through the circular list, deleting expired elements until + #we reach a non-expired element. Since elements in list are + #ordered in time, we can break once we reach the first non-expired + #element + index = self.firstIndex + while index != self.lastIndex: + if currentTime - self.entriesList[index][1] > self.maxAge: + del(self.entriesDict[self.entriesList[index][0]]) + index = (index+1) % len(self.entriesList) + else: + break + self.firstIndex = index + +def _test(): + import doctest, SessionCache + return doctest.testmod(SessionCache) + +if __name__ == "__main__": + _test() diff --git a/patches/gdata/tlslite/SharedKeyDB.py b/patches/gdata/tlslite/SharedKeyDB.py new file mode 100755 index 0000000..3246ec7 --- /dev/null +++ b/patches/gdata/tlslite/SharedKeyDB.py @@ -0,0 +1,58 @@ +"""Class for storing shared keys.""" + +from utils.cryptomath import * +from utils.compat import * +from mathtls import * +from Session import Session +from BaseDB import BaseDB + +class SharedKeyDB(BaseDB): + """This class represent an in-memory or on-disk database of shared + keys. + + A SharedKeyDB can be passed to a server handshake function to + authenticate a client based on one of the shared keys. + + This class is thread-safe. + """ + + def __init__(self, filename=None): + """Create a new SharedKeyDB. + + @type filename: str + @param filename: Filename for an on-disk database, or None for + an in-memory database. If the filename already exists, follow + this with a call to open(). To create a new on-disk database, + follow this with a call to create(). + """ + BaseDB.__init__(self, filename, "shared key") + + def _getItem(self, username, valueStr): + session = Session() + session._createSharedKey(username, valueStr) + return session + + def __setitem__(self, username, sharedKey): + """Add a shared key to the database. + + @type username: str + @param username: The username to associate the shared key with. + Must be less than or equal to 16 characters in length, and must + not already be in the database. + + @type sharedKey: str + @param sharedKey: The shared key to add. Must be less than 48 + characters in length. + """ + BaseDB.__setitem__(self, username, sharedKey) + + def _setItem(self, username, value): + if len(username)>16: + raise ValueError("username too long") + if len(value)>=48: + raise ValueError("shared key too long") + return value + + def _checkItem(self, value, username, param): + newSession = self._getItem(username, param) + return value.masterSecret == newSession.masterSecret \ No newline at end of file diff --git a/patches/gdata/tlslite/TLSConnection.py b/patches/gdata/tlslite/TLSConnection.py new file mode 100755 index 0000000..d125f8f --- /dev/null +++ b/patches/gdata/tlslite/TLSConnection.py @@ -0,0 +1,1600 @@ +""" +MAIN CLASS FOR TLS LITE (START HERE!). +""" +from __future__ import generators + +import socket +from utils.compat import formatExceptionTrace +from TLSRecordLayer import TLSRecordLayer +from Session import Session +from constants import * +from utils.cryptomath import getRandomBytes +from errors import * +from messages import * +from mathtls import * +from HandshakeSettings import HandshakeSettings + + +class TLSConnection(TLSRecordLayer): + """ + This class wraps a socket and provides TLS handshaking and data + transfer. + + To use this class, create a new instance, passing a connected + socket into the constructor. Then call some handshake function. + If the handshake completes without raising an exception, then a TLS + connection has been negotiated. You can transfer data over this + connection as if it were a socket. + + This class provides both synchronous and asynchronous versions of + its key functions. The synchronous versions should be used when + writing single-or multi-threaded code using blocking sockets. The + asynchronous versions should be used when performing asynchronous, + event-based I/O with non-blocking sockets. + + Asynchronous I/O is a complicated subject; typically, you should + not use the asynchronous functions directly, but should use some + framework like asyncore or Twisted which TLS Lite integrates with + (see + L{tlslite.integration.TLSAsyncDispatcherMixIn.TLSAsyncDispatcherMixIn} or + L{tlslite.integration.TLSTwistedProtocolWrapper.TLSTwistedProtocolWrapper}). + """ + + + def __init__(self, sock): + """Create a new TLSConnection instance. + + @param sock: The socket data will be transmitted on. The + socket should already be connected. It may be in blocking or + non-blocking mode. + + @type sock: L{socket.socket} + """ + TLSRecordLayer.__init__(self, sock) + + def handshakeClientSRP(self, username, password, session=None, + settings=None, checker=None, async=False): + """Perform an SRP handshake in the role of client. + + This function performs a TLS/SRP handshake. SRP mutually + authenticates both parties to each other using only a + username and password. This function may also perform a + combined SRP and server-certificate handshake, if the server + chooses to authenticate itself with a certificate chain in + addition to doing SRP. + + TLS/SRP is non-standard. Most TLS implementations don't + support it. See + U{http://www.ietf.org/html.charters/tls-charter.html} or + U{http://trevp.net/tlssrp/} for the latest information on + TLS/SRP. + + Like any handshake function, this can be called on a closed + TLS connection, or on a TLS connection that is already open. + If called on an open connection it performs a re-handshake. + + If the function completes without raising an exception, the + TLS connection will be open and available for data transfer. + + If an exception is raised, the connection will have been + automatically closed (if it was ever open). + + @type username: str + @param username: The SRP username. + + @type password: str + @param password: The SRP password. + + @type session: L{tlslite.Session.Session} + @param session: A TLS session to attempt to resume. This + session must be an SRP session performed with the same username + and password as were passed in. If the resumption does not + succeed, a full SRP handshake will be performed. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + + @type checker: L{tlslite.Checker.Checker} + @param checker: A Checker instance. This instance will be + invoked to examine the other party's authentication + credentials, if the handshake completes succesfully. + + @type async: bool + @param async: If False, this function will block until the + handshake is completed. If True, this function will return a + generator. Successive invocations of the generator will + return 0 if it is waiting to read from the socket, 1 if it is + waiting to write to the socket, or will raise StopIteration if + the handshake operation is completed. + + @rtype: None or an iterable + @return: If 'async' is True, a generator object will be + returned. + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + @raise tlslite.errors.TLSAuthenticationError: If the checker + doesn't like the other party's authentication credentials. + """ + handshaker = self._handshakeClientAsync(srpParams=(username, password), + session=session, settings=settings, checker=checker) + if async: + return handshaker + for result in handshaker: + pass + + def handshakeClientCert(self, certChain=None, privateKey=None, + session=None, settings=None, checker=None, + async=False): + """Perform a certificate-based handshake in the role of client. + + This function performs an SSL or TLS handshake. The server + will authenticate itself using an X.509 or cryptoID certificate + chain. If the handshake succeeds, the server's certificate + chain will be stored in the session's serverCertChain attribute. + Unless a checker object is passed in, this function does no + validation or checking of the server's certificate chain. + + If the server requests client authentication, the + client will send the passed-in certificate chain, and use the + passed-in private key to authenticate itself. If no + certificate chain and private key were passed in, the client + will attempt to proceed without client authentication. The + server may or may not allow this. + + Like any handshake function, this can be called on a closed + TLS connection, or on a TLS connection that is already open. + If called on an open connection it performs a re-handshake. + + If the function completes without raising an exception, the + TLS connection will be open and available for data transfer. + + If an exception is raised, the connection will have been + automatically closed (if it was ever open). + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: The certificate chain to be used if the + server requests client authentication. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: The private key to be used if the server + requests client authentication. + + @type session: L{tlslite.Session.Session} + @param session: A TLS session to attempt to resume. If the + resumption does not succeed, a full handshake will be + performed. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + + @type checker: L{tlslite.Checker.Checker} + @param checker: A Checker instance. This instance will be + invoked to examine the other party's authentication + credentials, if the handshake completes succesfully. + + @type async: bool + @param async: If False, this function will block until the + handshake is completed. If True, this function will return a + generator. Successive invocations of the generator will + return 0 if it is waiting to read from the socket, 1 if it is + waiting to write to the socket, or will raise StopIteration if + the handshake operation is completed. + + @rtype: None or an iterable + @return: If 'async' is True, a generator object will be + returned. + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + @raise tlslite.errors.TLSAuthenticationError: If the checker + doesn't like the other party's authentication credentials. + """ + handshaker = self._handshakeClientAsync(certParams=(certChain, + privateKey), session=session, settings=settings, + checker=checker) + if async: + return handshaker + for result in handshaker: + pass + + def handshakeClientUnknown(self, srpCallback=None, certCallback=None, + session=None, settings=None, checker=None, + async=False): + """Perform a to-be-determined type of handshake in the role of client. + + This function performs an SSL or TLS handshake. If the server + requests client certificate authentication, the + certCallback will be invoked and should return a (certChain, + privateKey) pair. If the callback returns None, the library + will attempt to proceed without client authentication. The + server may or may not allow this. + + If the server requests SRP authentication, the srpCallback + will be invoked and should return a (username, password) pair. + If the callback returns None, the local implementation will + signal a user_canceled error alert. + + After the handshake completes, the client can inspect the + connection's session attribute to determine what type of + authentication was performed. + + Like any handshake function, this can be called on a closed + TLS connection, or on a TLS connection that is already open. + If called on an open connection it performs a re-handshake. + + If the function completes without raising an exception, the + TLS connection will be open and available for data transfer. + + If an exception is raised, the connection will have been + automatically closed (if it was ever open). + + @type srpCallback: callable + @param srpCallback: The callback to be used if the server + requests SRP authentication. If None, the client will not + offer support for SRP ciphersuites. + + @type certCallback: callable + @param certCallback: The callback to be used if the server + requests client certificate authentication. + + @type session: L{tlslite.Session.Session} + @param session: A TLS session to attempt to resume. If the + resumption does not succeed, a full handshake will be + performed. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + + @type checker: L{tlslite.Checker.Checker} + @param checker: A Checker instance. This instance will be + invoked to examine the other party's authentication + credentials, if the handshake completes succesfully. + + @type async: bool + @param async: If False, this function will block until the + handshake is completed. If True, this function will return a + generator. Successive invocations of the generator will + return 0 if it is waiting to read from the socket, 1 if it is + waiting to write to the socket, or will raise StopIteration if + the handshake operation is completed. + + @rtype: None or an iterable + @return: If 'async' is True, a generator object will be + returned. + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + @raise tlslite.errors.TLSAuthenticationError: If the checker + doesn't like the other party's authentication credentials. + """ + handshaker = self._handshakeClientAsync(unknownParams=(srpCallback, + certCallback), session=session, settings=settings, + checker=checker) + if async: + return handshaker + for result in handshaker: + pass + + def handshakeClientSharedKey(self, username, sharedKey, settings=None, + checker=None, async=False): + """Perform a shared-key handshake in the role of client. + + This function performs a shared-key handshake. Using shared + symmetric keys of high entropy (128 bits or greater) mutually + authenticates both parties to each other. + + TLS with shared-keys is non-standard. Most TLS + implementations don't support it. See + U{http://www.ietf.org/html.charters/tls-charter.html} for the + latest information on TLS with shared-keys. If the shared-keys + Internet-Draft changes or is superceded, TLS Lite will track + those changes, so the shared-key support in later versions of + TLS Lite may become incompatible with this version. + + Like any handshake function, this can be called on a closed + TLS connection, or on a TLS connection that is already open. + If called on an open connection it performs a re-handshake. + + If the function completes without raising an exception, the + TLS connection will be open and available for data transfer. + + If an exception is raised, the connection will have been + automatically closed (if it was ever open). + + @type username: str + @param username: The shared-key username. + + @type sharedKey: str + @param sharedKey: The shared key. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + + @type checker: L{tlslite.Checker.Checker} + @param checker: A Checker instance. This instance will be + invoked to examine the other party's authentication + credentials, if the handshake completes succesfully. + + @type async: bool + @param async: If False, this function will block until the + handshake is completed. If True, this function will return a + generator. Successive invocations of the generator will + return 0 if it is waiting to read from the socket, 1 if it is + waiting to write to the socket, or will raise StopIteration if + the handshake operation is completed. + + @rtype: None or an iterable + @return: If 'async' is True, a generator object will be + returned. + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + @raise tlslite.errors.TLSAuthenticationError: If the checker + doesn't like the other party's authentication credentials. + """ + handshaker = self._handshakeClientAsync(sharedKeyParams=(username, + sharedKey), settings=settings, checker=checker) + if async: + return handshaker + for result in handshaker: + pass + + def _handshakeClientAsync(self, srpParams=(), certParams=(), + unknownParams=(), sharedKeyParams=(), + session=None, settings=None, checker=None, + recursive=False): + + handshaker = self._handshakeClientAsyncHelper(srpParams=srpParams, + certParams=certParams, unknownParams=unknownParams, + sharedKeyParams=sharedKeyParams, session=session, + settings=settings, recursive=recursive) + for result in self._handshakeWrapperAsync(handshaker, checker): + yield result + + + def _handshakeClientAsyncHelper(self, srpParams, certParams, unknownParams, + sharedKeyParams, session, settings, recursive): + if not recursive: + self._handshakeStart(client=True) + + #Unpack parameters + srpUsername = None # srpParams + password = None # srpParams + clientCertChain = None # certParams + privateKey = None # certParams + srpCallback = None # unknownParams + certCallback = None # unknownParams + #session # sharedKeyParams (or session) + #settings # settings + + if srpParams: + srpUsername, password = srpParams + elif certParams: + clientCertChain, privateKey = certParams + elif unknownParams: + srpCallback, certCallback = unknownParams + elif sharedKeyParams: + session = Session()._createSharedKey(*sharedKeyParams) + + if not settings: + settings = HandshakeSettings() + settings = settings._filter() + + #Validate parameters + if srpUsername and not password: + raise ValueError("Caller passed a username but no password") + if password and not srpUsername: + raise ValueError("Caller passed a password but no username") + + if clientCertChain and not privateKey: + raise ValueError("Caller passed a certChain but no privateKey") + if privateKey and not clientCertChain: + raise ValueError("Caller passed a privateKey but no certChain") + + if clientCertChain: + foundType = False + try: + import cryptoIDlib.CertChain + if isinstance(clientCertChain, cryptoIDlib.CertChain.CertChain): + if "cryptoID" not in settings.certificateTypes: + raise ValueError("Client certificate doesn't "\ + "match Handshake Settings") + settings.certificateTypes = ["cryptoID"] + foundType = True + except ImportError: + pass + if not foundType and isinstance(clientCertChain, + X509CertChain): + if "x509" not in settings.certificateTypes: + raise ValueError("Client certificate doesn't match "\ + "Handshake Settings") + settings.certificateTypes = ["x509"] + foundType = True + if not foundType: + raise ValueError("Unrecognized certificate type") + + + if session: + if not session.valid(): + session = None #ignore non-resumable sessions... + elif session.resumable and \ + (session.srpUsername != srpUsername): + raise ValueError("Session username doesn't match") + + #Add Faults to parameters + if srpUsername and self.fault == Fault.badUsername: + srpUsername += "GARBAGE" + if password and self.fault == Fault.badPassword: + password += "GARBAGE" + if sharedKeyParams: + identifier = sharedKeyParams[0] + sharedKey = sharedKeyParams[1] + if self.fault == Fault.badIdentifier: + identifier += "GARBAGE" + session = Session()._createSharedKey(identifier, sharedKey) + elif self.fault == Fault.badSharedKey: + sharedKey += "GARBAGE" + session = Session()._createSharedKey(identifier, sharedKey) + + + #Initialize locals + serverCertChain = None + cipherSuite = 0 + certificateType = CertificateType.x509 + premasterSecret = None + + #Get client nonce + clientRandom = getRandomBytes(32) + + #Initialize acceptable ciphersuites + cipherSuites = [] + if srpParams: + cipherSuites += CipherSuite.getSrpRsaSuites(settings.cipherNames) + cipherSuites += CipherSuite.getSrpSuites(settings.cipherNames) + elif certParams: + cipherSuites += CipherSuite.getRsaSuites(settings.cipherNames) + elif unknownParams: + if srpCallback: + cipherSuites += \ + CipherSuite.getSrpRsaSuites(settings.cipherNames) + cipherSuites += \ + CipherSuite.getSrpSuites(settings.cipherNames) + cipherSuites += CipherSuite.getRsaSuites(settings.cipherNames) + elif sharedKeyParams: + cipherSuites += CipherSuite.getRsaSuites(settings.cipherNames) + else: + cipherSuites += CipherSuite.getRsaSuites(settings.cipherNames) + + #Initialize acceptable certificate types + certificateTypes = settings._getCertificateTypes() + + #Tentatively set the version to the client's minimum version. + #We'll use this for the ClientHello, and if an error occurs + #parsing the Server Hello, we'll use this version for the response + self.version = settings.maxVersion + + #Either send ClientHello (with a resumable session)... + if session: + #If it's a resumable (i.e. not a shared-key session), then its + #ciphersuite must be one of the acceptable ciphersuites + if (not sharedKeyParams) and \ + session.cipherSuite not in cipherSuites: + raise ValueError("Session's cipher suite not consistent "\ + "with parameters") + else: + clientHello = ClientHello() + clientHello.create(settings.maxVersion, clientRandom, + session.sessionID, cipherSuites, + certificateTypes, session.srpUsername) + + #Or send ClientHello (without) + else: + clientHello = ClientHello() + clientHello.create(settings.maxVersion, clientRandom, + createByteArraySequence([]), cipherSuites, + certificateTypes, srpUsername) + for result in self._sendMsg(clientHello): + yield result + + #Get ServerHello (or missing_srp_username) + for result in self._getMsg((ContentType.handshake, + ContentType.alert), + HandshakeType.server_hello): + if result in (0,1): + yield result + else: + break + msg = result + + if isinstance(msg, ServerHello): + serverHello = msg + elif isinstance(msg, Alert): + alert = msg + + #If it's not a missing_srp_username, re-raise + if alert.description != AlertDescription.missing_srp_username: + self._shutdown(False) + raise TLSRemoteAlert(alert) + + #If we're not in SRP callback mode, we won't have offered SRP + #without a username, so we shouldn't get this alert + if not srpCallback: + for result in self._sendError(\ + AlertDescription.unexpected_message): + yield result + srpParams = srpCallback() + #If the callback returns None, cancel the handshake + if srpParams == None: + for result in self._sendError(AlertDescription.user_canceled): + yield result + + #Recursively perform handshake + for result in self._handshakeClientAsyncHelper(srpParams, + None, None, None, None, settings, True): + yield result + return + + #Get the server version. Do this before anything else, so any + #error alerts will use the server's version + self.version = serverHello.server_version + + #Future responses from server must use this version + self._versionCheck = True + + #Check ServerHello + if serverHello.server_version < settings.minVersion: + for result in self._sendError(\ + AlertDescription.protocol_version, + "Too old version: %s" % str(serverHello.server_version)): + yield result + if serverHello.server_version > settings.maxVersion: + for result in self._sendError(\ + AlertDescription.protocol_version, + "Too new version: %s" % str(serverHello.server_version)): + yield result + if serverHello.cipher_suite not in cipherSuites: + for result in self._sendError(\ + AlertDescription.illegal_parameter, + "Server responded with incorrect ciphersuite"): + yield result + if serverHello.certificate_type not in certificateTypes: + for result in self._sendError(\ + AlertDescription.illegal_parameter, + "Server responded with incorrect certificate type"): + yield result + if serverHello.compression_method != 0: + for result in self._sendError(\ + AlertDescription.illegal_parameter, + "Server responded with incorrect compression method"): + yield result + + #Get the server nonce + serverRandom = serverHello.random + + #If the server agrees to resume + if session and session.sessionID and \ + serverHello.session_id == session.sessionID: + + #If a shared-key, we're flexible about suites; otherwise the + #server-chosen suite has to match the session's suite + if sharedKeyParams: + session.cipherSuite = serverHello.cipher_suite + elif serverHello.cipher_suite != session.cipherSuite: + for result in self._sendError(\ + AlertDescription.illegal_parameter,\ + "Server's ciphersuite doesn't match session"): + yield result + + #Set the session for this connection + self.session = session + + #Calculate pending connection states + self._calcPendingStates(clientRandom, serverRandom, + settings.cipherImplementations) + + #Exchange ChangeCipherSpec and Finished messages + for result in self._getFinished(): + yield result + for result in self._sendFinished(): + yield result + + #Mark the connection as open + self._handshakeDone(resumed=True) + + #If server DOES NOT agree to resume + else: + + if sharedKeyParams: + for result in self._sendError(\ + AlertDescription.user_canceled, + "Was expecting a shared-key resumption"): + yield result + + #We've already validated these + cipherSuite = serverHello.cipher_suite + certificateType = serverHello.certificate_type + + #If the server chose an SRP suite... + if cipherSuite in CipherSuite.srpSuites: + #Get ServerKeyExchange, ServerHelloDone + for result in self._getMsg(ContentType.handshake, + HandshakeType.server_key_exchange, cipherSuite): + if result in (0,1): + yield result + else: + break + serverKeyExchange = result + + for result in self._getMsg(ContentType.handshake, + HandshakeType.server_hello_done): + if result in (0,1): + yield result + else: + break + serverHelloDone = result + + #If the server chose an SRP+RSA suite... + elif cipherSuite in CipherSuite.srpRsaSuites: + #Get Certificate, ServerKeyExchange, ServerHelloDone + for result in self._getMsg(ContentType.handshake, + HandshakeType.certificate, certificateType): + if result in (0,1): + yield result + else: + break + serverCertificate = result + + for result in self._getMsg(ContentType.handshake, + HandshakeType.server_key_exchange, cipherSuite): + if result in (0,1): + yield result + else: + break + serverKeyExchange = result + + for result in self._getMsg(ContentType.handshake, + HandshakeType.server_hello_done): + if result in (0,1): + yield result + else: + break + serverHelloDone = result + + #If the server chose an RSA suite... + elif cipherSuite in CipherSuite.rsaSuites: + #Get Certificate[, CertificateRequest], ServerHelloDone + for result in self._getMsg(ContentType.handshake, + HandshakeType.certificate, certificateType): + if result in (0,1): + yield result + else: + break + serverCertificate = result + + for result in self._getMsg(ContentType.handshake, + (HandshakeType.server_hello_done, + HandshakeType.certificate_request)): + if result in (0,1): + yield result + else: + break + msg = result + + certificateRequest = None + if isinstance(msg, CertificateRequest): + certificateRequest = msg + for result in self._getMsg(ContentType.handshake, + HandshakeType.server_hello_done): + if result in (0,1): + yield result + else: + break + serverHelloDone = result + elif isinstance(msg, ServerHelloDone): + serverHelloDone = msg + else: + raise AssertionError() + + + #Calculate SRP premaster secret, if server chose an SRP or + #SRP+RSA suite + if cipherSuite in CipherSuite.srpSuites + \ + CipherSuite.srpRsaSuites: + #Get and check the server's group parameters and B value + N = serverKeyExchange.srp_N + g = serverKeyExchange.srp_g + s = serverKeyExchange.srp_s + B = serverKeyExchange.srp_B + + if (g,N) not in goodGroupParameters: + for result in self._sendError(\ + AlertDescription.untrusted_srp_parameters, + "Unknown group parameters"): + yield result + if numBits(N) < settings.minKeySize: + for result in self._sendError(\ + AlertDescription.untrusted_srp_parameters, + "N value is too small: %d" % numBits(N)): + yield result + if numBits(N) > settings.maxKeySize: + for result in self._sendError(\ + AlertDescription.untrusted_srp_parameters, + "N value is too large: %d" % numBits(N)): + yield result + if B % N == 0: + for result in self._sendError(\ + AlertDescription.illegal_parameter, + "Suspicious B value"): + yield result + + #Check the server's signature, if server chose an + #SRP+RSA suite + if cipherSuite in CipherSuite.srpRsaSuites: + #Hash ServerKeyExchange/ServerSRPParams + hashBytes = serverKeyExchange.hash(clientRandom, + serverRandom) + + #Extract signature bytes from ServerKeyExchange + sigBytes = serverKeyExchange.signature + if len(sigBytes) == 0: + for result in self._sendError(\ + AlertDescription.illegal_parameter, + "Server sent an SRP ServerKeyExchange "\ + "message without a signature"): + yield result + + #Get server's public key from the Certificate message + for result in self._getKeyFromChain(serverCertificate, + settings): + if result in (0,1): + yield result + else: + break + publicKey, serverCertChain = result + + #Verify signature + if not publicKey.verify(sigBytes, hashBytes): + for result in self._sendError(\ + AlertDescription.decrypt_error, + "Signature failed to verify"): + yield result + + + #Calculate client's ephemeral DH values (a, A) + a = bytesToNumber(getRandomBytes(32)) + A = powMod(g, a, N) + + #Calculate client's static DH values (x, v) + x = makeX(bytesToString(s), srpUsername, password) + v = powMod(g, x, N) + + #Calculate u + u = makeU(N, A, B) + + #Calculate premaster secret + k = makeK(N, g) + S = powMod((B - (k*v)) % N, a+(u*x), N) + + if self.fault == Fault.badA: + A = N + S = 0 + premasterSecret = numberToBytes(S) + + #Send ClientKeyExchange + for result in self._sendMsg(\ + ClientKeyExchange(cipherSuite).createSRP(A)): + yield result + + + #Calculate RSA premaster secret, if server chose an RSA suite + elif cipherSuite in CipherSuite.rsaSuites: + + #Handle the presence of a CertificateRequest + if certificateRequest: + if unknownParams and certCallback: + certParamsNew = certCallback() + if certParamsNew: + clientCertChain, privateKey = certParamsNew + + #Get server's public key from the Certificate message + for result in self._getKeyFromChain(serverCertificate, + settings): + if result in (0,1): + yield result + else: + break + publicKey, serverCertChain = result + + + #Calculate premaster secret + premasterSecret = getRandomBytes(48) + premasterSecret[0] = settings.maxVersion[0] + premasterSecret[1] = settings.maxVersion[1] + + if self.fault == Fault.badPremasterPadding: + premasterSecret[0] = 5 + if self.fault == Fault.shortPremasterSecret: + premasterSecret = premasterSecret[:-1] + + #Encrypt premaster secret to server's public key + encryptedPreMasterSecret = publicKey.encrypt(premasterSecret) + + #If client authentication was requested, send Certificate + #message, either with certificates or empty + if certificateRequest: + clientCertificate = Certificate(certificateType) + + if clientCertChain: + #Check to make sure we have the same type of + #certificates the server requested + wrongType = False + if certificateType == CertificateType.x509: + if not isinstance(clientCertChain, X509CertChain): + wrongType = True + elif certificateType == CertificateType.cryptoID: + if not isinstance(clientCertChain, + cryptoIDlib.CertChain.CertChain): + wrongType = True + if wrongType: + for result in self._sendError(\ + AlertDescription.handshake_failure, + "Client certificate is of wrong type"): + yield result + + clientCertificate.create(clientCertChain) + + for result in self._sendMsg(clientCertificate): + yield result + else: + #The server didn't request client auth, so we + #zeroize these so the clientCertChain won't be + #stored in the session. + privateKey = None + clientCertChain = None + + #Send ClientKeyExchange + clientKeyExchange = ClientKeyExchange(cipherSuite, + self.version) + clientKeyExchange.createRSA(encryptedPreMasterSecret) + for result in self._sendMsg(clientKeyExchange): + yield result + + #If client authentication was requested and we have a + #private key, send CertificateVerify + if certificateRequest and privateKey: + if self.version == (3,0): + #Create a temporary session object, just for the + #purpose of creating the CertificateVerify + session = Session() + session._calcMasterSecret(self.version, + premasterSecret, + clientRandom, + serverRandom) + verifyBytes = self._calcSSLHandshakeHash(\ + session.masterSecret, "") + elif self.version in ((3,1), (3,2)): + verifyBytes = stringToBytes(\ + self._handshake_md5.digest() + \ + self._handshake_sha.digest()) + if self.fault == Fault.badVerifyMessage: + verifyBytes[0] = ((verifyBytes[0]+1) % 256) + signedBytes = privateKey.sign(verifyBytes) + certificateVerify = CertificateVerify() + certificateVerify.create(signedBytes) + for result in self._sendMsg(certificateVerify): + yield result + + + #Create the session object + self.session = Session() + self.session._calcMasterSecret(self.version, premasterSecret, + clientRandom, serverRandom) + self.session.sessionID = serverHello.session_id + self.session.cipherSuite = cipherSuite + self.session.srpUsername = srpUsername + self.session.clientCertChain = clientCertChain + self.session.serverCertChain = serverCertChain + + #Calculate pending connection states + self._calcPendingStates(clientRandom, serverRandom, + settings.cipherImplementations) + + #Exchange ChangeCipherSpec and Finished messages + for result in self._sendFinished(): + yield result + for result in self._getFinished(): + yield result + + #Mark the connection as open + self.session._setResumable(True) + self._handshakeDone(resumed=False) + + + + def handshakeServer(self, sharedKeyDB=None, verifierDB=None, + certChain=None, privateKey=None, reqCert=False, + sessionCache=None, settings=None, checker=None): + """Perform a handshake in the role of server. + + This function performs an SSL or TLS handshake. Depending on + the arguments and the behavior of the client, this function can + perform a shared-key, SRP, or certificate-based handshake. It + can also perform a combined SRP and server-certificate + handshake. + + Like any handshake function, this can be called on a closed + TLS connection, or on a TLS connection that is already open. + If called on an open connection it performs a re-handshake. + This function does not send a Hello Request message before + performing the handshake, so if re-handshaking is required, + the server must signal the client to begin the re-handshake + through some other means. + + If the function completes without raising an exception, the + TLS connection will be open and available for data transfer. + + If an exception is raised, the connection will have been + automatically closed (if it was ever open). + + @type sharedKeyDB: L{tlslite.SharedKeyDB.SharedKeyDB} + @param sharedKeyDB: A database of shared symmetric keys + associated with usernames. If the client performs a + shared-key handshake, the session's sharedKeyUsername + attribute will be set. + + @type verifierDB: L{tlslite.VerifierDB.VerifierDB} + @param verifierDB: A database of SRP password verifiers + associated with usernames. If the client performs an SRP + handshake, the session's srpUsername attribute will be set. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: The certificate chain to be used if the + client requests server certificate authentication. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: The private key to be used if the client + requests server certificate authentication. + + @type reqCert: bool + @param reqCert: Whether to request client certificate + authentication. This only applies if the client chooses server + certificate authentication; if the client chooses SRP or + shared-key authentication, this will be ignored. If the client + performs a client certificate authentication, the sessions's + clientCertChain attribute will be set. + + @type sessionCache: L{tlslite.SessionCache.SessionCache} + @param sessionCache: An in-memory cache of resumable sessions. + The client can resume sessions from this cache. Alternatively, + if the client performs a full handshake, a new session will be + added to the cache. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites and SSL/TLS version chosen by the server. + + @type checker: L{tlslite.Checker.Checker} + @param checker: A Checker instance. This instance will be + invoked to examine the other party's authentication + credentials, if the handshake completes succesfully. + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + @raise tlslite.errors.TLSAuthenticationError: If the checker + doesn't like the other party's authentication credentials. + """ + for result in self.handshakeServerAsync(sharedKeyDB, verifierDB, + certChain, privateKey, reqCert, sessionCache, settings, + checker): + pass + + + def handshakeServerAsync(self, sharedKeyDB=None, verifierDB=None, + certChain=None, privateKey=None, reqCert=False, + sessionCache=None, settings=None, checker=None): + """Start a server handshake operation on the TLS connection. + + This function returns a generator which behaves similarly to + handshakeServer(). Successive invocations of the generator + will return 0 if it is waiting to read from the socket, 1 if it is + waiting to write to the socket, or it will raise StopIteration + if the handshake operation is complete. + + @rtype: iterable + @return: A generator; see above for details. + """ + handshaker = self._handshakeServerAsyncHelper(\ + sharedKeyDB=sharedKeyDB, + verifierDB=verifierDB, certChain=certChain, + privateKey=privateKey, reqCert=reqCert, + sessionCache=sessionCache, settings=settings) + for result in self._handshakeWrapperAsync(handshaker, checker): + yield result + + + def _handshakeServerAsyncHelper(self, sharedKeyDB, verifierDB, + certChain, privateKey, reqCert, sessionCache, + settings): + + self._handshakeStart(client=False) + + if (not sharedKeyDB) and (not verifierDB) and (not certChain): + raise ValueError("Caller passed no authentication credentials") + if certChain and not privateKey: + raise ValueError("Caller passed a certChain but no privateKey") + if privateKey and not certChain: + raise ValueError("Caller passed a privateKey but no certChain") + + if not settings: + settings = HandshakeSettings() + settings = settings._filter() + + #Initialize acceptable cipher suites + cipherSuites = [] + if verifierDB: + if certChain: + cipherSuites += \ + CipherSuite.getSrpRsaSuites(settings.cipherNames) + cipherSuites += CipherSuite.getSrpSuites(settings.cipherNames) + if sharedKeyDB or certChain: + cipherSuites += CipherSuite.getRsaSuites(settings.cipherNames) + + #Initialize acceptable certificate type + certificateType = None + if certChain: + try: + import cryptoIDlib.CertChain + if isinstance(certChain, cryptoIDlib.CertChain.CertChain): + certificateType = CertificateType.cryptoID + except ImportError: + pass + if isinstance(certChain, X509CertChain): + certificateType = CertificateType.x509 + if certificateType == None: + raise ValueError("Unrecognized certificate type") + + #Initialize locals + clientCertChain = None + serverCertChain = None #We may set certChain to this later + postFinishedError = None + + #Tentatively set version to most-desirable version, so if an error + #occurs parsing the ClientHello, this is what we'll use for the + #error alert + self.version = settings.maxVersion + + #Get ClientHello + for result in self._getMsg(ContentType.handshake, + HandshakeType.client_hello): + if result in (0,1): + yield result + else: + break + clientHello = result + + #If client's version is too low, reject it + if clientHello.client_version < settings.minVersion: + self.version = settings.minVersion + for result in self._sendError(\ + AlertDescription.protocol_version, + "Too old version: %s" % str(clientHello.client_version)): + yield result + + #If client's version is too high, propose my highest version + elif clientHello.client_version > settings.maxVersion: + self.version = settings.maxVersion + + else: + #Set the version to the client's version + self.version = clientHello.client_version + + #Get the client nonce; create server nonce + clientRandom = clientHello.random + serverRandom = getRandomBytes(32) + + #Calculate the first cipher suite intersection. + #This is the 'privileged' ciphersuite. We'll use it if we're + #doing a shared-key resumption or a new negotiation. In fact, + #the only time we won't use it is if we're resuming a non-sharedkey + #session, in which case we use the ciphersuite from the session. + # + #Given the current ciphersuite ordering, this means we prefer SRP + #over non-SRP. + for cipherSuite in cipherSuites: + if cipherSuite in clientHello.cipher_suites: + break + else: + for result in self._sendError(\ + AlertDescription.handshake_failure): + yield result + + #If resumption was requested... + if clientHello.session_id and (sharedKeyDB or sessionCache): + session = None + + #Check in the sharedKeys container + if sharedKeyDB and len(clientHello.session_id)==16: + try: + #Trim off zero padding, if any + for x in range(16): + if clientHello.session_id[x]==0: + break + self.allegedSharedKeyUsername = bytesToString(\ + clientHello.session_id[:x]) + session = sharedKeyDB[self.allegedSharedKeyUsername] + if not session.sharedKey: + raise AssertionError() + #use privileged ciphersuite + session.cipherSuite = cipherSuite + except KeyError: + pass + + #Then check in the session cache + if sessionCache and not session: + try: + session = sessionCache[bytesToString(\ + clientHello.session_id)] + if session.sharedKey: + raise AssertionError() + if not session.resumable: + raise AssertionError() + #Check for consistency with ClientHello + if session.cipherSuite not in cipherSuites: + for result in self._sendError(\ + AlertDescription.handshake_failure): + yield result + if session.cipherSuite not in clientHello.cipher_suites: + for result in self._sendError(\ + AlertDescription.handshake_failure): + yield result + if clientHello.srp_username: + if clientHello.srp_username != session.srpUsername: + for result in self._sendError(\ + AlertDescription.handshake_failure): + yield result + except KeyError: + pass + + #If a session is found.. + if session: + #Set the session + self.session = session + + #Send ServerHello + serverHello = ServerHello() + serverHello.create(self.version, serverRandom, + session.sessionID, session.cipherSuite, + certificateType) + for result in self._sendMsg(serverHello): + yield result + + #From here on, the client's messages must have the right version + self._versionCheck = True + + #Calculate pending connection states + self._calcPendingStates(clientRandom, serverRandom, + settings.cipherImplementations) + + #Exchange ChangeCipherSpec and Finished messages + for result in self._sendFinished(): + yield result + for result in self._getFinished(): + yield result + + #Mark the connection as open + self._handshakeDone(resumed=True) + return + + + #If not a resumption... + + #TRICKY: we might have chosen an RSA suite that was only deemed + #acceptable because of the shared-key resumption. If the shared- + #key resumption failed, because the identifier wasn't recognized, + #we might fall through to here, where we have an RSA suite + #chosen, but no certificate. + if cipherSuite in CipherSuite.rsaSuites and not certChain: + for result in self._sendError(\ + AlertDescription.handshake_failure): + yield result + + #If an RSA suite is chosen, check for certificate type intersection + #(We do this check down here because if the mismatch occurs but the + # client is using a shared-key session, it's okay) + if cipherSuite in CipherSuite.rsaSuites + \ + CipherSuite.srpRsaSuites: + if certificateType not in clientHello.certificate_types: + for result in self._sendError(\ + AlertDescription.handshake_failure, + "the client doesn't support my certificate type"): + yield result + + #Move certChain -> serverCertChain, now that we're using it + serverCertChain = certChain + + + #Create sessionID + if sessionCache: + sessionID = getRandomBytes(32) + else: + sessionID = createByteArraySequence([]) + + #If we've selected an SRP suite, exchange keys and calculate + #premaster secret: + if cipherSuite in CipherSuite.srpSuites + CipherSuite.srpRsaSuites: + + #If there's no SRP username... + if not clientHello.srp_username: + + #Ask the client to re-send ClientHello with one + for result in self._sendMsg(Alert().create(\ + AlertDescription.missing_srp_username, + AlertLevel.warning)): + yield result + + #Get ClientHello + for result in self._getMsg(ContentType.handshake, + HandshakeType.client_hello): + if result in (0,1): + yield result + else: + break + clientHello = result + + #Check ClientHello + #If client's version is too low, reject it (COPIED CODE; BAD!) + if clientHello.client_version < settings.minVersion: + self.version = settings.minVersion + for result in self._sendError(\ + AlertDescription.protocol_version, + "Too old version: %s" % str(clientHello.client_version)): + yield result + + #If client's version is too high, propose my highest version + elif clientHello.client_version > settings.maxVersion: + self.version = settings.maxVersion + + else: + #Set the version to the client's version + self.version = clientHello.client_version + + #Recalculate the privileged cipher suite, making sure to + #pick an SRP suite + cipherSuites = [c for c in cipherSuites if c in \ + CipherSuite.srpSuites + \ + CipherSuite.srpRsaSuites] + for cipherSuite in cipherSuites: + if cipherSuite in clientHello.cipher_suites: + break + else: + for result in self._sendError(\ + AlertDescription.handshake_failure): + yield result + + #Get the client nonce; create server nonce + clientRandom = clientHello.random + serverRandom = getRandomBytes(32) + + #The username better be there, this time + if not clientHello.srp_username: + for result in self._sendError(\ + AlertDescription.illegal_parameter, + "Client resent a hello, but without the SRP"\ + " username"): + yield result + + + #Get username + self.allegedSrpUsername = clientHello.srp_username + + #Get parameters from username + try: + entry = verifierDB[self.allegedSrpUsername] + except KeyError: + for result in self._sendError(\ + AlertDescription.unknown_srp_username): + yield result + (N, g, s, v) = entry + + #Calculate server's ephemeral DH values (b, B) + b = bytesToNumber(getRandomBytes(32)) + k = makeK(N, g) + B = (powMod(g, b, N) + (k*v)) % N + + #Create ServerKeyExchange, signing it if necessary + serverKeyExchange = ServerKeyExchange(cipherSuite) + serverKeyExchange.createSRP(N, g, stringToBytes(s), B) + if cipherSuite in CipherSuite.srpRsaSuites: + hashBytes = serverKeyExchange.hash(clientRandom, + serverRandom) + serverKeyExchange.signature = privateKey.sign(hashBytes) + + #Send ServerHello[, Certificate], ServerKeyExchange, + #ServerHelloDone + msgs = [] + serverHello = ServerHello() + serverHello.create(self.version, serverRandom, sessionID, + cipherSuite, certificateType) + msgs.append(serverHello) + if cipherSuite in CipherSuite.srpRsaSuites: + certificateMsg = Certificate(certificateType) + certificateMsg.create(serverCertChain) + msgs.append(certificateMsg) + msgs.append(serverKeyExchange) + msgs.append(ServerHelloDone()) + for result in self._sendMsgs(msgs): + yield result + + #From here on, the client's messages must have the right version + self._versionCheck = True + + #Get and check ClientKeyExchange + for result in self._getMsg(ContentType.handshake, + HandshakeType.client_key_exchange, + cipherSuite): + if result in (0,1): + yield result + else: + break + clientKeyExchange = result + A = clientKeyExchange.srp_A + if A % N == 0: + postFinishedError = (AlertDescription.illegal_parameter, + "Suspicious A value") + #Calculate u + u = makeU(N, A, B) + + #Calculate premaster secret + S = powMod((A * powMod(v,u,N)) % N, b, N) + premasterSecret = numberToBytes(S) + + + #If we've selected an RSA suite, exchange keys and calculate + #premaster secret: + elif cipherSuite in CipherSuite.rsaSuites: + + #Send ServerHello, Certificate[, CertificateRequest], + #ServerHelloDone + msgs = [] + msgs.append(ServerHello().create(self.version, serverRandom, + sessionID, cipherSuite, certificateType)) + msgs.append(Certificate(certificateType).create(serverCertChain)) + if reqCert: + msgs.append(CertificateRequest()) + msgs.append(ServerHelloDone()) + for result in self._sendMsgs(msgs): + yield result + + #From here on, the client's messages must have the right version + self._versionCheck = True + + #Get [Certificate,] (if was requested) + if reqCert: + if self.version == (3,0): + for result in self._getMsg((ContentType.handshake, + ContentType.alert), + HandshakeType.certificate, + certificateType): + if result in (0,1): + yield result + else: + break + msg = result + + if isinstance(msg, Alert): + #If it's not a no_certificate alert, re-raise + alert = msg + if alert.description != \ + AlertDescription.no_certificate: + self._shutdown(False) + raise TLSRemoteAlert(alert) + elif isinstance(msg, Certificate): + clientCertificate = msg + if clientCertificate.certChain and \ + clientCertificate.certChain.getNumCerts()!=0: + clientCertChain = clientCertificate.certChain + else: + raise AssertionError() + elif self.version in ((3,1), (3,2)): + for result in self._getMsg(ContentType.handshake, + HandshakeType.certificate, + certificateType): + if result in (0,1): + yield result + else: + break + clientCertificate = result + if clientCertificate.certChain and \ + clientCertificate.certChain.getNumCerts()!=0: + clientCertChain = clientCertificate.certChain + else: + raise AssertionError() + + #Get ClientKeyExchange + for result in self._getMsg(ContentType.handshake, + HandshakeType.client_key_exchange, + cipherSuite): + if result in (0,1): + yield result + else: + break + clientKeyExchange = result + + #Decrypt ClientKeyExchange + premasterSecret = privateKey.decrypt(\ + clientKeyExchange.encryptedPreMasterSecret) + + randomPreMasterSecret = getRandomBytes(48) + versionCheck = (premasterSecret[0], premasterSecret[1]) + if not premasterSecret: + premasterSecret = randomPreMasterSecret + elif len(premasterSecret)!=48: + premasterSecret = randomPreMasterSecret + elif versionCheck != clientHello.client_version: + if versionCheck != self.version: #Tolerate buggy IE clients + premasterSecret = randomPreMasterSecret + + #Get and check CertificateVerify, if relevant + if clientCertChain: + if self.version == (3,0): + #Create a temporary session object, just for the purpose + #of checking the CertificateVerify + session = Session() + session._calcMasterSecret(self.version, premasterSecret, + clientRandom, serverRandom) + verifyBytes = self._calcSSLHandshakeHash(\ + session.masterSecret, "") + elif self.version in ((3,1), (3,2)): + verifyBytes = stringToBytes(self._handshake_md5.digest() +\ + self._handshake_sha.digest()) + for result in self._getMsg(ContentType.handshake, + HandshakeType.certificate_verify): + if result in (0,1): + yield result + else: + break + certificateVerify = result + publicKey = clientCertChain.getEndEntityPublicKey() + if len(publicKey) < settings.minKeySize: + postFinishedError = (AlertDescription.handshake_failure, + "Client's public key too small: %d" % len(publicKey)) + if len(publicKey) > settings.maxKeySize: + postFinishedError = (AlertDescription.handshake_failure, + "Client's public key too large: %d" % len(publicKey)) + + if not publicKey.verify(certificateVerify.signature, + verifyBytes): + postFinishedError = (AlertDescription.decrypt_error, + "Signature failed to verify") + + + #Create the session object + self.session = Session() + self.session._calcMasterSecret(self.version, premasterSecret, + clientRandom, serverRandom) + self.session.sessionID = sessionID + self.session.cipherSuite = cipherSuite + self.session.srpUsername = self.allegedSrpUsername + self.session.clientCertChain = clientCertChain + self.session.serverCertChain = serverCertChain + + #Calculate pending connection states + self._calcPendingStates(clientRandom, serverRandom, + settings.cipherImplementations) + + #Exchange ChangeCipherSpec and Finished messages + for result in self._getFinished(): + yield result + + #If we were holding a post-finished error until receiving the client + #finished message, send it now. We delay the call until this point + #because calling sendError() throws an exception, and our caller might + #shut down the socket upon receiving the exception. If he did, and the + #client was still sending its ChangeCipherSpec or Finished messages, it + #would cause a socket error on the client side. This is a lot of + #consideration to show to misbehaving clients, but this would also + #cause problems with fault-testing. + if postFinishedError: + for result in self._sendError(*postFinishedError): + yield result + + for result in self._sendFinished(): + yield result + + #Add the session object to the session cache + if sessionCache and sessionID: + sessionCache[bytesToString(sessionID)] = self.session + + #Mark the connection as open + self.session._setResumable(True) + self._handshakeDone(resumed=False) + + + def _handshakeWrapperAsync(self, handshaker, checker): + if not self.fault: + try: + for result in handshaker: + yield result + if checker: + try: + checker(self) + except TLSAuthenticationError: + alert = Alert().create(AlertDescription.close_notify, + AlertLevel.fatal) + for result in self._sendMsg(alert): + yield result + raise + except: + self._shutdown(False) + raise + else: + try: + for result in handshaker: + yield result + if checker: + try: + checker(self) + except TLSAuthenticationError: + alert = Alert().create(AlertDescription.close_notify, + AlertLevel.fatal) + for result in self._sendMsg(alert): + yield result + raise + except socket.error, e: + raise TLSFaultError("socket error!") + except TLSAbruptCloseError, e: + raise TLSFaultError("abrupt close error!") + except TLSAlert, alert: + if alert.description not in Fault.faultAlerts[self.fault]: + raise TLSFaultError(str(alert)) + else: + pass + except: + self._shutdown(False) + raise + else: + raise TLSFaultError("No error!") + + + def _getKeyFromChain(self, certificate, settings): + #Get and check cert chain from the Certificate message + certChain = certificate.certChain + if not certChain or certChain.getNumCerts() == 0: + for result in self._sendError(AlertDescription.illegal_parameter, + "Other party sent a Certificate message without "\ + "certificates"): + yield result + + #Get and check public key from the cert chain + publicKey = certChain.getEndEntityPublicKey() + if len(publicKey) < settings.minKeySize: + for result in self._sendError(AlertDescription.handshake_failure, + "Other party's public key too small: %d" % len(publicKey)): + yield result + if len(publicKey) > settings.maxKeySize: + for result in self._sendError(AlertDescription.handshake_failure, + "Other party's public key too large: %d" % len(publicKey)): + yield result + + yield publicKey, certChain diff --git a/patches/gdata/tlslite/TLSRecordLayer.py b/patches/gdata/tlslite/TLSRecordLayer.py new file mode 100755 index 0000000..875ce80 --- /dev/null +++ b/patches/gdata/tlslite/TLSRecordLayer.py @@ -0,0 +1,1123 @@ +"""Helper class for TLSConnection.""" +from __future__ import generators + +from utils.compat import * +from utils.cryptomath import * +from utils.cipherfactory import createAES, createRC4, createTripleDES +from utils.codec import * +from errors import * +from messages import * +from mathtls import * +from constants import * +from utils.cryptomath import getRandomBytes +from utils import hmac +from FileObject import FileObject +import sha +import md5 +import socket +import errno +import traceback + +class _ConnectionState: + def __init__(self): + self.macContext = None + self.encContext = None + self.seqnum = 0 + + def getSeqNumStr(self): + w = Writer(8) + w.add(self.seqnum, 8) + seqnumStr = bytesToString(w.bytes) + self.seqnum += 1 + return seqnumStr + + +class TLSRecordLayer: + """ + This class handles data transmission for a TLS connection. + + Its only subclass is L{tlslite.TLSConnection.TLSConnection}. We've + separated the code in this class from TLSConnection to make things + more readable. + + + @type sock: socket.socket + @ivar sock: The underlying socket object. + + @type session: L{tlslite.Session.Session} + @ivar session: The session corresponding to this connection. + + Due to TLS session resumption, multiple connections can correspond + to the same underlying session. + + @type version: tuple + @ivar version: The TLS version being used for this connection. + + (3,0) means SSL 3.0, and (3,1) means TLS 1.0. + + @type closed: bool + @ivar closed: If this connection is closed. + + @type resumed: bool + @ivar resumed: If this connection is based on a resumed session. + + @type allegedSharedKeyUsername: str or None + @ivar allegedSharedKeyUsername: This is set to the shared-key + username asserted by the client, whether the handshake succeeded or + not. If the handshake fails, this can be inspected to + determine if a guessing attack is in progress against a particular + user account. + + @type allegedSrpUsername: str or None + @ivar allegedSrpUsername: This is set to the SRP username + asserted by the client, whether the handshake succeeded or not. + If the handshake fails, this can be inspected to determine + if a guessing attack is in progress against a particular user + account. + + @type closeSocket: bool + @ivar closeSocket: If the socket should be closed when the + connection is closed (writable). + + If you set this to True, TLS Lite will assume the responsibility of + closing the socket when the TLS Connection is shutdown (either + through an error or through the user calling close()). The default + is False. + + @type ignoreAbruptClose: bool + @ivar ignoreAbruptClose: If an abrupt close of the socket should + raise an error (writable). + + If you set this to True, TLS Lite will not raise a + L{tlslite.errors.TLSAbruptCloseError} exception if the underlying + socket is unexpectedly closed. Such an unexpected closure could be + caused by an attacker. However, it also occurs with some incorrect + TLS implementations. + + You should set this to True only if you're not worried about an + attacker truncating the connection, and only if necessary to avoid + spurious errors. The default is False. + + @sort: __init__, read, readAsync, write, writeAsync, close, closeAsync, + getCipherImplementation, getCipherName + """ + + def __init__(self, sock): + self.sock = sock + + #My session object (Session instance; read-only) + self.session = None + + #Am I a client or server? + self._client = None + + #Buffers for processing messages + self._handshakeBuffer = [] + self._readBuffer = "" + + #Handshake digests + self._handshake_md5 = md5.md5() + self._handshake_sha = sha.sha() + + #TLS Protocol Version + self.version = (0,0) #read-only + self._versionCheck = False #Once we choose a version, this is True + + #Current and Pending connection states + self._writeState = _ConnectionState() + self._readState = _ConnectionState() + self._pendingWriteState = _ConnectionState() + self._pendingReadState = _ConnectionState() + + #Is the connection open? + self.closed = True #read-only + self._refCount = 0 #Used to trigger closure + + #Is this a resumed (or shared-key) session? + self.resumed = False #read-only + + #What username did the client claim in his handshake? + self.allegedSharedKeyUsername = None + self.allegedSrpUsername = None + + #On a call to close(), do we close the socket? (writeable) + self.closeSocket = False + + #If the socket is abruptly closed, do we ignore it + #and pretend the connection was shut down properly? (writeable) + self.ignoreAbruptClose = False + + #Fault we will induce, for testing purposes + self.fault = None + + #********************************************************* + # Public Functions START + #********************************************************* + + def read(self, max=None, min=1): + """Read some data from the TLS connection. + + This function will block until at least 'min' bytes are + available (or the connection is closed). + + If an exception is raised, the connection will have been + automatically closed. + + @type max: int + @param max: The maximum number of bytes to return. + + @type min: int + @param min: The minimum number of bytes to return + + @rtype: str + @return: A string of no more than 'max' bytes, and no fewer + than 'min' (unless the connection has been closed, in which + case fewer than 'min' bytes may be returned). + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + """ + for result in self.readAsync(max, min): + pass + return result + + def readAsync(self, max=None, min=1): + """Start a read operation on the TLS connection. + + This function returns a generator which behaves similarly to + read(). Successive invocations of the generator will return 0 + if it is waiting to read from the socket, 1 if it is waiting + to write to the socket, or a string if the read operation has + completed. + + @rtype: iterable + @return: A generator; see above for details. + """ + try: + while len(self._readBuffer)= len(s): + break + if endIndex > len(s): + endIndex = len(s) + block = stringToBytes(s[startIndex : endIndex]) + applicationData = ApplicationData().create(block) + for result in self._sendMsg(applicationData, skipEmptyFrag): + yield result + skipEmptyFrag = True #only send an empy fragment on 1st message + index += 1 + except: + self._shutdown(False) + raise + + def close(self): + """Close the TLS connection. + + This function will block until it has exchanged close_notify + alerts with the other party. After doing so, it will shut down the + TLS connection. Further attempts to read through this connection + will return "". Further attempts to write through this connection + will raise ValueError. + + If makefile() has been called on this connection, the connection + will be not be closed until the connection object and all file + objects have been closed. + + Even if an exception is raised, the connection will have been + closed. + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + """ + if not self.closed: + for result in self._decrefAsync(): + pass + + def closeAsync(self): + """Start a close operation on the TLS connection. + + This function returns a generator which behaves similarly to + close(). Successive invocations of the generator will return 0 + if it is waiting to read from the socket, 1 if it is waiting + to write to the socket, or will raise StopIteration if the + close operation has completed. + + @rtype: iterable + @return: A generator; see above for details. + """ + if not self.closed: + for result in self._decrefAsync(): + yield result + + def _decrefAsync(self): + self._refCount -= 1 + if self._refCount == 0 and not self.closed: + try: + for result in self._sendMsg(Alert().create(\ + AlertDescription.close_notify, AlertLevel.warning)): + yield result + alert = None + while not alert: + for result in self._getMsg((ContentType.alert, \ + ContentType.application_data)): + if result in (0,1): + yield result + if result.contentType == ContentType.alert: + alert = result + if alert.description == AlertDescription.close_notify: + self._shutdown(True) + else: + raise TLSRemoteAlert(alert) + except (socket.error, TLSAbruptCloseError): + #If the other side closes the socket, that's okay + self._shutdown(True) + except: + self._shutdown(False) + raise + + def getCipherName(self): + """Get the name of the cipher used with this connection. + + @rtype: str + @return: The name of the cipher used with this connection. + Either 'aes128', 'aes256', 'rc4', or '3des'. + """ + if not self._writeState.encContext: + return None + return self._writeState.encContext.name + + def getCipherImplementation(self): + """Get the name of the cipher implementation used with + this connection. + + @rtype: str + @return: The name of the cipher implementation used with + this connection. Either 'python', 'cryptlib', 'openssl', + or 'pycrypto'. + """ + if not self._writeState.encContext: + return None + return self._writeState.encContext.implementation + + + + #Emulate a socket, somewhat - + def send(self, s): + """Send data to the TLS connection (socket emulation). + + @raise socket.error: If a socket error occurs. + """ + self.write(s) + return len(s) + + def sendall(self, s): + """Send data to the TLS connection (socket emulation). + + @raise socket.error: If a socket error occurs. + """ + self.write(s) + + def recv(self, bufsize): + """Get some data from the TLS connection (socket emulation). + + @raise socket.error: If a socket error occurs. + @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed + without a preceding alert. + @raise tlslite.errors.TLSAlert: If a TLS alert is signalled. + """ + return self.read(bufsize) + + def makefile(self, mode='r', bufsize=-1): + """Create a file object for the TLS connection (socket emulation). + + @rtype: L{tlslite.FileObject.FileObject} + """ + self._refCount += 1 + return FileObject(self, mode, bufsize) + + def getsockname(self): + """Return the socket's own address (socket emulation).""" + return self.sock.getsockname() + + def getpeername(self): + """Return the remote address to which the socket is connected + (socket emulation).""" + return self.sock.getpeername() + + def settimeout(self, value): + """Set a timeout on blocking socket operations (socket emulation).""" + return self.sock.settimeout(value) + + def gettimeout(self): + """Return the timeout associated with socket operations (socket + emulation).""" + return self.sock.gettimeout() + + def setsockopt(self, level, optname, value): + """Set the value of the given socket option (socket emulation).""" + return self.sock.setsockopt(level, optname, value) + + + #********************************************************* + # Public Functions END + #********************************************************* + + def _shutdown(self, resumable): + self._writeState = _ConnectionState() + self._readState = _ConnectionState() + #Don't do this: self._readBuffer = "" + self.version = (0,0) + self._versionCheck = False + self.closed = True + if self.closeSocket: + self.sock.close() + + #Even if resumable is False, we'll never toggle this on + if not resumable and self.session: + self.session.resumable = False + + + def _sendError(self, alertDescription, errorStr=None): + alert = Alert().create(alertDescription, AlertLevel.fatal) + for result in self._sendMsg(alert): + yield result + self._shutdown(False) + raise TLSLocalAlert(alert, errorStr) + + def _sendMsgs(self, msgs): + skipEmptyFrag = False + for msg in msgs: + for result in self._sendMsg(msg, skipEmptyFrag): + yield result + skipEmptyFrag = True + + def _sendMsg(self, msg, skipEmptyFrag=False): + bytes = msg.write() + contentType = msg.contentType + + #Whenever we're connected and asked to send a message, + #we first send an empty Application Data message. This prevents + #an attacker from launching a chosen-plaintext attack based on + #knowing the next IV. + if not self.closed and not skipEmptyFrag and self.version == (3,1): + if self._writeState.encContext: + if self._writeState.encContext.isBlockCipher: + for result in self._sendMsg(ApplicationData(), + skipEmptyFrag=True): + yield result + + #Update handshake hashes + if contentType == ContentType.handshake: + bytesStr = bytesToString(bytes) + self._handshake_md5.update(bytesStr) + self._handshake_sha.update(bytesStr) + + #Calculate MAC + if self._writeState.macContext: + seqnumStr = self._writeState.getSeqNumStr() + bytesStr = bytesToString(bytes) + mac = self._writeState.macContext.copy() + mac.update(seqnumStr) + mac.update(chr(contentType)) + if self.version == (3,0): + mac.update( chr( int(len(bytes)/256) ) ) + mac.update( chr( int(len(bytes)%256) ) ) + elif self.version in ((3,1), (3,2)): + mac.update(chr(self.version[0])) + mac.update(chr(self.version[1])) + mac.update( chr( int(len(bytes)/256) ) ) + mac.update( chr( int(len(bytes)%256) ) ) + else: + raise AssertionError() + mac.update(bytesStr) + macString = mac.digest() + macBytes = stringToBytes(macString) + if self.fault == Fault.badMAC: + macBytes[0] = (macBytes[0]+1) % 256 + + #Encrypt for Block or Stream Cipher + if self._writeState.encContext: + #Add padding and encrypt (for Block Cipher): + if self._writeState.encContext.isBlockCipher: + + #Add TLS 1.1 fixed block + if self.version == (3,2): + bytes = self.fixedIVBlock + bytes + + #Add padding: bytes = bytes + (macBytes + paddingBytes) + currentLength = len(bytes) + len(macBytes) + 1 + blockLength = self._writeState.encContext.block_size + paddingLength = blockLength-(currentLength % blockLength) + + paddingBytes = createByteArraySequence([paddingLength] * \ + (paddingLength+1)) + if self.fault == Fault.badPadding: + paddingBytes[0] = (paddingBytes[0]+1) % 256 + endBytes = concatArrays(macBytes, paddingBytes) + bytes = concatArrays(bytes, endBytes) + #Encrypt + plaintext = stringToBytes(bytes) + ciphertext = self._writeState.encContext.encrypt(plaintext) + bytes = stringToBytes(ciphertext) + + #Encrypt (for Stream Cipher) + else: + bytes = concatArrays(bytes, macBytes) + plaintext = bytesToString(bytes) + ciphertext = self._writeState.encContext.encrypt(plaintext) + bytes = stringToBytes(ciphertext) + + #Add record header and send + r = RecordHeader3().create(self.version, contentType, len(bytes)) + s = bytesToString(concatArrays(r.write(), bytes)) + while 1: + try: + bytesSent = self.sock.send(s) #Might raise socket.error + except socket.error, why: + if why[0] == errno.EWOULDBLOCK: + yield 1 + continue + else: + raise + if bytesSent == len(s): + return + s = s[bytesSent:] + yield 1 + + + def _getMsg(self, expectedType, secondaryType=None, constructorType=None): + try: + if not isinstance(expectedType, tuple): + expectedType = (expectedType,) + + #Spin in a loop, until we've got a non-empty record of a type we + #expect. The loop will be repeated if: + # - we receive a renegotiation attempt; we send no_renegotiation, + # then try again + # - we receive an empty application-data fragment; we try again + while 1: + for result in self._getNextRecord(): + if result in (0,1): + yield result + recordHeader, p = result + + #If this is an empty application-data fragment, try again + if recordHeader.type == ContentType.application_data: + if p.index == len(p.bytes): + continue + + #If we received an unexpected record type... + if recordHeader.type not in expectedType: + + #If we received an alert... + if recordHeader.type == ContentType.alert: + alert = Alert().parse(p) + + #We either received a fatal error, a warning, or a + #close_notify. In any case, we're going to close the + #connection. In the latter two cases we respond with + #a close_notify, but ignore any socket errors, since + #the other side might have already closed the socket. + if alert.level == AlertLevel.warning or \ + alert.description == AlertDescription.close_notify: + + #If the sendMsg() call fails because the socket has + #already been closed, we will be forgiving and not + #report the error nor invalidate the "resumability" + #of the session. + try: + alertMsg = Alert() + alertMsg.create(AlertDescription.close_notify, + AlertLevel.warning) + for result in self._sendMsg(alertMsg): + yield result + except socket.error: + pass + + if alert.description == \ + AlertDescription.close_notify: + self._shutdown(True) + elif alert.level == AlertLevel.warning: + self._shutdown(False) + + else: #Fatal alert: + self._shutdown(False) + + #Raise the alert as an exception + raise TLSRemoteAlert(alert) + + #If we received a renegotiation attempt... + if recordHeader.type == ContentType.handshake: + subType = p.get(1) + reneg = False + if self._client: + if subType == HandshakeType.hello_request: + reneg = True + else: + if subType == HandshakeType.client_hello: + reneg = True + #Send no_renegotiation, then try again + if reneg: + alertMsg = Alert() + alertMsg.create(AlertDescription.no_renegotiation, + AlertLevel.warning) + for result in self._sendMsg(alertMsg): + yield result + continue + + #Otherwise: this is an unexpected record, but neither an + #alert nor renegotiation + for result in self._sendError(\ + AlertDescription.unexpected_message, + "received type=%d" % recordHeader.type): + yield result + + break + + #Parse based on content_type + if recordHeader.type == ContentType.change_cipher_spec: + yield ChangeCipherSpec().parse(p) + elif recordHeader.type == ContentType.alert: + yield Alert().parse(p) + elif recordHeader.type == ContentType.application_data: + yield ApplicationData().parse(p) + elif recordHeader.type == ContentType.handshake: + #Convert secondaryType to tuple, if it isn't already + if not isinstance(secondaryType, tuple): + secondaryType = (secondaryType,) + + #If it's a handshake message, check handshake header + if recordHeader.ssl2: + subType = p.get(1) + if subType != HandshakeType.client_hello: + for result in self._sendError(\ + AlertDescription.unexpected_message, + "Can only handle SSLv2 ClientHello messages"): + yield result + if HandshakeType.client_hello not in secondaryType: + for result in self._sendError(\ + AlertDescription.unexpected_message): + yield result + subType = HandshakeType.client_hello + else: + subType = p.get(1) + if subType not in secondaryType: + for result in self._sendError(\ + AlertDescription.unexpected_message, + "Expecting %s, got %s" % (str(secondaryType), subType)): + yield result + + #Update handshake hashes + sToHash = bytesToString(p.bytes) + self._handshake_md5.update(sToHash) + self._handshake_sha.update(sToHash) + + #Parse based on handshake type + if subType == HandshakeType.client_hello: + yield ClientHello(recordHeader.ssl2).parse(p) + elif subType == HandshakeType.server_hello: + yield ServerHello().parse(p) + elif subType == HandshakeType.certificate: + yield Certificate(constructorType).parse(p) + elif subType == HandshakeType.certificate_request: + yield CertificateRequest().parse(p) + elif subType == HandshakeType.certificate_verify: + yield CertificateVerify().parse(p) + elif subType == HandshakeType.server_key_exchange: + yield ServerKeyExchange(constructorType).parse(p) + elif subType == HandshakeType.server_hello_done: + yield ServerHelloDone().parse(p) + elif subType == HandshakeType.client_key_exchange: + yield ClientKeyExchange(constructorType, \ + self.version).parse(p) + elif subType == HandshakeType.finished: + yield Finished(self.version).parse(p) + else: + raise AssertionError() + + #If an exception was raised by a Parser or Message instance: + except SyntaxError, e: + for result in self._sendError(AlertDescription.decode_error, + formatExceptionTrace(e)): + yield result + + + #Returns next record or next handshake message + def _getNextRecord(self): + + #If there's a handshake message waiting, return it + if self._handshakeBuffer: + recordHeader, bytes = self._handshakeBuffer[0] + self._handshakeBuffer = self._handshakeBuffer[1:] + yield (recordHeader, Parser(bytes)) + return + + #Otherwise... + #Read the next record header + bytes = createByteArraySequence([]) + recordHeaderLength = 1 + ssl2 = False + while 1: + try: + s = self.sock.recv(recordHeaderLength-len(bytes)) + except socket.error, why: + if why[0] == errno.EWOULDBLOCK: + yield 0 + continue + else: + raise + + #If the connection was abruptly closed, raise an error + if len(s)==0: + raise TLSAbruptCloseError() + + bytes += stringToBytes(s) + if len(bytes)==1: + if bytes[0] in ContentType.all: + ssl2 = False + recordHeaderLength = 5 + elif bytes[0] == 128: + ssl2 = True + recordHeaderLength = 2 + else: + raise SyntaxError() + if len(bytes) == recordHeaderLength: + break + + #Parse the record header + if ssl2: + r = RecordHeader2().parse(Parser(bytes)) + else: + r = RecordHeader3().parse(Parser(bytes)) + + #Check the record header fields + if r.length > 18432: + for result in self._sendError(AlertDescription.record_overflow): + yield result + + #Read the record contents + bytes = createByteArraySequence([]) + while 1: + try: + s = self.sock.recv(r.length - len(bytes)) + except socket.error, why: + if why[0] == errno.EWOULDBLOCK: + yield 0 + continue + else: + raise + + #If the connection is closed, raise a socket error + if len(s)==0: + raise TLSAbruptCloseError() + + bytes += stringToBytes(s) + if len(bytes) == r.length: + break + + #Check the record header fields (2) + #We do this after reading the contents from the socket, so that + #if there's an error, we at least don't leave extra bytes in the + #socket.. + # + # THIS CHECK HAS NO SECURITY RELEVANCE (?), BUT COULD HURT INTEROP. + # SO WE LEAVE IT OUT FOR NOW. + # + #if self._versionCheck and r.version != self.version: + # for result in self._sendError(AlertDescription.protocol_version, + # "Version in header field: %s, should be %s" % (str(r.version), + # str(self.version))): + # yield result + + #Decrypt the record + for result in self._decryptRecord(r.type, bytes): + if result in (0,1): + yield result + else: + break + bytes = result + p = Parser(bytes) + + #If it doesn't contain handshake messages, we can just return it + if r.type != ContentType.handshake: + yield (r, p) + #If it's an SSLv2 ClientHello, we can return it as well + elif r.ssl2: + yield (r, p) + else: + #Otherwise, we loop through and add the handshake messages to the + #handshake buffer + while 1: + if p.index == len(bytes): #If we're at the end + if not self._handshakeBuffer: + for result in self._sendError(\ + AlertDescription.decode_error, \ + "Received empty handshake record"): + yield result + break + #There needs to be at least 4 bytes to get a header + if p.index+4 > len(bytes): + for result in self._sendError(\ + AlertDescription.decode_error, + "A record has a partial handshake message (1)"): + yield result + p.get(1) # skip handshake type + msgLength = p.get(3) + if p.index+msgLength > len(bytes): + for result in self._sendError(\ + AlertDescription.decode_error, + "A record has a partial handshake message (2)"): + yield result + + handshakePair = (r, bytes[p.index-4 : p.index+msgLength]) + self._handshakeBuffer.append(handshakePair) + p.index += msgLength + + #We've moved at least one handshake message into the + #handshakeBuffer, return the first one + recordHeader, bytes = self._handshakeBuffer[0] + self._handshakeBuffer = self._handshakeBuffer[1:] + yield (recordHeader, Parser(bytes)) + + + def _decryptRecord(self, recordType, bytes): + if self._readState.encContext: + + #Decrypt if it's a block cipher + if self._readState.encContext.isBlockCipher: + blockLength = self._readState.encContext.block_size + if len(bytes) % blockLength != 0: + for result in self._sendError(\ + AlertDescription.decryption_failed, + "Encrypted data not a multiple of blocksize"): + yield result + ciphertext = bytesToString(bytes) + plaintext = self._readState.encContext.decrypt(ciphertext) + if self.version == (3,2): #For TLS 1.1, remove explicit IV + plaintext = plaintext[self._readState.encContext.block_size : ] + bytes = stringToBytes(plaintext) + + #Check padding + paddingGood = True + paddingLength = bytes[-1] + if (paddingLength+1) > len(bytes): + paddingGood=False + totalPaddingLength = 0 + else: + if self.version == (3,0): + totalPaddingLength = paddingLength+1 + elif self.version in ((3,1), (3,2)): + totalPaddingLength = paddingLength+1 + paddingBytes = bytes[-totalPaddingLength:-1] + for byte in paddingBytes: + if byte != paddingLength: + paddingGood = False + totalPaddingLength = 0 + else: + raise AssertionError() + + #Decrypt if it's a stream cipher + else: + paddingGood = True + ciphertext = bytesToString(bytes) + plaintext = self._readState.encContext.decrypt(ciphertext) + bytes = stringToBytes(plaintext) + totalPaddingLength = 0 + + #Check MAC + macGood = True + macLength = self._readState.macContext.digest_size + endLength = macLength + totalPaddingLength + if endLength > len(bytes): + macGood = False + else: + #Read MAC + startIndex = len(bytes) - endLength + endIndex = startIndex + macLength + checkBytes = bytes[startIndex : endIndex] + + #Calculate MAC + seqnumStr = self._readState.getSeqNumStr() + bytes = bytes[:-endLength] + bytesStr = bytesToString(bytes) + mac = self._readState.macContext.copy() + mac.update(seqnumStr) + mac.update(chr(recordType)) + if self.version == (3,0): + mac.update( chr( int(len(bytes)/256) ) ) + mac.update( chr( int(len(bytes)%256) ) ) + elif self.version in ((3,1), (3,2)): + mac.update(chr(self.version[0])) + mac.update(chr(self.version[1])) + mac.update( chr( int(len(bytes)/256) ) ) + mac.update( chr( int(len(bytes)%256) ) ) + else: + raise AssertionError() + mac.update(bytesStr) + macString = mac.digest() + macBytes = stringToBytes(macString) + + #Compare MACs + if macBytes != checkBytes: + macGood = False + + if not (paddingGood and macGood): + for result in self._sendError(AlertDescription.bad_record_mac, + "MAC failure (or padding failure)"): + yield result + + yield bytes + + def _handshakeStart(self, client): + self._client = client + self._handshake_md5 = md5.md5() + self._handshake_sha = sha.sha() + self._handshakeBuffer = [] + self.allegedSharedKeyUsername = None + self.allegedSrpUsername = None + self._refCount = 1 + + def _handshakeDone(self, resumed): + self.resumed = resumed + self.closed = False + + def _calcPendingStates(self, clientRandom, serverRandom, implementations): + if self.session.cipherSuite in CipherSuite.aes128Suites: + macLength = 20 + keyLength = 16 + ivLength = 16 + createCipherFunc = createAES + elif self.session.cipherSuite in CipherSuite.aes256Suites: + macLength = 20 + keyLength = 32 + ivLength = 16 + createCipherFunc = createAES + elif self.session.cipherSuite in CipherSuite.rc4Suites: + macLength = 20 + keyLength = 16 + ivLength = 0 + createCipherFunc = createRC4 + elif self.session.cipherSuite in CipherSuite.tripleDESSuites: + macLength = 20 + keyLength = 24 + ivLength = 8 + createCipherFunc = createTripleDES + else: + raise AssertionError() + + if self.version == (3,0): + createMACFunc = MAC_SSL + elif self.version in ((3,1), (3,2)): + createMACFunc = hmac.HMAC + + outputLength = (macLength*2) + (keyLength*2) + (ivLength*2) + + #Calculate Keying Material from Master Secret + if self.version == (3,0): + keyBlock = PRF_SSL(self.session.masterSecret, + concatArrays(serverRandom, clientRandom), + outputLength) + elif self.version in ((3,1), (3,2)): + keyBlock = PRF(self.session.masterSecret, + "key expansion", + concatArrays(serverRandom,clientRandom), + outputLength) + else: + raise AssertionError() + + #Slice up Keying Material + clientPendingState = _ConnectionState() + serverPendingState = _ConnectionState() + p = Parser(keyBlock) + clientMACBlock = bytesToString(p.getFixBytes(macLength)) + serverMACBlock = bytesToString(p.getFixBytes(macLength)) + clientKeyBlock = bytesToString(p.getFixBytes(keyLength)) + serverKeyBlock = bytesToString(p.getFixBytes(keyLength)) + clientIVBlock = bytesToString(p.getFixBytes(ivLength)) + serverIVBlock = bytesToString(p.getFixBytes(ivLength)) + clientPendingState.macContext = createMACFunc(clientMACBlock, + digestmod=sha) + serverPendingState.macContext = createMACFunc(serverMACBlock, + digestmod=sha) + clientPendingState.encContext = createCipherFunc(clientKeyBlock, + clientIVBlock, + implementations) + serverPendingState.encContext = createCipherFunc(serverKeyBlock, + serverIVBlock, + implementations) + + #Assign new connection states to pending states + if self._client: + self._pendingWriteState = clientPendingState + self._pendingReadState = serverPendingState + else: + self._pendingWriteState = serverPendingState + self._pendingReadState = clientPendingState + + if self.version == (3,2) and ivLength: + #Choose fixedIVBlock for TLS 1.1 (this is encrypted with the CBC + #residue to create the IV for each sent block) + self.fixedIVBlock = getRandomBytes(ivLength) + + def _changeWriteState(self): + self._writeState = self._pendingWriteState + self._pendingWriteState = _ConnectionState() + + def _changeReadState(self): + self._readState = self._pendingReadState + self._pendingReadState = _ConnectionState() + + def _sendFinished(self): + #Send ChangeCipherSpec + for result in self._sendMsg(ChangeCipherSpec()): + yield result + + #Switch to pending write state + self._changeWriteState() + + #Calculate verification data + verifyData = self._calcFinished(True) + if self.fault == Fault.badFinished: + verifyData[0] = (verifyData[0]+1)%256 + + #Send Finished message under new state + finished = Finished(self.version).create(verifyData) + for result in self._sendMsg(finished): + yield result + + def _getFinished(self): + #Get and check ChangeCipherSpec + for result in self._getMsg(ContentType.change_cipher_spec): + if result in (0,1): + yield result + changeCipherSpec = result + + if changeCipherSpec.type != 1: + for result in self._sendError(AlertDescription.illegal_parameter, + "ChangeCipherSpec type incorrect"): + yield result + + #Switch to pending read state + self._changeReadState() + + #Calculate verification data + verifyData = self._calcFinished(False) + + #Get and check Finished message under new state + for result in self._getMsg(ContentType.handshake, + HandshakeType.finished): + if result in (0,1): + yield result + finished = result + if finished.verify_data != verifyData: + for result in self._sendError(AlertDescription.decrypt_error, + "Finished message is incorrect"): + yield result + + def _calcFinished(self, send=True): + if self.version == (3,0): + if (self._client and send) or (not self._client and not send): + senderStr = "\x43\x4C\x4E\x54" + else: + senderStr = "\x53\x52\x56\x52" + + verifyData = self._calcSSLHandshakeHash(self.session.masterSecret, + senderStr) + return verifyData + + elif self.version in ((3,1), (3,2)): + if (self._client and send) or (not self._client and not send): + label = "client finished" + else: + label = "server finished" + + handshakeHashes = stringToBytes(self._handshake_md5.digest() + \ + self._handshake_sha.digest()) + verifyData = PRF(self.session.masterSecret, label, handshakeHashes, + 12) + return verifyData + else: + raise AssertionError() + + #Used for Finished messages and CertificateVerify messages in SSL v3 + def _calcSSLHandshakeHash(self, masterSecret, label): + masterSecretStr = bytesToString(masterSecret) + + imac_md5 = self._handshake_md5.copy() + imac_sha = self._handshake_sha.copy() + + imac_md5.update(label + masterSecretStr + '\x36'*48) + imac_sha.update(label + masterSecretStr + '\x36'*40) + + md5Str = md5.md5(masterSecretStr + ('\x5c'*48) + \ + imac_md5.digest()).digest() + shaStr = sha.sha(masterSecretStr + ('\x5c'*40) + \ + imac_sha.digest()).digest() + + return stringToBytes(md5Str + shaStr) + diff --git a/patches/gdata/tlslite/VerifierDB.py b/patches/gdata/tlslite/VerifierDB.py new file mode 100755 index 0000000..f706b17 --- /dev/null +++ b/patches/gdata/tlslite/VerifierDB.py @@ -0,0 +1,90 @@ +"""Class for storing SRP password verifiers.""" + +from utils.cryptomath import * +from utils.compat import * +import mathtls +from BaseDB import BaseDB + +class VerifierDB(BaseDB): + """This class represent an in-memory or on-disk database of SRP + password verifiers. + + A VerifierDB can be passed to a server handshake to authenticate + a client based on one of the verifiers. + + This class is thread-safe. + """ + def __init__(self, filename=None): + """Create a new VerifierDB instance. + + @type filename: str + @param filename: Filename for an on-disk database, or None for + an in-memory database. If the filename already exists, follow + this with a call to open(). To create a new on-disk database, + follow this with a call to create(). + """ + BaseDB.__init__(self, filename, "verifier") + + def _getItem(self, username, valueStr): + (N, g, salt, verifier) = valueStr.split(" ") + N = base64ToNumber(N) + g = base64ToNumber(g) + salt = base64ToString(salt) + verifier = base64ToNumber(verifier) + return (N, g, salt, verifier) + + def __setitem__(self, username, verifierEntry): + """Add a verifier entry to the database. + + @type username: str + @param username: The username to associate the verifier with. + Must be less than 256 characters in length. Must not already + be in the database. + + @type verifierEntry: tuple + @param verifierEntry: The verifier entry to add. Use + L{tlslite.VerifierDB.VerifierDB.makeVerifier} to create a + verifier entry. + """ + BaseDB.__setitem__(self, username, verifierEntry) + + + def _setItem(self, username, value): + if len(username)>=256: + raise ValueError("username too long") + N, g, salt, verifier = value + N = numberToBase64(N) + g = numberToBase64(g) + salt = stringToBase64(salt) + verifier = numberToBase64(verifier) + valueStr = " ".join( (N, g, salt, verifier) ) + return valueStr + + def _checkItem(self, value, username, param): + (N, g, salt, verifier) = value + x = mathtls.makeX(salt, username, param) + v = powMod(g, x, N) + return (verifier == v) + + + def makeVerifier(username, password, bits): + """Create a verifier entry which can be stored in a VerifierDB. + + @type username: str + @param username: The username for this verifier. Must be less + than 256 characters in length. + + @type password: str + @param password: The password for this verifier. + + @type bits: int + @param bits: This values specifies which SRP group parameters + to use. It must be one of (1024, 1536, 2048, 3072, 4096, 6144, + 8192). Larger values are more secure but slower. 2048 is a + good compromise between safety and speed. + + @rtype: tuple + @return: A tuple which may be stored in a VerifierDB. + """ + return mathtls.makeVerifier(username, password, bits) + makeVerifier = staticmethod(makeVerifier) \ No newline at end of file diff --git a/patches/gdata/tlslite/X509.py b/patches/gdata/tlslite/X509.py new file mode 100755 index 0000000..a47ddcf --- /dev/null +++ b/patches/gdata/tlslite/X509.py @@ -0,0 +1,133 @@ +"""Class representing an X.509 certificate.""" + +from utils.ASN1Parser import ASN1Parser +from utils.cryptomath import * +from utils.keyfactory import _createPublicRSAKey + + +class X509: + """This class represents an X.509 certificate. + + @type bytes: L{array.array} of unsigned bytes + @ivar bytes: The DER-encoded ASN.1 certificate + + @type publicKey: L{tlslite.utils.RSAKey.RSAKey} + @ivar publicKey: The subject public key from the certificate. + """ + + def __init__(self): + self.bytes = createByteArraySequence([]) + self.publicKey = None + + def parse(self, s): + """Parse a PEM-encoded X.509 certificate. + + @type s: str + @param s: A PEM-encoded X.509 certificate (i.e. a base64-encoded + certificate wrapped with "-----BEGIN CERTIFICATE-----" and + "-----END CERTIFICATE-----" tags). + """ + + start = s.find("-----BEGIN CERTIFICATE-----") + end = s.find("-----END CERTIFICATE-----") + if start == -1: + raise SyntaxError("Missing PEM prefix") + if end == -1: + raise SyntaxError("Missing PEM postfix") + s = s[start+len("-----BEGIN CERTIFICATE-----") : end] + + bytes = base64ToBytes(s) + self.parseBinary(bytes) + return self + + def parseBinary(self, bytes): + """Parse a DER-encoded X.509 certificate. + + @type bytes: str or L{array.array} of unsigned bytes + @param bytes: A DER-encoded X.509 certificate. + """ + + if isinstance(bytes, type("")): + bytes = stringToBytes(bytes) + + self.bytes = bytes + p = ASN1Parser(bytes) + + #Get the tbsCertificate + tbsCertificateP = p.getChild(0) + + #Is the optional version field present? + #This determines which index the key is at. + if tbsCertificateP.value[0]==0xA0: + subjectPublicKeyInfoIndex = 6 + else: + subjectPublicKeyInfoIndex = 5 + + #Get the subjectPublicKeyInfo + subjectPublicKeyInfoP = tbsCertificateP.getChild(\ + subjectPublicKeyInfoIndex) + + #Get the algorithm + algorithmP = subjectPublicKeyInfoP.getChild(0) + rsaOID = algorithmP.value + if list(rsaOID) != [6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]: + raise SyntaxError("Unrecognized AlgorithmIdentifier") + + #Get the subjectPublicKey + subjectPublicKeyP = subjectPublicKeyInfoP.getChild(1) + + #Adjust for BIT STRING encapsulation + if (subjectPublicKeyP.value[0] !=0): + raise SyntaxError() + subjectPublicKeyP = ASN1Parser(subjectPublicKeyP.value[1:]) + + #Get the modulus and exponent + modulusP = subjectPublicKeyP.getChild(0) + publicExponentP = subjectPublicKeyP.getChild(1) + + #Decode them into numbers + n = bytesToNumber(modulusP.value) + e = bytesToNumber(publicExponentP.value) + + #Create a public key instance + self.publicKey = _createPublicRSAKey(n, e) + + def getFingerprint(self): + """Get the hex-encoded fingerprint of this certificate. + + @rtype: str + @return: A hex-encoded fingerprint. + """ + return sha.sha(self.bytes).hexdigest() + + def getCommonName(self): + """Get the Subject's Common Name from the certificate. + + The cryptlib_py module must be installed in order to use this + function. + + @rtype: str or None + @return: The CN component of the certificate's subject DN, if + present. + """ + import cryptlib_py + import array + c = cryptlib_py.cryptImportCert(self.bytes, cryptlib_py.CRYPT_UNUSED) + name = cryptlib_py.CRYPT_CERTINFO_COMMONNAME + try: + try: + length = cryptlib_py.cryptGetAttributeString(c, name, None) + returnVal = array.array('B', [0] * length) + cryptlib_py.cryptGetAttributeString(c, name, returnVal) + returnVal = returnVal.tostring() + except cryptlib_py.CryptException, e: + if e[0] == cryptlib_py.CRYPT_ERROR_NOTFOUND: + returnVal = None + return returnVal + finally: + cryptlib_py.cryptDestroyCert(c) + + def writeBytes(self): + return self.bytes + + diff --git a/patches/gdata/tlslite/X509CertChain.py b/patches/gdata/tlslite/X509CertChain.py new file mode 100755 index 0000000..d5f0b4d --- /dev/null +++ b/patches/gdata/tlslite/X509CertChain.py @@ -0,0 +1,181 @@ +"""Class representing an X.509 certificate chain.""" + +from utils import cryptomath + +class X509CertChain: + """This class represents a chain of X.509 certificates. + + @type x509List: list + @ivar x509List: A list of L{tlslite.X509.X509} instances, + starting with the end-entity certificate and with every + subsequent certificate certifying the previous. + """ + + def __init__(self, x509List=None): + """Create a new X509CertChain. + + @type x509List: list + @param x509List: A list of L{tlslite.X509.X509} instances, + starting with the end-entity certificate and with every + subsequent certificate certifying the previous. + """ + if x509List: + self.x509List = x509List + else: + self.x509List = [] + + def getNumCerts(self): + """Get the number of certificates in this chain. + + @rtype: int + """ + return len(self.x509List) + + def getEndEntityPublicKey(self): + """Get the public key from the end-entity certificate. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + """ + if self.getNumCerts() == 0: + raise AssertionError() + return self.x509List[0].publicKey + + def getFingerprint(self): + """Get the hex-encoded fingerprint of the end-entity certificate. + + @rtype: str + @return: A hex-encoded fingerprint. + """ + if self.getNumCerts() == 0: + raise AssertionError() + return self.x509List[0].getFingerprint() + + def getCommonName(self): + """Get the Subject's Common Name from the end-entity certificate. + + The cryptlib_py module must be installed in order to use this + function. + + @rtype: str or None + @return: The CN component of the certificate's subject DN, if + present. + """ + if self.getNumCerts() == 0: + raise AssertionError() + return self.x509List[0].getCommonName() + + def validate(self, x509TrustList): + """Check the validity of the certificate chain. + + This checks that every certificate in the chain validates with + the subsequent one, until some certificate validates with (or + is identical to) one of the passed-in root certificates. + + The cryptlib_py module must be installed in order to use this + function. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + certificate chain must extend to one of these certificates to + be considered valid. + """ + + import cryptlib_py + c1 = None + c2 = None + lastC = None + rootC = None + + try: + rootFingerprints = [c.getFingerprint() for c in x509TrustList] + + #Check that every certificate in the chain validates with the + #next one + for cert1, cert2 in zip(self.x509List, self.x509List[1:]): + + #If we come upon a root certificate, we're done. + if cert1.getFingerprint() in rootFingerprints: + return True + + c1 = cryptlib_py.cryptImportCert(cert1.writeBytes(), + cryptlib_py.CRYPT_UNUSED) + c2 = cryptlib_py.cryptImportCert(cert2.writeBytes(), + cryptlib_py.CRYPT_UNUSED) + try: + cryptlib_py.cryptCheckCert(c1, c2) + except: + return False + cryptlib_py.cryptDestroyCert(c1) + c1 = None + cryptlib_py.cryptDestroyCert(c2) + c2 = None + + #If the last certificate is one of the root certificates, we're + #done. + if self.x509List[-1].getFingerprint() in rootFingerprints: + return True + + #Otherwise, find a root certificate that the last certificate + #chains to, and validate them. + lastC = cryptlib_py.cryptImportCert(self.x509List[-1].writeBytes(), + cryptlib_py.CRYPT_UNUSED) + for rootCert in x509TrustList: + rootC = cryptlib_py.cryptImportCert(rootCert.writeBytes(), + cryptlib_py.CRYPT_UNUSED) + if self._checkChaining(lastC, rootC): + try: + cryptlib_py.cryptCheckCert(lastC, rootC) + return True + except: + return False + return False + finally: + if not (c1 is None): + cryptlib_py.cryptDestroyCert(c1) + if not (c2 is None): + cryptlib_py.cryptDestroyCert(c2) + if not (lastC is None): + cryptlib_py.cryptDestroyCert(lastC) + if not (rootC is None): + cryptlib_py.cryptDestroyCert(rootC) + + + + def _checkChaining(self, lastC, rootC): + import cryptlib_py + import array + def compareNames(name): + try: + length = cryptlib_py.cryptGetAttributeString(lastC, name, None) + lastName = array.array('B', [0] * length) + cryptlib_py.cryptGetAttributeString(lastC, name, lastName) + lastName = lastName.tostring() + except cryptlib_py.CryptException, e: + if e[0] == cryptlib_py.CRYPT_ERROR_NOTFOUND: + lastName = None + try: + length = cryptlib_py.cryptGetAttributeString(rootC, name, None) + rootName = array.array('B', [0] * length) + cryptlib_py.cryptGetAttributeString(rootC, name, rootName) + rootName = rootName.tostring() + except cryptlib_py.CryptException, e: + if e[0] == cryptlib_py.CRYPT_ERROR_NOTFOUND: + rootName = None + + return lastName == rootName + + cryptlib_py.cryptSetAttribute(lastC, + cryptlib_py.CRYPT_CERTINFO_ISSUERNAME, + cryptlib_py.CRYPT_UNUSED) + + if not compareNames(cryptlib_py.CRYPT_CERTINFO_COUNTRYNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_LOCALITYNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_ORGANIZATIONNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_ORGANIZATIONALUNITNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_COMMONNAME): + return False + return True \ No newline at end of file diff --git a/patches/gdata/tlslite/__init__.py b/patches/gdata/tlslite/__init__.py new file mode 100755 index 0000000..47cfd1c --- /dev/null +++ b/patches/gdata/tlslite/__init__.py @@ -0,0 +1,39 @@ +""" +TLS Lite is a free python library that implements SSL v3, TLS v1, and +TLS v1.1. TLS Lite supports non-traditional authentication methods +such as SRP, shared keys, and cryptoIDs, in addition to X.509 +certificates. TLS Lite is pure python, however it can access OpenSSL, +cryptlib, pycrypto, and GMPY for faster crypto operations. TLS Lite +integrates with httplib, xmlrpclib, poplib, imaplib, smtplib, +SocketServer, asyncore, and Twisted. + +To use, do:: + + from tlslite.api import * + +Then use the L{tlslite.TLSConnection.TLSConnection} class with a socket, +or use one of the integration classes in L{tlslite.integration}. + +@version: 0.3.8 +""" +__version__ = "0.3.8" + +__all__ = ["api", + "BaseDB", + "Checker", + "constants", + "errors", + "FileObject", + "HandshakeSettings", + "mathtls", + "messages", + "Session", + "SessionCache", + "SharedKeyDB", + "TLSConnection", + "TLSRecordLayer", + "VerifierDB", + "X509", + "X509CertChain", + "integration", + "utils"] diff --git a/patches/gdata/tlslite/api.py b/patches/gdata/tlslite/api.py new file mode 100755 index 0000000..eebfbc6 --- /dev/null +++ b/patches/gdata/tlslite/api.py @@ -0,0 +1,75 @@ +"""Import this module for easy access to TLS Lite objects. + +The TLS Lite API consists of classes, functions, and variables spread +throughout this package. Instead of importing them individually with:: + + from tlslite.TLSConnection import TLSConnection + from tlslite.HandshakeSettings import HandshakeSettings + from tlslite.errors import * + . + . + +It's easier to do:: + + from tlslite.api import * + +This imports all the important objects (TLSConnection, Checker, +HandshakeSettings, etc.) into the global namespace. In particular, it +imports:: + + from constants import AlertLevel, AlertDescription, Fault + from errors import * + from Checker import Checker + from HandshakeSettings import HandshakeSettings + from Session import Session + from SessionCache import SessionCache + from SharedKeyDB import SharedKeyDB + from TLSConnection import TLSConnection + from VerifierDB import VerifierDB + from X509 import X509 + from X509CertChain import X509CertChain + + from integration.HTTPTLSConnection import HTTPTLSConnection + from integration.POP3_TLS import POP3_TLS + from integration.IMAP4_TLS import IMAP4_TLS + from integration.SMTP_TLS import SMTP_TLS + from integration.XMLRPCTransport import XMLRPCTransport + from integration.TLSSocketServerMixIn import TLSSocketServerMixIn + from integration.TLSAsyncDispatcherMixIn import TLSAsyncDispatcherMixIn + from integration.TLSTwistedProtocolWrapper import TLSTwistedProtocolWrapper + from utils.cryptomath import cryptlibpyLoaded, m2cryptoLoaded, + gmpyLoaded, pycryptoLoaded, prngName + from utils.keyfactory import generateRSAKey, parsePEMKey, parseXMLKey, + parseAsPublicKey, parsePrivateKey +""" + +from constants import AlertLevel, AlertDescription, Fault +from errors import * +from Checker import Checker +from HandshakeSettings import HandshakeSettings +from Session import Session +from SessionCache import SessionCache +from SharedKeyDB import SharedKeyDB +from TLSConnection import TLSConnection +from VerifierDB import VerifierDB +from X509 import X509 +from X509CertChain import X509CertChain + +from integration.HTTPTLSConnection import HTTPTLSConnection +from integration.TLSSocketServerMixIn import TLSSocketServerMixIn +from integration.TLSAsyncDispatcherMixIn import TLSAsyncDispatcherMixIn +from integration.POP3_TLS import POP3_TLS +from integration.IMAP4_TLS import IMAP4_TLS +from integration.SMTP_TLS import SMTP_TLS +from integration.XMLRPCTransport import XMLRPCTransport +try: + import twisted + del(twisted) + from integration.TLSTwistedProtocolWrapper import TLSTwistedProtocolWrapper +except ImportError: + pass + +from utils.cryptomath import cryptlibpyLoaded, m2cryptoLoaded, gmpyLoaded, \ + pycryptoLoaded, prngName +from utils.keyfactory import generateRSAKey, parsePEMKey, parseXMLKey, \ + parseAsPublicKey, parsePrivateKey diff --git a/patches/gdata/tlslite/constants.py b/patches/gdata/tlslite/constants.py new file mode 100755 index 0000000..8f2d559 --- /dev/null +++ b/patches/gdata/tlslite/constants.py @@ -0,0 +1,225 @@ +"""Constants used in various places.""" + +class CertificateType: + x509 = 0 + openpgp = 1 + cryptoID = 2 + +class HandshakeType: + hello_request = 0 + client_hello = 1 + server_hello = 2 + certificate = 11 + server_key_exchange = 12 + certificate_request = 13 + server_hello_done = 14 + certificate_verify = 15 + client_key_exchange = 16 + finished = 20 + +class ContentType: + change_cipher_spec = 20 + alert = 21 + handshake = 22 + application_data = 23 + all = (20,21,22,23) + +class AlertLevel: + warning = 1 + fatal = 2 + +class AlertDescription: + """ + @cvar bad_record_mac: A TLS record failed to decrypt properly. + + If this occurs during a shared-key or SRP handshake it most likely + indicates a bad password. It may also indicate an implementation + error, or some tampering with the data in transit. + + This alert will be signalled by the server if the SRP password is bad. It + may also be signalled by the server if the SRP username is unknown to the + server, but it doesn't wish to reveal that fact. + + This alert will be signalled by the client if the shared-key username is + bad. + + @cvar handshake_failure: A problem occurred while handshaking. + + This typically indicates a lack of common ciphersuites between client and + server, or some other disagreement (about SRP parameters or key sizes, + for example). + + @cvar protocol_version: The other party's SSL/TLS version was unacceptable. + + This indicates that the client and server couldn't agree on which version + of SSL or TLS to use. + + @cvar user_canceled: The handshake is being cancelled for some reason. + + """ + + close_notify = 0 + unexpected_message = 10 + bad_record_mac = 20 + decryption_failed = 21 + record_overflow = 22 + decompression_failure = 30 + handshake_failure = 40 + no_certificate = 41 #SSLv3 + bad_certificate = 42 + unsupported_certificate = 43 + certificate_revoked = 44 + certificate_expired = 45 + certificate_unknown = 46 + illegal_parameter = 47 + unknown_ca = 48 + access_denied = 49 + decode_error = 50 + decrypt_error = 51 + export_restriction = 60 + protocol_version = 70 + insufficient_security = 71 + internal_error = 80 + user_canceled = 90 + no_renegotiation = 100 + unknown_srp_username = 120 + missing_srp_username = 121 + untrusted_srp_parameters = 122 + +class CipherSuite: + TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA = 0x0050 + TLS_SRP_SHA_WITH_AES_128_CBC_SHA = 0x0053 + TLS_SRP_SHA_WITH_AES_256_CBC_SHA = 0x0056 + + TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA = 0x0051 + TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA = 0x0054 + TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA = 0x0057 + + TLS_RSA_WITH_3DES_EDE_CBC_SHA = 0x000A + TLS_RSA_WITH_AES_128_CBC_SHA = 0x002F + TLS_RSA_WITH_AES_256_CBC_SHA = 0x0035 + TLS_RSA_WITH_RC4_128_SHA = 0x0005 + + srpSuites = [] + srpSuites.append(TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA) + srpSuites.append(TLS_SRP_SHA_WITH_AES_128_CBC_SHA) + srpSuites.append(TLS_SRP_SHA_WITH_AES_256_CBC_SHA) + def getSrpSuites(ciphers): + suites = [] + for cipher in ciphers: + if cipher == "aes128": + suites.append(CipherSuite.TLS_SRP_SHA_WITH_AES_128_CBC_SHA) + elif cipher == "aes256": + suites.append(CipherSuite.TLS_SRP_SHA_WITH_AES_256_CBC_SHA) + elif cipher == "3des": + suites.append(CipherSuite.TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA) + return suites + getSrpSuites = staticmethod(getSrpSuites) + + srpRsaSuites = [] + srpRsaSuites.append(TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA) + srpRsaSuites.append(TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA) + srpRsaSuites.append(TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA) + def getSrpRsaSuites(ciphers): + suites = [] + for cipher in ciphers: + if cipher == "aes128": + suites.append(CipherSuite.TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA) + elif cipher == "aes256": + suites.append(CipherSuite.TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA) + elif cipher == "3des": + suites.append(CipherSuite.TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA) + return suites + getSrpRsaSuites = staticmethod(getSrpRsaSuites) + + rsaSuites = [] + rsaSuites.append(TLS_RSA_WITH_3DES_EDE_CBC_SHA) + rsaSuites.append(TLS_RSA_WITH_AES_128_CBC_SHA) + rsaSuites.append(TLS_RSA_WITH_AES_256_CBC_SHA) + rsaSuites.append(TLS_RSA_WITH_RC4_128_SHA) + def getRsaSuites(ciphers): + suites = [] + for cipher in ciphers: + if cipher == "aes128": + suites.append(CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA) + elif cipher == "aes256": + suites.append(CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA) + elif cipher == "rc4": + suites.append(CipherSuite.TLS_RSA_WITH_RC4_128_SHA) + elif cipher == "3des": + suites.append(CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA) + return suites + getRsaSuites = staticmethod(getRsaSuites) + + tripleDESSuites = [] + tripleDESSuites.append(TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA) + tripleDESSuites.append(TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA) + tripleDESSuites.append(TLS_RSA_WITH_3DES_EDE_CBC_SHA) + + aes128Suites = [] + aes128Suites.append(TLS_SRP_SHA_WITH_AES_128_CBC_SHA) + aes128Suites.append(TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA) + aes128Suites.append(TLS_RSA_WITH_AES_128_CBC_SHA) + + aes256Suites = [] + aes256Suites.append(TLS_SRP_SHA_WITH_AES_256_CBC_SHA) + aes256Suites.append(TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA) + aes256Suites.append(TLS_RSA_WITH_AES_256_CBC_SHA) + + rc4Suites = [] + rc4Suites.append(TLS_RSA_WITH_RC4_128_SHA) + + +class Fault: + badUsername = 101 + badPassword = 102 + badA = 103 + clientSrpFaults = range(101,104) + + badVerifyMessage = 601 + clientCertFaults = range(601,602) + + badPremasterPadding = 501 + shortPremasterSecret = 502 + clientNoAuthFaults = range(501,503) + + badIdentifier = 401 + badSharedKey = 402 + clientSharedKeyFaults = range(401,403) + + badB = 201 + serverFaults = range(201,202) + + badFinished = 300 + badMAC = 301 + badPadding = 302 + genericFaults = range(300,303) + + faultAlerts = {\ + badUsername: (AlertDescription.unknown_srp_username, \ + AlertDescription.bad_record_mac),\ + badPassword: (AlertDescription.bad_record_mac,),\ + badA: (AlertDescription.illegal_parameter,),\ + badIdentifier: (AlertDescription.handshake_failure,),\ + badSharedKey: (AlertDescription.bad_record_mac,),\ + badPremasterPadding: (AlertDescription.bad_record_mac,),\ + shortPremasterSecret: (AlertDescription.bad_record_mac,),\ + badVerifyMessage: (AlertDescription.decrypt_error,),\ + badFinished: (AlertDescription.decrypt_error,),\ + badMAC: (AlertDescription.bad_record_mac,),\ + badPadding: (AlertDescription.bad_record_mac,) + } + + faultNames = {\ + badUsername: "bad username",\ + badPassword: "bad password",\ + badA: "bad A",\ + badIdentifier: "bad identifier",\ + badSharedKey: "bad sharedkey",\ + badPremasterPadding: "bad premaster padding",\ + shortPremasterSecret: "short premaster secret",\ + badVerifyMessage: "bad verify message",\ + badFinished: "bad finished message",\ + badMAC: "bad MAC",\ + badPadding: "bad padding" + } diff --git a/patches/gdata/tlslite/errors.py b/patches/gdata/tlslite/errors.py new file mode 100755 index 0000000..c7f7ba8 --- /dev/null +++ b/patches/gdata/tlslite/errors.py @@ -0,0 +1,149 @@ +"""Exception classes. +@sort: TLSError, TLSAbruptCloseError, TLSAlert, TLSLocalAlert, TLSRemoteAlert, +TLSAuthenticationError, TLSNoAuthenticationError, TLSAuthenticationTypeError, +TLSFingerprintError, TLSAuthorizationError, TLSValidationError, TLSFaultError +""" + +from constants import AlertDescription, AlertLevel + +class TLSError(Exception): + """Base class for all TLS Lite exceptions.""" + pass + +class TLSAbruptCloseError(TLSError): + """The socket was closed without a proper TLS shutdown. + + The TLS specification mandates that an alert of some sort + must be sent before the underlying socket is closed. If the socket + is closed without this, it could signify that an attacker is trying + to truncate the connection. It could also signify a misbehaving + TLS implementation, or a random network failure. + """ + pass + +class TLSAlert(TLSError): + """A TLS alert has been signalled.""" + pass + + _descriptionStr = {\ + AlertDescription.close_notify: "close_notify",\ + AlertDescription.unexpected_message: "unexpected_message",\ + AlertDescription.bad_record_mac: "bad_record_mac",\ + AlertDescription.decryption_failed: "decryption_failed",\ + AlertDescription.record_overflow: "record_overflow",\ + AlertDescription.decompression_failure: "decompression_failure",\ + AlertDescription.handshake_failure: "handshake_failure",\ + AlertDescription.no_certificate: "no certificate",\ + AlertDescription.bad_certificate: "bad_certificate",\ + AlertDescription.unsupported_certificate: "unsupported_certificate",\ + AlertDescription.certificate_revoked: "certificate_revoked",\ + AlertDescription.certificate_expired: "certificate_expired",\ + AlertDescription.certificate_unknown: "certificate_unknown",\ + AlertDescription.illegal_parameter: "illegal_parameter",\ + AlertDescription.unknown_ca: "unknown_ca",\ + AlertDescription.access_denied: "access_denied",\ + AlertDescription.decode_error: "decode_error",\ + AlertDescription.decrypt_error: "decrypt_error",\ + AlertDescription.export_restriction: "export_restriction",\ + AlertDescription.protocol_version: "protocol_version",\ + AlertDescription.insufficient_security: "insufficient_security",\ + AlertDescription.internal_error: "internal_error",\ + AlertDescription.user_canceled: "user_canceled",\ + AlertDescription.no_renegotiation: "no_renegotiation",\ + AlertDescription.unknown_srp_username: "unknown_srp_username",\ + AlertDescription.missing_srp_username: "missing_srp_username"} + +class TLSLocalAlert(TLSAlert): + """A TLS alert has been signalled by the local implementation. + + @type description: int + @ivar description: Set to one of the constants in + L{tlslite.constants.AlertDescription} + + @type level: int + @ivar level: Set to one of the constants in + L{tlslite.constants.AlertLevel} + + @type message: str + @ivar message: Description of what went wrong. + """ + def __init__(self, alert, message=None): + self.description = alert.description + self.level = alert.level + self.message = message + + def __str__(self): + alertStr = TLSAlert._descriptionStr.get(self.description) + if alertStr == None: + alertStr = str(self.description) + if self.message: + return alertStr + ": " + self.message + else: + return alertStr + +class TLSRemoteAlert(TLSAlert): + """A TLS alert has been signalled by the remote implementation. + + @type description: int + @ivar description: Set to one of the constants in + L{tlslite.constants.AlertDescription} + + @type level: int + @ivar level: Set to one of the constants in + L{tlslite.constants.AlertLevel} + """ + def __init__(self, alert): + self.description = alert.description + self.level = alert.level + + def __str__(self): + alertStr = TLSAlert._descriptionStr.get(self.description) + if alertStr == None: + alertStr = str(self.description) + return alertStr + +class TLSAuthenticationError(TLSError): + """The handshake succeeded, but the other party's authentication + was inadequate. + + This exception will only be raised when a + L{tlslite.Checker.Checker} has been passed to a handshake function. + The Checker will be invoked once the handshake completes, and if + the Checker objects to how the other party authenticated, a + subclass of this exception will be raised. + """ + pass + +class TLSNoAuthenticationError(TLSAuthenticationError): + """The Checker was expecting the other party to authenticate with a + certificate chain, but this did not occur.""" + pass + +class TLSAuthenticationTypeError(TLSAuthenticationError): + """The Checker was expecting the other party to authenticate with a + different type of certificate chain.""" + pass + +class TLSFingerprintError(TLSAuthenticationError): + """The Checker was expecting the other party to authenticate with a + certificate chain that matches a different fingerprint.""" + pass + +class TLSAuthorizationError(TLSAuthenticationError): + """The Checker was expecting the other party to authenticate with a + certificate chain that has a different authorization.""" + pass + +class TLSValidationError(TLSAuthenticationError): + """The Checker has determined that the other party's certificate + chain is invalid.""" + pass + +class TLSFaultError(TLSError): + """The other party responded incorrectly to an induced fault. + + This exception will only occur during fault testing, when a + TLSConnection's fault variable is set to induce some sort of + faulty behavior, and the other party doesn't respond appropriately. + """ + pass diff --git a/patches/gdata/tlslite/integration/AsyncStateMachine.py b/patches/gdata/tlslite/integration/AsyncStateMachine.py new file mode 100755 index 0000000..abed604 --- /dev/null +++ b/patches/gdata/tlslite/integration/AsyncStateMachine.py @@ -0,0 +1,235 @@ +""" +A state machine for using TLS Lite with asynchronous I/O. +""" + +class AsyncStateMachine: + """ + This is an abstract class that's used to integrate TLS Lite with + asyncore and Twisted. + + This class signals wantsReadsEvent() and wantsWriteEvent(). When + the underlying socket has become readable or writeable, the event + should be passed to this class by calling inReadEvent() or + inWriteEvent(). This class will then try to read or write through + the socket, and will update its state appropriately. + + This class will forward higher-level events to its subclass. For + example, when a complete TLS record has been received, + outReadEvent() will be called with the decrypted data. + """ + + def __init__(self): + self._clear() + + def _clear(self): + #These store the various asynchronous operations (i.e. + #generators). Only one of them, at most, is ever active at a + #time. + self.handshaker = None + self.closer = None + self.reader = None + self.writer = None + + #This stores the result from the last call to the + #currently active operation. If 0 it indicates that the + #operation wants to read, if 1 it indicates that the + #operation wants to write. If None, there is no active + #operation. + self.result = None + + def _checkAssert(self, maxActive=1): + #This checks that only one operation, at most, is + #active, and that self.result is set appropriately. + activeOps = 0 + if self.handshaker: + activeOps += 1 + if self.closer: + activeOps += 1 + if self.reader: + activeOps += 1 + if self.writer: + activeOps += 1 + + if self.result == None: + if activeOps != 0: + raise AssertionError() + elif self.result in (0,1): + if activeOps != 1: + raise AssertionError() + else: + raise AssertionError() + if activeOps > maxActive: + raise AssertionError() + + def wantsReadEvent(self): + """If the state machine wants to read. + + If an operation is active, this returns whether or not the + operation wants to read from the socket. If an operation is + not active, this returns None. + + @rtype: bool or None + @return: If the state machine wants to read. + """ + if self.result != None: + return self.result == 0 + return None + + def wantsWriteEvent(self): + """If the state machine wants to write. + + If an operation is active, this returns whether or not the + operation wants to write to the socket. If an operation is + not active, this returns None. + + @rtype: bool or None + @return: If the state machine wants to write. + """ + if self.result != None: + return self.result == 1 + return None + + def outConnectEvent(self): + """Called when a handshake operation completes. + + May be overridden in subclass. + """ + pass + + def outCloseEvent(self): + """Called when a close operation completes. + + May be overridden in subclass. + """ + pass + + def outReadEvent(self, readBuffer): + """Called when a read operation completes. + + May be overridden in subclass.""" + pass + + def outWriteEvent(self): + """Called when a write operation completes. + + May be overridden in subclass.""" + pass + + def inReadEvent(self): + """Tell the state machine it can read from the socket.""" + try: + self._checkAssert() + if self.handshaker: + self._doHandshakeOp() + elif self.closer: + self._doCloseOp() + elif self.reader: + self._doReadOp() + elif self.writer: + self._doWriteOp() + else: + self.reader = self.tlsConnection.readAsync(16384) + self._doReadOp() + except: + self._clear() + raise + + def inWriteEvent(self): + """Tell the state machine it can write to the socket.""" + try: + self._checkAssert() + if self.handshaker: + self._doHandshakeOp() + elif self.closer: + self._doCloseOp() + elif self.reader: + self._doReadOp() + elif self.writer: + self._doWriteOp() + else: + self.outWriteEvent() + except: + self._clear() + raise + + def _doHandshakeOp(self): + try: + self.result = self.handshaker.next() + except StopIteration: + self.handshaker = None + self.result = None + self.outConnectEvent() + + def _doCloseOp(self): + try: + self.result = self.closer.next() + except StopIteration: + self.closer = None + self.result = None + self.outCloseEvent() + + def _doReadOp(self): + self.result = self.reader.next() + if not self.result in (0,1): + readBuffer = self.result + self.reader = None + self.result = None + self.outReadEvent(readBuffer) + + def _doWriteOp(self): + try: + self.result = self.writer.next() + except StopIteration: + self.writer = None + self.result = None + + def setHandshakeOp(self, handshaker): + """Start a handshake operation. + + @type handshaker: generator + @param handshaker: A generator created by using one of the + asynchronous handshake functions (i.e. handshakeServerAsync, or + handshakeClientxxx(..., async=True). + """ + try: + self._checkAssert(0) + self.handshaker = handshaker + self._doHandshakeOp() + except: + self._clear() + raise + + def setServerHandshakeOp(self, **args): + """Start a handshake operation. + + The arguments passed to this function will be forwarded to + L{tlslite.TLSConnection.TLSConnection.handshakeServerAsync}. + """ + handshaker = self.tlsConnection.handshakeServerAsync(**args) + self.setHandshakeOp(handshaker) + + def setCloseOp(self): + """Start a close operation. + """ + try: + self._checkAssert(0) + self.closer = self.tlsConnection.closeAsync() + self._doCloseOp() + except: + self._clear() + raise + + def setWriteOp(self, writeBuffer): + """Start a write operation. + + @type writeBuffer: str + @param writeBuffer: The string to transmit. + """ + try: + self._checkAssert(0) + self.writer = self.tlsConnection.writeAsync(writeBuffer) + self._doWriteOp() + except: + self._clear() + raise + diff --git a/patches/gdata/tlslite/integration/ClientHelper.py b/patches/gdata/tlslite/integration/ClientHelper.py new file mode 100755 index 0000000..58e0152 --- /dev/null +++ b/patches/gdata/tlslite/integration/ClientHelper.py @@ -0,0 +1,163 @@ +""" +A helper class for using TLS Lite with stdlib clients +(httplib, xmlrpclib, imaplib, poplib). +""" + +from gdata.tlslite.Checker import Checker + +class ClientHelper: + """This is a helper class used to integrate TLS Lite with various + TLS clients (e.g. poplib, smtplib, httplib, etc.)""" + + def __init__(self, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings = None): + """ + For client authentication, use one of these argument + combinations: + - username, password (SRP) + - username, sharedKey (shared-key) + - certChain, privateKey (certificate) + + For server authentication, you can either rely on the + implicit mutual authentication performed by SRP or + shared-keys, or you can do certificate-based server + authentication with one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + Certificate-based server authentication is compatible with + SRP or certificate-based client authentication. It is + not compatible with shared-keys. + + The constructor does not perform the TLS handshake itself, but + simply stores these arguments for later. The handshake is + performed only when this class needs to connect with the + server. Then you should be prepared to handle TLS-specific + exceptions. See the client handshake functions in + L{tlslite.TLSConnection.TLSConnection} for details on which + exceptions might be raised. + + @type username: str + @param username: SRP or shared-key username. Requires the + 'password' or 'sharedKey' argument. + + @type password: str + @param password: SRP password for mutual authentication. + Requires the 'username' argument. + + @type sharedKey: str + @param sharedKey: Shared key for mutual authentication. + Requires the 'username' argument. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: Certificate chain for client authentication. + Requires the 'privateKey' argument. Excludes the SRP or + shared-key related arguments. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: Private key for client authentication. + Requires the 'certChain' argument. Excludes the SRP or + shared-key related arguments. + + @type cryptoID: str + @param cryptoID: cryptoID for server authentication. Mutually + exclusive with the 'x509...' arguments. + + @type protocol: str + @param protocol: cryptoID protocol URI for server + authentication. Requires the 'cryptoID' argument. + + @type x509Fingerprint: str + @param x509Fingerprint: Hex-encoded X.509 fingerprint for + server authentication. Mutually exclusive with the 'cryptoID' + and 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed to use this parameter. Mutually exclusive with the + 'cryptoID' and 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + """ + + self.username = None + self.password = None + self.sharedKey = None + self.certChain = None + self.privateKey = None + self.checker = None + + #SRP Authentication + if username and password and not \ + (sharedKey or certChain or privateKey): + self.username = username + self.password = password + + #Shared Key Authentication + elif username and sharedKey and not \ + (password or certChain or privateKey): + self.username = username + self.sharedKey = sharedKey + + #Certificate Chain Authentication + elif certChain and privateKey and not \ + (username or password or sharedKey): + self.certChain = certChain + self.privateKey = privateKey + + #No Authentication + elif not password and not username and not \ + sharedKey and not certChain and not privateKey: + pass + + else: + raise ValueError("Bad parameters") + + #Authenticate the server based on its cryptoID or fingerprint + if sharedKey and (cryptoID or protocol or x509Fingerprint): + raise ValueError("Can't use shared keys with other forms of"\ + "authentication") + + self.checker = Checker(cryptoID, protocol, x509Fingerprint, + x509TrustList, x509CommonName) + self.settings = settings + + self.tlsSession = None + + def _handshake(self, tlsConnection): + if self.username and self.password: + tlsConnection.handshakeClientSRP(username=self.username, + password=self.password, + checker=self.checker, + settings=self.settings, + session=self.tlsSession) + elif self.username and self.sharedKey: + tlsConnection.handshakeClientSharedKey(username=self.username, + sharedKey=self.sharedKey, + settings=self.settings) + else: + tlsConnection.handshakeClientCert(certChain=self.certChain, + privateKey=self.privateKey, + checker=self.checker, + settings=self.settings, + session=self.tlsSession) + self.tlsSession = tlsConnection.session diff --git a/patches/gdata/tlslite/integration/HTTPTLSConnection.py b/patches/gdata/tlslite/integration/HTTPTLSConnection.py new file mode 100755 index 0000000..58e31a1 --- /dev/null +++ b/patches/gdata/tlslite/integration/HTTPTLSConnection.py @@ -0,0 +1,169 @@ +"""TLS Lite + httplib.""" + +import socket +import httplib +from gdata.tlslite.TLSConnection import TLSConnection +from gdata.tlslite.integration.ClientHelper import ClientHelper + + +class HTTPBaseTLSConnection(httplib.HTTPConnection): + """This abstract class provides a framework for adding TLS support + to httplib.""" + + default_port = 443 + + def __init__(self, host, port=None, strict=None): + if strict == None: + #Python 2.2 doesn't support strict + httplib.HTTPConnection.__init__(self, host, port) + else: + httplib.HTTPConnection.__init__(self, host, port, strict) + + def connect(self): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if hasattr(sock, 'settimeout'): + sock.settimeout(10) + sock.connect((self.host, self.port)) + + #Use a TLSConnection to emulate a socket + self.sock = TLSConnection(sock) + + #When httplib closes this, close the socket + self.sock.closeSocket = True + self._handshake(self.sock) + + def _handshake(self, tlsConnection): + """Called to perform some sort of handshake. + + This method must be overridden in a subclass to do some type of + handshake. This method will be called after the socket has + been connected but before any data has been sent. If this + method does not raise an exception, the TLS connection will be + considered valid. + + This method may (or may not) be called every time an HTTP + request is performed, depending on whether the underlying HTTP + connection is persistent. + + @type tlsConnection: L{tlslite.TLSConnection.TLSConnection} + @param tlsConnection: The connection to perform the handshake + on. + """ + raise NotImplementedError() + + +class HTTPTLSConnection(HTTPBaseTLSConnection, ClientHelper): + """This class extends L{HTTPBaseTLSConnection} to support the + common types of handshaking.""" + + def __init__(self, host, port=None, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings = None): + """Create a new HTTPTLSConnection. + + For client authentication, use one of these argument + combinations: + - username, password (SRP) + - username, sharedKey (shared-key) + - certChain, privateKey (certificate) + + For server authentication, you can either rely on the + implicit mutual authentication performed by SRP or + shared-keys, or you can do certificate-based server + authentication with one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + Certificate-based server authentication is compatible with + SRP or certificate-based client authentication. It is + not compatible with shared-keys. + + The constructor does not perform the TLS handshake itself, but + simply stores these arguments for later. The handshake is + performed only when this class needs to connect with the + server. Thus you should be prepared to handle TLS-specific + exceptions when calling methods inherited from + L{httplib.HTTPConnection} such as request(), connect(), and + send(). See the client handshake functions in + L{tlslite.TLSConnection.TLSConnection} for details on which + exceptions might be raised. + + @type host: str + @param host: Server to connect to. + + @type port: int + @param port: Port to connect to. + + @type username: str + @param username: SRP or shared-key username. Requires the + 'password' or 'sharedKey' argument. + + @type password: str + @param password: SRP password for mutual authentication. + Requires the 'username' argument. + + @type sharedKey: str + @param sharedKey: Shared key for mutual authentication. + Requires the 'username' argument. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: Certificate chain for client authentication. + Requires the 'privateKey' argument. Excludes the SRP or + shared-key related arguments. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: Private key for client authentication. + Requires the 'certChain' argument. Excludes the SRP or + shared-key related arguments. + + @type cryptoID: str + @param cryptoID: cryptoID for server authentication. Mutually + exclusive with the 'x509...' arguments. + + @type protocol: str + @param protocol: cryptoID protocol URI for server + authentication. Requires the 'cryptoID' argument. + + @type x509Fingerprint: str + @param x509Fingerprint: Hex-encoded X.509 fingerprint for + server authentication. Mutually exclusive with the 'cryptoID' + and 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed to use this parameter. Mutually exclusive with the + 'cryptoID' and 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + """ + + HTTPBaseTLSConnection.__init__(self, host, port) + + ClientHelper.__init__(self, + username, password, sharedKey, + certChain, privateKey, + cryptoID, protocol, + x509Fingerprint, + x509TrustList, x509CommonName, + settings) + + def _handshake(self, tlsConnection): + ClientHelper._handshake(self, tlsConnection) diff --git a/patches/gdata/tlslite/integration/IMAP4_TLS.py b/patches/gdata/tlslite/integration/IMAP4_TLS.py new file mode 100755 index 0000000..e47076c --- /dev/null +++ b/patches/gdata/tlslite/integration/IMAP4_TLS.py @@ -0,0 +1,132 @@ +"""TLS Lite + imaplib.""" + +import socket +from imaplib import IMAP4 +from gdata.tlslite.TLSConnection import TLSConnection +from gdata.tlslite.integration.ClientHelper import ClientHelper + +# IMAP TLS PORT +IMAP4_TLS_PORT = 993 + +class IMAP4_TLS(IMAP4, ClientHelper): + """This class extends L{imaplib.IMAP4} with TLS support.""" + + def __init__(self, host = '', port = IMAP4_TLS_PORT, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings=None): + """Create a new IMAP4_TLS. + + For client authentication, use one of these argument + combinations: + - username, password (SRP) + - username, sharedKey (shared-key) + - certChain, privateKey (certificate) + + For server authentication, you can either rely on the + implicit mutual authentication performed by SRP or + shared-keys, or you can do certificate-based server + authentication with one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + Certificate-based server authentication is compatible with + SRP or certificate-based client authentication. It is + not compatible with shared-keys. + + The caller should be prepared to handle TLS-specific + exceptions. See the client handshake functions in + L{tlslite.TLSConnection.TLSConnection} for details on which + exceptions might be raised. + + @type host: str + @param host: Server to connect to. + + @type port: int + @param port: Port to connect to. + + @type username: str + @param username: SRP or shared-key username. Requires the + 'password' or 'sharedKey' argument. + + @type password: str + @param password: SRP password for mutual authentication. + Requires the 'username' argument. + + @type sharedKey: str + @param sharedKey: Shared key for mutual authentication. + Requires the 'username' argument. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: Certificate chain for client authentication. + Requires the 'privateKey' argument. Excludes the SRP or + shared-key related arguments. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: Private key for client authentication. + Requires the 'certChain' argument. Excludes the SRP or + shared-key related arguments. + + @type cryptoID: str + @param cryptoID: cryptoID for server authentication. Mutually + exclusive with the 'x509...' arguments. + + @type protocol: str + @param protocol: cryptoID protocol URI for server + authentication. Requires the 'cryptoID' argument. + + @type x509Fingerprint: str + @param x509Fingerprint: Hex-encoded X.509 fingerprint for + server authentication. Mutually exclusive with the 'cryptoID' + and 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed to use this parameter. Mutually exclusive with the + 'cryptoID' and 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + """ + + ClientHelper.__init__(self, + username, password, sharedKey, + certChain, privateKey, + cryptoID, protocol, + x509Fingerprint, + x509TrustList, x509CommonName, + settings) + + IMAP4.__init__(self, host, port) + + + def open(self, host = '', port = IMAP4_TLS_PORT): + """Setup connection to remote server on "host:port". + + This connection will be used by the routines: + read, readline, send, shutdown. + """ + self.host = host + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((host, port)) + self.sock = TLSConnection(self.sock) + self.sock.closeSocket = True + ClientHelper._handshake(self, self.sock) + self.file = self.sock.makefile('rb') diff --git a/patches/gdata/tlslite/integration/IntegrationHelper.py b/patches/gdata/tlslite/integration/IntegrationHelper.py new file mode 100755 index 0000000..af5193b --- /dev/null +++ b/patches/gdata/tlslite/integration/IntegrationHelper.py @@ -0,0 +1,52 @@ + +class IntegrationHelper: + + def __init__(self, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings = None): + + self.username = None + self.password = None + self.sharedKey = None + self.certChain = None + self.privateKey = None + self.checker = None + + #SRP Authentication + if username and password and not \ + (sharedKey or certChain or privateKey): + self.username = username + self.password = password + + #Shared Key Authentication + elif username and sharedKey and not \ + (password or certChain or privateKey): + self.username = username + self.sharedKey = sharedKey + + #Certificate Chain Authentication + elif certChain and privateKey and not \ + (username or password or sharedKey): + self.certChain = certChain + self.privateKey = privateKey + + #No Authentication + elif not password and not username and not \ + sharedKey and not certChain and not privateKey: + pass + + else: + raise ValueError("Bad parameters") + + #Authenticate the server based on its cryptoID or fingerprint + if sharedKey and (cryptoID or protocol or x509Fingerprint): + raise ValueError("Can't use shared keys with other forms of"\ + "authentication") + + self.checker = Checker(cryptoID, protocol, x509Fingerprint, + x509TrustList, x509CommonName) + self.settings = settings \ No newline at end of file diff --git a/patches/gdata/tlslite/integration/POP3_TLS.py b/patches/gdata/tlslite/integration/POP3_TLS.py new file mode 100755 index 0000000..26b37fd --- /dev/null +++ b/patches/gdata/tlslite/integration/POP3_TLS.py @@ -0,0 +1,142 @@ +"""TLS Lite + poplib.""" + +import socket +from poplib import POP3 +from gdata.tlslite.TLSConnection import TLSConnection +from gdata.tlslite.integration.ClientHelper import ClientHelper + +# POP TLS PORT +POP3_TLS_PORT = 995 + +class POP3_TLS(POP3, ClientHelper): + """This class extends L{poplib.POP3} with TLS support.""" + + def __init__(self, host, port = POP3_TLS_PORT, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings=None): + """Create a new POP3_TLS. + + For client authentication, use one of these argument + combinations: + - username, password (SRP) + - username, sharedKey (shared-key) + - certChain, privateKey (certificate) + + For server authentication, you can either rely on the + implicit mutual authentication performed by SRP or + shared-keys, or you can do certificate-based server + authentication with one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + Certificate-based server authentication is compatible with + SRP or certificate-based client authentication. It is + not compatible with shared-keys. + + The caller should be prepared to handle TLS-specific + exceptions. See the client handshake functions in + L{tlslite.TLSConnection.TLSConnection} for details on which + exceptions might be raised. + + @type host: str + @param host: Server to connect to. + + @type port: int + @param port: Port to connect to. + + @type username: str + @param username: SRP or shared-key username. Requires the + 'password' or 'sharedKey' argument. + + @type password: str + @param password: SRP password for mutual authentication. + Requires the 'username' argument. + + @type sharedKey: str + @param sharedKey: Shared key for mutual authentication. + Requires the 'username' argument. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: Certificate chain for client authentication. + Requires the 'privateKey' argument. Excludes the SRP or + shared-key related arguments. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: Private key for client authentication. + Requires the 'certChain' argument. Excludes the SRP or + shared-key related arguments. + + @type cryptoID: str + @param cryptoID: cryptoID for server authentication. Mutually + exclusive with the 'x509...' arguments. + + @type protocol: str + @param protocol: cryptoID protocol URI for server + authentication. Requires the 'cryptoID' argument. + + @type x509Fingerprint: str + @param x509Fingerprint: Hex-encoded X.509 fingerprint for + server authentication. Mutually exclusive with the 'cryptoID' + and 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed to use this parameter. Mutually exclusive with the + 'cryptoID' and 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + """ + + self.host = host + self.port = port + msg = "getaddrinfo returns an empty list" + self.sock = None + for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + self.sock = socket.socket(af, socktype, proto) + self.sock.connect(sa) + except socket.error, msg: + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + + ### New code below (all else copied from poplib) + ClientHelper.__init__(self, + username, password, sharedKey, + certChain, privateKey, + cryptoID, protocol, + x509Fingerprint, + x509TrustList, x509CommonName, + settings) + + self.sock = TLSConnection(self.sock) + self.sock.closeSocket = True + ClientHelper._handshake(self, self.sock) + ### + + self.file = self.sock.makefile('rb') + self._debugging = 0 + self.welcome = self._getresp() diff --git a/patches/gdata/tlslite/integration/SMTP_TLS.py b/patches/gdata/tlslite/integration/SMTP_TLS.py new file mode 100755 index 0000000..67e0feb --- /dev/null +++ b/patches/gdata/tlslite/integration/SMTP_TLS.py @@ -0,0 +1,114 @@ +"""TLS Lite + smtplib.""" + +from smtplib import SMTP +from gdata.tlslite.TLSConnection import TLSConnection +from gdata.tlslite.integration.ClientHelper import ClientHelper + +class SMTP_TLS(SMTP): + """This class extends L{smtplib.SMTP} with TLS support.""" + + def starttls(self, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings=None): + """Puts the connection to the SMTP server into TLS mode. + + If the server supports TLS, this will encrypt the rest of the SMTP + session. + + For client authentication, use one of these argument + combinations: + - username, password (SRP) + - username, sharedKey (shared-key) + - certChain, privateKey (certificate) + + For server authentication, you can either rely on the + implicit mutual authentication performed by SRP or + shared-keys, or you can do certificate-based server + authentication with one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + Certificate-based server authentication is compatible with + SRP or certificate-based client authentication. It is + not compatible with shared-keys. + + The caller should be prepared to handle TLS-specific + exceptions. See the client handshake functions in + L{tlslite.TLSConnection.TLSConnection} for details on which + exceptions might be raised. + + @type username: str + @param username: SRP or shared-key username. Requires the + 'password' or 'sharedKey' argument. + + @type password: str + @param password: SRP password for mutual authentication. + Requires the 'username' argument. + + @type sharedKey: str + @param sharedKey: Shared key for mutual authentication. + Requires the 'username' argument. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: Certificate chain for client authentication. + Requires the 'privateKey' argument. Excludes the SRP or + shared-key related arguments. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: Private key for client authentication. + Requires the 'certChain' argument. Excludes the SRP or + shared-key related arguments. + + @type cryptoID: str + @param cryptoID: cryptoID for server authentication. Mutually + exclusive with the 'x509...' arguments. + + @type protocol: str + @param protocol: cryptoID protocol URI for server + authentication. Requires the 'cryptoID' argument. + + @type x509Fingerprint: str + @param x509Fingerprint: Hex-encoded X.509 fingerprint for + server authentication. Mutually exclusive with the 'cryptoID' + and 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed to use this parameter. Mutually exclusive with the + 'cryptoID' and 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + """ + (resp, reply) = self.docmd("STARTTLS") + if resp == 220: + helper = ClientHelper( + username, password, sharedKey, + certChain, privateKey, + cryptoID, protocol, + x509Fingerprint, + x509TrustList, x509CommonName, + settings) + conn = TLSConnection(self.sock) + conn.closeSocket = True + helper._handshake(conn) + self.sock = conn + self.file = conn.makefile('rb') + return (resp, reply) diff --git a/patches/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py b/patches/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py new file mode 100755 index 0000000..f732f62 --- /dev/null +++ b/patches/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py @@ -0,0 +1,139 @@ +"""TLS Lite + asyncore.""" + + +import asyncore +from gdata.tlslite.TLSConnection import TLSConnection +from AsyncStateMachine import AsyncStateMachine + + +class TLSAsyncDispatcherMixIn(AsyncStateMachine): + """This class can be "mixed in" with an + L{asyncore.dispatcher} to add TLS support. + + This class essentially sits between the dispatcher and the select + loop, intercepting events and only calling the dispatcher when + applicable. + + In the case of handle_read(), a read operation will be activated, + and when it completes, the bytes will be placed in a buffer where + the dispatcher can retrieve them by calling recv(), and the + dispatcher's handle_read() will be called. + + In the case of handle_write(), the dispatcher's handle_write() will + be called, and when it calls send(), a write operation will be + activated. + + To use this class, you must combine it with an asyncore.dispatcher, + and pass in a handshake operation with setServerHandshakeOp(). + + Below is an example of using this class with medusa. This class is + mixed in with http_channel to create http_tls_channel. Note: + 1. the mix-in is listed first in the inheritance list + + 2. the input buffer size must be at least 16K, otherwise the + dispatcher might not read all the bytes from the TLS layer, + leaving some bytes in limbo. + + 3. IE seems to have a problem receiving a whole HTTP response in a + single TLS record, so HTML pages containing '\\r\\n\\r\\n' won't + be displayed on IE. + + Add the following text into 'start_medusa.py', in the 'HTTP Server' + section:: + + from tlslite.api import * + s = open("./serverX509Cert.pem").read() + x509 = X509() + x509.parse(s) + certChain = X509CertChain([x509]) + + s = open("./serverX509Key.pem").read() + privateKey = parsePEMKey(s, private=True) + + class http_tls_channel(TLSAsyncDispatcherMixIn, + http_server.http_channel): + ac_in_buffer_size = 16384 + + def __init__ (self, server, conn, addr): + http_server.http_channel.__init__(self, server, conn, addr) + TLSAsyncDispatcherMixIn.__init__(self, conn) + self.tlsConnection.ignoreAbruptClose = True + self.setServerHandshakeOp(certChain=certChain, + privateKey=privateKey) + + hs.channel_class = http_tls_channel + + If the TLS layer raises an exception, the exception will be caught + in asyncore.dispatcher, which will call close() on this class. The + TLS layer always closes the TLS connection before raising an + exception, so the close operation will complete right away, causing + asyncore.dispatcher.close() to be called, which closes the socket + and removes this instance from the asyncore loop. + + """ + + + def __init__(self, sock=None): + AsyncStateMachine.__init__(self) + + if sock: + self.tlsConnection = TLSConnection(sock) + + #Calculate the sibling I'm being mixed in with. + #This is necessary since we override functions + #like readable(), handle_read(), etc., but we + #also want to call the sibling's versions. + for cl in self.__class__.__bases__: + if cl != TLSAsyncDispatcherMixIn and cl != AsyncStateMachine: + self.siblingClass = cl + break + else: + raise AssertionError() + + def readable(self): + result = self.wantsReadEvent() + if result != None: + return result + return self.siblingClass.readable(self) + + def writable(self): + result = self.wantsWriteEvent() + if result != None: + return result + return self.siblingClass.writable(self) + + def handle_read(self): + self.inReadEvent() + + def handle_write(self): + self.inWriteEvent() + + def outConnectEvent(self): + self.siblingClass.handle_connect(self) + + def outCloseEvent(self): + asyncore.dispatcher.close(self) + + def outReadEvent(self, readBuffer): + self.readBuffer = readBuffer + self.siblingClass.handle_read(self) + + def outWriteEvent(self): + self.siblingClass.handle_write(self) + + def recv(self, bufferSize=16384): + if bufferSize < 16384 or self.readBuffer == None: + raise AssertionError() + returnValue = self.readBuffer + self.readBuffer = None + return returnValue + + def send(self, writeBuffer): + self.setWriteOp(writeBuffer) + return len(writeBuffer) + + def close(self): + if hasattr(self, "tlsConnection"): + self.setCloseOp() + else: + asyncore.dispatcher.close(self) diff --git a/patches/gdata/tlslite/integration/TLSSocketServerMixIn.py b/patches/gdata/tlslite/integration/TLSSocketServerMixIn.py new file mode 100755 index 0000000..10224b6 --- /dev/null +++ b/patches/gdata/tlslite/integration/TLSSocketServerMixIn.py @@ -0,0 +1,59 @@ +"""TLS Lite + SocketServer.""" + +from gdata.tlslite.TLSConnection import TLSConnection + +class TLSSocketServerMixIn: + """ + This class can be mixed in with any L{SocketServer.TCPServer} to + add TLS support. + + To use this class, define a new class that inherits from it and + some L{SocketServer.TCPServer} (with the mix-in first). Then + implement the handshake() method, doing some sort of server + handshake on the connection argument. If the handshake method + returns True, the RequestHandler will be triggered. Below is a + complete example of a threaded HTTPS server:: + + from SocketServer import * + from BaseHTTPServer import * + from SimpleHTTPServer import * + from tlslite.api import * + + s = open("./serverX509Cert.pem").read() + x509 = X509() + x509.parse(s) + certChain = X509CertChain([x509]) + + s = open("./serverX509Key.pem").read() + privateKey = parsePEMKey(s, private=True) + + sessionCache = SessionCache() + + class MyHTTPServer(ThreadingMixIn, TLSSocketServerMixIn, + HTTPServer): + def handshake(self, tlsConnection): + try: + tlsConnection.handshakeServer(certChain=certChain, + privateKey=privateKey, + sessionCache=sessionCache) + tlsConnection.ignoreAbruptClose = True + return True + except TLSError, error: + print "Handshake failure:", str(error) + return False + + httpd = MyHTTPServer(('localhost', 443), SimpleHTTPRequestHandler) + httpd.serve_forever() + """ + + + def finish_request(self, sock, client_address): + tlsConnection = TLSConnection(sock) + if self.handshake(tlsConnection) == True: + self.RequestHandlerClass(tlsConnection, client_address, self) + tlsConnection.close() + + #Implement this method to do some form of handshaking. Return True + #if the handshake finishes properly and the request is authorized. + def handshake(self, tlsConnection): + raise NotImplementedError() diff --git a/patches/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py b/patches/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py new file mode 100755 index 0000000..c88703c --- /dev/null +++ b/patches/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py @@ -0,0 +1,196 @@ +"""TLS Lite + Twisted.""" + +from twisted.protocols.policies import ProtocolWrapper, WrappingFactory +from twisted.python.failure import Failure + +from AsyncStateMachine import AsyncStateMachine +from gdata.tlslite.TLSConnection import TLSConnection +from gdata.tlslite.errors import * + +import socket +import errno + + +#The TLSConnection is created around a "fake socket" that +#plugs it into the underlying Twisted transport +class _FakeSocket: + def __init__(self, wrapper): + self.wrapper = wrapper + self.data = "" + + def send(self, data): + ProtocolWrapper.write(self.wrapper, data) + return len(data) + + def recv(self, numBytes): + if self.data == "": + raise socket.error, (errno.EWOULDBLOCK, "") + returnData = self.data[:numBytes] + self.data = self.data[numBytes:] + return returnData + +class TLSTwistedProtocolWrapper(ProtocolWrapper, AsyncStateMachine): + """This class can wrap Twisted protocols to add TLS support. + + Below is a complete example of using TLS Lite with a Twisted echo + server. + + There are two server implementations below. Echo is the original + protocol, which is oblivious to TLS. Echo1 subclasses Echo and + negotiates TLS when the client connects. Echo2 subclasses Echo and + negotiates TLS when the client sends "STARTTLS":: + + from twisted.internet.protocol import Protocol, Factory + from twisted.internet import reactor + from twisted.protocols.policies import WrappingFactory + from twisted.protocols.basic import LineReceiver + from twisted.python import log + from twisted.python.failure import Failure + import sys + from tlslite.api import * + + s = open("./serverX509Cert.pem").read() + x509 = X509() + x509.parse(s) + certChain = X509CertChain([x509]) + + s = open("./serverX509Key.pem").read() + privateKey = parsePEMKey(s, private=True) + + verifierDB = VerifierDB("verifierDB") + verifierDB.open() + + class Echo(LineReceiver): + def connectionMade(self): + self.transport.write("Welcome to the echo server!\\r\\n") + + def lineReceived(self, line): + self.transport.write(line + "\\r\\n") + + class Echo1(Echo): + def connectionMade(self): + if not self.transport.tlsStarted: + self.transport.setServerHandshakeOp(certChain=certChain, + privateKey=privateKey, + verifierDB=verifierDB) + else: + Echo.connectionMade(self) + + def connectionLost(self, reason): + pass #Handle any TLS exceptions here + + class Echo2(Echo): + def lineReceived(self, data): + if data == "STARTTLS": + self.transport.setServerHandshakeOp(certChain=certChain, + privateKey=privateKey, + verifierDB=verifierDB) + else: + Echo.lineReceived(self, data) + + def connectionLost(self, reason): + pass #Handle any TLS exceptions here + + factory = Factory() + factory.protocol = Echo1 + #factory.protocol = Echo2 + + wrappingFactory = WrappingFactory(factory) + wrappingFactory.protocol = TLSTwistedProtocolWrapper + + log.startLogging(sys.stdout) + reactor.listenTCP(1079, wrappingFactory) + reactor.run() + + This class works as follows: + + Data comes in and is given to the AsyncStateMachine for handling. + AsyncStateMachine will forward events to this class, and we'll + pass them on to the ProtocolHandler, which will proxy them to the + wrapped protocol. The wrapped protocol may then call back into + this class, and these calls will be proxied into the + AsyncStateMachine. + + The call graph looks like this: + - self.dataReceived + - AsyncStateMachine.inReadEvent + - self.out(Connect|Close|Read)Event + - ProtocolWrapper.(connectionMade|loseConnection|dataReceived) + - self.(loseConnection|write|writeSequence) + - AsyncStateMachine.(setCloseOp|setWriteOp) + """ + + #WARNING: IF YOU COPY-AND-PASTE THE ABOVE CODE, BE SURE TO REMOVE + #THE EXTRA ESCAPING AROUND "\\r\\n" + + def __init__(self, factory, wrappedProtocol): + ProtocolWrapper.__init__(self, factory, wrappedProtocol) + AsyncStateMachine.__init__(self) + self.fakeSocket = _FakeSocket(self) + self.tlsConnection = TLSConnection(self.fakeSocket) + self.tlsStarted = False + self.connectionLostCalled = False + + def connectionMade(self): + try: + ProtocolWrapper.connectionMade(self) + except TLSError, e: + self.connectionLost(Failure(e)) + ProtocolWrapper.loseConnection(self) + + def dataReceived(self, data): + try: + if not self.tlsStarted: + ProtocolWrapper.dataReceived(self, data) + else: + self.fakeSocket.data += data + while self.fakeSocket.data: + AsyncStateMachine.inReadEvent(self) + except TLSError, e: + self.connectionLost(Failure(e)) + ProtocolWrapper.loseConnection(self) + + def connectionLost(self, reason): + if not self.connectionLostCalled: + ProtocolWrapper.connectionLost(self, reason) + self.connectionLostCalled = True + + + def outConnectEvent(self): + ProtocolWrapper.connectionMade(self) + + def outCloseEvent(self): + ProtocolWrapper.loseConnection(self) + + def outReadEvent(self, data): + if data == "": + ProtocolWrapper.loseConnection(self) + else: + ProtocolWrapper.dataReceived(self, data) + + + def setServerHandshakeOp(self, **args): + self.tlsStarted = True + AsyncStateMachine.setServerHandshakeOp(self, **args) + + def loseConnection(self): + if not self.tlsStarted: + ProtocolWrapper.loseConnection(self) + else: + AsyncStateMachine.setCloseOp(self) + + def write(self, data): + if not self.tlsStarted: + ProtocolWrapper.write(self, data) + else: + #Because of the FakeSocket, write operations are guaranteed to + #terminate immediately. + AsyncStateMachine.setWriteOp(self, data) + + def writeSequence(self, seq): + if not self.tlsStarted: + ProtocolWrapper.writeSequence(self, seq) + else: + #Because of the FakeSocket, write operations are guaranteed to + #terminate immediately. + AsyncStateMachine.setWriteOp(self, "".join(seq)) diff --git a/patches/gdata/tlslite/integration/XMLRPCTransport.py b/patches/gdata/tlslite/integration/XMLRPCTransport.py new file mode 100755 index 0000000..3f025e4 --- /dev/null +++ b/patches/gdata/tlslite/integration/XMLRPCTransport.py @@ -0,0 +1,137 @@ +"""TLS Lite + xmlrpclib.""" + +import xmlrpclib +import httplib +from gdata.tlslite.integration.HTTPTLSConnection import HTTPTLSConnection +from gdata.tlslite.integration.ClientHelper import ClientHelper + + +class XMLRPCTransport(xmlrpclib.Transport, ClientHelper): + """Handles an HTTPS transaction to an XML-RPC server.""" + + def __init__(self, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings=None): + """Create a new XMLRPCTransport. + + An instance of this class can be passed to L{xmlrpclib.ServerProxy} + to use TLS with XML-RPC calls:: + + from tlslite.api import XMLRPCTransport + from xmlrpclib import ServerProxy + + transport = XMLRPCTransport(user="alice", password="abra123") + server = ServerProxy("https://localhost", transport) + + For client authentication, use one of these argument + combinations: + - username, password (SRP) + - username, sharedKey (shared-key) + - certChain, privateKey (certificate) + + For server authentication, you can either rely on the + implicit mutual authentication performed by SRP or + shared-keys, or you can do certificate-based server + authentication with one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + Certificate-based server authentication is compatible with + SRP or certificate-based client authentication. It is + not compatible with shared-keys. + + The constructor does not perform the TLS handshake itself, but + simply stores these arguments for later. The handshake is + performed only when this class needs to connect with the + server. Thus you should be prepared to handle TLS-specific + exceptions when calling methods of L{xmlrpclib.ServerProxy}. See the + client handshake functions in + L{tlslite.TLSConnection.TLSConnection} for details on which + exceptions might be raised. + + @type username: str + @param username: SRP or shared-key username. Requires the + 'password' or 'sharedKey' argument. + + @type password: str + @param password: SRP password for mutual authentication. + Requires the 'username' argument. + + @type sharedKey: str + @param sharedKey: Shared key for mutual authentication. + Requires the 'username' argument. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: Certificate chain for client authentication. + Requires the 'privateKey' argument. Excludes the SRP or + shared-key related arguments. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: Private key for client authentication. + Requires the 'certChain' argument. Excludes the SRP or + shared-key related arguments. + + @type cryptoID: str + @param cryptoID: cryptoID for server authentication. Mutually + exclusive with the 'x509...' arguments. + + @type protocol: str + @param protocol: cryptoID protocol URI for server + authentication. Requires the 'cryptoID' argument. + + @type x509Fingerprint: str + @param x509Fingerprint: Hex-encoded X.509 fingerprint for + server authentication. Mutually exclusive with the 'cryptoID' + and 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed to use this parameter. Mutually exclusive with the + 'cryptoID' and 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + """ + + ClientHelper.__init__(self, + username, password, sharedKey, + certChain, privateKey, + cryptoID, protocol, + x509Fingerprint, + x509TrustList, x509CommonName, + settings) + + + def make_connection(self, host): + # create a HTTPS connection object from a host descriptor + host, extra_headers, x509 = self.get_host_info(host) + http = HTTPTLSConnection(host, None, + self.username, self.password, + self.sharedKey, + self.certChain, self.privateKey, + self.checker.cryptoID, + self.checker.protocol, + self.checker.x509Fingerprint, + self.checker.x509TrustList, + self.checker.x509CommonName, + self.settings) + http2 = httplib.HTTP() + http2._setup(http) + return http2 diff --git a/patches/gdata/tlslite/integration/__init__.py b/patches/gdata/tlslite/integration/__init__.py new file mode 100755 index 0000000..960f406 --- /dev/null +++ b/patches/gdata/tlslite/integration/__init__.py @@ -0,0 +1,17 @@ +"""Classes for integrating TLS Lite with other packages.""" + +__all__ = ["AsyncStateMachine", + "HTTPTLSConnection", + "POP3_TLS", + "IMAP4_TLS", + "SMTP_TLS", + "XMLRPCTransport", + "TLSSocketServerMixIn", + "TLSAsyncDispatcherMixIn", + "TLSTwistedProtocolWrapper"] + +try: + import twisted + del twisted +except ImportError: + del __all__[__all__.index("TLSTwistedProtocolWrapper")] diff --git a/patches/gdata/tlslite/mathtls.py b/patches/gdata/tlslite/mathtls.py new file mode 100755 index 0000000..3b8ede6 --- /dev/null +++ b/patches/gdata/tlslite/mathtls.py @@ -0,0 +1,170 @@ +"""Miscellaneous helper functions.""" + +from utils.compat import * +from utils.cryptomath import * + +import hmac +import md5 +import sha + +#1024, 1536, 2048, 3072, 4096, 6144, and 8192 bit groups] +goodGroupParameters = [(2,0xEEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3),\ + (2,0x9DEF3CAFB939277AB1F12A8617A47BBBDBA51DF499AC4C80BEEEA9614B19CC4D5F4F5F556E27CBDE51C6A94BE4607A291558903BA0D0F84380B655BB9A22E8DCDF028A7CEC67F0D08134B1C8B97989149B609E0BE3BAB63D47548381DBC5B1FC764E3F4B53DD9DA1158BFD3E2B9C8CF56EDF019539349627DB2FD53D24B7C48665772E437D6C7F8CE442734AF7CCB7AE837C264AE3A9BEB87F8A2FE9B8B5292E5A021FFF5E91479E8CE7A28C2442C6F315180F93499A234DCF76E3FED135F9BB),\ + (2,0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73),\ + (2,0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF),\ + (5,0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF),\ + (5,0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF),\ + (5,0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF)] + +def P_hash(hashModule, secret, seed, length): + bytes = createByteArrayZeros(length) + secret = bytesToString(secret) + seed = bytesToString(seed) + A = seed + index = 0 + while 1: + A = hmac.HMAC(secret, A, hashModule).digest() + output = hmac.HMAC(secret, A+seed, hashModule).digest() + for c in output: + if index >= length: + return bytes + bytes[index] = ord(c) + index += 1 + return bytes + +def PRF(secret, label, seed, length): + #Split the secret into left and right halves + S1 = secret[ : int(math.ceil(len(secret)/2.0))] + S2 = secret[ int(math.floor(len(secret)/2.0)) : ] + + #Run the left half through P_MD5 and the right half through P_SHA1 + p_md5 = P_hash(md5, S1, concatArrays(stringToBytes(label), seed), length) + p_sha1 = P_hash(sha, S2, concatArrays(stringToBytes(label), seed), length) + + #XOR the output values and return the result + for x in range(length): + p_md5[x] ^= p_sha1[x] + return p_md5 + + +def PRF_SSL(secret, seed, length): + secretStr = bytesToString(secret) + seedStr = bytesToString(seed) + bytes = createByteArrayZeros(length) + index = 0 + for x in range(26): + A = chr(ord('A')+x) * (x+1) # 'A', 'BB', 'CCC', etc.. + input = secretStr + sha.sha(A + secretStr + seedStr).digest() + output = md5.md5(input).digest() + for c in output: + if index >= length: + return bytes + bytes[index] = ord(c) + index += 1 + return bytes + +def makeX(salt, username, password): + if len(username)>=256: + raise ValueError("username too long") + if len(salt)>=256: + raise ValueError("salt too long") + return stringToNumber(sha.sha(salt + sha.sha(username + ":" + password)\ + .digest()).digest()) + +#This function is used by VerifierDB.makeVerifier +def makeVerifier(username, password, bits): + bitsIndex = {1024:0, 1536:1, 2048:2, 3072:3, 4096:4, 6144:5, 8192:6}[bits] + g,N = goodGroupParameters[bitsIndex] + salt = bytesToString(getRandomBytes(16)) + x = makeX(salt, username, password) + verifier = powMod(g, x, N) + return N, g, salt, verifier + +def PAD(n, x): + nLength = len(numberToString(n)) + s = numberToString(x) + if len(s) < nLength: + s = ("\0" * (nLength-len(s))) + s + return s + +def makeU(N, A, B): + return stringToNumber(sha.sha(PAD(N, A) + PAD(N, B)).digest()) + +def makeK(N, g): + return stringToNumber(sha.sha(numberToString(N) + PAD(N, g)).digest()) + + +""" +MAC_SSL +Modified from Python HMAC by Trevor +""" + +class MAC_SSL: + """MAC_SSL class. + + This supports the API for Cryptographic Hash Functions (PEP 247). + """ + + def __init__(self, key, msg = None, digestmod = None): + """Create a new MAC_SSL object. + + key: key for the keyed hash object. + msg: Initial input for the hash, if provided. + digestmod: A module supporting PEP 247. Defaults to the md5 module. + """ + if digestmod is None: + import md5 + digestmod = md5 + + if key == None: #TREVNEW - for faster copying + return #TREVNEW + + self.digestmod = digestmod + self.outer = digestmod.new() + self.inner = digestmod.new() + self.digest_size = digestmod.digest_size + + ipad = "\x36" * 40 + opad = "\x5C" * 40 + + self.inner.update(key) + self.inner.update(ipad) + self.outer.update(key) + self.outer.update(opad) + if msg is not None: + self.update(msg) + + + def update(self, msg): + """Update this hashing object with the string msg. + """ + self.inner.update(msg) + + def copy(self): + """Return a separate copy of this hashing object. + + An update to this copy won't affect the original object. + """ + other = MAC_SSL(None) #TREVNEW - for faster copying + other.digest_size = self.digest_size #TREVNEW + other.digestmod = self.digestmod + other.inner = self.inner.copy() + other.outer = self.outer.copy() + return other + + def digest(self): + """Return the hash value of this hashing object. + + This returns a string containing 8-bit data. The object is + not altered in any way by this function; you can continue + updating the object after calling this function. + """ + h = self.outer.copy() + h.update(self.inner.digest()) + return h.digest() + + def hexdigest(self): + """Like digest(), but returns a string of hexadecimal digits instead. + """ + return "".join([hex(ord(x))[2:].zfill(2) + for x in tuple(self.digest())]) diff --git a/patches/gdata/tlslite/messages.py b/patches/gdata/tlslite/messages.py new file mode 100755 index 0000000..afccc79 --- /dev/null +++ b/patches/gdata/tlslite/messages.py @@ -0,0 +1,561 @@ +"""Classes representing TLS messages.""" + +from utils.compat import * +from utils.cryptomath import * +from errors import * +from utils.codec import * +from constants import * +from X509 import X509 +from X509CertChain import X509CertChain + +import sha +import md5 + +class RecordHeader3: + def __init__(self): + self.type = 0 + self.version = (0,0) + self.length = 0 + self.ssl2 = False + + def create(self, version, type, length): + self.type = type + self.version = version + self.length = length + return self + + def write(self): + w = Writer(5) + w.add(self.type, 1) + w.add(self.version[0], 1) + w.add(self.version[1], 1) + w.add(self.length, 2) + return w.bytes + + def parse(self, p): + self.type = p.get(1) + self.version = (p.get(1), p.get(1)) + self.length = p.get(2) + self.ssl2 = False + return self + +class RecordHeader2: + def __init__(self): + self.type = 0 + self.version = (0,0) + self.length = 0 + self.ssl2 = True + + def parse(self, p): + if p.get(1)!=128: + raise SyntaxError() + self.type = ContentType.handshake + self.version = (2,0) + #We don't support 2-byte-length-headers; could be a problem + self.length = p.get(1) + return self + + +class Msg: + def preWrite(self, trial): + if trial: + w = Writer() + else: + length = self.write(True) + w = Writer(length) + return w + + def postWrite(self, w, trial): + if trial: + return w.index + else: + return w.bytes + +class Alert(Msg): + def __init__(self): + self.contentType = ContentType.alert + self.level = 0 + self.description = 0 + + def create(self, description, level=AlertLevel.fatal): + self.level = level + self.description = description + return self + + def parse(self, p): + p.setLengthCheck(2) + self.level = p.get(1) + self.description = p.get(1) + p.stopLengthCheck() + return self + + def write(self): + w = Writer(2) + w.add(self.level, 1) + w.add(self.description, 1) + return w.bytes + + +class HandshakeMsg(Msg): + def preWrite(self, handshakeType, trial): + if trial: + w = Writer() + w.add(handshakeType, 1) + w.add(0, 3) + else: + length = self.write(True) + w = Writer(length) + w.add(handshakeType, 1) + w.add(length-4, 3) + return w + + +class ClientHello(HandshakeMsg): + def __init__(self, ssl2=False): + self.contentType = ContentType.handshake + self.ssl2 = ssl2 + self.client_version = (0,0) + self.random = createByteArrayZeros(32) + self.session_id = createByteArraySequence([]) + self.cipher_suites = [] # a list of 16-bit values + self.certificate_types = [CertificateType.x509] + self.compression_methods = [] # a list of 8-bit values + self.srp_username = None # a string + + def create(self, version, random, session_id, cipher_suites, + certificate_types=None, srp_username=None): + self.client_version = version + self.random = random + self.session_id = session_id + self.cipher_suites = cipher_suites + self.certificate_types = certificate_types + self.compression_methods = [0] + self.srp_username = srp_username + return self + + def parse(self, p): + if self.ssl2: + self.client_version = (p.get(1), p.get(1)) + cipherSpecsLength = p.get(2) + sessionIDLength = p.get(2) + randomLength = p.get(2) + self.cipher_suites = p.getFixList(3, int(cipherSpecsLength/3)) + self.session_id = p.getFixBytes(sessionIDLength) + self.random = p.getFixBytes(randomLength) + if len(self.random) < 32: + zeroBytes = 32-len(self.random) + self.random = createByteArrayZeros(zeroBytes) + self.random + self.compression_methods = [0]#Fake this value + + #We're not doing a stopLengthCheck() for SSLv2, oh well.. + else: + p.startLengthCheck(3) + self.client_version = (p.get(1), p.get(1)) + self.random = p.getFixBytes(32) + self.session_id = p.getVarBytes(1) + self.cipher_suites = p.getVarList(2, 2) + self.compression_methods = p.getVarList(1, 1) + if not p.atLengthCheck(): + totalExtLength = p.get(2) + soFar = 0 + while soFar != totalExtLength: + extType = p.get(2) + extLength = p.get(2) + if extType == 6: + self.srp_username = bytesToString(p.getVarBytes(1)) + elif extType == 7: + self.certificate_types = p.getVarList(1, 1) + else: + p.getFixBytes(extLength) + soFar += 4 + extLength + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.client_hello, trial) + w.add(self.client_version[0], 1) + w.add(self.client_version[1], 1) + w.addFixSeq(self.random, 1) + w.addVarSeq(self.session_id, 1, 1) + w.addVarSeq(self.cipher_suites, 2, 2) + w.addVarSeq(self.compression_methods, 1, 1) + + extLength = 0 + if self.certificate_types and self.certificate_types != \ + [CertificateType.x509]: + extLength += 5 + len(self.certificate_types) + if self.srp_username: + extLength += 5 + len(self.srp_username) + if extLength > 0: + w.add(extLength, 2) + + if self.certificate_types and self.certificate_types != \ + [CertificateType.x509]: + w.add(7, 2) + w.add(len(self.certificate_types)+1, 2) + w.addVarSeq(self.certificate_types, 1, 1) + if self.srp_username: + w.add(6, 2) + w.add(len(self.srp_username)+1, 2) + w.addVarSeq(stringToBytes(self.srp_username), 1, 1) + + return HandshakeMsg.postWrite(self, w, trial) + + +class ServerHello(HandshakeMsg): + def __init__(self): + self.contentType = ContentType.handshake + self.server_version = (0,0) + self.random = createByteArrayZeros(32) + self.session_id = createByteArraySequence([]) + self.cipher_suite = 0 + self.certificate_type = CertificateType.x509 + self.compression_method = 0 + + def create(self, version, random, session_id, cipher_suite, + certificate_type): + self.server_version = version + self.random = random + self.session_id = session_id + self.cipher_suite = cipher_suite + self.certificate_type = certificate_type + self.compression_method = 0 + return self + + def parse(self, p): + p.startLengthCheck(3) + self.server_version = (p.get(1), p.get(1)) + self.random = p.getFixBytes(32) + self.session_id = p.getVarBytes(1) + self.cipher_suite = p.get(2) + self.compression_method = p.get(1) + if not p.atLengthCheck(): + totalExtLength = p.get(2) + soFar = 0 + while soFar != totalExtLength: + extType = p.get(2) + extLength = p.get(2) + if extType == 7: + self.certificate_type = p.get(1) + else: + p.getFixBytes(extLength) + soFar += 4 + extLength + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.server_hello, trial) + w.add(self.server_version[0], 1) + w.add(self.server_version[1], 1) + w.addFixSeq(self.random, 1) + w.addVarSeq(self.session_id, 1, 1) + w.add(self.cipher_suite, 2) + w.add(self.compression_method, 1) + + extLength = 0 + if self.certificate_type and self.certificate_type != \ + CertificateType.x509: + extLength += 5 + + if extLength != 0: + w.add(extLength, 2) + + if self.certificate_type and self.certificate_type != \ + CertificateType.x509: + w.add(7, 2) + w.add(1, 2) + w.add(self.certificate_type, 1) + + return HandshakeMsg.postWrite(self, w, trial) + +class Certificate(HandshakeMsg): + def __init__(self, certificateType): + self.certificateType = certificateType + self.contentType = ContentType.handshake + self.certChain = None + + def create(self, certChain): + self.certChain = certChain + return self + + def parse(self, p): + p.startLengthCheck(3) + if self.certificateType == CertificateType.x509: + chainLength = p.get(3) + index = 0 + certificate_list = [] + while index != chainLength: + certBytes = p.getVarBytes(3) + x509 = X509() + x509.parseBinary(certBytes) + certificate_list.append(x509) + index += len(certBytes)+3 + if certificate_list: + self.certChain = X509CertChain(certificate_list) + elif self.certificateType == CertificateType.cryptoID: + s = bytesToString(p.getVarBytes(2)) + if s: + try: + import cryptoIDlib.CertChain + except ImportError: + raise SyntaxError(\ + "cryptoID cert chain received, cryptoIDlib not present") + self.certChain = cryptoIDlib.CertChain.CertChain().parse(s) + else: + raise AssertionError() + + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.certificate, trial) + if self.certificateType == CertificateType.x509: + chainLength = 0 + if self.certChain: + certificate_list = self.certChain.x509List + else: + certificate_list = [] + #determine length + for cert in certificate_list: + bytes = cert.writeBytes() + chainLength += len(bytes)+3 + #add bytes + w.add(chainLength, 3) + for cert in certificate_list: + bytes = cert.writeBytes() + w.addVarSeq(bytes, 1, 3) + elif self.certificateType == CertificateType.cryptoID: + if self.certChain: + bytes = stringToBytes(self.certChain.write()) + else: + bytes = createByteArraySequence([]) + w.addVarSeq(bytes, 1, 2) + else: + raise AssertionError() + return HandshakeMsg.postWrite(self, w, trial) + +class CertificateRequest(HandshakeMsg): + def __init__(self): + self.contentType = ContentType.handshake + self.certificate_types = [] + #treat as opaque bytes for now + self.certificate_authorities = createByteArraySequence([]) + + def create(self, certificate_types, certificate_authorities): + self.certificate_types = certificate_types + self.certificate_authorities = certificate_authorities + return self + + def parse(self, p): + p.startLengthCheck(3) + self.certificate_types = p.getVarList(1, 1) + self.certificate_authorities = p.getVarBytes(2) + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.certificate_request, + trial) + w.addVarSeq(self.certificate_types, 1, 1) + w.addVarSeq(self.certificate_authorities, 1, 2) + return HandshakeMsg.postWrite(self, w, trial) + +class ServerKeyExchange(HandshakeMsg): + def __init__(self, cipherSuite): + self.cipherSuite = cipherSuite + self.contentType = ContentType.handshake + self.srp_N = 0L + self.srp_g = 0L + self.srp_s = createByteArraySequence([]) + self.srp_B = 0L + self.signature = createByteArraySequence([]) + + def createSRP(self, srp_N, srp_g, srp_s, srp_B): + self.srp_N = srp_N + self.srp_g = srp_g + self.srp_s = srp_s + self.srp_B = srp_B + return self + + def parse(self, p): + p.startLengthCheck(3) + self.srp_N = bytesToNumber(p.getVarBytes(2)) + self.srp_g = bytesToNumber(p.getVarBytes(2)) + self.srp_s = p.getVarBytes(1) + self.srp_B = bytesToNumber(p.getVarBytes(2)) + if self.cipherSuite in CipherSuite.srpRsaSuites: + self.signature = p.getVarBytes(2) + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.server_key_exchange, + trial) + w.addVarSeq(numberToBytes(self.srp_N), 1, 2) + w.addVarSeq(numberToBytes(self.srp_g), 1, 2) + w.addVarSeq(self.srp_s, 1, 1) + w.addVarSeq(numberToBytes(self.srp_B), 1, 2) + if self.cipherSuite in CipherSuite.srpRsaSuites: + w.addVarSeq(self.signature, 1, 2) + return HandshakeMsg.postWrite(self, w, trial) + + def hash(self, clientRandom, serverRandom): + oldCipherSuite = self.cipherSuite + self.cipherSuite = None + try: + bytes = clientRandom + serverRandom + self.write()[4:] + s = bytesToString(bytes) + return stringToBytes(md5.md5(s).digest() + sha.sha(s).digest()) + finally: + self.cipherSuite = oldCipherSuite + +class ServerHelloDone(HandshakeMsg): + def __init__(self): + self.contentType = ContentType.handshake + + def create(self): + return self + + def parse(self, p): + p.startLengthCheck(3) + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.server_hello_done, trial) + return HandshakeMsg.postWrite(self, w, trial) + +class ClientKeyExchange(HandshakeMsg): + def __init__(self, cipherSuite, version=None): + self.cipherSuite = cipherSuite + self.version = version + self.contentType = ContentType.handshake + self.srp_A = 0 + self.encryptedPreMasterSecret = createByteArraySequence([]) + + def createSRP(self, srp_A): + self.srp_A = srp_A + return self + + def createRSA(self, encryptedPreMasterSecret): + self.encryptedPreMasterSecret = encryptedPreMasterSecret + return self + + def parse(self, p): + p.startLengthCheck(3) + if self.cipherSuite in CipherSuite.srpSuites + \ + CipherSuite.srpRsaSuites: + self.srp_A = bytesToNumber(p.getVarBytes(2)) + elif self.cipherSuite in CipherSuite.rsaSuites: + if self.version in ((3,1), (3,2)): + self.encryptedPreMasterSecret = p.getVarBytes(2) + elif self.version == (3,0): + self.encryptedPreMasterSecret = \ + p.getFixBytes(len(p.bytes)-p.index) + else: + raise AssertionError() + else: + raise AssertionError() + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.client_key_exchange, + trial) + if self.cipherSuite in CipherSuite.srpSuites + \ + CipherSuite.srpRsaSuites: + w.addVarSeq(numberToBytes(self.srp_A), 1, 2) + elif self.cipherSuite in CipherSuite.rsaSuites: + if self.version in ((3,1), (3,2)): + w.addVarSeq(self.encryptedPreMasterSecret, 1, 2) + elif self.version == (3,0): + w.addFixSeq(self.encryptedPreMasterSecret, 1) + else: + raise AssertionError() + else: + raise AssertionError() + return HandshakeMsg.postWrite(self, w, trial) + +class CertificateVerify(HandshakeMsg): + def __init__(self): + self.contentType = ContentType.handshake + self.signature = createByteArraySequence([]) + + def create(self, signature): + self.signature = signature + return self + + def parse(self, p): + p.startLengthCheck(3) + self.signature = p.getVarBytes(2) + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.certificate_verify, + trial) + w.addVarSeq(self.signature, 1, 2) + return HandshakeMsg.postWrite(self, w, trial) + +class ChangeCipherSpec(Msg): + def __init__(self): + self.contentType = ContentType.change_cipher_spec + self.type = 1 + + def create(self): + self.type = 1 + return self + + def parse(self, p): + p.setLengthCheck(1) + self.type = p.get(1) + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = Msg.preWrite(self, trial) + w.add(self.type,1) + return Msg.postWrite(self, w, trial) + + +class Finished(HandshakeMsg): + def __init__(self, version): + self.contentType = ContentType.handshake + self.version = version + self.verify_data = createByteArraySequence([]) + + def create(self, verify_data): + self.verify_data = verify_data + return self + + def parse(self, p): + p.startLengthCheck(3) + if self.version == (3,0): + self.verify_data = p.getFixBytes(36) + elif self.version in ((3,1), (3,2)): + self.verify_data = p.getFixBytes(12) + else: + raise AssertionError() + p.stopLengthCheck() + return self + + def write(self, trial=False): + w = HandshakeMsg.preWrite(self, HandshakeType.finished, trial) + w.addFixSeq(self.verify_data, 1) + return HandshakeMsg.postWrite(self, w, trial) + +class ApplicationData(Msg): + def __init__(self): + self.contentType = ContentType.application_data + self.bytes = createByteArraySequence([]) + + def create(self, bytes): + self.bytes = bytes + return self + + def parse(self, p): + self.bytes = p.bytes + return self + + def write(self): + return self.bytes \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/AES.py b/patches/gdata/tlslite/utils/AES.py new file mode 100755 index 0000000..8413f4c --- /dev/null +++ b/patches/gdata/tlslite/utils/AES.py @@ -0,0 +1,31 @@ +"""Abstract class for AES.""" + +class AES: + def __init__(self, key, mode, IV, implementation): + if len(key) not in (16, 24, 32): + raise AssertionError() + if mode != 2: + raise AssertionError() + if len(IV) != 16: + raise AssertionError() + self.isBlockCipher = True + self.block_size = 16 + self.implementation = implementation + if len(key)==16: + self.name = "aes128" + elif len(key)==24: + self.name = "aes192" + elif len(key)==32: + self.name = "aes256" + else: + raise AssertionError() + + #CBC-Mode encryption, returns ciphertext + #WARNING: *MAY* modify the input as well + def encrypt(self, plaintext): + assert(len(plaintext) % 16 == 0) + + #CBC-Mode decryption, returns plaintext + #WARNING: *MAY* modify the input as well + def decrypt(self, ciphertext): + assert(len(ciphertext) % 16 == 0) \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/ASN1Parser.py b/patches/gdata/tlslite/utils/ASN1Parser.py new file mode 100755 index 0000000..16b50f2 --- /dev/null +++ b/patches/gdata/tlslite/utils/ASN1Parser.py @@ -0,0 +1,34 @@ +"""Class for parsing ASN.1""" +from compat import * +from codec import * + +#Takes a byte array which has a DER TLV field at its head +class ASN1Parser: + def __init__(self, bytes): + p = Parser(bytes) + p.get(1) #skip Type + + #Get Length + self.length = self._getASN1Length(p) + + #Get Value + self.value = p.getFixBytes(self.length) + + #Assuming this is a sequence... + def getChild(self, which): + p = Parser(self.value) + for x in range(which+1): + markIndex = p.index + p.get(1) #skip Type + length = self._getASN1Length(p) + p.getFixBytes(length) + return ASN1Parser(p.bytes[markIndex : p.index]) + + #Decode the ASN.1 DER length field + def _getASN1Length(self, p): + firstLength = p.get(1) + if firstLength<=127: + return firstLength + else: + lengthLength = firstLength & 0x7F + return p.get(lengthLength) diff --git a/patches/gdata/tlslite/utils/Cryptlib_AES.py b/patches/gdata/tlslite/utils/Cryptlib_AES.py new file mode 100755 index 0000000..9e101fc --- /dev/null +++ b/patches/gdata/tlslite/utils/Cryptlib_AES.py @@ -0,0 +1,34 @@ +"""Cryptlib AES implementation.""" + +from cryptomath import * +from AES import * + +if cryptlibpyLoaded: + + def new(key, mode, IV): + return Cryptlib_AES(key, mode, IV) + + class Cryptlib_AES(AES): + + def __init__(self, key, mode, IV): + AES.__init__(self, key, mode, IV, "cryptlib") + self.context = cryptlib_py.cryptCreateContext(cryptlib_py.CRYPT_UNUSED, cryptlib_py.CRYPT_ALGO_AES) + cryptlib_py.cryptSetAttribute(self.context, cryptlib_py.CRYPT_CTXINFO_MODE, cryptlib_py.CRYPT_MODE_CBC) + cryptlib_py.cryptSetAttribute(self.context, cryptlib_py.CRYPT_CTXINFO_KEYSIZE, len(key)) + cryptlib_py.cryptSetAttributeString(self.context, cryptlib_py.CRYPT_CTXINFO_KEY, key) + cryptlib_py.cryptSetAttributeString(self.context, cryptlib_py.CRYPT_CTXINFO_IV, IV) + + def __del__(self): + cryptlib_py.cryptDestroyContext(self.context) + + def encrypt(self, plaintext): + AES.encrypt(self, plaintext) + bytes = stringToBytes(plaintext) + cryptlib_py.cryptEncrypt(self.context, bytes) + return bytesToString(bytes) + + def decrypt(self, ciphertext): + AES.decrypt(self, ciphertext) + bytes = stringToBytes(ciphertext) + cryptlib_py.cryptDecrypt(self.context, bytes) + return bytesToString(bytes) diff --git a/patches/gdata/tlslite/utils/Cryptlib_RC4.py b/patches/gdata/tlslite/utils/Cryptlib_RC4.py new file mode 100755 index 0000000..7c6d087 --- /dev/null +++ b/patches/gdata/tlslite/utils/Cryptlib_RC4.py @@ -0,0 +1,28 @@ +"""Cryptlib RC4 implementation.""" + +from cryptomath import * +from RC4 import RC4 + +if cryptlibpyLoaded: + + def new(key): + return Cryptlib_RC4(key) + + class Cryptlib_RC4(RC4): + + def __init__(self, key): + RC4.__init__(self, key, "cryptlib") + self.context = cryptlib_py.cryptCreateContext(cryptlib_py.CRYPT_UNUSED, cryptlib_py.CRYPT_ALGO_RC4) + cryptlib_py.cryptSetAttribute(self.context, cryptlib_py.CRYPT_CTXINFO_KEYSIZE, len(key)) + cryptlib_py.cryptSetAttributeString(self.context, cryptlib_py.CRYPT_CTXINFO_KEY, key) + + def __del__(self): + cryptlib_py.cryptDestroyContext(self.context) + + def encrypt(self, plaintext): + bytes = stringToBytes(plaintext) + cryptlib_py.cryptEncrypt(self.context, bytes) + return bytesToString(bytes) + + def decrypt(self, ciphertext): + return self.encrypt(ciphertext) \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/Cryptlib_TripleDES.py b/patches/gdata/tlslite/utils/Cryptlib_TripleDES.py new file mode 100755 index 0000000..a4f8155 --- /dev/null +++ b/patches/gdata/tlslite/utils/Cryptlib_TripleDES.py @@ -0,0 +1,35 @@ +"""Cryptlib 3DES implementation.""" + +from cryptomath import * + +from TripleDES import * + +if cryptlibpyLoaded: + + def new(key, mode, IV): + return Cryptlib_TripleDES(key, mode, IV) + + class Cryptlib_TripleDES(TripleDES): + + def __init__(self, key, mode, IV): + TripleDES.__init__(self, key, mode, IV, "cryptlib") + self.context = cryptlib_py.cryptCreateContext(cryptlib_py.CRYPT_UNUSED, cryptlib_py.CRYPT_ALGO_3DES) + cryptlib_py.cryptSetAttribute(self.context, cryptlib_py.CRYPT_CTXINFO_MODE, cryptlib_py.CRYPT_MODE_CBC) + cryptlib_py.cryptSetAttribute(self.context, cryptlib_py.CRYPT_CTXINFO_KEYSIZE, len(key)) + cryptlib_py.cryptSetAttributeString(self.context, cryptlib_py.CRYPT_CTXINFO_KEY, key) + cryptlib_py.cryptSetAttributeString(self.context, cryptlib_py.CRYPT_CTXINFO_IV, IV) + + def __del__(self): + cryptlib_py.cryptDestroyContext(self.context) + + def encrypt(self, plaintext): + TripleDES.encrypt(self, plaintext) + bytes = stringToBytes(plaintext) + cryptlib_py.cryptEncrypt(self.context, bytes) + return bytesToString(bytes) + + def decrypt(self, ciphertext): + TripleDES.decrypt(self, ciphertext) + bytes = stringToBytes(ciphertext) + cryptlib_py.cryptDecrypt(self.context, bytes) + return bytesToString(bytes) \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/OpenSSL_AES.py b/patches/gdata/tlslite/utils/OpenSSL_AES.py new file mode 100755 index 0000000..e60679b --- /dev/null +++ b/patches/gdata/tlslite/utils/OpenSSL_AES.py @@ -0,0 +1,49 @@ +"""OpenSSL/M2Crypto AES implementation.""" + +from cryptomath import * +from AES import * + +if m2cryptoLoaded: + + def new(key, mode, IV): + return OpenSSL_AES(key, mode, IV) + + class OpenSSL_AES(AES): + + def __init__(self, key, mode, IV): + AES.__init__(self, key, mode, IV, "openssl") + self.key = key + self.IV = IV + + def _createContext(self, encrypt): + context = m2.cipher_ctx_new() + if len(self.key)==16: + cipherType = m2.aes_128_cbc() + if len(self.key)==24: + cipherType = m2.aes_192_cbc() + if len(self.key)==32: + cipherType = m2.aes_256_cbc() + m2.cipher_init(context, cipherType, self.key, self.IV, encrypt) + return context + + def encrypt(self, plaintext): + AES.encrypt(self, plaintext) + context = self._createContext(1) + ciphertext = m2.cipher_update(context, plaintext) + m2.cipher_ctx_free(context) + self.IV = ciphertext[-self.block_size:] + return ciphertext + + def decrypt(self, ciphertext): + AES.decrypt(self, ciphertext) + context = self._createContext(0) + #I think M2Crypto has a bug - it fails to decrypt and return the last block passed in. + #To work around this, we append sixteen zeros to the string, below: + plaintext = m2.cipher_update(context, ciphertext+('\0'*16)) + + #If this bug is ever fixed, then plaintext will end up having a garbage + #plaintext block on the end. That's okay - the below code will discard it. + plaintext = plaintext[:len(ciphertext)] + m2.cipher_ctx_free(context) + self.IV = ciphertext[-self.block_size:] + return plaintext diff --git a/patches/gdata/tlslite/utils/OpenSSL_RC4.py b/patches/gdata/tlslite/utils/OpenSSL_RC4.py new file mode 100755 index 0000000..ac433aa --- /dev/null +++ b/patches/gdata/tlslite/utils/OpenSSL_RC4.py @@ -0,0 +1,25 @@ +"""OpenSSL/M2Crypto RC4 implementation.""" + +from cryptomath import * +from RC4 import RC4 + +if m2cryptoLoaded: + + def new(key): + return OpenSSL_RC4(key) + + class OpenSSL_RC4(RC4): + + def __init__(self, key): + RC4.__init__(self, key, "openssl") + self.rc4 = m2.rc4_new() + m2.rc4_set_key(self.rc4, key) + + def __del__(self): + m2.rc4_free(self.rc4) + + def encrypt(self, plaintext): + return m2.rc4_update(self.rc4, plaintext) + + def decrypt(self, ciphertext): + return self.encrypt(ciphertext) diff --git a/patches/gdata/tlslite/utils/OpenSSL_RSAKey.py b/patches/gdata/tlslite/utils/OpenSSL_RSAKey.py new file mode 100755 index 0000000..fe1a3cd --- /dev/null +++ b/patches/gdata/tlslite/utils/OpenSSL_RSAKey.py @@ -0,0 +1,148 @@ +"""OpenSSL/M2Crypto RSA implementation.""" + +from cryptomath import * + +from RSAKey import * +from Python_RSAKey import Python_RSAKey + +#copied from M2Crypto.util.py, so when we load the local copy of m2 +#we can still use it +def password_callback(v, prompt1='Enter private key passphrase:', + prompt2='Verify passphrase:'): + from getpass import getpass + while 1: + try: + p1=getpass(prompt1) + if v: + p2=getpass(prompt2) + if p1==p2: + break + else: + break + except KeyboardInterrupt: + return None + return p1 + + +if m2cryptoLoaded: + class OpenSSL_RSAKey(RSAKey): + def __init__(self, n=0, e=0): + self.rsa = None + self._hasPrivateKey = False + if (n and not e) or (e and not n): + raise AssertionError() + if n and e: + self.rsa = m2.rsa_new() + m2.rsa_set_n(self.rsa, numberToMPI(n)) + m2.rsa_set_e(self.rsa, numberToMPI(e)) + + def __del__(self): + if self.rsa: + m2.rsa_free(self.rsa) + + def __getattr__(self, name): + if name == 'e': + if not self.rsa: + return 0 + return mpiToNumber(m2.rsa_get_e(self.rsa)) + elif name == 'n': + if not self.rsa: + return 0 + return mpiToNumber(m2.rsa_get_n(self.rsa)) + else: + raise AttributeError + + def hasPrivateKey(self): + return self._hasPrivateKey + + def hash(self): + return Python_RSAKey(self.n, self.e).hash() + + def _rawPrivateKeyOp(self, m): + s = numberToString(m) + byteLength = numBytes(self.n) + if len(s)== byteLength: + pass + elif len(s) == byteLength-1: + s = '\0' + s + else: + raise AssertionError() + c = stringToNumber(m2.rsa_private_encrypt(self.rsa, s, + m2.no_padding)) + return c + + def _rawPublicKeyOp(self, c): + s = numberToString(c) + byteLength = numBytes(self.n) + if len(s)== byteLength: + pass + elif len(s) == byteLength-1: + s = '\0' + s + else: + raise AssertionError() + m = stringToNumber(m2.rsa_public_decrypt(self.rsa, s, + m2.no_padding)) + return m + + def acceptsPassword(self): return True + + def write(self, password=None): + bio = m2.bio_new(m2.bio_s_mem()) + if self._hasPrivateKey: + if password: + def f(v): return password + m2.rsa_write_key(self.rsa, bio, m2.des_ede_cbc(), f) + else: + def f(): pass + m2.rsa_write_key_no_cipher(self.rsa, bio, f) + else: + if password: + raise AssertionError() + m2.rsa_write_pub_key(self.rsa, bio) + s = m2.bio_read(bio, m2.bio_ctrl_pending(bio)) + m2.bio_free(bio) + return s + + def writeXMLPublicKey(self, indent=''): + return Python_RSAKey(self.n, self.e).write(indent) + + def generate(bits): + key = OpenSSL_RSAKey() + def f():pass + key.rsa = m2.rsa_generate_key(bits, 3, f) + key._hasPrivateKey = True + return key + generate = staticmethod(generate) + + def parse(s, passwordCallback=None): + if s.startswith("-----BEGIN "): + if passwordCallback==None: + callback = password_callback + else: + def f(v, prompt1=None, prompt2=None): + return passwordCallback() + callback = f + bio = m2.bio_new(m2.bio_s_mem()) + try: + m2.bio_write(bio, s) + key = OpenSSL_RSAKey() + if s.startswith("-----BEGIN RSA PRIVATE KEY-----"): + def f():pass + key.rsa = m2.rsa_read_key(bio, callback) + if key.rsa == None: + raise SyntaxError() + key._hasPrivateKey = True + elif s.startswith("-----BEGIN PUBLIC KEY-----"): + key.rsa = m2.rsa_read_pub_key(bio) + if key.rsa == None: + raise SyntaxError() + key._hasPrivateKey = False + else: + raise SyntaxError() + return key + finally: + m2.bio_free(bio) + else: + raise SyntaxError() + + parse = staticmethod(parse) diff --git a/patches/gdata/tlslite/utils/OpenSSL_TripleDES.py b/patches/gdata/tlslite/utils/OpenSSL_TripleDES.py new file mode 100755 index 0000000..f5ba165 --- /dev/null +++ b/patches/gdata/tlslite/utils/OpenSSL_TripleDES.py @@ -0,0 +1,44 @@ +"""OpenSSL/M2Crypto 3DES implementation.""" + +from cryptomath import * +from TripleDES import * + +if m2cryptoLoaded: + + def new(key, mode, IV): + return OpenSSL_TripleDES(key, mode, IV) + + class OpenSSL_TripleDES(TripleDES): + + def __init__(self, key, mode, IV): + TripleDES.__init__(self, key, mode, IV, "openssl") + self.key = key + self.IV = IV + + def _createContext(self, encrypt): + context = m2.cipher_ctx_new() + cipherType = m2.des_ede3_cbc() + m2.cipher_init(context, cipherType, self.key, self.IV, encrypt) + return context + + def encrypt(self, plaintext): + TripleDES.encrypt(self, plaintext) + context = self._createContext(1) + ciphertext = m2.cipher_update(context, plaintext) + m2.cipher_ctx_free(context) + self.IV = ciphertext[-self.block_size:] + return ciphertext + + def decrypt(self, ciphertext): + TripleDES.decrypt(self, ciphertext) + context = self._createContext(0) + #I think M2Crypto has a bug - it fails to decrypt and return the last block passed in. + #To work around this, we append sixteen zeros to the string, below: + plaintext = m2.cipher_update(context, ciphertext+('\0'*16)) + + #If this bug is ever fixed, then plaintext will end up having a garbage + #plaintext block on the end. That's okay - the below code will ignore it. + plaintext = plaintext[:len(ciphertext)] + m2.cipher_ctx_free(context) + self.IV = ciphertext[-self.block_size:] + return plaintext \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/PyCrypto_AES.py b/patches/gdata/tlslite/utils/PyCrypto_AES.py new file mode 100755 index 0000000..e38b19d --- /dev/null +++ b/patches/gdata/tlslite/utils/PyCrypto_AES.py @@ -0,0 +1,22 @@ +"""PyCrypto AES implementation.""" + +from cryptomath import * +from AES import * + +if pycryptoLoaded: + import Crypto.Cipher.AES + + def new(key, mode, IV): + return PyCrypto_AES(key, mode, IV) + + class PyCrypto_AES(AES): + + def __init__(self, key, mode, IV): + AES.__init__(self, key, mode, IV, "pycrypto") + self.context = Crypto.Cipher.AES.new(key, mode, IV) + + def encrypt(self, plaintext): + return self.context.encrypt(plaintext) + + def decrypt(self, ciphertext): + return self.context.decrypt(ciphertext) \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/PyCrypto_RC4.py b/patches/gdata/tlslite/utils/PyCrypto_RC4.py new file mode 100755 index 0000000..6c6d86a --- /dev/null +++ b/patches/gdata/tlslite/utils/PyCrypto_RC4.py @@ -0,0 +1,22 @@ +"""PyCrypto RC4 implementation.""" + +from cryptomath import * +from RC4 import * + +if pycryptoLoaded: + import Crypto.Cipher.ARC4 + + def new(key): + return PyCrypto_RC4(key) + + class PyCrypto_RC4(RC4): + + def __init__(self, key): + RC4.__init__(self, key, "pycrypto") + self.context = Crypto.Cipher.ARC4.new(key) + + def encrypt(self, plaintext): + return self.context.encrypt(plaintext) + + def decrypt(self, ciphertext): + return self.context.decrypt(ciphertext) \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/PyCrypto_RSAKey.py b/patches/gdata/tlslite/utils/PyCrypto_RSAKey.py new file mode 100755 index 0000000..48b5cef --- /dev/null +++ b/patches/gdata/tlslite/utils/PyCrypto_RSAKey.py @@ -0,0 +1,61 @@ +"""PyCrypto RSA implementation.""" + +from cryptomath import * + +from RSAKey import * +from Python_RSAKey import Python_RSAKey + +if pycryptoLoaded: + + from Crypto.PublicKey import RSA + + class PyCrypto_RSAKey(RSAKey): + def __init__(self, n=0, e=0, d=0, p=0, q=0, dP=0, dQ=0, qInv=0): + if not d: + self.rsa = RSA.construct( (n, e) ) + else: + self.rsa = RSA.construct( (n, e, d, p, q) ) + + def __getattr__(self, name): + return getattr(self.rsa, name) + + def hasPrivateKey(self): + return self.rsa.has_private() + + def hash(self): + return Python_RSAKey(self.n, self.e).hash() + + def _rawPrivateKeyOp(self, m): + s = numberToString(m) + byteLength = numBytes(self.n) + if len(s)== byteLength: + pass + elif len(s) == byteLength-1: + s = '\0' + s + else: + raise AssertionError() + c = stringToNumber(self.rsa.decrypt((s,))) + return c + + def _rawPublicKeyOp(self, c): + s = numberToString(c) + byteLength = numBytes(self.n) + if len(s)== byteLength: + pass + elif len(s) == byteLength-1: + s = '\0' + s + else: + raise AssertionError() + m = stringToNumber(self.rsa.encrypt(s, None)[0]) + return m + + def writeXMLPublicKey(self, indent=''): + return Python_RSAKey(self.n, self.e).write(indent) + + def generate(bits): + key = PyCrypto_RSAKey() + def f(numBytes): + return bytesToString(getRandomBytes(numBytes)) + key.rsa = RSA.generate(bits, f) + return key + generate = staticmethod(generate) diff --git a/patches/gdata/tlslite/utils/PyCrypto_TripleDES.py b/patches/gdata/tlslite/utils/PyCrypto_TripleDES.py new file mode 100755 index 0000000..8c22bb8 --- /dev/null +++ b/patches/gdata/tlslite/utils/PyCrypto_TripleDES.py @@ -0,0 +1,22 @@ +"""PyCrypto 3DES implementation.""" + +from cryptomath import * +from TripleDES import * + +if pycryptoLoaded: + import Crypto.Cipher.DES3 + + def new(key, mode, IV): + return PyCrypto_TripleDES(key, mode, IV) + + class PyCrypto_TripleDES(TripleDES): + + def __init__(self, key, mode, IV): + TripleDES.__init__(self, key, mode, IV, "pycrypto") + self.context = Crypto.Cipher.DES3.new(key, mode, IV) + + def encrypt(self, plaintext): + return self.context.encrypt(plaintext) + + def decrypt(self, ciphertext): + return self.context.decrypt(ciphertext) \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/Python_AES.py b/patches/gdata/tlslite/utils/Python_AES.py new file mode 100755 index 0000000..657152f --- /dev/null +++ b/patches/gdata/tlslite/utils/Python_AES.py @@ -0,0 +1,68 @@ +"""Pure-Python AES implementation.""" + +from cryptomath import * + +from AES import * +from rijndael import rijndael + +def new(key, mode, IV): + return Python_AES(key, mode, IV) + +class Python_AES(AES): + def __init__(self, key, mode, IV): + AES.__init__(self, key, mode, IV, "python") + self.rijndael = rijndael(key, 16) + self.IV = IV + + def encrypt(self, plaintext): + AES.encrypt(self, plaintext) + + plaintextBytes = stringToBytes(plaintext) + chainBytes = stringToBytes(self.IV) + + #CBC Mode: For each block... + for x in range(len(plaintextBytes)/16): + + #XOR with the chaining block + blockBytes = plaintextBytes[x*16 : (x*16)+16] + for y in range(16): + blockBytes[y] ^= chainBytes[y] + blockString = bytesToString(blockBytes) + + #Encrypt it + encryptedBytes = stringToBytes(self.rijndael.encrypt(blockString)) + + #Overwrite the input with the output + for y in range(16): + plaintextBytes[(x*16)+y] = encryptedBytes[y] + + #Set the next chaining block + chainBytes = encryptedBytes + + self.IV = bytesToString(chainBytes) + return bytesToString(plaintextBytes) + + def decrypt(self, ciphertext): + AES.decrypt(self, ciphertext) + + ciphertextBytes = stringToBytes(ciphertext) + chainBytes = stringToBytes(self.IV) + + #CBC Mode: For each block... + for x in range(len(ciphertextBytes)/16): + + #Decrypt it + blockBytes = ciphertextBytes[x*16 : (x*16)+16] + blockString = bytesToString(blockBytes) + decryptedBytes = stringToBytes(self.rijndael.decrypt(blockString)) + + #XOR with the chaining block and overwrite the input with output + for y in range(16): + decryptedBytes[y] ^= chainBytes[y] + ciphertextBytes[(x*16)+y] = decryptedBytes[y] + + #Set the next chaining block + chainBytes = blockBytes + + self.IV = bytesToString(chainBytes) + return bytesToString(ciphertextBytes) diff --git a/patches/gdata/tlslite/utils/Python_RC4.py b/patches/gdata/tlslite/utils/Python_RC4.py new file mode 100755 index 0000000..56ce5fb --- /dev/null +++ b/patches/gdata/tlslite/utils/Python_RC4.py @@ -0,0 +1,39 @@ +"""Pure-Python RC4 implementation.""" + +from RC4 import RC4 +from cryptomath import * + +def new(key): + return Python_RC4(key) + +class Python_RC4(RC4): + def __init__(self, key): + RC4.__init__(self, key, "python") + keyBytes = stringToBytes(key) + S = [i for i in range(256)] + j = 0 + for i in range(256): + j = (j + S[i] + keyBytes[i % len(keyBytes)]) % 256 + S[i], S[j] = S[j], S[i] + + self.S = S + self.i = 0 + self.j = 0 + + def encrypt(self, plaintext): + plaintextBytes = stringToBytes(plaintext) + S = self.S + i = self.i + j = self.j + for x in range(len(plaintextBytes)): + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + t = (S[i] + S[j]) % 256 + plaintextBytes[x] ^= S[t] + self.i = i + self.j = j + return bytesToString(plaintextBytes) + + def decrypt(self, ciphertext): + return self.encrypt(ciphertext) diff --git a/patches/gdata/tlslite/utils/Python_RSAKey.py b/patches/gdata/tlslite/utils/Python_RSAKey.py new file mode 100755 index 0000000..2c469b5 --- /dev/null +++ b/patches/gdata/tlslite/utils/Python_RSAKey.py @@ -0,0 +1,209 @@ +"""Pure-Python RSA implementation.""" + +from cryptomath import * +import xmltools +from ASN1Parser import ASN1Parser +from RSAKey import * + +class Python_RSAKey(RSAKey): + def __init__(self, n=0, e=0, d=0, p=0, q=0, dP=0, dQ=0, qInv=0): + if (n and not e) or (e and not n): + raise AssertionError() + self.n = n + self.e = e + self.d = d + self.p = p + self.q = q + self.dP = dP + self.dQ = dQ + self.qInv = qInv + self.blinder = 0 + self.unblinder = 0 + + def hasPrivateKey(self): + return self.d != 0 + + def hash(self): + s = self.writeXMLPublicKey('\t\t') + return hashAndBase64(s.strip()) + + def _rawPrivateKeyOp(self, m): + #Create blinding values, on the first pass: + if not self.blinder: + self.unblinder = getRandomNumber(2, self.n) + self.blinder = powMod(invMod(self.unblinder, self.n), self.e, + self.n) + + #Blind the input + m = (m * self.blinder) % self.n + + #Perform the RSA operation + c = self._rawPrivateKeyOpHelper(m) + + #Unblind the output + c = (c * self.unblinder) % self.n + + #Update blinding values + self.blinder = (self.blinder * self.blinder) % self.n + self.unblinder = (self.unblinder * self.unblinder) % self.n + + #Return the output + return c + + + def _rawPrivateKeyOpHelper(self, m): + #Non-CRT version + #c = powMod(m, self.d, self.n) + + #CRT version (~3x faster) + s1 = powMod(m, self.dP, self.p) + s2 = powMod(m, self.dQ, self.q) + h = ((s1 - s2) * self.qInv) % self.p + c = s2 + self.q * h + return c + + def _rawPublicKeyOp(self, c): + m = powMod(c, self.e, self.n) + return m + + def acceptsPassword(self): return False + + def write(self, indent=''): + if self.d: + s = indent+'\n' + else: + s = indent+'\n' + s += indent+'\t%s\n' % numberToBase64(self.n) + s += indent+'\t%s\n' % numberToBase64(self.e) + if self.d: + s += indent+'\t%s\n' % numberToBase64(self.d) + s += indent+'\t

%s

\n' % numberToBase64(self.p) + s += indent+'\t%s\n' % numberToBase64(self.q) + s += indent+'\t%s\n' % numberToBase64(self.dP) + s += indent+'\t%s\n' % numberToBase64(self.dQ) + s += indent+'\t%s\n' % numberToBase64(self.qInv) + s += indent+'
' + else: + s += indent+'' + #Only add \n if part of a larger structure + if indent != '': + s += '\n' + return s + + def writeXMLPublicKey(self, indent=''): + return Python_RSAKey(self.n, self.e).write(indent) + + def generate(bits): + key = Python_RSAKey() + p = getRandomPrime(bits/2, False) + q = getRandomPrime(bits/2, False) + t = lcm(p-1, q-1) + key.n = p * q + key.e = 3L #Needed to be long, for Java + key.d = invMod(key.e, t) + key.p = p + key.q = q + key.dP = key.d % (p-1) + key.dQ = key.d % (q-1) + key.qInv = invMod(q, p) + return key + generate = staticmethod(generate) + + def parsePEM(s, passwordCallback=None): + """Parse a string containing a or , or + PEM-encoded key.""" + + start = s.find("-----BEGIN PRIVATE KEY-----") + if start != -1: + end = s.find("-----END PRIVATE KEY-----") + if end == -1: + raise SyntaxError("Missing PEM Postfix") + s = s[start+len("-----BEGIN PRIVATE KEY -----") : end] + bytes = base64ToBytes(s) + return Python_RSAKey._parsePKCS8(bytes) + else: + start = s.find("-----BEGIN RSA PRIVATE KEY-----") + if start != -1: + end = s.find("-----END RSA PRIVATE KEY-----") + if end == -1: + raise SyntaxError("Missing PEM Postfix") + s = s[start+len("-----BEGIN RSA PRIVATE KEY -----") : end] + bytes = base64ToBytes(s) + return Python_RSAKey._parseSSLeay(bytes) + raise SyntaxError("Missing PEM Prefix") + parsePEM = staticmethod(parsePEM) + + def parseXML(s): + element = xmltools.parseAndStripWhitespace(s) + return Python_RSAKey._parseXML(element) + parseXML = staticmethod(parseXML) + + def _parsePKCS8(bytes): + p = ASN1Parser(bytes) + + version = p.getChild(0).value[0] + if version != 0: + raise SyntaxError("Unrecognized PKCS8 version") + + rsaOID = p.getChild(1).value + if list(rsaOID) != [6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]: + raise SyntaxError("Unrecognized AlgorithmIdentifier") + + #Get the privateKey + privateKeyP = p.getChild(2) + + #Adjust for OCTET STRING encapsulation + privateKeyP = ASN1Parser(privateKeyP.value) + + return Python_RSAKey._parseASN1PrivateKey(privateKeyP) + _parsePKCS8 = staticmethod(_parsePKCS8) + + def _parseSSLeay(bytes): + privateKeyP = ASN1Parser(bytes) + return Python_RSAKey._parseASN1PrivateKey(privateKeyP) + _parseSSLeay = staticmethod(_parseSSLeay) + + def _parseASN1PrivateKey(privateKeyP): + version = privateKeyP.getChild(0).value[0] + if version != 0: + raise SyntaxError("Unrecognized RSAPrivateKey version") + n = bytesToNumber(privateKeyP.getChild(1).value) + e = bytesToNumber(privateKeyP.getChild(2).value) + d = bytesToNumber(privateKeyP.getChild(3).value) + p = bytesToNumber(privateKeyP.getChild(4).value) + q = bytesToNumber(privateKeyP.getChild(5).value) + dP = bytesToNumber(privateKeyP.getChild(6).value) + dQ = bytesToNumber(privateKeyP.getChild(7).value) + qInv = bytesToNumber(privateKeyP.getChild(8).value) + return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv) + _parseASN1PrivateKey = staticmethod(_parseASN1PrivateKey) + + def _parseXML(element): + try: + xmltools.checkName(element, "privateKey") + except SyntaxError: + xmltools.checkName(element, "publicKey") + + #Parse attributes + xmltools.getReqAttribute(element, "xmlns", "http://trevp.net/rsa\Z") + xmltools.checkNoMoreAttributes(element) + + #Parse public values ( and ) + n = base64ToNumber(xmltools.getText(xmltools.getChild(element, 0, "n"), xmltools.base64RegEx)) + e = base64ToNumber(xmltools.getText(xmltools.getChild(element, 1, "e"), xmltools.base64RegEx)) + d = 0 + p = 0 + q = 0 + dP = 0 + dQ = 0 + qInv = 0 + #Parse private values, if present + if element.childNodes.length>=3: + d = base64ToNumber(xmltools.getText(xmltools.getChild(element, 2, "d"), xmltools.base64RegEx)) + p = base64ToNumber(xmltools.getText(xmltools.getChild(element, 3, "p"), xmltools.base64RegEx)) + q = base64ToNumber(xmltools.getText(xmltools.getChild(element, 4, "q"), xmltools.base64RegEx)) + dP = base64ToNumber(xmltools.getText(xmltools.getChild(element, 5, "dP"), xmltools.base64RegEx)) + dQ = base64ToNumber(xmltools.getText(xmltools.getChild(element, 6, "dQ"), xmltools.base64RegEx)) + qInv = base64ToNumber(xmltools.getText(xmltools.getLastChild(element, 7, "qInv"), xmltools.base64RegEx)) + return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv) + _parseXML = staticmethod(_parseXML) diff --git a/patches/gdata/tlslite/utils/RC4.py b/patches/gdata/tlslite/utils/RC4.py new file mode 100755 index 0000000..5506923 --- /dev/null +++ b/patches/gdata/tlslite/utils/RC4.py @@ -0,0 +1,17 @@ +"""Abstract class for RC4.""" + +from compat import * #For False + +class RC4: + def __init__(self, keyBytes, implementation): + if len(keyBytes) < 16 or len(keyBytes) > 256: + raise ValueError() + self.isBlockCipher = False + self.name = "rc4" + self.implementation = implementation + + def encrypt(self, plaintext): + raise NotImplementedError() + + def decrypt(self, ciphertext): + raise NotImplementedError() \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/RSAKey.py b/patches/gdata/tlslite/utils/RSAKey.py new file mode 100755 index 0000000..2f5d286 --- /dev/null +++ b/patches/gdata/tlslite/utils/RSAKey.py @@ -0,0 +1,264 @@ +"""Abstract class for RSA.""" + +from cryptomath import * + + +class RSAKey: + """This is an abstract base class for RSA keys. + + Particular implementations of RSA keys, such as + L{OpenSSL_RSAKey.OpenSSL_RSAKey}, + L{Python_RSAKey.Python_RSAKey}, and + L{PyCrypto_RSAKey.PyCrypto_RSAKey}, + inherit from this. + + To create or parse an RSA key, don't use one of these classes + directly. Instead, use the factory functions in + L{tlslite.utils.keyfactory}. + """ + + def __init__(self, n=0, e=0): + """Create a new RSA key. + + If n and e are passed in, the new key will be initialized. + + @type n: int + @param n: RSA modulus. + + @type e: int + @param e: RSA public exponent. + """ + raise NotImplementedError() + + def __len__(self): + """Return the length of this key in bits. + + @rtype: int + """ + return numBits(self.n) + + def hasPrivateKey(self): + """Return whether or not this key has a private component. + + @rtype: bool + """ + raise NotImplementedError() + + def hash(self): + """Return the cryptoID value corresponding to this + key. + + @rtype: str + """ + raise NotImplementedError() + + def getSigningAlgorithm(self): + """Return the cryptoID sigAlgo value corresponding to this key. + + @rtype: str + """ + return "pkcs1-sha1" + + def hashAndSign(self, bytes): + """Hash and sign the passed-in bytes. + + This requires the key to have a private component. It performs + a PKCS1-SHA1 signature on the passed-in data. + + @type bytes: str or L{array.array} of unsigned bytes + @param bytes: The value which will be hashed and signed. + + @rtype: L{array.array} of unsigned bytes. + @return: A PKCS1-SHA1 signature on the passed-in data. + """ + if not isinstance(bytes, type("")): + bytes = bytesToString(bytes) + hashBytes = stringToBytes(sha1(bytes).digest()) + prefixedHashBytes = self._addPKCS1SHA1Prefix(hashBytes) + sigBytes = self.sign(prefixedHashBytes) + return sigBytes + + def hashAndVerify(self, sigBytes, bytes): + """Hash and verify the passed-in bytes with the signature. + + This verifies a PKCS1-SHA1 signature on the passed-in data. + + @type sigBytes: L{array.array} of unsigned bytes + @param sigBytes: A PKCS1-SHA1 signature. + + @type bytes: str or L{array.array} of unsigned bytes + @param bytes: The value which will be hashed and verified. + + @rtype: bool + @return: Whether the signature matches the passed-in data. + """ + if not isinstance(bytes, type("")): + bytes = bytesToString(bytes) + hashBytes = stringToBytes(sha1(bytes).digest()) + prefixedHashBytes = self._addPKCS1SHA1Prefix(hashBytes) + return self.verify(sigBytes, prefixedHashBytes) + + def sign(self, bytes): + """Sign the passed-in bytes. + + This requires the key to have a private component. It performs + a PKCS1 signature on the passed-in data. + + @type bytes: L{array.array} of unsigned bytes + @param bytes: The value which will be signed. + + @rtype: L{array.array} of unsigned bytes. + @return: A PKCS1 signature on the passed-in data. + """ + if not self.hasPrivateKey(): + raise AssertionError() + paddedBytes = self._addPKCS1Padding(bytes, 1) + m = bytesToNumber(paddedBytes) + if m >= self.n: + raise ValueError() + c = self._rawPrivateKeyOp(m) + sigBytes = numberToBytes(c) + return sigBytes + + def verify(self, sigBytes, bytes): + """Verify the passed-in bytes with the signature. + + This verifies a PKCS1 signature on the passed-in data. + + @type sigBytes: L{array.array} of unsigned bytes + @param sigBytes: A PKCS1 signature. + + @type bytes: L{array.array} of unsigned bytes + @param bytes: The value which will be verified. + + @rtype: bool + @return: Whether the signature matches the passed-in data. + """ + paddedBytes = self._addPKCS1Padding(bytes, 1) + c = bytesToNumber(sigBytes) + if c >= self.n: + return False + m = self._rawPublicKeyOp(c) + checkBytes = numberToBytes(m) + return checkBytes == paddedBytes + + def encrypt(self, bytes): + """Encrypt the passed-in bytes. + + This performs PKCS1 encryption of the passed-in data. + + @type bytes: L{array.array} of unsigned bytes + @param bytes: The value which will be encrypted. + + @rtype: L{array.array} of unsigned bytes. + @return: A PKCS1 encryption of the passed-in data. + """ + paddedBytes = self._addPKCS1Padding(bytes, 2) + m = bytesToNumber(paddedBytes) + if m >= self.n: + raise ValueError() + c = self._rawPublicKeyOp(m) + encBytes = numberToBytes(c) + return encBytes + + def decrypt(self, encBytes): + """Decrypt the passed-in bytes. + + This requires the key to have a private component. It performs + PKCS1 decryption of the passed-in data. + + @type encBytes: L{array.array} of unsigned bytes + @param encBytes: The value which will be decrypted. + + @rtype: L{array.array} of unsigned bytes or None. + @return: A PKCS1 decryption of the passed-in data or None if + the data is not properly formatted. + """ + if not self.hasPrivateKey(): + raise AssertionError() + c = bytesToNumber(encBytes) + if c >= self.n: + return None + m = self._rawPrivateKeyOp(c) + decBytes = numberToBytes(m) + if (len(decBytes) != numBytes(self.n)-1): #Check first byte + return None + if decBytes[0] != 2: #Check second byte + return None + for x in range(len(decBytes)-1): #Scan through for zero separator + if decBytes[x]== 0: + break + else: + return None + return decBytes[x+1:] #Return everything after the separator + + def _rawPrivateKeyOp(self, m): + raise NotImplementedError() + + def _rawPublicKeyOp(self, c): + raise NotImplementedError() + + def acceptsPassword(self): + """Return True if the write() method accepts a password for use + in encrypting the private key. + + @rtype: bool + """ + raise NotImplementedError() + + def write(self, password=None): + """Return a string containing the key. + + @rtype: str + @return: A string describing the key, in whichever format (PEM + or XML) is native to the implementation. + """ + raise NotImplementedError() + + def writeXMLPublicKey(self, indent=''): + """Return a string containing the key. + + @rtype: str + @return: A string describing the public key, in XML format. + """ + return Python_RSAKey(self.n, self.e).write(indent) + + def generate(bits): + """Generate a new key with the specified bit length. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + """ + raise NotImplementedError() + generate = staticmethod(generate) + + + # ************************************************************************** + # Helper Functions for RSA Keys + # ************************************************************************** + + def _addPKCS1SHA1Prefix(self, bytes): + prefixBytes = createByteArraySequence(\ + [48,33,48,9,6,5,43,14,3,2,26,5,0,4,20]) + prefixedBytes = prefixBytes + bytes + return prefixedBytes + + def _addPKCS1Padding(self, bytes, blockType): + padLength = (numBytes(self.n) - (len(bytes)+3)) + if blockType == 1: #Signature padding + pad = [0xFF] * padLength + elif blockType == 2: #Encryption padding + pad = createByteArraySequence([]) + while len(pad) < padLength: + padBytes = getRandomBytes(padLength * 2) + pad = [b for b in padBytes if b != 0] + pad = pad[:padLength] + else: + raise AssertionError() + + #NOTE: To be proper, we should add [0,blockType]. However, + #the zero is lost when the returned padding is converted + #to a number, so we don't even bother with it. Also, + #adding it would cause a misalignment in verify() + padding = createByteArraySequence([blockType] + pad + [0]) + paddedBytes = padding + bytes + return paddedBytes diff --git a/patches/gdata/tlslite/utils/TripleDES.py b/patches/gdata/tlslite/utils/TripleDES.py new file mode 100755 index 0000000..2db4588 --- /dev/null +++ b/patches/gdata/tlslite/utils/TripleDES.py @@ -0,0 +1,26 @@ +"""Abstract class for 3DES.""" + +from compat import * #For True + +class TripleDES: + def __init__(self, key, mode, IV, implementation): + if len(key) != 24: + raise ValueError() + if mode != 2: + raise ValueError() + if len(IV) != 8: + raise ValueError() + self.isBlockCipher = True + self.block_size = 8 + self.implementation = implementation + self.name = "3des" + + #CBC-Mode encryption, returns ciphertext + #WARNING: *MAY* modify the input as well + def encrypt(self, plaintext): + assert(len(plaintext) % 8 == 0) + + #CBC-Mode decryption, returns plaintext + #WARNING: *MAY* modify the input as well + def decrypt(self, ciphertext): + assert(len(ciphertext) % 8 == 0) diff --git a/patches/gdata/tlslite/utils/__init__.py b/patches/gdata/tlslite/utils/__init__.py new file mode 100755 index 0000000..e96b4be --- /dev/null +++ b/patches/gdata/tlslite/utils/__init__.py @@ -0,0 +1,31 @@ +"""Toolkit for crypto and other stuff.""" + +__all__ = ["AES", + "ASN1Parser", + "cipherfactory", + "codec", + "Cryptlib_AES", + "Cryptlib_RC4", + "Cryptlib_TripleDES", + "cryptomath: cryptomath module", + "dateFuncs", + "hmac", + "JCE_RSAKey", + "compat", + "keyfactory", + "OpenSSL_AES", + "OpenSSL_RC4", + "OpenSSL_RSAKey", + "OpenSSL_TripleDES", + "PyCrypto_AES", + "PyCrypto_RC4", + "PyCrypto_RSAKey", + "PyCrypto_TripleDES", + "Python_AES", + "Python_RC4", + "Python_RSAKey", + "RC4", + "rijndael", + "RSAKey", + "TripleDES", + "xmltools"] diff --git a/patches/gdata/tlslite/utils/cipherfactory.py b/patches/gdata/tlslite/utils/cipherfactory.py new file mode 100755 index 0000000..ccbb6b5 --- /dev/null +++ b/patches/gdata/tlslite/utils/cipherfactory.py @@ -0,0 +1,111 @@ +"""Factory functions for symmetric cryptography.""" + +import os + +import Python_AES +import Python_RC4 + +import cryptomath + +tripleDESPresent = False + +if cryptomath.m2cryptoLoaded: + import OpenSSL_AES + import OpenSSL_RC4 + import OpenSSL_TripleDES + tripleDESPresent = True + +if cryptomath.cryptlibpyLoaded: + import Cryptlib_AES + import Cryptlib_RC4 + import Cryptlib_TripleDES + tripleDESPresent = True + +if cryptomath.pycryptoLoaded: + import PyCrypto_AES + import PyCrypto_RC4 + import PyCrypto_TripleDES + tripleDESPresent = True + +# ************************************************************************** +# Factory Functions for AES +# ************************************************************************** + +def createAES(key, IV, implList=None): + """Create a new AES object. + + @type key: str + @param key: A 16, 24, or 32 byte string. + + @type IV: str + @param IV: A 16 byte string + + @rtype: L{tlslite.utils.AES} + @return: An AES object. + """ + if implList == None: + implList = ["cryptlib", "openssl", "pycrypto", "python"] + + for impl in implList: + if impl == "cryptlib" and cryptomath.cryptlibpyLoaded: + return Cryptlib_AES.new(key, 2, IV) + elif impl == "openssl" and cryptomath.m2cryptoLoaded: + return OpenSSL_AES.new(key, 2, IV) + elif impl == "pycrypto" and cryptomath.pycryptoLoaded: + return PyCrypto_AES.new(key, 2, IV) + elif impl == "python": + return Python_AES.new(key, 2, IV) + raise NotImplementedError() + +def createRC4(key, IV, implList=None): + """Create a new RC4 object. + + @type key: str + @param key: A 16 to 32 byte string. + + @type IV: object + @param IV: Ignored, whatever it is. + + @rtype: L{tlslite.utils.RC4} + @return: An RC4 object. + """ + if implList == None: + implList = ["cryptlib", "openssl", "pycrypto", "python"] + + if len(IV) != 0: + raise AssertionError() + for impl in implList: + if impl == "cryptlib" and cryptomath.cryptlibpyLoaded: + return Cryptlib_RC4.new(key) + elif impl == "openssl" and cryptomath.m2cryptoLoaded: + return OpenSSL_RC4.new(key) + elif impl == "pycrypto" and cryptomath.pycryptoLoaded: + return PyCrypto_RC4.new(key) + elif impl == "python": + return Python_RC4.new(key) + raise NotImplementedError() + +#Create a new TripleDES instance +def createTripleDES(key, IV, implList=None): + """Create a new 3DES object. + + @type key: str + @param key: A 24 byte string. + + @type IV: str + @param IV: An 8 byte string + + @rtype: L{tlslite.utils.TripleDES} + @return: A 3DES object. + """ + if implList == None: + implList = ["cryptlib", "openssl", "pycrypto"] + + for impl in implList: + if impl == "cryptlib" and cryptomath.cryptlibpyLoaded: + return Cryptlib_TripleDES.new(key, 2, IV) + elif impl == "openssl" and cryptomath.m2cryptoLoaded: + return OpenSSL_TripleDES.new(key, 2, IV) + elif impl == "pycrypto" and cryptomath.pycryptoLoaded: + return PyCrypto_TripleDES.new(key, 2, IV) + raise NotImplementedError() \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/codec.py b/patches/gdata/tlslite/utils/codec.py new file mode 100755 index 0000000..13022a0 --- /dev/null +++ b/patches/gdata/tlslite/utils/codec.py @@ -0,0 +1,94 @@ +"""Classes for reading/writing binary data (such as TLS records).""" + +from compat import * + +class Writer: + def __init__(self, length=0): + #If length is zero, then this is just a "trial run" to determine length + self.index = 0 + self.bytes = createByteArrayZeros(length) + + def add(self, x, length): + if self.bytes: + newIndex = self.index+length-1 + while newIndex >= self.index: + self.bytes[newIndex] = x & 0xFF + x >>= 8 + newIndex -= 1 + self.index += length + + def addFixSeq(self, seq, length): + if self.bytes: + for e in seq: + self.add(e, length) + else: + self.index += len(seq)*length + + def addVarSeq(self, seq, length, lengthLength): + if self.bytes: + self.add(len(seq)*length, lengthLength) + for e in seq: + self.add(e, length) + else: + self.index += lengthLength + (len(seq)*length) + + +class Parser: + def __init__(self, bytes): + self.bytes = bytes + self.index = 0 + + def get(self, length): + if self.index + length > len(self.bytes): + raise SyntaxError() + x = 0 + for count in range(length): + x <<= 8 + x |= self.bytes[self.index] + self.index += 1 + return x + + def getFixBytes(self, lengthBytes): + bytes = self.bytes[self.index : self.index+lengthBytes] + self.index += lengthBytes + return bytes + + def getVarBytes(self, lengthLength): + lengthBytes = self.get(lengthLength) + return self.getFixBytes(lengthBytes) + + def getFixList(self, length, lengthList): + l = [0] * lengthList + for x in range(lengthList): + l[x] = self.get(length) + return l + + def getVarList(self, length, lengthLength): + lengthList = self.get(lengthLength) + if lengthList % length != 0: + raise SyntaxError() + lengthList = int(lengthList/length) + l = [0] * lengthList + for x in range(lengthList): + l[x] = self.get(length) + return l + + def startLengthCheck(self, lengthLength): + self.lengthCheck = self.get(lengthLength) + self.indexCheck = self.index + + def setLengthCheck(self, length): + self.lengthCheck = length + self.indexCheck = self.index + + def stopLengthCheck(self): + if (self.index - self.indexCheck) != self.lengthCheck: + raise SyntaxError() + + def atLengthCheck(self): + if (self.index - self.indexCheck) < self.lengthCheck: + return False + elif (self.index - self.indexCheck) == self.lengthCheck: + return True + else: + raise SyntaxError() \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/compat.py b/patches/gdata/tlslite/utils/compat.py new file mode 100755 index 0000000..7d2d925 --- /dev/null +++ b/patches/gdata/tlslite/utils/compat.py @@ -0,0 +1,140 @@ +"""Miscellaneous functions to mask Python version differences.""" + +import sys +import os + +if sys.version_info < (2,2): + raise AssertionError("Python 2.2 or later required") + +if sys.version_info < (2,3): + + def enumerate(collection): + return zip(range(len(collection)), collection) + + class Set: + def __init__(self, seq=None): + self.values = {} + if seq: + for e in seq: + self.values[e] = None + + def add(self, e): + self.values[e] = None + + def discard(self, e): + if e in self.values.keys(): + del(self.values[e]) + + def union(self, s): + ret = Set() + for e in self.values.keys(): + ret.values[e] = None + for e in s.values.keys(): + ret.values[e] = None + return ret + + def issubset(self, other): + for e in self.values.keys(): + if e not in other.values.keys(): + return False + return True + + def __nonzero__( self): + return len(self.values.keys()) + + def __contains__(self, e): + return e in self.values.keys() + + def __iter__(self): + return iter(set.values.keys()) + + +if os.name != "java": + + import array + def createByteArraySequence(seq): + return array.array('B', seq) + def createByteArrayZeros(howMany): + return array.array('B', [0] * howMany) + def concatArrays(a1, a2): + return a1+a2 + + def bytesToString(bytes): + return bytes.tostring() + def stringToBytes(s): + bytes = createByteArrayZeros(0) + bytes.fromstring(s) + return bytes + + import math + def numBits(n): + if n==0: + return 0 + s = "%x" % n + return ((len(s)-1)*4) + \ + {'0':0, '1':1, '2':2, '3':2, + '4':3, '5':3, '6':3, '7':3, + '8':4, '9':4, 'a':4, 'b':4, + 'c':4, 'd':4, 'e':4, 'f':4, + }[s[0]] + return int(math.floor(math.log(n, 2))+1) + + BaseException = Exception + import sys + import traceback + def formatExceptionTrace(e): + newStr = "".join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) + return newStr + +else: + #Jython 2.1 is missing lots of python 2.3 stuff, + #which we have to emulate here: + #NOTE: JYTHON SUPPORT NO LONGER WORKS, DUE TO USE OF GENERATORS. + #THIS CODE IS LEFT IN SO THAT ONE JYTHON UPDATES TO 2.2, IT HAS A + #CHANCE OF WORKING AGAIN. + + import java + import jarray + + def createByteArraySequence(seq): + if isinstance(seq, type("")): #If it's a string, convert + seq = [ord(c) for c in seq] + return jarray.array(seq, 'h') #use short instead of bytes, cause bytes are signed + def createByteArrayZeros(howMany): + return jarray.zeros(howMany, 'h') #use short instead of bytes, cause bytes are signed + def concatArrays(a1, a2): + l = list(a1)+list(a2) + return createByteArraySequence(l) + + #WAY TOO SLOW - MUST BE REPLACED------------ + def bytesToString(bytes): + return "".join([chr(b) for b in bytes]) + + def stringToBytes(s): + bytes = createByteArrayZeros(len(s)) + for count, c in enumerate(s): + bytes[count] = ord(c) + return bytes + #WAY TOO SLOW - MUST BE REPLACED------------ + + def numBits(n): + if n==0: + return 0 + n= 1L * n; #convert to long, if it isn't already + return n.__tojava__(java.math.BigInteger).bitLength() + + #Adjust the string to an array of bytes + def stringToJavaByteArray(s): + bytes = jarray.zeros(len(s), 'b') + for count, c in enumerate(s): + x = ord(c) + if x >= 128: x -= 256 + bytes[count] = x + return bytes + + BaseException = java.lang.Exception + import sys + import traceback + def formatExceptionTrace(e): + newStr = "".join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) + return newStr \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/cryptomath.py b/patches/gdata/tlslite/utils/cryptomath.py new file mode 100755 index 0000000..92fb774 --- /dev/null +++ b/patches/gdata/tlslite/utils/cryptomath.py @@ -0,0 +1,404 @@ +"""cryptomath module + +This module has basic math/crypto code.""" + +import os +import sys +import math +import base64 +import binascii +if sys.version_info[:2] <= (2, 4): + from sha import sha as sha1 +else: + from hashlib import sha1 + +from compat import * + + +# ************************************************************************** +# Load Optional Modules +# ************************************************************************** + +# Try to load M2Crypto/OpenSSL +try: + from M2Crypto import m2 + m2cryptoLoaded = True + +except ImportError: + m2cryptoLoaded = False + + +# Try to load cryptlib +try: + import cryptlib_py + try: + cryptlib_py.cryptInit() + except cryptlib_py.CryptException, e: + #If tlslite and cryptoIDlib are both present, + #they might each try to re-initialize this, + #so we're tolerant of that. + if e[0] != cryptlib_py.CRYPT_ERROR_INITED: + raise + cryptlibpyLoaded = True + +except ImportError: + cryptlibpyLoaded = False + +#Try to load GMPY +try: + import gmpy + gmpyLoaded = True +except ImportError: + gmpyLoaded = False + +#Try to load pycrypto +try: + import Crypto.Cipher.AES + pycryptoLoaded = True +except ImportError: + pycryptoLoaded = False + + +# ************************************************************************** +# PRNG Functions +# ************************************************************************** + +# Get os.urandom PRNG +try: + os.urandom(1) + def getRandomBytes(howMany): + return stringToBytes(os.urandom(howMany)) + prngName = "os.urandom" + +except: + # Else get cryptlib PRNG + if cryptlibpyLoaded: + def getRandomBytes(howMany): + randomKey = cryptlib_py.cryptCreateContext(cryptlib_py.CRYPT_UNUSED, + cryptlib_py.CRYPT_ALGO_AES) + cryptlib_py.cryptSetAttribute(randomKey, + cryptlib_py.CRYPT_CTXINFO_MODE, + cryptlib_py.CRYPT_MODE_OFB) + cryptlib_py.cryptGenerateKey(randomKey) + bytes = createByteArrayZeros(howMany) + cryptlib_py.cryptEncrypt(randomKey, bytes) + return bytes + prngName = "cryptlib" + + else: + #Else get UNIX /dev/urandom PRNG + try: + devRandomFile = open("/dev/urandom", "rb") + def getRandomBytes(howMany): + return stringToBytes(devRandomFile.read(howMany)) + prngName = "/dev/urandom" + except IOError: + #Else get Win32 CryptoAPI PRNG + try: + import win32prng + def getRandomBytes(howMany): + s = win32prng.getRandomBytes(howMany) + if len(s) != howMany: + raise AssertionError() + return stringToBytes(s) + prngName ="CryptoAPI" + except ImportError: + #Else no PRNG :-( + def getRandomBytes(howMany): + raise NotImplementedError("No Random Number Generator "\ + "available.") + prngName = "None" + +# ************************************************************************** +# Converter Functions +# ************************************************************************** + +def bytesToNumber(bytes): + total = 0L + multiplier = 1L + for count in range(len(bytes)-1, -1, -1): + byte = bytes[count] + total += multiplier * byte + multiplier *= 256 + return total + +def numberToBytes(n): + howManyBytes = numBytes(n) + bytes = createByteArrayZeros(howManyBytes) + for count in range(howManyBytes-1, -1, -1): + bytes[count] = int(n % 256) + n >>= 8 + return bytes + +def bytesToBase64(bytes): + s = bytesToString(bytes) + return stringToBase64(s) + +def base64ToBytes(s): + s = base64ToString(s) + return stringToBytes(s) + +def numberToBase64(n): + bytes = numberToBytes(n) + return bytesToBase64(bytes) + +def base64ToNumber(s): + bytes = base64ToBytes(s) + return bytesToNumber(bytes) + +def stringToNumber(s): + bytes = stringToBytes(s) + return bytesToNumber(bytes) + +def numberToString(s): + bytes = numberToBytes(s) + return bytesToString(bytes) + +def base64ToString(s): + try: + return base64.decodestring(s) + except binascii.Error, e: + raise SyntaxError(e) + except binascii.Incomplete, e: + raise SyntaxError(e) + +def stringToBase64(s): + return base64.encodestring(s).replace("\n", "") + +def mpiToNumber(mpi): #mpi is an openssl-format bignum string + if (ord(mpi[4]) & 0x80) !=0: #Make sure this is a positive number + raise AssertionError() + bytes = stringToBytes(mpi[4:]) + return bytesToNumber(bytes) + +def numberToMPI(n): + bytes = numberToBytes(n) + ext = 0 + #If the high-order bit is going to be set, + #add an extra byte of zeros + if (numBits(n) & 0x7)==0: + ext = 1 + length = numBytes(n) + ext + bytes = concatArrays(createByteArrayZeros(4+ext), bytes) + bytes[0] = (length >> 24) & 0xFF + bytes[1] = (length >> 16) & 0xFF + bytes[2] = (length >> 8) & 0xFF + bytes[3] = length & 0xFF + return bytesToString(bytes) + + + +# ************************************************************************** +# Misc. Utility Functions +# ************************************************************************** + +def numBytes(n): + if n==0: + return 0 + bits = numBits(n) + return int(math.ceil(bits / 8.0)) + +def hashAndBase64(s): + return stringToBase64(sha1(s).digest()) + +def getBase64Nonce(numChars=22): #defaults to an 132 bit nonce + bytes = getRandomBytes(numChars) + bytesStr = "".join([chr(b) for b in bytes]) + return stringToBase64(bytesStr)[:numChars] + + +# ************************************************************************** +# Big Number Math +# ************************************************************************** + +def getRandomNumber(low, high): + if low >= high: + raise AssertionError() + howManyBits = numBits(high) + howManyBytes = numBytes(high) + lastBits = howManyBits % 8 + while 1: + bytes = getRandomBytes(howManyBytes) + if lastBits: + bytes[0] = bytes[0] % (1 << lastBits) + n = bytesToNumber(bytes) + if n >= low and n < high: + return n + +def gcd(a,b): + a, b = max(a,b), min(a,b) + while b: + a, b = b, a % b + return a + +def lcm(a, b): + #This will break when python division changes, but we can't use // cause + #of Jython + return (a * b) / gcd(a, b) + +#Returns inverse of a mod b, zero if none +#Uses Extended Euclidean Algorithm +def invMod(a, b): + c, d = a, b + uc, ud = 1, 0 + while c != 0: + #This will break when python division changes, but we can't use // + #cause of Jython + q = d / c + c, d = d-(q*c), c + uc, ud = ud - (q * uc), uc + if d == 1: + return ud % b + return 0 + + +if gmpyLoaded: + def powMod(base, power, modulus): + base = gmpy.mpz(base) + power = gmpy.mpz(power) + modulus = gmpy.mpz(modulus) + result = pow(base, power, modulus) + return long(result) + +else: + #Copied from Bryan G. Olson's post to comp.lang.python + #Does left-to-right instead of pow()'s right-to-left, + #thus about 30% faster than the python built-in with small bases + def powMod(base, power, modulus): + nBitScan = 5 + + """ Return base**power mod modulus, using multi bit scanning + with nBitScan bits at a time.""" + + #TREV - Added support for negative exponents + negativeResult = False + if (power < 0): + power *= -1 + negativeResult = True + + exp2 = 2**nBitScan + mask = exp2 - 1 + + # Break power into a list of digits of nBitScan bits. + # The list is recursive so easy to read in reverse direction. + nibbles = None + while power: + nibbles = int(power & mask), nibbles + power = power >> nBitScan + + # Make a table of powers of base up to 2**nBitScan - 1 + lowPowers = [1] + for i in xrange(1, exp2): + lowPowers.append((lowPowers[i-1] * base) % modulus) + + # To exponentiate by the first nibble, look it up in the table + nib, nibbles = nibbles + prod = lowPowers[nib] + + # For the rest, square nBitScan times, then multiply by + # base^nibble + while nibbles: + nib, nibbles = nibbles + for i in xrange(nBitScan): + prod = (prod * prod) % modulus + if nib: prod = (prod * lowPowers[nib]) % modulus + + #TREV - Added support for negative exponents + if negativeResult: + prodInv = invMod(prod, modulus) + #Check to make sure the inverse is correct + if (prod * prodInv) % modulus != 1: + raise AssertionError() + return prodInv + return prod + + +#Pre-calculate a sieve of the ~100 primes < 1000: +def makeSieve(n): + sieve = range(n) + for count in range(2, int(math.sqrt(n))): + if sieve[count] == 0: + continue + x = sieve[count] * 2 + while x < len(sieve): + sieve[x] = 0 + x += sieve[count] + sieve = [x for x in sieve[2:] if x] + return sieve + +sieve = makeSieve(1000) + +def isPrime(n, iterations=5, display=False): + #Trial division with sieve + for x in sieve: + if x >= n: return True + if n % x == 0: return False + #Passed trial division, proceed to Rabin-Miller + #Rabin-Miller implemented per Ferguson & Schneier + #Compute s, t for Rabin-Miller + if display: print "*", + s, t = n-1, 0 + while s % 2 == 0: + s, t = s/2, t+1 + #Repeat Rabin-Miller x times + a = 2 #Use 2 as a base for first iteration speedup, per HAC + for count in range(iterations): + v = powMod(a, s, n) + if v==1: + continue + i = 0 + while v != n-1: + if i == t-1: + return False + else: + v, i = powMod(v, 2, n), i+1 + a = getRandomNumber(2, n) + return True + +def getRandomPrime(bits, display=False): + if bits < 10: + raise AssertionError() + #The 1.5 ensures the 2 MSBs are set + #Thus, when used for p,q in RSA, n will have its MSB set + # + #Since 30 is lcm(2,3,5), we'll set our test numbers to + #29 % 30 and keep them there + low = (2L ** (bits-1)) * 3/2 + high = 2L ** bits - 30 + p = getRandomNumber(low, high) + p += 29 - (p % 30) + while 1: + if display: print ".", + p += 30 + if p >= high: + p = getRandomNumber(low, high) + p += 29 - (p % 30) + if isPrime(p, display=display): + return p + +#Unused at the moment... +def getRandomSafePrime(bits, display=False): + if bits < 10: + raise AssertionError() + #The 1.5 ensures the 2 MSBs are set + #Thus, when used for p,q in RSA, n will have its MSB set + # + #Since 30 is lcm(2,3,5), we'll set our test numbers to + #29 % 30 and keep them there + low = (2 ** (bits-2)) * 3/2 + high = (2 ** (bits-1)) - 30 + q = getRandomNumber(low, high) + q += 29 - (q % 30) + while 1: + if display: print ".", + q += 30 + if (q >= high): + q = getRandomNumber(low, high) + q += 29 - (q % 30) + #Ideas from Tom Wu's SRP code + #Do trial division on p and q before Rabin-Miller + if isPrime(q, 0, display=display): + p = (2 * q) + 1 + if isPrime(p, display=display): + if isPrime(q, display=display): + return p diff --git a/patches/gdata/tlslite/utils/dateFuncs.py b/patches/gdata/tlslite/utils/dateFuncs.py new file mode 100755 index 0000000..38812eb --- /dev/null +++ b/patches/gdata/tlslite/utils/dateFuncs.py @@ -0,0 +1,75 @@ + +import os + +#Functions for manipulating datetime objects +#CCYY-MM-DDThh:mm:ssZ +def parseDateClass(s): + year, month, day = s.split("-") + day, tail = day[:2], day[2:] + hour, minute, second = tail[1:].split(":") + second = second[:2] + year, month, day = int(year), int(month), int(day) + hour, minute, second = int(hour), int(minute), int(second) + return createDateClass(year, month, day, hour, minute, second) + + +if os.name != "java": + from datetime import datetime, timedelta + + #Helper functions for working with a date/time class + def createDateClass(year, month, day, hour, minute, second): + return datetime(year, month, day, hour, minute, second) + + def printDateClass(d): + #Split off fractional seconds, append 'Z' + return d.isoformat().split(".")[0]+"Z" + + def getNow(): + return datetime.utcnow() + + def getHoursFromNow(hours): + return datetime.utcnow() + timedelta(hours=hours) + + def getMinutesFromNow(minutes): + return datetime.utcnow() + timedelta(minutes=minutes) + + def isDateClassExpired(d): + return d < datetime.utcnow() + + def isDateClassBefore(d1, d2): + return d1 < d2 + +else: + #Jython 2.1 is missing lots of python 2.3 stuff, + #which we have to emulate here: + import java + import jarray + + def createDateClass(year, month, day, hour, minute, second): + c = java.util.Calendar.getInstance() + c.setTimeZone(java.util.TimeZone.getTimeZone("UTC")) + c.set(year, month-1, day, hour, minute, second) + return c + + def printDateClass(d): + return "%04d-%02d-%02dT%02d:%02d:%02dZ" % \ + (d.get(d.YEAR), d.get(d.MONTH)+1, d.get(d.DATE), \ + d.get(d.HOUR_OF_DAY), d.get(d.MINUTE), d.get(d.SECOND)) + + def getNow(): + c = java.util.Calendar.getInstance() + c.setTimeZone(java.util.TimeZone.getTimeZone("UTC")) + c.get(c.HOUR) #force refresh? + return c + + def getHoursFromNow(hours): + d = getNow() + d.add(d.HOUR, hours) + return d + + def isDateClassExpired(d): + n = getNow() + return d.before(n) + + def isDateClassBefore(d1, d2): + return d1.before(d2) diff --git a/patches/gdata/tlslite/utils/entropy.c b/patches/gdata/tlslite/utils/entropy.c new file mode 100755 index 0000000..c627794 --- /dev/null +++ b/patches/gdata/tlslite/utils/entropy.c @@ -0,0 +1,173 @@ + +#include "Python.h" + + +#ifdef MS_WINDOWS + +/* The following #define is not needed on VC6 with the Platform SDK, and it +may not be needed on VC7, I'm not sure. I don't think it hurts anything.*/ +#define _WIN32_WINNT 0x0400 + +#include + + +typedef BOOL (WINAPI *CRYPTACQUIRECONTEXTA)(HCRYPTPROV *phProv,\ + LPCSTR pszContainer, LPCSTR pszProvider, DWORD dwProvType,\ + DWORD dwFlags ); +typedef BOOL (WINAPI *CRYPTGENRANDOM)(HCRYPTPROV hProv, DWORD dwLen,\ + BYTE *pbBuffer ); +typedef BOOL (WINAPI *CRYPTRELEASECONTEXT)(HCRYPTPROV hProv,\ + DWORD dwFlags); + + +static PyObject* entropy(PyObject *self, PyObject *args) +{ + int howMany = 0; + HINSTANCE hAdvAPI32 = NULL; + CRYPTACQUIRECONTEXTA pCryptAcquireContextA = NULL; + CRYPTGENRANDOM pCryptGenRandom = NULL; + CRYPTRELEASECONTEXT pCryptReleaseContext = NULL; + HCRYPTPROV hCryptProv = 0; + unsigned char* bytes = NULL; + PyObject* returnVal = NULL; + + + /* Read arguments */ + if (!PyArg_ParseTuple(args, "i", &howMany)) + return(NULL); + + /* Obtain handle to the DLL containing CryptoAPI + This should not fail */ + if( (hAdvAPI32 = GetModuleHandle("advapi32.dll")) == NULL) { + PyErr_Format(PyExc_SystemError, + "Advapi32.dll not found"); + return NULL; + } + + /* Obtain pointers to the CryptoAPI functions + This will fail on some early version of Win95 */ + pCryptAcquireContextA = (CRYPTACQUIRECONTEXTA)GetProcAddress(hAdvAPI32,\ + "CryptAcquireContextA"); + pCryptGenRandom = (CRYPTGENRANDOM)GetProcAddress(hAdvAPI32,\ + "CryptGenRandom"); + pCryptReleaseContext = (CRYPTRELEASECONTEXT) GetProcAddress(hAdvAPI32,\ + "CryptReleaseContext"); + if (pCryptAcquireContextA == NULL || pCryptGenRandom == NULL || + pCryptReleaseContext == NULL) { + PyErr_Format(PyExc_NotImplementedError, + "CryptoAPI not available on this version of Windows"); + return NULL; + } + + /* Allocate bytes */ + if ((bytes = (unsigned char*)PyMem_Malloc(howMany)) == NULL) + return PyErr_NoMemory(); + + + /* Acquire context */ + if(!pCryptAcquireContextA(&hCryptProv, NULL, NULL, PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT)) { + PyErr_Format(PyExc_SystemError, + "CryptAcquireContext failed, error %d", GetLastError()); + PyMem_Free(bytes); + return NULL; + } + + /* Get random data */ + if(!pCryptGenRandom(hCryptProv, howMany, bytes)) { + PyErr_Format(PyExc_SystemError, + "CryptGenRandom failed, error %d", GetLastError()); + PyMem_Free(bytes); + CryptReleaseContext(hCryptProv, 0); + return NULL; + } + + /* Build return value */ + returnVal = Py_BuildValue("s#", bytes, howMany); + PyMem_Free(bytes); + + /* Release context */ + if (!pCryptReleaseContext(hCryptProv, 0)) { + PyErr_Format(PyExc_SystemError, + "CryptReleaseContext failed, error %d", GetLastError()); + return NULL; + } + + return returnVal; +} + +#elif defined(HAVE_UNISTD_H) && defined(HAVE_FCNTL_H) + +#include +#include + +static PyObject* entropy(PyObject *self, PyObject *args) +{ + int howMany; + int fd; + unsigned char* bytes = NULL; + PyObject* returnVal = NULL; + + + /* Read arguments */ + if (!PyArg_ParseTuple(args, "i", &howMany)) + return(NULL); + + /* Allocate bytes */ + if ((bytes = (unsigned char*)PyMem_Malloc(howMany)) == NULL) + return PyErr_NoMemory(); + + /* Open device */ + if ((fd = open("/dev/urandom", O_RDONLY, 0)) == -1) { + PyErr_Format(PyExc_NotImplementedError, + "No entropy source found"); + PyMem_Free(bytes); + return NULL; + } + + /* Get random data */ + if (read(fd, bytes, howMany) < howMany) { + PyErr_Format(PyExc_SystemError, + "Reading from /dev/urandom failed"); + PyMem_Free(bytes); + close(fd); + return NULL; + } + + /* Build return value */ + returnVal = Py_BuildValue("s#", bytes, howMany); + PyMem_Free(bytes); + + /* Close device */ + close(fd); + + return returnVal; +} + +#else + +static PyObject* entropy(PyObject *self, PyObject *args) +{ + PyErr_Format(PyExc_NotImplementedError, + "Function not supported"); + return NULL; +} + +#endif + + + +/* List of functions exported by this module */ + +static struct PyMethodDef entropy_functions[] = { + {"entropy", (PyCFunction)entropy, METH_VARARGS, "Return a string of random bytes produced by a platform-specific\nentropy source."}, + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +PyMODINIT_FUNC initentropy(void) +{ + Py_InitModule("entropy", entropy_functions); +} \ No newline at end of file diff --git a/patches/gdata/tlslite/utils/hmac.py b/patches/gdata/tlslite/utils/hmac.py new file mode 100755 index 0000000..fe8feec --- /dev/null +++ b/patches/gdata/tlslite/utils/hmac.py @@ -0,0 +1,104 @@ +"""HMAC (Keyed-Hashing for Message Authentication) Python module. + +Implements the HMAC algorithm as described by RFC 2104. + +(This file is modified from the standard library version to do faster +copying) +""" + +def _strxor(s1, s2): + """Utility method. XOR the two strings s1 and s2 (must have same length). + """ + return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), s1, s2)) + +# The size of the digests returned by HMAC depends on the underlying +# hashing module used. +digest_size = None + +class HMAC: + """RFC2104 HMAC class. + + This supports the API for Cryptographic Hash Functions (PEP 247). + """ + + def __init__(self, key, msg = None, digestmod = None): + """Create a new HMAC object. + + key: key for the keyed hash object. + msg: Initial input for the hash, if provided. + digestmod: A module supporting PEP 247. Defaults to the md5 module. + """ + if digestmod is None: + import md5 + digestmod = md5 + + if key == None: #TREVNEW - for faster copying + return #TREVNEW + + self.digestmod = digestmod + self.outer = digestmod.new() + self.inner = digestmod.new() + self.digest_size = digestmod.digest_size + + blocksize = 64 + ipad = "\x36" * blocksize + opad = "\x5C" * blocksize + + if len(key) > blocksize: + key = digestmod.new(key).digest() + + key = key + chr(0) * (blocksize - len(key)) + self.outer.update(_strxor(key, opad)) + self.inner.update(_strxor(key, ipad)) + if msg is not None: + self.update(msg) + +## def clear(self): +## raise NotImplementedError, "clear() method not available in HMAC." + + def update(self, msg): + """Update this hashing object with the string msg. + """ + self.inner.update(msg) + + def copy(self): + """Return a separate copy of this hashing object. + + An update to this copy won't affect the original object. + """ + other = HMAC(None) #TREVNEW - for faster copying + other.digest_size = self.digest_size #TREVNEW + other.digestmod = self.digestmod + other.inner = self.inner.copy() + other.outer = self.outer.copy() + return other + + def digest(self): + """Return the hash value of this hashing object. + + This returns a string containing 8-bit data. The object is + not altered in any way by this function; you can continue + updating the object after calling this function. + """ + h = self.outer.copy() + h.update(self.inner.digest()) + return h.digest() + + def hexdigest(self): + """Like digest(), but returns a string of hexadecimal digits instead. + """ + return "".join([hex(ord(x))[2:].zfill(2) + for x in tuple(self.digest())]) + +def new(key, msg = None, digestmod = None): + """Create a new hashing object and return it. + + key: The starting key for the hash. + msg: if available, will immediately be hashed into the object's starting + state. + + You can now feed arbitrary strings into the object using its update() + method, and can ask for the hash value at any time by calling its digest() + method. + """ + return HMAC(key, msg, digestmod) diff --git a/patches/gdata/tlslite/utils/jython_compat.py b/patches/gdata/tlslite/utils/jython_compat.py new file mode 100755 index 0000000..1245183 --- /dev/null +++ b/patches/gdata/tlslite/utils/jython_compat.py @@ -0,0 +1,195 @@ +"""Miscellaneous functions to mask Python/Jython differences.""" + +import os +import sha + +if os.name != "java": + BaseException = Exception + + from sets import Set + import array + import math + + def createByteArraySequence(seq): + return array.array('B', seq) + def createByteArrayZeros(howMany): + return array.array('B', [0] * howMany) + def concatArrays(a1, a2): + return a1+a2 + + def bytesToString(bytes): + return bytes.tostring() + + def stringToBytes(s): + bytes = createByteArrayZeros(0) + bytes.fromstring(s) + return bytes + + def numBits(n): + if n==0: + return 0 + return int(math.floor(math.log(n, 2))+1) + + class CertChainBase: pass + class SelfTestBase: pass + class ReportFuncBase: pass + + #Helper functions for working with sets (from Python 2.3) + def iterSet(set): + return iter(set) + + def getListFromSet(set): + return list(set) + + #Factory function for getting a SHA1 object + def getSHA1(s): + return sha.sha(s) + + import sys + import traceback + + def formatExceptionTrace(e): + newStr = "".join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) + return newStr + +else: + #Jython 2.1 is missing lots of python 2.3 stuff, + #which we have to emulate here: + import java + import jarray + + BaseException = java.lang.Exception + + def createByteArraySequence(seq): + if isinstance(seq, type("")): #If it's a string, convert + seq = [ord(c) for c in seq] + return jarray.array(seq, 'h') #use short instead of bytes, cause bytes are signed + def createByteArrayZeros(howMany): + return jarray.zeros(howMany, 'h') #use short instead of bytes, cause bytes are signed + def concatArrays(a1, a2): + l = list(a1)+list(a2) + return createByteArraySequence(l) + + #WAY TOO SLOW - MUST BE REPLACED------------ + def bytesToString(bytes): + return "".join([chr(b) for b in bytes]) + + def stringToBytes(s): + bytes = createByteArrayZeros(len(s)) + for count, c in enumerate(s): + bytes[count] = ord(c) + return bytes + #WAY TOO SLOW - MUST BE REPLACED------------ + + def numBits(n): + if n==0: + return 0 + n= 1L * n; #convert to long, if it isn't already + return n.__tojava__(java.math.BigInteger).bitLength() + + #This properly creates static methods for Jython + class staticmethod: + def __init__(self, anycallable): self.__call__ = anycallable + + #Properties are not supported for Jython + class property: + def __init__(self, anycallable): pass + + #True and False have to be specially defined + False = 0 + True = 1 + + class StopIteration(Exception): pass + + def enumerate(collection): + return zip(range(len(collection)), collection) + + class Set: + def __init__(self, seq=None): + self.values = {} + if seq: + for e in seq: + self.values[e] = None + + def add(self, e): + self.values[e] = None + + def discard(self, e): + if e in self.values.keys(): + del(self.values[e]) + + def union(self, s): + ret = Set() + for e in self.values.keys(): + ret.values[e] = None + for e in s.values.keys(): + ret.values[e] = None + return ret + + def issubset(self, other): + for e in self.values.keys(): + if e not in other.values.keys(): + return False + return True + + def __nonzero__( self): + return len(self.values.keys()) + + def __contains__(self, e): + return e in self.values.keys() + + def iterSet(set): + return set.values.keys() + + def getListFromSet(set): + return set.values.keys() + + """ + class JCE_SHA1: + def __init__(self, s=None): + self.md = java.security.MessageDigest.getInstance("SHA1") + if s: + self.update(s) + + def update(self, s): + self.md.update(s) + + def copy(self): + sha1 = JCE_SHA1() + sha1.md = self.md.clone() + return sha1 + + def digest(self): + digest = self.md.digest() + bytes = jarray.zeros(20, 'h') + for count in xrange(20): + x = digest[count] + if x < 0: x += 256 + bytes[count] = x + return bytes + """ + + #Factory function for getting a SHA1 object + #The JCE_SHA1 class is way too slow... + #the sha.sha object we use instead is broken in the jython 2.1 + #release, and needs to be patched + def getSHA1(s): + #return JCE_SHA1(s) + return sha.sha(s) + + + #Adjust the string to an array of bytes + def stringToJavaByteArray(s): + bytes = jarray.zeros(len(s), 'b') + for count, c in enumerate(s): + x = ord(c) + if x >= 128: x -= 256 + bytes[count] = x + return bytes + + import sys + import traceback + + def formatExceptionTrace(e): + newStr = "".join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) + return newStr diff --git a/patches/gdata/tlslite/utils/keyfactory.py b/patches/gdata/tlslite/utils/keyfactory.py new file mode 100755 index 0000000..5005af7 --- /dev/null +++ b/patches/gdata/tlslite/utils/keyfactory.py @@ -0,0 +1,243 @@ +"""Factory functions for asymmetric cryptography. +@sort: generateRSAKey, parseXMLKey, parsePEMKey, parseAsPublicKey, +parseAsPrivateKey +""" + +from compat import * + +from RSAKey import RSAKey +from Python_RSAKey import Python_RSAKey +import cryptomath + +if cryptomath.m2cryptoLoaded: + from OpenSSL_RSAKey import OpenSSL_RSAKey + +if cryptomath.pycryptoLoaded: + from PyCrypto_RSAKey import PyCrypto_RSAKey + +# ************************************************************************** +# Factory Functions for RSA Keys +# ************************************************************************** + +def generateRSAKey(bits, implementations=["openssl", "python"]): + """Generate an RSA key with the specified bit length. + + @type bits: int + @param bits: Desired bit length of the new key's modulus. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + @return: A new RSA private key. + """ + for implementation in implementations: + if implementation == "openssl" and cryptomath.m2cryptoLoaded: + return OpenSSL_RSAKey.generate(bits) + elif implementation == "python": + return Python_RSAKey.generate(bits) + raise ValueError("No acceptable implementations") + +def parseXMLKey(s, private=False, public=False, implementations=["python"]): + """Parse an XML-format key. + + The XML format used here is specific to tlslite and cryptoIDlib. The + format can store the public component of a key, or the public and + private components. For example:: + + + 4a5yzB8oGNlHo866CAspAC47M4Fvx58zwK8pou... + Aw== + + + + 4a5yzB8oGNlHo866CAspAC47M4Fvx58zwK8pou... + Aw== + JZ0TIgUxWXmL8KJ0VqyG1V0J3ern9pqIoB0xmy... +

5PreIj6z6ldIGL1V4+1C36dQFHNCQHJvW52GXc... + /E/wDit8YXPCxx126zTq2ilQ3IcW54NJYyNjiZ... + mKc+wX8inDowEH45Qp4slRo1YveBgExKPROu6... + qDVKtBz9lk0shL5PR3ickXDgkwS576zbl2ztB... + j6E8EA7dNsTImaXexAmLA1DoeArsYeFAInr... + + + @type s: str + @param s: A string containing an XML public or private key. + + @type private: bool + @param private: If True, a L{SyntaxError} will be raised if the private + key component is not present. + + @type public: bool + @param public: If True, the private key component (if present) will be + discarded, so this function will always return a public key. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + @return: An RSA key. + + @raise SyntaxError: If the key is not properly formatted. + """ + for implementation in implementations: + if implementation == "python": + key = Python_RSAKey.parseXML(s) + break + else: + raise ValueError("No acceptable implementations") + + return _parseKeyHelper(key, private, public) + +#Parse as an OpenSSL or Python key +def parsePEMKey(s, private=False, public=False, passwordCallback=None, + implementations=["openssl", "python"]): + """Parse a PEM-format key. + + The PEM format is used by OpenSSL and other tools. The + format is typically used to store both the public and private + components of a key. For example:: + + -----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDYscuoMzsGmW0pAYsmyHltxB2TdwHS0dImfjCMfaSDkfLdZY5+ + dOWORVns9etWnr194mSGA1F0Pls/VJW8+cX9+3vtJV8zSdANPYUoQf0TP7VlJxkH + dSRkUbEoz5bAAs/+970uos7n7iXQIni+3erUTdYEk2iWnMBjTljfgbK/dQIDAQAB + AoGAJHoJZk75aKr7DSQNYIHuruOMdv5ZeDuJvKERWxTrVJqE32/xBKh42/IgqRrc + esBN9ZregRCd7YtxoL+EVUNWaJNVx2mNmezEznrc9zhcYUrgeaVdFO2yBF1889zO + gCOVwrO8uDgeyj6IKa25H6c1N13ih/o7ZzEgWbGG+ylU1yECQQDv4ZSJ4EjSh/Fl + aHdz3wbBa/HKGTjC8iRy476Cyg2Fm8MZUe9Yy3udOrb5ZnS2MTpIXt5AF3h2TfYV + VoFXIorjAkEA50FcJmzT8sNMrPaV8vn+9W2Lu4U7C+K/O2g1iXMaZms5PC5zV5aV + CKXZWUX1fq2RaOzlbQrpgiolhXpeh8FjxwJBAOFHzSQfSsTNfttp3KUpU0LbiVvv + i+spVSnA0O4rq79KpVNmK44Mq67hsW1P11QzrzTAQ6GVaUBRv0YS061td1kCQHnP + wtN2tboFR6lABkJDjxoGRvlSt4SOPr7zKGgrWjeiuTZLHXSAnCY+/hr5L9Q3ZwXG + 6x6iBdgLjVIe4BZQNtcCQQDXGv/gWinCNTN3MPWfTW/RGzuMYVmyBFais0/VrgdH + h1dLpztmpQqfyH/zrBXQ9qL/zR4ojS6XYneO/U18WpEe + -----END RSA PRIVATE KEY----- + + To generate a key like this with OpenSSL, run:: + + openssl genrsa 2048 > key.pem + + This format also supports password-encrypted private keys. TLS + Lite can only handle password-encrypted private keys when OpenSSL + and M2Crypto are installed. In this case, passwordCallback will be + invoked to query the user for the password. + + @type s: str + @param s: A string containing a PEM-encoded public or private key. + + @type private: bool + @param private: If True, a L{SyntaxError} will be raised if the + private key component is not present. + + @type public: bool + @param public: If True, the private key component (if present) will + be discarded, so this function will always return a public key. + + @type passwordCallback: callable + @param passwordCallback: This function will be called, with no + arguments, if the PEM-encoded private key is password-encrypted. + The callback should return the password string. If the password is + incorrect, SyntaxError will be raised. If no callback is passed + and the key is password-encrypted, a prompt will be displayed at + the console. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + @return: An RSA key. + + @raise SyntaxError: If the key is not properly formatted. + """ + for implementation in implementations: + if implementation == "openssl" and cryptomath.m2cryptoLoaded: + key = OpenSSL_RSAKey.parse(s, passwordCallback) + break + elif implementation == "python": + key = Python_RSAKey.parsePEM(s) + break + else: + raise ValueError("No acceptable implementations") + + return _parseKeyHelper(key, private, public) + + +def _parseKeyHelper(key, private, public): + if private: + if not key.hasPrivateKey(): + raise SyntaxError("Not a private key!") + + if public: + return _createPublicKey(key) + + if private: + if hasattr(key, "d"): + return _createPrivateKey(key) + else: + return key + + return key + +def parseAsPublicKey(s): + """Parse an XML or PEM-formatted public key. + + @type s: str + @param s: A string containing an XML or PEM-encoded public or private key. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + @return: An RSA public key. + + @raise SyntaxError: If the key is not properly formatted. + """ + try: + return parsePEMKey(s, public=True) + except: + return parseXMLKey(s, public=True) + +def parsePrivateKey(s): + """Parse an XML or PEM-formatted private key. + + @type s: str + @param s: A string containing an XML or PEM-encoded private key. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + @return: An RSA private key. + + @raise SyntaxError: If the key is not properly formatted. + """ + try: + return parsePEMKey(s, private=True) + except: + return parseXMLKey(s, private=True) + +def _createPublicKey(key): + """ + Create a new public key. Discard any private component, + and return the most efficient key possible. + """ + if not isinstance(key, RSAKey): + raise AssertionError() + return _createPublicRSAKey(key.n, key.e) + +def _createPrivateKey(key): + """ + Create a new private key. Return the most efficient key possible. + """ + if not isinstance(key, RSAKey): + raise AssertionError() + if not key.hasPrivateKey(): + raise AssertionError() + return _createPrivateRSAKey(key.n, key.e, key.d, key.p, key.q, key.dP, + key.dQ, key.qInv) + +def _createPublicRSAKey(n, e, implementations = ["openssl", "pycrypto", + "python"]): + for implementation in implementations: + if implementation == "openssl" and cryptomath.m2cryptoLoaded: + return OpenSSL_RSAKey(n, e) + elif implementation == "pycrypto" and cryptomath.pycryptoLoaded: + return PyCrypto_RSAKey(n, e) + elif implementation == "python": + return Python_RSAKey(n, e) + raise ValueError("No acceptable implementations") + +def _createPrivateRSAKey(n, e, d, p, q, dP, dQ, qInv, + implementations = ["pycrypto", "python"]): + for implementation in implementations: + if implementation == "pycrypto" and cryptomath.pycryptoLoaded: + return PyCrypto_RSAKey(n, e, d, p, q, dP, dQ, qInv) + elif implementation == "python": + return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv) + raise ValueError("No acceptable implementations") diff --git a/patches/gdata/tlslite/utils/rijndael.py b/patches/gdata/tlslite/utils/rijndael.py new file mode 100755 index 0000000..cb2f547 --- /dev/null +++ b/patches/gdata/tlslite/utils/rijndael.py @@ -0,0 +1,392 @@ +""" +A pure python (slow) implementation of rijndael with a decent interface + +To include - + +from rijndael import rijndael + +To do a key setup - + +r = rijndael(key, block_size = 16) + +key must be a string of length 16, 24, or 32 +blocksize must be 16, 24, or 32. Default is 16 + +To use - + +ciphertext = r.encrypt(plaintext) +plaintext = r.decrypt(ciphertext) + +If any strings are of the wrong length a ValueError is thrown +""" + +# ported from the Java reference code by Bram Cohen, bram@gawth.com, April 2001 +# this code is public domain, unless someone makes +# an intellectual property claim against the reference +# code, in which case it can be made public domain by +# deleting all the comments and renaming all the variables + +import copy +import string + + + +#----------------------- +#TREV - ADDED BECAUSE THERE'S WARNINGS ABOUT INT OVERFLOW BEHAVIOR CHANGING IN +#2.4..... +import os +if os.name != "java": + import exceptions + if hasattr(exceptions, "FutureWarning"): + import warnings + warnings.filterwarnings("ignore", category=FutureWarning, append=1) +#----------------------- + + + +shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]], + [[0, 0], [1, 5], [2, 4], [3, 3]], + [[0, 0], [1, 7], [3, 5], [4, 4]]] + +# [keysize][block_size] +num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}} + +A = [[1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 1, 1], + [1, 1, 1, 1, 0, 0, 0, 1]] + +# produce log and alog tables, needed for multiplying in the +# field GF(2^m) (generator = 3) +alog = [1] +for i in xrange(255): + j = (alog[-1] << 1) ^ alog[-1] + if j & 0x100 != 0: + j ^= 0x11B + alog.append(j) + +log = [0] * 256 +for i in xrange(1, 255): + log[alog[i]] = i + +# multiply two elements of GF(2^m) +def mul(a, b): + if a == 0 or b == 0: + return 0 + return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255] + +# substitution box based on F^{-1}(x) +box = [[0] * 8 for i in xrange(256)] +box[1][7] = 1 +for i in xrange(2, 256): + j = alog[255 - log[i]] + for t in xrange(8): + box[i][t] = (j >> (7 - t)) & 0x01 + +B = [0, 1, 1, 0, 0, 0, 1, 1] + +# affine transform: box[i] <- B + A*box[i] +cox = [[0] * 8 for i in xrange(256)] +for i in xrange(256): + for t in xrange(8): + cox[i][t] = B[t] + for j in xrange(8): + cox[i][t] ^= A[t][j] * box[i][j] + +# S-boxes and inverse S-boxes +S = [0] * 256 +Si = [0] * 256 +for i in xrange(256): + S[i] = cox[i][0] << 7 + for t in xrange(1, 8): + S[i] ^= cox[i][t] << (7-t) + Si[S[i] & 0xFF] = i + +# T-boxes +G = [[2, 1, 1, 3], + [3, 2, 1, 1], + [1, 3, 2, 1], + [1, 1, 3, 2]] + +AA = [[0] * 8 for i in xrange(4)] + +for i in xrange(4): + for j in xrange(4): + AA[i][j] = G[i][j] + AA[i][i+4] = 1 + +for i in xrange(4): + pivot = AA[i][i] + if pivot == 0: + t = i + 1 + while AA[t][i] == 0 and t < 4: + t += 1 + assert t != 4, 'G matrix must be invertible' + for j in xrange(8): + AA[i][j], AA[t][j] = AA[t][j], AA[i][j] + pivot = AA[i][i] + for j in xrange(8): + if AA[i][j] != 0: + AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255] + for t in xrange(4): + if i != t: + for j in xrange(i+1, 8): + AA[t][j] ^= mul(AA[i][j], AA[t][i]) + AA[t][i] = 0 + +iG = [[0] * 4 for i in xrange(4)] + +for i in xrange(4): + for j in xrange(4): + iG[i][j] = AA[i][j + 4] + +def mul4(a, bs): + if a == 0: + return 0 + r = 0 + for b in bs: + r <<= 8 + if b != 0: + r = r | mul(a, b) + return r + +T1 = [] +T2 = [] +T3 = [] +T4 = [] +T5 = [] +T6 = [] +T7 = [] +T8 = [] +U1 = [] +U2 = [] +U3 = [] +U4 = [] + +for t in xrange(256): + s = S[t] + T1.append(mul4(s, G[0])) + T2.append(mul4(s, G[1])) + T3.append(mul4(s, G[2])) + T4.append(mul4(s, G[3])) + + s = Si[t] + T5.append(mul4(s, iG[0])) + T6.append(mul4(s, iG[1])) + T7.append(mul4(s, iG[2])) + T8.append(mul4(s, iG[3])) + + U1.append(mul4(t, iG[0])) + U2.append(mul4(t, iG[1])) + U3.append(mul4(t, iG[2])) + U4.append(mul4(t, iG[3])) + +# round constants +rcon = [1] +r = 1 +for t in xrange(1, 30): + r = mul(2, r) + rcon.append(r) + +del A +del AA +del pivot +del B +del G +del box +del log +del alog +del i +del j +del r +del s +del t +del mul +del mul4 +del cox +del iG + +class rijndael: + def __init__(self, key, block_size = 16): + if block_size != 16 and block_size != 24 and block_size != 32: + raise ValueError('Invalid block size: ' + str(block_size)) + if len(key) != 16 and len(key) != 24 and len(key) != 32: + raise ValueError('Invalid key size: ' + str(len(key))) + self.block_size = block_size + + ROUNDS = num_rounds[len(key)][block_size] + BC = block_size / 4 + # encryption round keys + Ke = [[0] * BC for i in xrange(ROUNDS + 1)] + # decryption round keys + Kd = [[0] * BC for i in xrange(ROUNDS + 1)] + ROUND_KEY_COUNT = (ROUNDS + 1) * BC + KC = len(key) / 4 + + # copy user material bytes into temporary ints + tk = [] + for i in xrange(0, KC): + tk.append((ord(key[i * 4]) << 24) | (ord(key[i * 4 + 1]) << 16) | + (ord(key[i * 4 + 2]) << 8) | ord(key[i * 4 + 3])) + + # copy values into round key arrays + t = 0 + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t / BC][t % BC] = tk[j] + Kd[ROUNDS - (t / BC)][t % BC] = tk[j] + j += 1 + t += 1 + tt = 0 + rconpointer = 0 + while t < ROUND_KEY_COUNT: + # extrapolate using phi (the round key evolution function) + tt = tk[KC - 1] + tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \ + (S[ tt & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) ^ \ + (rcon[rconpointer] & 0xFF) << 24 + rconpointer += 1 + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i-1] + else: + for i in xrange(1, KC / 2): + tk[i] ^= tk[i-1] + tt = tk[KC / 2 - 1] + tk[KC / 2] ^= (S[ tt & 0xFF] & 0xFF) ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) << 24 + for i in xrange(KC / 2 + 1, KC): + tk[i] ^= tk[i-1] + # copy values into round key arrays + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t / BC][t % BC] = tk[j] + Kd[ROUNDS - (t / BC)][t % BC] = tk[j] + j += 1 + t += 1 + # inverse MixColumn where needed + for r in xrange(1, ROUNDS): + for j in xrange(BC): + tt = Kd[r][j] + Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \ + U2[(tt >> 16) & 0xFF] ^ \ + U3[(tt >> 8) & 0xFF] ^ \ + U4[ tt & 0xFF] + self.Ke = Ke + self.Kd = Kd + + def encrypt(self, plaintext): + if len(plaintext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext))) + Ke = self.Ke + + BC = self.block_size / 4 + ROUNDS = len(Ke) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][0] + s2 = shifts[SC][2][0] + s3 = shifts[SC][3][0] + a = [0] * BC + # temporary work array + t = [] + # plaintext to ints + key + for i in xrange(BC): + t.append((ord(plaintext[i * 4 ]) << 24 | + ord(plaintext[i * 4 + 1]) << 16 | + ord(plaintext[i * 4 + 2]) << 8 | + ord(plaintext[i * 4 + 3]) ) ^ Ke[0][i]) + # apply round transforms + for r in xrange(1, ROUNDS): + for i in xrange(BC): + a[i] = (T1[(t[ i ] >> 24) & 0xFF] ^ + T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T4[ t[(i + s3) % BC] & 0xFF] ) ^ Ke[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in xrange(BC): + tt = Ke[ROUNDS][i] + result.append((S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((S[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return string.join(map(chr, result), '') + + def decrypt(self, ciphertext): + if len(ciphertext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext))) + Kd = self.Kd + + BC = self.block_size / 4 + ROUNDS = len(Kd) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][1] + s2 = shifts[SC][2][1] + s3 = shifts[SC][3][1] + a = [0] * BC + # temporary work array + t = [0] * BC + # ciphertext to ints + key + for i in xrange(BC): + t[i] = (ord(ciphertext[i * 4 ]) << 24 | + ord(ciphertext[i * 4 + 1]) << 16 | + ord(ciphertext[i * 4 + 2]) << 8 | + ord(ciphertext[i * 4 + 3]) ) ^ Kd[0][i] + # apply round transforms + for r in xrange(1, ROUNDS): + for i in xrange(BC): + a[i] = (T5[(t[ i ] >> 24) & 0xFF] ^ + T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T8[ t[(i + s3) % BC] & 0xFF] ) ^ Kd[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in xrange(BC): + tt = Kd[ROUNDS][i] + result.append((Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((Si[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return string.join(map(chr, result), '') + +def encrypt(key, block): + return rijndael(key, len(block)).encrypt(block) + +def decrypt(key, block): + return rijndael(key, len(block)).decrypt(block) + +def test(): + def t(kl, bl): + b = 'b' * bl + r = rijndael('a' * kl, bl) + assert r.decrypt(r.encrypt(b)) == b + t(16, 16) + t(16, 24) + t(16, 32) + t(24, 16) + t(24, 24) + t(24, 32) + t(32, 16) + t(32, 24) + t(32, 32) + diff --git a/patches/gdata/tlslite/utils/win32prng.c b/patches/gdata/tlslite/utils/win32prng.c new file mode 100755 index 0000000..de08b3b --- /dev/null +++ b/patches/gdata/tlslite/utils/win32prng.c @@ -0,0 +1,63 @@ + +#include "Python.h" +#define _WIN32_WINNT 0x0400 /* Needed for CryptoAPI on some systems */ +#include + + +static PyObject* getRandomBytes(PyObject *self, PyObject *args) +{ + int howMany; + HCRYPTPROV hCryptProv; + unsigned char* bytes = NULL; + PyObject* returnVal = NULL; + + + /* Read Arguments */ + if (!PyArg_ParseTuple(args, "i", &howMany)) + return(NULL); + + /* Get Context */ + if(CryptAcquireContext( + &hCryptProv, + NULL, + NULL, + PROV_RSA_FULL, + CRYPT_VERIFYCONTEXT) == 0) + return Py_BuildValue("s#", NULL, 0); + + + /* Allocate bytes */ + bytes = malloc(howMany); + + + /* Get random data */ + if(CryptGenRandom( + hCryptProv, + howMany, + bytes) == 0) + returnVal = Py_BuildValue("s#", NULL, 0); + else + returnVal = Py_BuildValue("s#", bytes, howMany); + + free(bytes); + CryptReleaseContext(hCryptProv, 0); + + return returnVal; +} + + + +/* List of functions exported by this module */ + +static struct PyMethodDef win32prng_functions[] = { + {"getRandomBytes", (PyCFunction)getRandomBytes, METH_VARARGS}, + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +DL_EXPORT(void) initwin32prng(void) +{ + Py_InitModule("win32prng", win32prng_functions); +} diff --git a/patches/gdata/tlslite/utils/xmltools.py b/patches/gdata/tlslite/utils/xmltools.py new file mode 100755 index 0000000..c1e8c4d --- /dev/null +++ b/patches/gdata/tlslite/utils/xmltools.py @@ -0,0 +1,202 @@ +"""Helper functions for XML. + +This module has misc. helper functions for working with XML DOM nodes.""" + +from compat import * +import os +import re + +if os.name == "java": + # Only for Jython + from javax.xml.parsers import * + import java + + builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + + def parseDocument(s): + stream = java.io.ByteArrayInputStream(java.lang.String(s).getBytes()) + return builder.parse(stream) +else: + from xml.dom import minidom + from xml.sax import saxutils + + def parseDocument(s): + return minidom.parseString(s) + +def parseAndStripWhitespace(s): + try: + element = parseDocument(s).documentElement + except BaseException, e: + raise SyntaxError(str(e)) + stripWhitespace(element) + return element + +#Goes through a DOM tree and removes whitespace besides child elements, +#as long as this whitespace is correctly tab-ified +def stripWhitespace(element, tab=0): + element.normalize() + + lastSpacer = "\n" + ("\t"*tab) + spacer = lastSpacer + "\t" + + #Zero children aren't allowed (i.e. ) + #This makes writing output simpler, and matches Canonical XML + if element.childNodes.length==0: #DON'T DO len(element.childNodes) - doesn't work in Jython + raise SyntaxError("Empty XML elements not allowed") + + #If there's a single child, it must be text context + if element.childNodes.length==1: + if element.firstChild.nodeType == element.firstChild.TEXT_NODE: + #If it's an empty element, remove + if element.firstChild.data == lastSpacer: + element.removeChild(element.firstChild) + return + #If not text content, give an error + elif element.firstChild.nodeType == element.firstChild.ELEMENT_NODE: + raise SyntaxError("Bad whitespace under '%s'" % element.tagName) + else: + raise SyntaxError("Unexpected node type in XML document") + + #Otherwise there's multiple child element + child = element.firstChild + while child: + if child.nodeType == child.ELEMENT_NODE: + stripWhitespace(child, tab+1) + child = child.nextSibling + elif child.nodeType == child.TEXT_NODE: + if child == element.lastChild: + if child.data != lastSpacer: + raise SyntaxError("Bad whitespace under '%s'" % element.tagName) + elif child.data != spacer: + raise SyntaxError("Bad whitespace under '%s'" % element.tagName) + next = child.nextSibling + element.removeChild(child) + child = next + else: + raise SyntaxError("Unexpected node type in XML document") + + +def checkName(element, name): + if element.nodeType != element.ELEMENT_NODE: + raise SyntaxError("Missing element: '%s'" % name) + + if name == None: + return + + if element.tagName != name: + raise SyntaxError("Wrong element name: should be '%s', is '%s'" % (name, element.tagName)) + +def getChild(element, index, name=None): + if element.nodeType != element.ELEMENT_NODE: + raise SyntaxError("Wrong node type in getChild()") + + child = element.childNodes.item(index) + if child == None: + raise SyntaxError("Missing child: '%s'" % name) + checkName(child, name) + return child + +def getChildIter(element, index): + class ChildIter: + def __init__(self, element, index): + self.element = element + self.index = index + + def next(self): + if self.index < len(self.element.childNodes): + retVal = self.element.childNodes.item(self.index) + self.index += 1 + else: + retVal = None + return retVal + + def checkEnd(self): + if self.index != len(self.element.childNodes): + raise SyntaxError("Too many elements under: '%s'" % self.element.tagName) + return ChildIter(element, index) + +def getChildOrNone(element, index): + if element.nodeType != element.ELEMENT_NODE: + raise SyntaxError("Wrong node type in getChild()") + child = element.childNodes.item(index) + return child + +def getLastChild(element, index, name=None): + if element.nodeType != element.ELEMENT_NODE: + raise SyntaxError("Wrong node type in getLastChild()") + + child = element.childNodes.item(index) + if child == None: + raise SyntaxError("Missing child: '%s'" % name) + if child != element.lastChild: + raise SyntaxError("Too many elements under: '%s'" % element.tagName) + checkName(child, name) + return child + +#Regular expressions for syntax-checking attribute and element content +nsRegEx = "http://trevp.net/cryptoID\Z" +cryptoIDRegEx = "([a-km-z3-9]{5}\.){3}[a-km-z3-9]{5}\Z" +urlRegEx = "http(s)?://.{1,100}\Z" +sha1Base64RegEx = "[A-Za-z0-9+/]{27}=\Z" +base64RegEx = "[A-Za-z0-9+/]+={0,4}\Z" +certsListRegEx = "(0)?(1)?(2)?(3)?(4)?(5)?(6)?(7)?(8)?(9)?\Z" +keyRegEx = "[A-Z]\Z" +keysListRegEx = "(A)?(B)?(C)?(D)?(E)?(F)?(G)?(H)?(I)?(J)?(K)?(L)?(M)?(N)?(O)?(P)?(Q)?(R)?(S)?(T)?(U)?(V)?(W)?(X)?(Y)?(Z)?\Z" +dateTimeRegEx = "\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\Z" +shortStringRegEx = ".{1,100}\Z" +exprRegEx = "[a-zA-Z0-9 ,()]{1,200}\Z" +notAfterDeltaRegEx = "0|([1-9][0-9]{0,8})\Z" #A number from 0 to (1 billion)-1 +booleanRegEx = "(true)|(false)" + +def getReqAttribute(element, attrName, regEx=""): + if element.nodeType != element.ELEMENT_NODE: + raise SyntaxError("Wrong node type in getReqAttribute()") + + value = element.getAttribute(attrName) + if not value: + raise SyntaxError("Missing Attribute: " + attrName) + if not re.match(regEx, value): + raise SyntaxError("Bad Attribute Value for '%s': '%s' " % (attrName, value)) + element.removeAttribute(attrName) + return str(value) #de-unicode it; this is needed for bsddb, for example + +def getAttribute(element, attrName, regEx=""): + if element.nodeType != element.ELEMENT_NODE: + raise SyntaxError("Wrong node type in getAttribute()") + + value = element.getAttribute(attrName) + if value: + if not re.match(regEx, value): + raise SyntaxError("Bad Attribute Value for '%s': '%s' " % (attrName, value)) + element.removeAttribute(attrName) + return str(value) #de-unicode it; this is needed for bsddb, for example + +def checkNoMoreAttributes(element): + if element.nodeType != element.ELEMENT_NODE: + raise SyntaxError("Wrong node type in checkNoMoreAttributes()") + + if element.attributes.length!=0: + raise SyntaxError("Extra attributes on '%s'" % element.tagName) + +def getText(element, regEx=""): + textNode = element.firstChild + if textNode == None: + raise SyntaxError("Empty element '%s'" % element.tagName) + if textNode.nodeType != textNode.TEXT_NODE: + raise SyntaxError("Non-text node: '%s'" % element.tagName) + if not re.match(regEx, textNode.data): + raise SyntaxError("Bad Text Value for '%s': '%s' " % (element.tagName, textNode.data)) + return str(textNode.data) #de-unicode it; this is needed for bsddb, for example + +#Function for adding tabs to a string +def indent(s, steps, ch="\t"): + tabs = ch*steps + if s[-1] != "\n": + s = tabs + s.replace("\n", "\n"+tabs) + else: + s = tabs + s.replace("\n", "\n"+tabs) + s = s[ : -len(tabs)] + return s + +def escape(s): + return saxutils.escape(s) diff --git a/patches/gdata/urlfetch.py b/patches/gdata/urlfetch.py new file mode 100644 index 0000000..890b257 --- /dev/null +++ b/patches/gdata/urlfetch.py @@ -0,0 +1,247 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""Provides HTTP functions for gdata.service to use on Google App Engine + +AppEngineHttpClient: Provides an HTTP request method which uses App Engine's + urlfetch API. Set the http_client member of a GDataService object to an + instance of an AppEngineHttpClient to allow the gdata library to run on + Google App Engine. + +run_on_appengine: Function which will modify an existing GDataService object + to allow it to run on App Engine. It works by creating a new instance of + the AppEngineHttpClient and replacing the GDataService object's + http_client. + +HttpRequest: Function that wraps google.appengine.api.urlfetch.Fetch in a + common interface which is used by gdata.service.GDataService. In other + words, this module can be used as the gdata service request handler so + that all HTTP requests will be performed by the hosting Google App Engine + server. +""" + + +__author__ = 'api.jscudder (Jeff Scudder)' + + +import StringIO +import atom.service +import atom.http_interface +from google.appengine.api import urlfetch + + +def run_on_appengine(gdata_service): + """Modifies a GDataService object to allow it to run on App Engine. + + Args: + gdata_service: An instance of AtomService, GDataService, or any + of their subclasses which has an http_client member. + """ + gdata_service.http_client = AppEngineHttpClient() + + +class AppEngineHttpClient(atom.http_interface.GenericHttpClient): + def __init__(self, headers=None): + self.debug = False + self.headers = headers or {} + + def request(self, operation, url, data=None, headers=None): + """Performs an HTTP call to the server, supports GET, POST, PUT, and + DELETE. + + Usage example, perform and HTTP GET on http://www.google.com/: + import atom.http + client = atom.http.HttpClient() + http_response = client.request('GET', 'http://www.google.com/') + + Args: + operation: str The HTTP operation to be performed. This is usually one + of 'GET', 'POST', 'PUT', or 'DELETE' + data: filestream, list of parts, or other object which can be converted + to a string. Should be set to None when performing a GET or DELETE. + 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. + url: The full URL to which the request should be sent. Can be a string + or atom.url.Url. + headers: dict of strings. HTTP headers which should be sent + in the request. + """ + all_headers = self.headers.copy() + if headers: + all_headers.update(headers) + + # Construct the full payload. + # Assume that data is None or a string. + data_str = data + if data: + if isinstance(data, list): + # If data is a list of different objects, convert them all to strings + # and join them together. + converted_parts = [__ConvertDataPart(x) for x in data] + data_str = ''.join(converted_parts) + else: + data_str = __ConvertDataPart(data) + + # 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: + all_headers['Content-Length'] = len(data_str) + + # Set the content type to the default value if none was set. + if 'Content-Type' not in all_headers: + all_headers['Content-Type'] = 'application/atom+xml' + + # Lookup the urlfetch operation which corresponds to the desired HTTP verb. + if operation == 'GET': + method = urlfetch.GET + elif operation == 'POST': + method = urlfetch.POST + elif operation == 'PUT': + method = urlfetch.PUT + elif operation == 'DELETE': + method = urlfetch.DELETE + else: + method = None + return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str, + method=method, headers=all_headers)) + + +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 function is deprecated, use AppEngineHttpClient.request instead. + + To use this module with gdata.service, you can set this module to be the + http_request_handler so that HTTP requests use Google App Engine's urlfetch. + import gdata.service + import gdata.urlfetch + gdata.service.http_request_handler = gdata.urlfetch + + 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: 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. + """ + full_uri = atom.service.BuildUri(uri, url_params, escape_params) + (server, port, ssl, partial_uri) = atom.service.ProcessUrl(service, full_uri) + # Construct the full URL for the request. + if ssl: + full_url = 'https://%s%s' % (server, partial_uri) + else: + full_url = 'http://%s%s' % (server, partial_uri) + + # Construct the full payload. + # Assume that data is None or a string. + data_str = data + if data: + if isinstance(data, list): + # If data is a list of different objects, convert them all to strings + # and join them together. + converted_parts = [__ConvertDataPart(x) for x in data] + data_str = ''.join(converted_parts) + else: + data_str = __ConvertDataPart(data) + + # Construct the dictionary of HTTP headers. + headers = {} + if isinstance(service.additional_headers, dict): + headers = service.additional_headers.copy() + if isinstance(extra_headers, dict): + for header, value in extra_headers.iteritems(): + headers[header] = value + # Add the content type header (we don't need to calculate content length, + # since urlfetch.Fetch will calculate for us). + if content_type: + headers['Content-Type'] = content_type + + # Lookup the urlfetch operation which corresponds to the desired HTTP verb. + if operation == 'GET': + method = urlfetch.GET + elif operation == 'POST': + method = urlfetch.POST + elif operation == 'PUT': + method = urlfetch.PUT + elif operation == 'DELETE': + method = urlfetch.DELETE + else: + method = None + return HttpResponse(urlfetch.Fetch(url=full_url, payload=data_str, + method=method, headers=headers)) + + +def __ConvertDataPart(data): + if not data or isinstance(data, str): + return data + elif hasattr(data, 'read'): + # data is a file like object, so read it completely. + return data.read() + # The data object was not a file. + # Try to convert to a string and send the data. + return str(data) + + +class HttpResponse(object): + """Translates a urlfetch resoinse to look like an hhtplib resoinse. + + Used to allow the resoinse from HttpRequest to be usable by gdata.service + methods. + """ + + def __init__(self, urlfetch_response): + self.body = StringIO.StringIO(urlfetch_response.content) + self.headers = urlfetch_response.headers + self.status = urlfetch_response.status_code + self.reason = '' + + def read(self, length=None): + if not length: + return self.body.read() + else: + return self.body.read(length) + + def getheader(self, name): + if not self.headers.has_key(name): + return self.headers[name.lower()] + return self.headers[name] + diff --git a/patches/gdata/webmastertools/__init__.py b/patches/gdata/webmastertools/__init__.py new file mode 100644 index 0000000..7ad20ff --- /dev/null +++ b/patches/gdata/webmastertools/__init__.py @@ -0,0 +1,544 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Yu-Jie Lin +# +# 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. + +"""Contains extensions to Atom objects used with Google Webmaster Tools.""" + + +__author__ = 'livibetter (Yu-Jie Lin)' + + +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 +import gdata + + +# XML namespaces which are often used in Google Webmaster Tools entities. +GWEBMASTERTOOLS_NAMESPACE = 'http://schemas.google.com/webmasters/tools/2007' +GWEBMASTERTOOLS_TEMPLATE = '{http://schemas.google.com/webmasters/tools/2007}%s' + + +class Indexed(atom.AtomBase): + _tag = 'indexed' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def IndexedFromString(xml_string): + return atom.CreateClassFromXMLString(Indexed, xml_string) + + +class Crawled(atom.Date): + _tag = 'crawled' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def CrawledFromString(xml_string): + return atom.CreateClassFromXMLString(Crawled, xml_string) + + +class GeoLocation(atom.AtomBase): + _tag = 'geolocation' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def GeoLocationFromString(xml_string): + return atom.CreateClassFromXMLString(GeoLocation, xml_string) + + +class PreferredDomain(atom.AtomBase): + _tag = 'preferred-domain' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def PreferredDomainFromString(xml_string): + return atom.CreateClassFromXMLString(PreferredDomain, xml_string) + + +class CrawlRate(atom.AtomBase): + _tag = 'crawl-rate' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def CrawlRateFromString(xml_string): + return atom.CreateClassFromXMLString(CrawlRate, xml_string) + + +class EnhancedImageSearch(atom.AtomBase): + _tag = 'enhanced-image-search' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def EnhancedImageSearchFromString(xml_string): + return atom.CreateClassFromXMLString(EnhancedImageSearch, xml_string) + + +class Verified(atom.AtomBase): + _tag = 'verified' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def VerifiedFromString(xml_string): + return atom.CreateClassFromXMLString(Verified, xml_string) + + +class VerificationMethodMeta(atom.AtomBase): + _tag = 'meta' + _namespace = atom.ATOM_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + _attributes['content'] = 'content' + + def __init__(self, text=None, name=None, content=None, + extension_elements=None, extension_attributes=None): + self.text = text + self.name = name + self.content = content + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def VerificationMethodMetaFromString(xml_string): + return atom.CreateClassFromXMLString(VerificationMethodMeta, xml_string) + + +class VerificationMethod(atom.AtomBase): + _tag = 'verification-method' + _namespace = GWEBMASTERTOOLS_NAMESPACE + _children = atom.Text._children.copy() + _attributes = atom.Text._attributes.copy() + _children['{%s}meta' % atom.ATOM_NAMESPACE] = ( + 'meta', VerificationMethodMeta) + _attributes['in-use'] = 'in_use' + _attributes['type'] = 'type' + + def __init__(self, text=None, in_use=None, meta=None, type=None, + extension_elements=None, extension_attributes=None): + self.text = text + self.in_use = in_use + self.meta = meta + self.type = type + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def VerificationMethodFromString(xml_string): + return atom.CreateClassFromXMLString(VerificationMethod, xml_string) + + +class MarkupLanguage(atom.AtomBase): + _tag = 'markup-language' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def MarkupLanguageFromString(xml_string): + return atom.CreateClassFromXMLString(MarkupLanguage, xml_string) + + +class SitemapMobile(atom.AtomBase): + _tag = 'sitemap-mobile' + _namespace = GWEBMASTERTOOLS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}markup-language' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'markup_language', [MarkupLanguage]) + + def __init__(self, markup_language=None, + extension_elements=None, extension_attributes=None, text=None): + + self.markup_language = markup_language or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SitemapMobileFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapMobile, xml_string) + + +class SitemapMobileMarkupLanguage(atom.AtomBase): + _tag = 'sitemap-mobile-markup-language' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def SitemapMobileMarkupLanguageFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapMobileMarkupLanguage, xml_string) + + +class PublicationLabel(atom.AtomBase): + _tag = 'publication-label' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def PublicationLabelFromString(xml_string): + return atom.CreateClassFromXMLString(PublicationLabel, xml_string) + + +class SitemapNews(atom.AtomBase): + _tag = 'sitemap-news' + _namespace = GWEBMASTERTOOLS_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}publication-label' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'publication_label', [PublicationLabel]) + + def __init__(self, publication_label=None, + extension_elements=None, extension_attributes=None, text=None): + + self.publication_label = publication_label or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SitemapNewsFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapNews, xml_string) + + +class SitemapNewsPublicationLabel(atom.AtomBase): + _tag = 'sitemap-news-publication-label' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def SitemapNewsPublicationLabelFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapNewsPublicationLabel, xml_string) + + +class SitemapLastDownloaded(atom.Date): + _tag = 'sitemap-last-downloaded' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def SitemapLastDownloadedFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapLastDownloaded, xml_string) + + +class SitemapType(atom.AtomBase): + _tag = 'sitemap-type' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def SitemapTypeFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapType, xml_string) + + +class SitemapStatus(atom.AtomBase): + _tag = 'sitemap-status' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def SitemapStatusFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapStatus, xml_string) + + +class SitemapUrlCount(atom.AtomBase): + _tag = 'sitemap-url-count' + _namespace = GWEBMASTERTOOLS_NAMESPACE + + +def SitemapUrlCountFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapUrlCount, xml_string) + + +class LinkFinder(atom.LinkFinder): + """An "interface" providing methods to find link elements + + SitesEntry elements often contain multiple links which differ in the rel + attribute or content type. Often, developers are interested in a specific + type of link so this class provides methods to find specific classes of links. + + This class is used as a mixin in SitesEntry. + """ + + def GetSelfLink(self): + """Find the first link with rel set to 'self' + + Returns: + An atom.Link or none if none of the links had rel equal to 'self' + """ + + for a_link in self.link: + if a_link.rel == 'self': + return a_link + return None + + def GetEditLink(self): + for a_link in self.link: + if a_link.rel == 'edit': + return a_link + return None + + def GetPostLink(self): + """Get a link containing the POST target URL. + + The POST target URL is used to insert new entries. + + Returns: + A link object with a rel matching the POST type. + """ + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/g/2005#post': + return a_link + return None + + def GetFeedLink(self): + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/g/2005#feed': + return a_link + return None + + +class SitesEntry(atom.Entry, LinkFinder): + """A Google Webmaster Tools meta Entry flavor of an Atom Entry """ + + _tag = atom.Entry._tag + _namespace = atom.Entry._namespace + _children = atom.Entry._children.copy() + _attributes = atom.Entry._attributes.copy() + _children['{%s}entryLink' % gdata.GDATA_NAMESPACE] = ( + 'entry_link', [gdata.EntryLink]) + _children['{%s}indexed' % GWEBMASTERTOOLS_NAMESPACE] = ('indexed', Indexed) + _children['{%s}crawled' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'crawled', Crawled) + _children['{%s}geolocation' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'geolocation', GeoLocation) + _children['{%s}preferred-domain' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'preferred_domain', PreferredDomain) + _children['{%s}crawl-rate' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'crawl_rate', CrawlRate) + _children['{%s}enhanced-image-search' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'enhanced_image_search', EnhancedImageSearch) + _children['{%s}verified' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'verified', Verified) + _children['{%s}verification-method' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'verification_method', [VerificationMethod]) + + def __GetId(self): + return self.__id + + # This method was created to strip the unwanted whitespace from the id's + # text node. + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def __init__(self, category=None, content=None, + atom_id=None, link=None, title=None, updated=None, + entry_link=None, indexed=None, crawled=None, + geolocation=None, preferred_domain=None, crawl_rate=None, + enhanced_image_search=None, + verified=None, verification_method=None, + extension_elements=None, extension_attributes=None, text=None): + atom.Entry.__init__(self, category=category, + content=content, atom_id=atom_id, link=link, + title=title, updated=updated, text=text) + + self.entry_link = entry_link or [] + self.indexed = indexed + self.crawled = crawled + self.geolocation = geolocation + self.preferred_domain = preferred_domain + self.crawl_rate = crawl_rate + self.enhanced_image_search = enhanced_image_search + self.verified = verified + self.verification_method = verification_method or [] + + +def SitesEntryFromString(xml_string): + return atom.CreateClassFromXMLString(SitesEntry, xml_string) + + +class SitesFeed(atom.Feed, LinkFinder): + """A Google Webmaster Tools meta Sites feed flavor of an Atom Feed""" + + _tag = atom.Feed._tag + _namespace = atom.Feed._namespace + _children = atom.Feed._children.copy() + _attributes = atom.Feed._attributes.copy() + _children['{%s}startIndex' % gdata.OPENSEARCH_NAMESPACE] = ( + 'start_index', gdata.StartIndex) + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [SitesEntry]) + del _children['{%s}generator' % atom.ATOM_NAMESPACE] + del _children['{%s}author' % atom.ATOM_NAMESPACE] + del _children['{%s}contributor' % atom.ATOM_NAMESPACE] + del _children['{%s}logo' % atom.ATOM_NAMESPACE] + del _children['{%s}icon' % atom.ATOM_NAMESPACE] + del _children['{%s}rights' % atom.ATOM_NAMESPACE] + del _children['{%s}subtitle' % atom.ATOM_NAMESPACE] + + def __GetId(self): + return self.__id + + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def __init__(self, start_index=None, atom_id=None, title=None, entry=None, + category=None, link=None, updated=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for Source + + Args: + category: list (optional) A list of Category instances + id: Id (optional) The entry's Id element + link: list (optional) A list of Link instances + title: Title (optional) the entry's title element + updated: Updated (optional) the entry's updated element + entry: list (optional) A list of the Entry instances contained in the + feed. + text: String (optional) The text contents of the element. This is the + contents of the Entry's XML text node. + (Example: This is the text) + extension_elements: list (optional) A list of ExtensionElement instances + which are children of this element. + extension_attributes: dict (optional) A dictionary of strings which are + the values for additional XML attributes of this element. + """ + + self.start_index = start_index + self.category = category or [] + self.id = atom_id + self.link = link or [] + self.title = title + self.updated = updated + self.entry = entry or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SitesFeedFromString(xml_string): + return atom.CreateClassFromXMLString(SitesFeed, xml_string) + + +class SitemapsEntry(atom.Entry, LinkFinder): + """A Google Webmaster Tools meta Sitemaps Entry flavor of an Atom Entry """ + + _tag = atom.Entry._tag + _namespace = atom.Entry._namespace + _children = atom.Entry._children.copy() + _attributes = atom.Entry._attributes.copy() + _children['{%s}sitemap-type' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'sitemap_type', SitemapType) + _children['{%s}sitemap-status' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'sitemap_status', SitemapStatus) + _children['{%s}sitemap-last-downloaded' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'sitemap_last_downloaded', SitemapLastDownloaded) + _children['{%s}sitemap-url-count' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'sitemap_url_count', SitemapUrlCount) + _children['{%s}sitemap-mobile-markup-language' % GWEBMASTERTOOLS_NAMESPACE] \ + = ('sitemap_mobile_markup_language', SitemapMobileMarkupLanguage) + _children['{%s}sitemap-news-publication-label' % GWEBMASTERTOOLS_NAMESPACE] \ + = ('sitemap_news_publication_label', SitemapNewsPublicationLabel) + + def __GetId(self): + return self.__id + + # This method was created to strip the unwanted whitespace from the id's + # text node. + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def __init__(self, category=None, content=None, + atom_id=None, link=None, title=None, updated=None, + sitemap_type=None, sitemap_status=None, sitemap_last_downloaded=None, + sitemap_url_count=None, sitemap_mobile_markup_language=None, + sitemap_news_publication_label=None, + extension_elements=None, extension_attributes=None, text=None): + atom.Entry.__init__(self, category=category, + content=content, atom_id=atom_id, link=link, + title=title, updated=updated, text=text) + + self.sitemap_type = sitemap_type + self.sitemap_status = sitemap_status + self.sitemap_last_downloaded = sitemap_last_downloaded + self.sitemap_url_count = sitemap_url_count + self.sitemap_mobile_markup_language = sitemap_mobile_markup_language + self.sitemap_news_publication_label = sitemap_news_publication_label + + +def SitemapsEntryFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapsEntry, xml_string) + + +class SitemapsFeed(atom.Feed, LinkFinder): + """A Google Webmaster Tools meta Sitemaps feed flavor of an Atom Feed""" + + _tag = atom.Feed._tag + _namespace = atom.Feed._namespace + _children = atom.Feed._children.copy() + _attributes = atom.Feed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [SitemapsEntry]) + _children['{%s}sitemap-mobile' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'sitemap_mobile', SitemapMobile) + _children['{%s}sitemap-news' % GWEBMASTERTOOLS_NAMESPACE] = ( + 'sitemap_news', SitemapNews) + del _children['{%s}generator' % atom.ATOM_NAMESPACE] + del _children['{%s}author' % atom.ATOM_NAMESPACE] + del _children['{%s}contributor' % atom.ATOM_NAMESPACE] + del _children['{%s}logo' % atom.ATOM_NAMESPACE] + del _children['{%s}icon' % atom.ATOM_NAMESPACE] + del _children['{%s}rights' % atom.ATOM_NAMESPACE] + del _children['{%s}subtitle' % atom.ATOM_NAMESPACE] + + def __GetId(self): + return self.__id + + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def __init__(self, category=None, content=None, + atom_id=None, link=None, title=None, updated=None, + entry=None, sitemap_mobile=None, sitemap_news=None, + extension_elements=None, extension_attributes=None, text=None): + + self.category = category or [] + self.id = atom_id + self.link = link or [] + self.title = title + self.updated = updated + self.entry = entry or [] + self.text = text + self.sitemap_mobile = sitemap_mobile + self.sitemap_news = sitemap_news + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SitemapsFeedFromString(xml_string): + return atom.CreateClassFromXMLString(SitemapsFeed, xml_string) diff --git a/patches/gdata/webmastertools/data.py b/patches/gdata/webmastertools/data.py new file mode 100644 index 0000000..8b50a47 --- /dev/null +++ b/patches/gdata/webmastertools/data.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""Contains the data classes of the Google Webmaster Tools Data API""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core +import atom.data +import gdata.data +import gdata.opensearch.data + + +WT_TEMPLATE = '{http://schemas.google.com/webmaster/tools/2007/}%s' + + +class CrawlIssueCrawlType(atom.core.XmlElement): + """Type of crawl of the crawl issue""" + _qname = WT_TEMPLATE % 'crawl-type' + + +class CrawlIssueDateDetected(atom.core.XmlElement): + """Detection date for the issue""" + _qname = WT_TEMPLATE % 'date-detected' + + +class CrawlIssueDetail(atom.core.XmlElement): + """Detail of the crawl issue""" + _qname = WT_TEMPLATE % 'detail' + + +class CrawlIssueIssueType(atom.core.XmlElement): + """Type of crawl issue""" + _qname = WT_TEMPLATE % 'issue-type' + + +class CrawlIssueLinkedFromUrl(atom.core.XmlElement): + """Source URL that links to the issue URL""" + _qname = WT_TEMPLATE % 'linked-from' + + +class CrawlIssueUrl(atom.core.XmlElement): + """URL affected by the crawl issue""" + _qname = WT_TEMPLATE % 'url' + + +class CrawlIssueEntry(gdata.data.GDEntry): + """Describes a crawl issue entry""" + date_detected = CrawlIssueDateDetected + url = CrawlIssueUrl + detail = CrawlIssueDetail + issue_type = CrawlIssueIssueType + crawl_type = CrawlIssueCrawlType + linked_from = [CrawlIssueLinkedFromUrl] + + +class CrawlIssuesFeed(gdata.data.GDFeed): + """Feed of crawl issues for a particular site""" + entry = [CrawlIssueEntry] + + +class Indexed(atom.core.XmlElement): + """Describes the indexing status of a site""" + _qname = WT_TEMPLATE % 'indexed' + + +class Keyword(atom.core.XmlElement): + """A keyword in a site or in a link to a site""" + _qname = WT_TEMPLATE % 'keyword' + source = 'source' + + +class KeywordEntry(gdata.data.GDEntry): + """Describes a keyword entry""" + + +class KeywordsFeed(gdata.data.GDFeed): + """Feed of keywords for a particular site""" + entry = [KeywordEntry] + keyword = [Keyword] + + +class LastCrawled(atom.core.XmlElement): + """Describes the last crawled date of a site""" + _qname = WT_TEMPLATE % 'last-crawled' + + +class MessageBody(atom.core.XmlElement): + """Message body""" + _qname = WT_TEMPLATE % 'body' + + +class MessageDate(atom.core.XmlElement): + """Message date""" + _qname = WT_TEMPLATE % 'date' + + +class MessageLanguage(atom.core.XmlElement): + """Message language""" + _qname = WT_TEMPLATE % 'language' + + +class MessageRead(atom.core.XmlElement): + """Indicates if the message has already been read""" + _qname = WT_TEMPLATE % 'read' + + +class MessageSubject(atom.core.XmlElement): + """Message subject""" + _qname = WT_TEMPLATE % 'subject' + + +class SiteId(atom.core.XmlElement): + """Site URL""" + _qname = WT_TEMPLATE % 'id' + + +class MessageEntry(gdata.data.GDEntry): + """Describes a message entry""" + wt_id = SiteId + subject = MessageSubject + date = MessageDate + body = MessageBody + language = MessageLanguage + read = MessageRead + + +class MessagesFeed(gdata.data.GDFeed): + """Describes a messages feed""" + entry = [MessageEntry] + + +class SitemapEntry(gdata.data.GDEntry): + """Describes a sitemap entry""" + indexed = Indexed + wt_id = SiteId + + +class SitemapMobileMarkupLanguage(atom.core.XmlElement): + """Describes a markup language for URLs in this sitemap""" + _qname = WT_TEMPLATE % 'sitemap-mobile-markup-language' + + +class SitemapMobile(atom.core.XmlElement): + """Lists acceptable mobile markup languages for URLs in this sitemap""" + _qname = WT_TEMPLATE % 'sitemap-mobile' + sitemap_mobile_markup_language = [SitemapMobileMarkupLanguage] + + +class SitemapNewsPublicationLabel(atom.core.XmlElement): + """Specifies the publication label for this sitemap""" + _qname = WT_TEMPLATE % 'sitemap-news-publication-label' + + +class SitemapNews(atom.core.XmlElement): + """Lists publication labels for this sitemap""" + _qname = WT_TEMPLATE % 'sitemap-news' + sitemap_news_publication_label = [SitemapNewsPublicationLabel] + + +class SitemapType(atom.core.XmlElement): + """Indicates the type of sitemap. Not used for News or Mobile Sitemaps""" + _qname = WT_TEMPLATE % 'sitemap-type' + + +class SitemapUrlCount(atom.core.XmlElement): + """Indicates the number of URLs contained in the sitemap""" + _qname = WT_TEMPLATE % 'sitemap-url-count' + + +class SitemapsFeed(gdata.data.GDFeed): + """Describes a sitemaps feed""" + entry = [SitemapEntry] + + +class VerificationMethod(atom.core.XmlElement): + """Describes a verification method that may be used for a site""" + _qname = WT_TEMPLATE % 'verification-method' + in_use = 'in-use' + type = 'type' + + +class Verified(atom.core.XmlElement): + """Describes the verification status of a site""" + _qname = WT_TEMPLATE % 'verified' + + +class SiteEntry(gdata.data.GDEntry): + """Describes a site entry""" + indexed = Indexed + wt_id = SiteId + verified = Verified + last_crawled = LastCrawled + verification_method = [VerificationMethod] + + +class SitesFeed(gdata.data.GDFeed): + """Describes a sites feed""" + entry = [SiteEntry] + + diff --git a/patches/gdata/webmastertools/service.py b/patches/gdata/webmastertools/service.py new file mode 100644 index 0000000..8c3286d --- /dev/null +++ b/patches/gdata/webmastertools/service.py @@ -0,0 +1,516 @@ +#!/usr/bin/python +# +# Copyright (C) 2008 Yu-Jie Lin +# +# 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. + +"""GWebmasterToolsService extends the GDataService to streamline +Google Webmaster Tools operations. + + GWebmasterToolsService: Provides methods to query feeds and manipulate items. + Extends GDataService. +""" + +__author__ = 'livibetter (Yu-Jie Lin)' + +import urllib +import gdata +import atom.service +import gdata.service +import gdata.webmastertools as webmastertools +import atom + + +FEED_BASE = 'https://www.google.com/webmasters/tools/feeds/' +SITES_FEED = FEED_BASE + 'sites/' +SITE_TEMPLATE = SITES_FEED + '%s' +SITEMAPS_FEED_TEMPLATE = FEED_BASE + '%(site_id)s/sitemaps/' +SITEMAP_TEMPLATE = SITEMAPS_FEED_TEMPLATE + '%(sitemap_id)s' + + +class Error(Exception): + pass + + +class RequestError(Error): + pass + + +class GWebmasterToolsService(gdata.service.GDataService): + """Client for the Google Webmaster Tools service.""" + + def __init__(self, email=None, password=None, source=None, + server='www.google.com', **kwargs): + """Creates a client for the Google Webmaster Tools service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'www.google.com'. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + gdata.service.GDataService.__init__( + self, email=email, password=password, service='sitemaps', source=source, + server=server, **kwargs) + + def GetSitesFeed(self, uri=SITES_FEED, + converter=webmastertools.SitesFeedFromString): + """Gets sites feed. + + Args: + uri: str (optional) URI to retrieve sites feed. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitesFeedFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitesFeed object. + """ + return self.Get(uri, converter=converter) + + def AddSite(self, site_uri, uri=SITES_FEED, + url_params=None, escape_params=True, converter=None): + """Adds a site to Google Webmaster Tools. + + Args: + site_uri: str URI of which site to add. + uri: str (optional) URI to add a site. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitesEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitesEntry object. + """ + + site_entry = webmastertools.SitesEntry() + site_entry.content = atom.Content(src=site_uri) + response = self.Post(site_entry, uri, + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitesEntryFromString(response.ToString()) + return response + + def DeleteSite(self, site_uri, uri=SITE_TEMPLATE, + url_params=None, escape_params=True): + """Removes a site from Google Webmaster Tools. + + Args: + site_uri: str URI of which site to remove. + uri: str (optional) A URI template to send DELETE request. + Default SITE_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + True if the delete succeeded. + """ + + return self.Delete( + uri % urllib.quote_plus(site_uri), + url_params=url_params, escape_params=escape_params) + + def VerifySite(self, site_uri, verification_method, uri=SITE_TEMPLATE, + url_params=None, escape_params=True, converter=None): + """Requests a verification of a site. + + Args: + site_uri: str URI of which site to add sitemap for. + verification_method: str The method to verify a site. Valid values are + 'htmlpage', and 'metatag'. + uri: str (optional) URI template to update a site. + Default SITE_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitesEntry object. + """ + + site_entry = webmastertools.SitesEntry( + atom_id=atom.Id(text=site_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sites-info'), + verification_method=webmastertools.VerificationMethod( + type=verification_method, in_use='true') + ) + response = self.Put( + site_entry, + uri % urllib.quote_plus(site_uri), + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitesEntryFromString(response.ToString()) + return response + + + def UpdateGeoLocation(self, site_uri, geolocation, uri=SITE_TEMPLATE, + url_params=None, escape_params=True, converter=None): + """Updates geolocation setting of a site. + + Args: + site_uri: str URI of which site to add sitemap for. + geolocation: str The geographic location. Valid values are listed in + http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + uri: str (optional) URI template to update a site. + Default SITE_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitesEntry object. + """ + + site_entry = webmastertools.SitesEntry( + atom_id=atom.Id(text=site_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sites-info'), + geolocation=webmastertools.GeoLocation(text=geolocation) + ) + response = self.Put( + site_entry, + uri % urllib.quote_plus(site_uri), + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitesEntryFromString(response.ToString()) + return response + + def UpdateCrawlRate(self, site_uri, crawl_rate, uri=SITE_TEMPLATE, + url_params=None, escape_params=True, converter=None): + """Updates crawl rate setting of a site. + + Args: + site_uri: str URI of which site to add sitemap for. + crawl_rate: str The crawl rate for a site. Valid values are 'slower', + 'normal', and 'faster'. + uri: str (optional) URI template to update a site. + Default SITE_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitesEntry object. + """ + + site_entry = webmastertools.SitesEntry( + atom_id=atom.Id(text=site_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sites-info'), + crawl_rate=webmastertools.CrawlRate(text=crawl_rate) + ) + response = self.Put( + site_entry, + uri % urllib.quote_plus(site_uri), + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitesEntryFromString(response.ToString()) + return response + + def UpdatePreferredDomain(self, site_uri, preferred_domain, uri=SITE_TEMPLATE, + url_params=None, escape_params=True, converter=None): + """Updates preferred domain setting of a site. + + Note that if using 'preferwww', will also need www.example.com in account to + take effect. + + Args: + site_uri: str URI of which site to add sitemap for. + preferred_domain: str The preferred domain for a site. Valid values are 'none', + 'preferwww', and 'prefernowww'. + uri: str (optional) URI template to update a site. + Default SITE_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitesEntry object. + """ + + site_entry = webmastertools.SitesEntry( + atom_id=atom.Id(text=site_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sites-info'), + preferred_domain=webmastertools.PreferredDomain(text=preferred_domain) + ) + response = self.Put( + site_entry, + uri % urllib.quote_plus(site_uri), + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitesEntryFromString(response.ToString()) + return response + + def UpdateEnhancedImageSearch(self, site_uri, enhanced_image_search, + uri=SITE_TEMPLATE, url_params=None, escape_params=True, converter=None): + """Updates enhanced image search setting of a site. + + Args: + site_uri: str URI of which site to add sitemap for. + enhanced_image_search: str The enhanced image search setting for a site. + Valid values are 'true', and 'false'. + uri: str (optional) URI template to update a site. + Default SITE_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitesEntry object. + """ + + site_entry = webmastertools.SitesEntry( + atom_id=atom.Id(text=site_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sites-info'), + enhanced_image_search=webmastertools.EnhancedImageSearch( + text=enhanced_image_search) + ) + response = self.Put( + site_entry, + uri % urllib.quote_plus(site_uri), + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitesEntryFromString(response.ToString()) + return response + + def GetSitemapsFeed(self, site_uri, uri=SITEMAPS_FEED_TEMPLATE, + converter=webmastertools.SitemapsFeedFromString): + """Gets sitemaps feed of a site. + + Args: + site_uri: str (optional) URI of which site to retrieve its sitemaps feed. + uri: str (optional) URI to retrieve sites feed. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsFeedFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitemapsFeed object. + """ + return self.Get(uri % {'site_id': urllib.quote_plus(site_uri)}, + converter=converter) + + def AddSitemap(self, site_uri, sitemap_uri, sitemap_type='WEB', + uri=SITEMAPS_FEED_TEMPLATE, + url_params=None, escape_params=True, converter=None): + """Adds a regular sitemap to a site. + + Args: + site_uri: str URI of which site to add sitemap for. + sitemap_uri: str URI of sitemap to add to a site. + sitemap_type: str Type of added sitemap. Valid types: WEB, VIDEO, or CODE. + uri: str (optional) URI template to add a sitemap. + Default SITEMAP_FEED_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitemapsEntry object. + """ + + sitemap_entry = webmastertools.SitemapsEntry( + atom_id=atom.Id(text=sitemap_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sitemap-regular'), + sitemap_type=webmastertools.SitemapType(text=sitemap_type)) + response = self.Post( + sitemap_entry, + uri % {'site_id': urllib.quote_plus(site_uri)}, + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitemapsEntryFromString(response.ToString()) + return response + + def AddMobileSitemap(self, site_uri, sitemap_uri, + sitemap_mobile_markup_language='XHTML', uri=SITEMAPS_FEED_TEMPLATE, + url_params=None, escape_params=True, converter=None): + """Adds a mobile sitemap to a site. + + Args: + site_uri: str URI of which site to add sitemap for. + sitemap_uri: str URI of sitemap to add to a site. + sitemap_mobile_markup_language: str Format of added sitemap. Valid types: + XHTML, WML, or cHTML. + uri: str (optional) URI template to add a sitemap. + Default SITEMAP_FEED_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitemapsEntry object. + """ + # FIXME + sitemap_entry = webmastertools.SitemapsEntry( + atom_id=atom.Id(text=sitemap_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sitemap-mobile'), + sitemap_mobile_markup_language=\ + webmastertools.SitemapMobileMarkupLanguage( + text=sitemap_mobile_markup_language)) + print sitemap_entry + response = self.Post( + sitemap_entry, + uri % {'site_id': urllib.quote_plus(site_uri)}, + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitemapsEntryFromString(response.ToString()) + return response + + def AddNewsSitemap(self, site_uri, sitemap_uri, + sitemap_news_publication_label, uri=SITEMAPS_FEED_TEMPLATE, + url_params=None, escape_params=True, converter=None): + """Adds a news sitemap to a site. + + Args: + site_uri: str URI of which site to add sitemap for. + sitemap_uri: str URI of sitemap to add to a site. + sitemap_news_publication_label: str, list of str Publication Labels for + sitemap. + uri: str (optional) URI template to add a sitemap. + Default SITEMAP_FEED_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + converter: func (optional) Function which is executed on the server's + response before it is returned. Usually this is a function like + SitemapsEntryFromString which will parse the response and turn it into + an object. + + Returns: + If converter is defined, the results of running converter on the server's + response. Otherwise, it will be a SitemapsEntry object. + """ + + sitemap_entry = webmastertools.SitemapsEntry( + atom_id=atom.Id(text=sitemap_uri), + category=atom.Category( + scheme='http://schemas.google.com/g/2005#kind', + term='http://schemas.google.com/webmasters/tools/2007#sitemap-news'), + sitemap_news_publication_label=[], + ) + if isinstance(sitemap_news_publication_label, str): + sitemap_news_publication_label = [sitemap_news_publication_label] + for label in sitemap_news_publication_label: + sitemap_entry.sitemap_news_publication_label.append( + webmastertools.SitemapNewsPublicationLabel(text=label)) + print sitemap_entry + response = self.Post( + sitemap_entry, + uri % {'site_id': urllib.quote_plus(site_uri)}, + url_params=url_params, + escape_params=escape_params, converter=converter) + if not converter and isinstance(response, atom.Entry): + return webmastertools.SitemapsEntryFromString(response.ToString()) + return response + + def DeleteSitemap(self, site_uri, sitemap_uri, uri=SITEMAP_TEMPLATE, + url_params=None, escape_params=True): + """Removes a sitemap from a site. + + Args: + site_uri: str URI of which site to remove a sitemap from. + sitemap_uri: str URI of sitemap to remove from a site. + uri: str (optional) A URI template to send DELETE request. + Default SITEMAP_TEMPLATE. + url_params: dict (optional) Additional URL parameters to be included + in the insertion request. + escape_params: boolean (optional) If true, the url_parameters will be + escaped before they are included in the request. + + Returns: + True if the delete succeeded. + """ + + return self.Delete( + uri % {'site_id': urllib.quote_plus(site_uri), + 'sitemap_id': urllib.quote_plus(sitemap_uri)}, + url_params=url_params, escape_params=escape_params) diff --git a/patches/gdata/youtube/__init__.py b/patches/gdata/youtube/__init__.py new file mode 100755 index 0000000..c41aaea --- /dev/null +++ b/patches/gdata/youtube/__init__.py @@ -0,0 +1,684 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +__author__ = ('api.stephaniel@gmail.com (Stephanie Liu)' + ', api.jhartmann@gmail.com (Jochen Hartmann)') + +import atom +import gdata +import gdata.media as Media +import gdata.geo as Geo + +YOUTUBE_NAMESPACE = 'http://gdata.youtube.com/schemas/2007' +YOUTUBE_FORMAT = '{http://gdata.youtube.com/schemas/2007}format' +YOUTUBE_DEVELOPER_TAG_SCHEME = '%s/%s' % (YOUTUBE_NAMESPACE, + 'developertags.cat') +YOUTUBE_SUBSCRIPTION_TYPE_SCHEME = '%s/%s' % (YOUTUBE_NAMESPACE, + 'subscriptiontypes.cat') + +class Username(atom.AtomBase): + """The YouTube Username element""" + _tag = 'username' + _namespace = YOUTUBE_NAMESPACE + +class QueryString(atom.AtomBase): + """The YouTube QueryString element""" + _tag = 'queryString' + _namespace = YOUTUBE_NAMESPACE + + +class FirstName(atom.AtomBase): + """The YouTube FirstName element""" + _tag = 'firstName' + _namespace = YOUTUBE_NAMESPACE + + +class LastName(atom.AtomBase): + """The YouTube LastName element""" + _tag = 'lastName' + _namespace = YOUTUBE_NAMESPACE + + +class Age(atom.AtomBase): + """The YouTube Age element""" + _tag = 'age' + _namespace = YOUTUBE_NAMESPACE + + +class Books(atom.AtomBase): + """The YouTube Books element""" + _tag = 'books' + _namespace = YOUTUBE_NAMESPACE + + +class Gender(atom.AtomBase): + """The YouTube Gender element""" + _tag = 'gender' + _namespace = YOUTUBE_NAMESPACE + + +class Company(atom.AtomBase): + """The YouTube Company element""" + _tag = 'company' + _namespace = YOUTUBE_NAMESPACE + + +class Hobbies(atom.AtomBase): + """The YouTube Hobbies element""" + _tag = 'hobbies' + _namespace = YOUTUBE_NAMESPACE + + +class Hometown(atom.AtomBase): + """The YouTube Hometown element""" + _tag = 'hometown' + _namespace = YOUTUBE_NAMESPACE + + +class Location(atom.AtomBase): + """The YouTube Location element""" + _tag = 'location' + _namespace = YOUTUBE_NAMESPACE + + +class Movies(atom.AtomBase): + """The YouTube Movies element""" + _tag = 'movies' + _namespace = YOUTUBE_NAMESPACE + + +class Music(atom.AtomBase): + """The YouTube Music element""" + _tag = 'music' + _namespace = YOUTUBE_NAMESPACE + + +class Occupation(atom.AtomBase): + """The YouTube Occupation element""" + _tag = 'occupation' + _namespace = YOUTUBE_NAMESPACE + + +class School(atom.AtomBase): + """The YouTube School element""" + _tag = 'school' + _namespace = YOUTUBE_NAMESPACE + + +class Relationship(atom.AtomBase): + """The YouTube Relationship element""" + _tag = 'relationship' + _namespace = YOUTUBE_NAMESPACE + + +class Recorded(atom.AtomBase): + """The YouTube Recorded element""" + _tag = 'recorded' + _namespace = YOUTUBE_NAMESPACE + + +class Statistics(atom.AtomBase): + """The YouTube Statistics element.""" + _tag = 'statistics' + _namespace = YOUTUBE_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['viewCount'] = 'view_count' + _attributes['videoWatchCount'] = 'video_watch_count' + _attributes['subscriberCount'] = 'subscriber_count' + _attributes['lastWebAccess'] = 'last_web_access' + _attributes['favoriteCount'] = 'favorite_count' + + def __init__(self, view_count=None, video_watch_count=None, + favorite_count=None, subscriber_count=None, last_web_access=None, + extension_elements=None, extension_attributes=None, text=None): + + self.view_count = view_count + self.video_watch_count = video_watch_count + self.subscriber_count = subscriber_count + self.last_web_access = last_web_access + self.favorite_count = favorite_count + + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class Status(atom.AtomBase): + """The YouTube Status element""" + _tag = 'status' + _namespace = YOUTUBE_NAMESPACE + + +class Position(atom.AtomBase): + """The YouTube Position element. The position in a playlist feed.""" + _tag = 'position' + _namespace = YOUTUBE_NAMESPACE + + +class Racy(atom.AtomBase): + """The YouTube Racy element.""" + _tag = 'racy' + _namespace = YOUTUBE_NAMESPACE + +class Description(atom.AtomBase): + """The YouTube Description element.""" + _tag = 'description' + _namespace = YOUTUBE_NAMESPACE + + +class Private(atom.AtomBase): + """The YouTube Private element.""" + _tag = 'private' + _namespace = YOUTUBE_NAMESPACE + + +class NoEmbed(atom.AtomBase): + """The YouTube VideoShare element. Whether a video can be embedded or not.""" + _tag = 'noembed' + _namespace = YOUTUBE_NAMESPACE + + +class Comments(atom.AtomBase): + """The GData Comments element""" + _tag = 'comments' + _namespace = gdata.GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + [gdata.FeedLink]) + + def __init__(self, feed_link=None, extension_elements=None, + extension_attributes=None, text=None): + + self.feed_link = feed_link + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class Rating(atom.AtomBase): + """The GData Rating element""" + _tag = 'rating' + _namespace = gdata.GDATA_NAMESPACE + _attributes = atom.AtomBase._attributes.copy() + _attributes['min'] = 'min' + _attributes['max'] = 'max' + _attributes['numRaters'] = 'num_raters' + _attributes['average'] = 'average' + + def __init__(self, min=None, max=None, + num_raters=None, average=None, extension_elements=None, + extension_attributes=None, text=None): + + self.min = min + self.max = max + self.num_raters = num_raters + self.average = average + + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +class YouTubePlaylistVideoEntry(gdata.GDataEntry): + """Represents a YouTubeVideoEntry on a YouTubePlaylist.""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + [gdata.FeedLink]) + _children['{%s}description' % YOUTUBE_NAMESPACE] = ('description', + Description) + _children['{%s}rating' % gdata.GDATA_NAMESPACE] = ('rating', Rating) + _children['{%s}comments' % gdata.GDATA_NAMESPACE] = ('comments', Comments) + _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics) + _children['{%s}location' % YOUTUBE_NAMESPACE] = ('location', Location) + _children['{%s}position' % YOUTUBE_NAMESPACE] = ('position', Position) + _children['{%s}group' % gdata.media.MEDIA_NAMESPACE] = ('media', Media.Group) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, title=None, + updated=None, feed_link=None, description=None, + rating=None, comments=None, statistics=None, + location=None, position=None, media=None, + extension_elements=None, extension_attributes=None): + + self.feed_link = feed_link + self.description = description + self.rating = rating + self.comments = comments + self.statistics = statistics + self.location = location + self.position = position + self.media = media + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, + link=link, published=published, title=title, + updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + + +class YouTubeVideoCommentEntry(gdata.GDataEntry): + """Represents a comment on YouTube.""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + + +class YouTubeSubscriptionEntry(gdata.GDataEntry): + """Represents a subscription entry on YouTube.""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}username' % YOUTUBE_NAMESPACE] = ('username', Username) + _children['{%s}queryString' % YOUTUBE_NAMESPACE] = ( + 'query_string', QueryString) + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + [gdata.FeedLink]) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, title=None, + updated=None, username=None, query_string=None, feed_link=None, + extension_elements=None, extension_attributes=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, updated=updated) + + self.username = username + self.query_string = query_string + self.feed_link = feed_link + + + def GetSubscriptionType(self): + """Retrieve the type of this subscription. + + Returns: + A string that is either 'channel, 'query' or 'favorites' + """ + for category in self.category: + if category.scheme == YOUTUBE_SUBSCRIPTION_TYPE_SCHEME: + return category.term + + +class YouTubeVideoResponseEntry(gdata.GDataEntry): + """Represents a video response. """ + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}rating' % gdata.GDATA_NAMESPACE] = ('rating', Rating) + _children['{%s}noembed' % YOUTUBE_NAMESPACE] = ('noembed', NoEmbed) + _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics) + _children['{%s}racy' % YOUTUBE_NAMESPACE] = ('racy', Racy) + _children['{%s}group' % gdata.media.MEDIA_NAMESPACE] = ('media', Media.Group) + + def __init__(self, author=None, category=None, content=None, atom_id=None, + link=None, published=None, title=None, updated=None, rating=None, + noembed=None, statistics=None, racy=None, media=None, + extension_elements=None, extension_attributes=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, updated=updated) + + self.rating = rating + self.noembed = noembed + self.statistics = statistics + self.racy = racy + self.media = media or Media.Group() + + +class YouTubeContactEntry(gdata.GDataEntry): + """Represents a contact entry.""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}username' % YOUTUBE_NAMESPACE] = ('username', Username) + _children['{%s}status' % YOUTUBE_NAMESPACE] = ('status', Status) + + + def __init__(self, author=None, category=None, content=None, atom_id=None, + link=None, published=None, title=None, updated=None, + username=None, status=None, extension_elements=None, + extension_attributes=None, text=None): + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, updated=updated) + + self.username = username + self.status = status + + +class YouTubeVideoEntry(gdata.GDataEntry): + """Represents a video on YouTube.""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}rating' % gdata.GDATA_NAMESPACE] = ('rating', Rating) + _children['{%s}comments' % gdata.GDATA_NAMESPACE] = ('comments', Comments) + _children['{%s}noembed' % YOUTUBE_NAMESPACE] = ('noembed', NoEmbed) + _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics) + _children['{%s}recorded' % YOUTUBE_NAMESPACE] = ('recorded', Recorded) + _children['{%s}racy' % YOUTUBE_NAMESPACE] = ('racy', Racy) + _children['{%s}group' % gdata.media.MEDIA_NAMESPACE] = ('media', Media.Group) + _children['{%s}where' % gdata.geo.GEORSS_NAMESPACE] = ('geo', Geo.Where) + + def __init__(self, author=None, category=None, content=None, atom_id=None, + link=None, published=None, title=None, updated=None, rating=None, + noembed=None, statistics=None, racy=None, media=None, geo=None, + recorded=None, comments=None, extension_elements=None, + extension_attributes=None): + + self.rating = rating + self.noembed = noembed + self.statistics = statistics + self.racy = racy + self.comments = comments + self.media = media or Media.Group() + self.geo = geo + self.recorded = recorded + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, link=link, + published=published, title=title, updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + + def GetSwfUrl(self): + """Return the URL for the embeddable Video + + Returns: + URL of the embeddable video + """ + if self.media.content: + for content in self.media.content: + if content.extension_attributes[YOUTUBE_FORMAT] == '5': + return content.url + else: + return None + + def AddDeveloperTags(self, developer_tags): + """Add a developer tag for this entry. + + Developer tags can only be set during the initial upload. + + Arguments: + developer_tags: A list of developer tags as strings. + + Returns: + A list of all developer tags for this video entry. + """ + for tag_text in developer_tags: + self.media.category.append(gdata.media.Category( + text=tag_text, label=tag_text, scheme=YOUTUBE_DEVELOPER_TAG_SCHEME)) + + return self.GetDeveloperTags() + + def GetDeveloperTags(self): + """Retrieve developer tags for this video entry.""" + developer_tags = [] + for category in self.media.category: + if category.scheme == YOUTUBE_DEVELOPER_TAG_SCHEME: + developer_tags.append(category) + if len(developer_tags) > 0: + return developer_tags + + def GetYouTubeCategoryAsString(self): + """Convenience method to return the YouTube category as string. + + YouTubeVideoEntries can contain multiple Category objects with differing + schemes. This method returns only the category with the correct + scheme, ignoring developer tags. + """ + for category in self.media.category: + if category.scheme != YOUTUBE_DEVELOPER_TAG_SCHEME: + return category.text + +class YouTubeUserEntry(gdata.GDataEntry): + """Represents a user on YouTube.""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}username' % YOUTUBE_NAMESPACE] = ('username', Username) + _children['{%s}firstName' % YOUTUBE_NAMESPACE] = ('first_name', FirstName) + _children['{%s}lastName' % YOUTUBE_NAMESPACE] = ('last_name', LastName) + _children['{%s}age' % YOUTUBE_NAMESPACE] = ('age', Age) + _children['{%s}books' % YOUTUBE_NAMESPACE] = ('books', Books) + _children['{%s}gender' % YOUTUBE_NAMESPACE] = ('gender', Gender) + _children['{%s}company' % YOUTUBE_NAMESPACE] = ('company', Company) + _children['{%s}description' % YOUTUBE_NAMESPACE] = ('description', + Description) + _children['{%s}hobbies' % YOUTUBE_NAMESPACE] = ('hobbies', Hobbies) + _children['{%s}hometown' % YOUTUBE_NAMESPACE] = ('hometown', Hometown) + _children['{%s}location' % YOUTUBE_NAMESPACE] = ('location', Location) + _children['{%s}movies' % YOUTUBE_NAMESPACE] = ('movies', Movies) + _children['{%s}music' % YOUTUBE_NAMESPACE] = ('music', Music) + _children['{%s}occupation' % YOUTUBE_NAMESPACE] = ('occupation', Occupation) + _children['{%s}school' % YOUTUBE_NAMESPACE] = ('school', School) + _children['{%s}relationship' % YOUTUBE_NAMESPACE] = ('relationship', + Relationship) + _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics) + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + [gdata.FeedLink]) + _children['{%s}thumbnail' % gdata.media.MEDIA_NAMESPACE] = ('thumbnail', + Media.Thumbnail) + + def __init__(self, author=None, category=None, content=None, atom_id=None, + link=None, published=None, title=None, updated=None, + username=None, first_name=None, last_name=None, age=None, + books=None, gender=None, company=None, description=None, + hobbies=None, hometown=None, location=None, movies=None, + music=None, occupation=None, school=None, relationship=None, + statistics=None, feed_link=None, extension_elements=None, + extension_attributes=None, text=None): + + self.username = username + self.first_name = first_name + self.last_name = last_name + self.age = age + self.books = books + self.gender = gender + self.company = company + self.description = description + self.hobbies = hobbies + self.hometown = hometown + self.location = location + self.movies = movies + self.music = music + self.occupation = occupation + self.school = school + self.relationship = relationship + self.statistics = statistics + self.feed_link = feed_link + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, + link=link, published=published, + title=title, updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +class YouTubeVideoFeed(gdata.GDataFeed, gdata.LinkFinder): + """Represents a video feed on YouTube.""" + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [YouTubeVideoEntry]) + +class YouTubePlaylistEntry(gdata.GDataEntry): + """Represents a playlist in YouTube.""" + _tag = gdata.GDataEntry._tag + _namespace = gdata.GDataEntry._namespace + _children = gdata.GDataEntry._children.copy() + _attributes = gdata.GDataEntry._attributes.copy() + _children['{%s}description' % YOUTUBE_NAMESPACE] = ('description', + Description) + _children['{%s}private' % YOUTUBE_NAMESPACE] = ('private', + Private) + _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', + [gdata.FeedLink]) + + def __init__(self, author=None, category=None, content=None, + atom_id=None, link=None, published=None, title=None, + updated=None, private=None, feed_link=None, + description=None, extension_elements=None, + extension_attributes=None): + + self.description = description + self.private = private + self.feed_link = feed_link + + gdata.GDataEntry.__init__(self, author=author, category=category, + content=content, atom_id=atom_id, + link=link, published=published, title=title, + updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes) + + + +class YouTubePlaylistFeed(gdata.GDataFeed, gdata.LinkFinder): + """Represents a feed of a user's playlists """ + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [YouTubePlaylistEntry]) + + +class YouTubePlaylistVideoFeed(gdata.GDataFeed, gdata.LinkFinder): + """Represents a feed of video entry on a playlist.""" + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [YouTubePlaylistVideoEntry]) + + +class YouTubeContactFeed(gdata.GDataFeed, gdata.LinkFinder): + """Represents a feed of a users contacts.""" + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [YouTubeContactEntry]) + + +class YouTubeSubscriptionFeed(gdata.GDataFeed, gdata.LinkFinder): + """Represents a feed of a users subscriptions.""" + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [YouTubeSubscriptionEntry]) + + +class YouTubeVideoCommentFeed(gdata.GDataFeed, gdata.LinkFinder): + """Represents a feed of comments for a video.""" + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [YouTubeVideoCommentEntry]) + + +class YouTubeVideoResponseFeed(gdata.GDataFeed, gdata.LinkFinder): + """Represents a feed of video responses.""" + _tag = gdata.GDataFeed._tag + _namespace = gdata.GDataFeed._namespace + _children = gdata.GDataFeed._children.copy() + _attributes = gdata.GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', + [YouTubeVideoResponseEntry]) + + +def YouTubeVideoFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeVideoFeed, xml_string) + + +def YouTubeVideoEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeVideoEntry, xml_string) + + +def YouTubeContactFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeContactFeed, xml_string) + + +def YouTubeContactEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeContactEntry, xml_string) + + +def YouTubeVideoCommentFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeVideoCommentFeed, xml_string) + + +def YouTubeVideoCommentEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeVideoCommentEntry, xml_string) + + +def YouTubeUserFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeVideoFeed, xml_string) + + +def YouTubeUserEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeUserEntry, xml_string) + + +def YouTubePlaylistFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubePlaylistFeed, xml_string) + + +def YouTubePlaylistVideoFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubePlaylistVideoFeed, xml_string) + + +def YouTubePlaylistEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubePlaylistEntry, xml_string) + + +def YouTubePlaylistVideoEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubePlaylistVideoEntry, xml_string) + + +def YouTubeSubscriptionFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeSubscriptionFeed, xml_string) + + +def YouTubeSubscriptionEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeSubscriptionEntry, xml_string) + + +def YouTubeVideoResponseFeedFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeVideoResponseFeed, xml_string) + + +def YouTubeVideoResponseEntryFromString(xml_string): + return atom.CreateClassFromXMLString(YouTubeVideoResponseEntry, xml_string) diff --git a/patches/gdata/youtube/client.py b/patches/gdata/youtube/client.py new file mode 100644 index 0000000..2e34d6a --- /dev/null +++ b/patches/gdata/youtube/client.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# +# Copyright (C) 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. + +"""Contains a client to communicate with the YouTube servers. + + A quick and dirty port of the YouTube GDATA 1.0 Python client + libraries to version 2.0 of the GDATA library. + +""" + +# __author__ = 's.@google.com (John Skidgel)' + +import logging + +import gdata.client +import gdata.youtube.data +import atom.data +import atom.http_core + +# Constants +# ----------------------------------------------------------------------------- +YOUTUBE_CLIENTLOGIN_AUTHENTICATION_URL = 'https://www.google.com/youtube/accounts/ClientLogin' +YOUTUBE_SUPPORTED_UPLOAD_TYPES = ('mov', 'avi', 'wmv', 'mpg', 'quicktime', + 'flv') +YOUTUBE_QUERY_VALID_TIME_PARAMETERS = ('today', 'this_week', 'this_month', + 'all_time') +YOUTUBE_QUERY_VALID_ORDERBY_PARAMETERS = ('published', 'viewCount', 'rating', + 'relevance') +YOUTUBE_QUERY_VALID_RACY_PARAMETERS = ('include', 'exclude') +YOUTUBE_QUERY_VALID_FORMAT_PARAMETERS = ('1', '5', '6') +YOUTUBE_STANDARDFEEDS = ('most_recent', 'recently_featured', + 'top_rated', 'most_viewed','watch_on_mobile') + +YOUTUBE_UPLOAD_TOKEN_URI = 'http://gdata.youtube.com/action/GetUploadToken' +YOUTUBE_SERVER = 'gdata.youtube.com/feeds/api' +YOUTUBE_SERVICE = 'youtube' +YOUTUBE_VIDEO_FEED_URI = 'http://%s/videos' % YOUTUBE_SERVER +YOUTUBE_USER_FEED_URI = 'http://%s/users/' % YOUTUBE_SERVER + +# Takes a youtube video ID. +YOUTUBE_CAPTION_FEED_URI = 'http://gdata.youtube.com/feeds/api/videos/%s/captions' + +# Takes a youtube video ID and a caption track ID. +YOUTUBE_CAPTION_URI = 'http://gdata.youtube.com/feeds/api/videos/%s/captiondata/%s' + +YOUTUBE_CAPTION_MIME_TYPE = 'application/vnd.youtube.timedtext; charset=UTF-8' + + +# Classes +# ----------------------------------------------------------------------------- +class Error(Exception): + """Base class for errors within the YouTube service.""" + pass + + +class RequestError(Error): + """Error class that is thrown in response to an invalid HTTP Request.""" + pass + + +class YouTubeError(Error): + """YouTube service specific error class.""" + pass + + +class YouTubeClient(gdata.client.GDClient): + """Client for the YouTube service. + + Performs a partial list of Google Data YouTube API functions, such as + retrieving the videos feed for a user and the feed for a video. + YouTube Service requires authentication for any write, update or delete + actions. + """ + api_version = '2' + auth_service = YOUTUBE_SERVICE + auth_scopes = ['http://%s' % YOUTUBE_SERVER, 'https://%s' % YOUTUBE_SERVER] + + def get_videos(self, uri=YOUTUBE_VIDEO_FEED_URI, auth_token=None, + desired_class=gdata.youtube.data.VideoFeed, + **kwargs): + """Retrieves a YouTube video feed. + Args: + uri: A string representing the URI of the feed that is to be retrieved. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.get_feed(uri, auth_token=auth_token, + desired_class=desired_class, + **kwargs) + + GetVideos = get_videos + + + def get_user_feed(self, uri=None, username=None): + """Retrieve a YouTubeVideoFeed of user uploaded videos. + + Either a uri or a username must be provided. This will retrieve list + of videos uploaded by specified user. The uri will be of format + "http://gdata.youtube.com/feeds/api/users/{username}/uploads". + + Args: + uri: An optional string representing the URI of the user feed that is + to be retrieved. + username: An optional string representing the username. + + Returns: + A YouTubeUserFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a username to the + GetYouTubeUserFeed() method. + """ + if uri is None and username is None: + raise YouTubeError('You must provide at least a uri or a username ' + 'to the GetYouTubeUserFeed() method') + elif username and not uri: + uri = '%s%s/%s' % (YOUTUBE_USER_FEED_URI, username, 'uploads') + return self.get_feed(uri, desired_class=gdata.youtube.data.VideoFeed) + + GetUserFeed = get_user_feed + + + def get_video_entry(self, uri=None, video_id=None, + auth_token=None, **kwargs): + """Retrieve a YouTubeVideoEntry. + + Either a uri or a video_id must be provided. + + Args: + uri: An optional string representing the URI of the entry that is to + be retrieved. + video_id: An optional string representing the ID of the video. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a video_id to the + GetYouTubeVideoEntry() method. + """ + if uri is None and video_id is None: + raise YouTubeError('You must provide at least a uri or a video_id ' + 'to the get_youtube_video_entry() method') + elif video_id and uri is None: + uri = '%s/%s' % (YOUTUBE_VIDEO_FEED_URI, video_id) + return self.get_feed(uri, + desired_class=gdata.youtube.data.VideoEntry, + auth_token=auth_token, + **kwargs) + + GetVideoEntry = get_video_entry + + + def get_caption_feed(self, uri): + """Retrieve a Caption feed of tracks. + + Args: + uri: A string representing the caption feed's URI to be retrieved. + + Returns: + A YouTube CaptionFeed if successfully retrieved. + """ + return self.get_feed(uri, desired_class=gdata.youtube.data.CaptionFeed) + + GetCaptionFeed = get_caption_feed + + def get_caption_track(self, track_url, client_id, + developer_key, auth_token=None, **kwargs): + http_request = atom.http_core.HttpRequest(uri = track_url, method = 'GET') + dev_key = 'key=' + developer_key + authsub = 'AuthSub token="' + str(auth_token) + '"' + http_request.headers = { + 'Authorization': authsub, + 'X-GData-Client': client_id, + 'X-GData-Key': dev_key + } + return self.request(http_request=http_request, **kwargs) + + GetCaptionTrack = get_caption_track + + def create_track(self, video_id, title, language, body, client_id, + developer_key, auth_token=None, title_type='text', **kwargs): + """Creates a closed-caption track and adds to an existing YouTube video. + """ + new_entry = gdata.youtube.data.TrackEntry( + content = gdata.youtube.data.TrackContent(text = body, lang = language)) + uri = YOUTUBE_CAPTION_FEED_URI % video_id + http_request = atom.http_core.HttpRequest(uri = uri, method = 'POST') + dev_key = 'key=' + developer_key + authsub = 'AuthSub token="' + str(auth_token) + '"' + http_request.headers = { + 'Content-Type': YOUTUBE_CAPTION_MIME_TYPE, + 'Content-Language': language, + 'Slug': title, + 'Authorization': authsub, + 'GData-Version': self.api_version, + 'X-GData-Client': client_id, + 'X-GData-Key': dev_key + } + http_request.add_body_part(body, http_request.headers['Content-Type']) + return self.request(http_request = http_request, + desired_class = new_entry.__class__, **kwargs) + + + CreateTrack = create_track + + def delete_track(self, video_id, track, client_id, developer_key, + auth_token=None, **kwargs): + """Deletes a track.""" + if isinstance(track, gdata.youtube.data.TrackEntry): + track_id_text_node = track.get_id().split(':') + track_id = track_id_text_node[3] + else: + track_id = track + uri = YOUTUBE_CAPTION_URI % (video_id, track_id) + http_request = atom.http_core.HttpRequest(uri = uri, method = 'DELETE') + dev_key = 'key=' + developer_key + authsub = 'AuthSub token="' + str(auth_token) + '"' + http_request.headers = { + 'Authorization': authsub, + 'GData-Version': self.api_version, + 'X-GData-Client': client_id, + 'X-GData-Key': dev_key + } + return self.request(http_request=http_request, **kwargs) + + DeleteTrack = delete_track + + def update_track(self, video_id, track, body, client_id, developer_key, + auth_token=None, **kwargs): + """Updates a closed-caption track for an existing YouTube video. + """ + track_id_text_node = track.get_id().split(':') + track_id = track_id_text_node[3] + uri = YOUTUBE_CAPTION_URI % (video_id, track_id) + http_request = atom.http_core.HttpRequest(uri = uri, method = 'PUT') + dev_key = 'key=' + developer_key + authsub = 'AuthSub token="' + str(auth_token) + '"' + http_request.headers = { + 'Content-Type': YOUTUBE_CAPTION_MIME_TYPE, + 'Authorization': authsub, + 'GData-Version': self.api_version, + 'X-GData-Client': client_id, + 'X-GData-Key': dev_key + } + http_request.add_body_part(body, http_request.headers['Content-Type']) + return self.request(http_request = http_request, + desired_class = track.__class__, **kwargs) + + UpdateTrack = update_track diff --git a/patches/gdata/youtube/data.py b/patches/gdata/youtube/data.py new file mode 100644 index 0000000..4ef2d62 --- /dev/null +++ b/patches/gdata/youtube/data.py @@ -0,0 +1,502 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + + +"""Contains the data classes of the YouTube Data API""" + + +__author__ = 'j.s@google.com (Jeff Scudder)' + + +import atom.core +import atom.data +import gdata.data +import gdata.geo.data +import gdata.media.data +import gdata.opensearch.data +import gdata.youtube.data + + +YT_TEMPLATE = '{http://gdata.youtube.com/schemas/2007/}%s' + + +class ComplaintEntry(gdata.data.GDEntry): + """Describes a complaint about a video""" + + +class ComplaintFeed(gdata.data.GDFeed): + """Describes complaints about a video""" + entry = [ComplaintEntry] + + +class RatingEntry(gdata.data.GDEntry): + """A rating about a video""" + rating = gdata.data.Rating + + +class RatingFeed(gdata.data.GDFeed): + """Describes ratings for a video""" + entry = [RatingEntry] + + +class YouTubeMediaContent(gdata.media.data.MediaContent): + """Describes a you tube media content""" + _qname = gdata.media.data.MEDIA_TEMPLATE % 'content' + format = 'format' + + +class YtAge(atom.core.XmlElement): + """User's age""" + _qname = YT_TEMPLATE % 'age' + + +class YtBooks(atom.core.XmlElement): + """User's favorite books""" + _qname = YT_TEMPLATE % 'books' + + +class YtCompany(atom.core.XmlElement): + """User's company""" + _qname = YT_TEMPLATE % 'company' + + +class YtDescription(atom.core.XmlElement): + """Description""" + _qname = YT_TEMPLATE % 'description' + + +class YtDuration(atom.core.XmlElement): + """Video duration""" + _qname = YT_TEMPLATE % 'duration' + seconds = 'seconds' + + +class YtFirstName(atom.core.XmlElement): + """User's first name""" + _qname = YT_TEMPLATE % 'firstName' + + +class YtGender(atom.core.XmlElement): + """User's gender""" + _qname = YT_TEMPLATE % 'gender' + + +class YtHobbies(atom.core.XmlElement): + """User's hobbies""" + _qname = YT_TEMPLATE % 'hobbies' + + +class YtHometown(atom.core.XmlElement): + """User's hometown""" + _qname = YT_TEMPLATE % 'hometown' + + +class YtLastName(atom.core.XmlElement): + """User's last name""" + _qname = YT_TEMPLATE % 'lastName' + + +class YtLocation(atom.core.XmlElement): + """Location""" + _qname = YT_TEMPLATE % 'location' + + +class YtMovies(atom.core.XmlElement): + """User's favorite movies""" + _qname = YT_TEMPLATE % 'movies' + + +class YtMusic(atom.core.XmlElement): + """User's favorite music""" + _qname = YT_TEMPLATE % 'music' + + +class YtNoEmbed(atom.core.XmlElement): + """Disables embedding for the video""" + _qname = YT_TEMPLATE % 'noembed' + + +class YtOccupation(atom.core.XmlElement): + """User's occupation""" + _qname = YT_TEMPLATE % 'occupation' + + +class YtPlaylistId(atom.core.XmlElement): + """Playlist id""" + _qname = YT_TEMPLATE % 'playlistId' + + +class YtPosition(atom.core.XmlElement): + """Video position on the playlist""" + _qname = YT_TEMPLATE % 'position' + + +class YtPrivate(atom.core.XmlElement): + """Flags the entry as private""" + _qname = YT_TEMPLATE % 'private' + + +class YtQueryString(atom.core.XmlElement): + """Keywords or query string associated with a subscription""" + _qname = YT_TEMPLATE % 'queryString' + + +class YtRacy(atom.core.XmlElement): + """Mature content""" + _qname = YT_TEMPLATE % 'racy' + + +class YtRecorded(atom.core.XmlElement): + """Date when the video was recorded""" + _qname = YT_TEMPLATE % 'recorded' + + +class YtRelationship(atom.core.XmlElement): + """User's relationship status""" + _qname = YT_TEMPLATE % 'relationship' + + +class YtSchool(atom.core.XmlElement): + """User's school""" + _qname = YT_TEMPLATE % 'school' + + +class YtStatistics(atom.core.XmlElement): + """Video and user statistics""" + _qname = YT_TEMPLATE % 'statistics' + favorite_count = 'favoriteCount' + video_watch_count = 'videoWatchCount' + view_count = 'viewCount' + last_web_access = 'lastWebAccess' + subscriber_count = 'subscriberCount' + + +class YtStatus(atom.core.XmlElement): + """Status of a contact""" + _qname = YT_TEMPLATE % 'status' + + +class YtUserProfileStatistics(YtStatistics): + """User statistics""" + _qname = YT_TEMPLATE % 'statistics' + + +class YtUsername(atom.core.XmlElement): + """Youtube username""" + _qname = YT_TEMPLATE % 'username' + + +class FriendEntry(gdata.data.BatchEntry): + """Describes a contact in friend list""" + username = YtUsername + status = YtStatus + email = gdata.data.Email + + +class FriendFeed(gdata.data.BatchFeed): + """Describes user's friends""" + entry = [FriendEntry] + + +class YtVideoStatistics(YtStatistics): + """Video statistics""" + _qname = YT_TEMPLATE % 'statistics' + + +class ChannelEntry(gdata.data.GDEntry): + """Describes a video channel""" + + +class ChannelFeed(gdata.data.GDFeed): + """Describes channels""" + entry = [ChannelEntry] + + +class FavoriteEntry(gdata.data.BatchEntry): + """Describes a favorite video""" + + +class FavoriteFeed(gdata.data.BatchFeed): + """Describes favorite videos""" + entry = [FavoriteEntry] + + +class YouTubeMediaCredit(gdata.media.data.MediaCredit): + """Describes a you tube media credit""" + _qname = gdata.media.data.MEDIA_TEMPLATE % 'credit' + type = 'type' + + +class YouTubeMediaRating(gdata.media.data.MediaRating): + """Describes a you tube media rating""" + _qname = gdata.media.data.MEDIA_TEMPLATE % 'rating' + country = 'country' + + +class YtAboutMe(atom.core.XmlElement): + """User's self description""" + _qname = YT_TEMPLATE % 'aboutMe' + + +class UserProfileEntry(gdata.data.BatchEntry): + """Describes an user's profile""" + relationship = YtRelationship + description = YtDescription + location = YtLocation + statistics = YtUserProfileStatistics + school = YtSchool + music = YtMusic + first_name = YtFirstName + gender = YtGender + occupation = YtOccupation + hometown = YtHometown + company = YtCompany + movies = YtMovies + books = YtBooks + username = YtUsername + about_me = YtAboutMe + last_name = YtLastName + age = YtAge + thumbnail = gdata.media.data.MediaThumbnail + hobbies = YtHobbies + + +class UserProfileFeed(gdata.data.BatchFeed): + """Describes a feed of user's profile""" + entry = [UserProfileEntry] + + +class YtAspectRatio(atom.core.XmlElement): + """The aspect ratio of a media file""" + _qname = YT_TEMPLATE % 'aspectRatio' + + +class YtBasePublicationState(atom.core.XmlElement): + """Status of an unpublished entry""" + _qname = YT_TEMPLATE % 'state' + help_url = 'helpUrl' + + +class YtPublicationState(YtBasePublicationState): + """Status of an unpublished video""" + _qname = YT_TEMPLATE % 'state' + name = 'name' + reason_code = 'reasonCode' + + +class YouTubeAppControl(atom.data.Control): + """Describes a you tube app control""" + _qname = (atom.data.APP_TEMPLATE_V1 % 'control', + atom.data.APP_TEMPLATE_V2 % 'control') + state = YtPublicationState + + +class YtCaptionPublicationState(YtBasePublicationState): + """Status of an unpublished caption track""" + _qname = YT_TEMPLATE % 'state' + reason_code = 'reasonCode' + name = 'name' + + +class YouTubeCaptionAppControl(atom.data.Control): + """Describes a you tube caption app control""" + _qname = atom.data.APP_TEMPLATE_V2 % 'control' + state = YtCaptionPublicationState + + +class CaptionTrackEntry(gdata.data.GDEntry): + """Describes a caption track""" + + +class CaptionTrackFeed(gdata.data.GDFeed): + """Describes caption tracks""" + entry = [CaptionTrackEntry] + + +class YtCountHint(atom.core.XmlElement): + """Hint as to how many entries the linked feed contains""" + _qname = YT_TEMPLATE % 'countHint' + + +class PlaylistLinkEntry(gdata.data.BatchEntry): + """Describes a playlist""" + description = YtDescription + playlist_id = YtPlaylistId + count_hint = YtCountHint + private = YtPrivate + + +class PlaylistLinkFeed(gdata.data.BatchFeed): + """Describes list of playlists""" + entry = [PlaylistLinkEntry] + + +class YtModerationStatus(atom.core.XmlElement): + """Moderation status""" + _qname = YT_TEMPLATE % 'moderationStatus' + + +class YtPlaylistTitle(atom.core.XmlElement): + """Playlist title""" + _qname = YT_TEMPLATE % 'playlistTitle' + + +class SubscriptionEntry(gdata.data.BatchEntry): + """Describes user's channel subscritpions""" + count_hint = YtCountHint + playlist_title = YtPlaylistTitle + thumbnail = gdata.media.data.MediaThumbnail + username = YtUsername + query_string = YtQueryString + playlist_id = YtPlaylistId + + +class SubscriptionFeed(gdata.data.BatchFeed): + """Describes list of user's video subscriptions""" + entry = [SubscriptionEntry] + + +class YtSpam(atom.core.XmlElement): + """Indicates that the entry probably contains spam""" + _qname = YT_TEMPLATE % 'spam' + + +class CommentEntry(gdata.data.BatchEntry): + """Describes a comment for a video""" + spam = YtSpam + + +class CommentFeed(gdata.data.BatchFeed): + """Describes comments for a video""" + entry = [CommentEntry] + + +class YtUploaded(atom.core.XmlElement): + """Date/Time at which the video was uploaded""" + _qname = YT_TEMPLATE % 'uploaded' + + +class YtVideoId(atom.core.XmlElement): + """Video id""" + _qname = YT_TEMPLATE % 'videoid' + + +class YouTubeMediaGroup(gdata.media.data.MediaGroup): + """Describes a you tube media group""" + _qname = gdata.media.data.MEDIA_TEMPLATE % 'group' + videoid = YtVideoId + private = YtPrivate + duration = YtDuration + aspect_ratio = YtAspectRatio + uploaded = YtUploaded + + +class VideoEntryBase(gdata.data.GDEntry): + """Elements that describe or contain videos""" + group = YouTubeMediaGroup + statistics = YtVideoStatistics + racy = YtRacy + recorded = YtRecorded + where = gdata.geo.data.GeoRssWhere + rating = gdata.data.Rating + noembed = YtNoEmbed + location = YtLocation + comments = gdata.data.Comments + + +class PlaylistEntry(gdata.data.BatchEntry): + """Describes a video in a playlist""" + description = YtDescription + position = YtPosition + + +class PlaylistFeed(gdata.data.BatchFeed): + """Describes videos in a playlist""" + private = YtPrivate + group = YouTubeMediaGroup + playlist_id = YtPlaylistId + entry = [PlaylistEntry] + + +class VideoEntry(gdata.data.BatchEntry): + """Describes a video""" + + +class VideoFeed(gdata.data.BatchFeed): + """Describes a video feed""" + entry = [VideoEntry] + + +class VideoMessageEntry(gdata.data.BatchEntry): + """Describes a video message""" + description = YtDescription + + +class VideoMessageFeed(gdata.data.BatchFeed): + """Describes videos in a videoMessage""" + entry = [VideoMessageEntry] + + +class UserEventEntry(gdata.data.GDEntry): + """Describes a user event""" + playlist_id = YtPlaylistId + videoid = YtVideoId + username = YtUsername + query_string = YtQueryString + rating = gdata.data.Rating + + +class UserEventFeed(gdata.data.GDFeed): + """Describes list of events""" + entry = [UserEventEntry] + + +class VideoModerationEntry(gdata.data.GDEntry): + """Describes video moderation""" + moderation_status = YtModerationStatus + videoid = YtVideoId + + +class VideoModerationFeed(gdata.data.GDFeed): + """Describes a video moderation feed""" + entry = [VideoModerationEntry] + + +class TrackContent(atom.data.Content): + lang = atom.data.XML_TEMPLATE % 'lang' + + +class TrackEntry(gdata.data.GDEntry): + """Represents the URL for a caption track""" + content = TrackContent + + def get_caption_track_id(self): + """Extracts the ID of this caption track. + Returns: + The caption track's id as a string. + """ + if self.id.text: + match = CAPTION_TRACK_ID_PATTERN.match(self.id.text) + if match: + return match.group(2) + return None + + GetCaptionTrackId = get_caption_track_id + + +class CaptionFeed(gdata.data.GDFeed): + """Represents a caption feed for a video on YouTube.""" + entry = [TrackEntry] diff --git a/patches/gdata/youtube/service.py b/patches/gdata/youtube/service.py new file mode 100644 index 0000000..9e0346f --- /dev/null +++ b/patches/gdata/youtube/service.py @@ -0,0 +1,1563 @@ +#!/usr/bin/python +# +# Copyright (C) 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. + +"""YouTubeService extends GDataService to streamline YouTube operations. + + YouTubeService: Provides methods to perform CRUD operations on YouTube feeds. + Extends GDataService. +""" + +__author__ = ('api.stephaniel@gmail.com (Stephanie Liu), ' + 'api.jhartmann@gmail.com (Jochen Hartmann)') + +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 os +import atom +import gdata +import gdata.service +import gdata.youtube + +YOUTUBE_SERVER = 'gdata.youtube.com' +YOUTUBE_SERVICE = 'youtube' +YOUTUBE_CLIENTLOGIN_AUTHENTICATION_URL = 'https://www.google.com/youtube/accounts/ClientLogin' +YOUTUBE_SUPPORTED_UPLOAD_TYPES = ('mov', 'avi', 'wmv', 'mpg', 'quicktime', + 'flv', 'mp4', 'x-flv') +YOUTUBE_QUERY_VALID_TIME_PARAMETERS = ('today', 'this_week', 'this_month', + 'all_time') +YOUTUBE_QUERY_VALID_ORDERBY_PARAMETERS = ('published', 'viewCount', 'rating', + 'relevance') +YOUTUBE_QUERY_VALID_RACY_PARAMETERS = ('include', 'exclude') +YOUTUBE_QUERY_VALID_FORMAT_PARAMETERS = ('1', '5', '6') +YOUTUBE_STANDARDFEEDS = ('most_recent', 'recently_featured', + 'top_rated', 'most_viewed','watch_on_mobile') +YOUTUBE_UPLOAD_URI = 'http://uploads.gdata.youtube.com/feeds/api/users' +YOUTUBE_UPLOAD_TOKEN_URI = 'http://gdata.youtube.com/action/GetUploadToken' +YOUTUBE_VIDEO_URI = 'http://gdata.youtube.com/feeds/api/videos' +YOUTUBE_USER_FEED_URI = 'http://gdata.youtube.com/feeds/api/users' +YOUTUBE_PLAYLIST_FEED_URI = 'http://gdata.youtube.com/feeds/api/playlists' + +YOUTUBE_STANDARD_FEEDS = 'http://gdata.youtube.com/feeds/api/standardfeeds' +YOUTUBE_STANDARD_TOP_RATED_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, 'top_rated') +YOUTUBE_STANDARD_MOST_VIEWED_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'most_viewed') +YOUTUBE_STANDARD_RECENTLY_FEATURED_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'recently_featured') +YOUTUBE_STANDARD_WATCH_ON_MOBILE_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'watch_on_mobile') +YOUTUBE_STANDARD_TOP_FAVORITES_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'top_favorites') +YOUTUBE_STANDARD_MOST_RECENT_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'most_recent') +YOUTUBE_STANDARD_MOST_DISCUSSED_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'most_discussed') +YOUTUBE_STANDARD_MOST_LINKED_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'most_linked') +YOUTUBE_STANDARD_MOST_RESPONDED_URI = '%s/%s' % (YOUTUBE_STANDARD_FEEDS, + 'most_responded') +YOUTUBE_SCHEMA = 'http://gdata.youtube.com/schemas' + +YOUTUBE_RATING_LINK_REL = '%s#video.ratings' % YOUTUBE_SCHEMA + +YOUTUBE_COMPLAINT_CATEGORY_SCHEME = '%s/%s' % (YOUTUBE_SCHEMA, + 'complaint-reasons.cat') +YOUTUBE_SUBSCRIPTION_CATEGORY_SCHEME = '%s/%s' % (YOUTUBE_SCHEMA, + 'subscriptiontypes.cat') + +YOUTUBE_COMPLAINT_CATEGORY_TERMS = ('PORN', 'VIOLENCE', 'HATE', 'DANGEROUS', + 'RIGHTS', 'SPAM') +YOUTUBE_CONTACT_STATUS = ('accepted', 'rejected') +YOUTUBE_CONTACT_CATEGORY = ('Friends', 'Family') + +UNKOWN_ERROR = 1000 +YOUTUBE_BAD_REQUEST = 400 +YOUTUBE_CONFLICT = 409 +YOUTUBE_INTERNAL_SERVER_ERROR = 500 +YOUTUBE_INVALID_ARGUMENT = 601 +YOUTUBE_INVALID_CONTENT_TYPE = 602 +YOUTUBE_NOT_A_VIDEO = 603 +YOUTUBE_INVALID_KIND = 604 + + +class Error(Exception): + """Base class for errors within the YouTube service.""" + pass + +class RequestError(Error): + """Error class that is thrown in response to an invalid HTTP Request.""" + pass + +class YouTubeError(Error): + """YouTube service specific error class.""" + pass + +class YouTubeService(gdata.service.GDataService): + + """Client for the YouTube service. + + Performs all documented Google Data YouTube API functions, such as inserting, + updating and deleting videos, comments, playlist, subscriptions etc. + YouTube Service requires authentication for any write, update or delete + actions. + + Attributes: + email: An optional string identifying the user. Required only for + authenticated actions. + password: An optional string identifying the user's password. + source: An optional string identifying the name of your application. + server: An optional address of the YouTube API server. gdata.youtube.com + is provided as the default value. + additional_headers: An optional dictionary containing additional headers + to be passed along with each request. Use to store developer key. + client_id: An optional string identifying your application, required for + authenticated requests, along with a developer key. + developer_key: An optional string value. Register your application at + http://code.google.com/apis/youtube/dashboard to obtain a (free) key. + """ + + def __init__(self, email=None, password=None, source=None, + server=YOUTUBE_SERVER, additional_headers=None, client_id=None, + developer_key=None, **kwargs): + """Creates a client for the YouTube service. + + Args: + email: string (optional) The user's email address, used for + authentication. + password: string (optional) The user's password. + source: string (optional) The name of the user's application. + server: string (optional) The name of the server to which a connection + will be opened. Default value: 'gdata.youtube.com'. + client_id: string (optional) Identifies your application, required for + authenticated requests, along with a developer key. + developer_key: string (optional) Register your application at + http://code.google.com/apis/youtube/dashboard to obtain a (free) key. + **kwargs: The other parameters to pass to gdata.service.GDataService + constructor. + """ + + gdata.service.GDataService.__init__( + self, email=email, password=password, service=YOUTUBE_SERVICE, + source=source, server=server, additional_headers=additional_headers, + **kwargs) + + if client_id is not None: + self.additional_headers['X-Gdata-Client'] = client_id + + if developer_key is not None: + self.additional_headers['X-GData-Key'] = 'key=%s' % developer_key + + self.auth_service_url = YOUTUBE_CLIENTLOGIN_AUTHENTICATION_URL + + def GetYouTubeVideoFeed(self, uri): + """Retrieve a YouTubeVideoFeed. + + Args: + uri: A string representing the URI of the feed that is to be retrieved. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.Get(uri, converter=gdata.youtube.YouTubeVideoFeedFromString) + + def GetYouTubeVideoEntry(self, uri=None, video_id=None): + """Retrieve a YouTubeVideoEntry. + + Either a uri or a video_id must be provided. + + Args: + uri: An optional string representing the URI of the entry that is to + be retrieved. + video_id: An optional string representing the ID of the video. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a video_id to the + GetYouTubeVideoEntry() method. + """ + if uri is None and video_id is None: + raise YouTubeError('You must provide at least a uri or a video_id ' + 'to the GetYouTubeVideoEntry() method') + elif video_id and not uri: + uri = '%s/%s' % (YOUTUBE_VIDEO_URI, video_id) + return self.Get(uri, converter=gdata.youtube.YouTubeVideoEntryFromString) + + def GetYouTubeContactFeed(self, uri=None, username='default'): + """Retrieve a YouTubeContactFeed. + + Either a uri or a username must be provided. + + Args: + uri: An optional string representing the URI of the contact feed that + is to be retrieved. + username: An optional string representing the username. Defaults to the + currently authenticated user. + + Returns: + A YouTubeContactFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a username to the + GetYouTubeContactFeed() method. + """ + if uri is None: + uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, username, 'contacts') + return self.Get(uri, converter=gdata.youtube.YouTubeContactFeedFromString) + + def GetYouTubeContactEntry(self, uri): + """Retrieve a YouTubeContactEntry. + + Args: + uri: A string representing the URI of the contact entry that is to + be retrieved. + + Returns: + A YouTubeContactEntry if successfully retrieved. + """ + return self.Get(uri, converter=gdata.youtube.YouTubeContactEntryFromString) + + def GetYouTubeVideoCommentFeed(self, uri=None, video_id=None): + """Retrieve a YouTubeVideoCommentFeed. + + Either a uri or a video_id must be provided. + + Args: + uri: An optional string representing the URI of the comment feed that + is to be retrieved. + video_id: An optional string representing the ID of the video for which + to retrieve the comment feed. + + Returns: + A YouTubeVideoCommentFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a video_id to the + GetYouTubeVideoCommentFeed() method. + """ + if uri is None and video_id is None: + raise YouTubeError('You must provide at least a uri or a video_id ' + 'to the GetYouTubeVideoCommentFeed() method') + elif video_id and not uri: + uri = '%s/%s/%s' % (YOUTUBE_VIDEO_URI, video_id, 'comments') + return self.Get( + uri, converter=gdata.youtube.YouTubeVideoCommentFeedFromString) + + def GetYouTubeVideoCommentEntry(self, uri): + """Retrieve a YouTubeVideoCommentEntry. + + Args: + uri: A string representing the URI of the comment entry that is to + be retrieved. + + Returns: + A YouTubeCommentEntry if successfully retrieved. + """ + return self.Get( + uri, converter=gdata.youtube.YouTubeVideoCommentEntryFromString) + + def GetYouTubeUserFeed(self, uri=None, username=None): + """Retrieve a YouTubeVideoFeed of user uploaded videos + + Either a uri or a username must be provided. This will retrieve list + of videos uploaded by specified user. The uri will be of format + "http://gdata.youtube.com/feeds/api/users/{username}/uploads". + + Args: + uri: An optional string representing the URI of the user feed that is + to be retrieved. + username: An optional string representing the username. + + Returns: + A YouTubeUserFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a username to the + GetYouTubeUserFeed() method. + """ + if uri is None and username is None: + raise YouTubeError('You must provide at least a uri or a username ' + 'to the GetYouTubeUserFeed() method') + elif username and not uri: + uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, username, 'uploads') + return self.Get(uri, converter=gdata.youtube.YouTubeUserFeedFromString) + + def GetYouTubeUserEntry(self, uri=None, username=None): + """Retrieve a YouTubeUserEntry. + + Either a uri or a username must be provided. + + Args: + uri: An optional string representing the URI of the user entry that is + to be retrieved. + username: An optional string representing the username. + + Returns: + A YouTubeUserEntry if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a username to the + GetYouTubeUserEntry() method. + """ + if uri is None and username is None: + raise YouTubeError('You must provide at least a uri or a username ' + 'to the GetYouTubeUserEntry() method') + elif username and not uri: + uri = '%s/%s' % (YOUTUBE_USER_FEED_URI, username) + return self.Get(uri, converter=gdata.youtube.YouTubeUserEntryFromString) + + def GetYouTubePlaylistFeed(self, uri=None, username='default'): + """Retrieve a YouTubePlaylistFeed (a feed of playlists for a user). + + Either a uri or a username must be provided. + + Args: + uri: An optional string representing the URI of the playlist feed that + is to be retrieved. + username: An optional string representing the username. Defaults to the + currently authenticated user. + + Returns: + A YouTubePlaylistFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a username to the + GetYouTubePlaylistFeed() method. + """ + if uri is None: + uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, username, 'playlists') + return self.Get(uri, converter=gdata.youtube.YouTubePlaylistFeedFromString) + + def GetYouTubePlaylistEntry(self, uri): + """Retrieve a YouTubePlaylistEntry. + + Args: + uri: A string representing the URI of the playlist feed that is to + be retrieved. + + Returns: + A YouTubePlaylistEntry if successfully retrieved. + """ + return self.Get(uri, converter=gdata.youtube.YouTubePlaylistEntryFromString) + + def GetYouTubePlaylistVideoFeed(self, uri=None, playlist_id=None): + """Retrieve a YouTubePlaylistVideoFeed (a feed of videos on a playlist). + + Either a uri or a playlist_id must be provided. + + Args: + uri: An optional string representing the URI of the playlist video feed + that is to be retrieved. + playlist_id: An optional string representing the Id of the playlist whose + playlist video feed is to be retrieved. + + Returns: + A YouTubePlaylistVideoFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a playlist_id to the + GetYouTubePlaylistVideoFeed() method. + """ + if uri is None and playlist_id is None: + raise YouTubeError('You must provide at least a uri or a playlist_id ' + 'to the GetYouTubePlaylistVideoFeed() method') + elif playlist_id and not uri: + uri = '%s/%s' % (YOUTUBE_PLAYLIST_FEED_URI, playlist_id) + return self.Get( + uri, converter=gdata.youtube.YouTubePlaylistVideoFeedFromString) + + def GetYouTubeVideoResponseFeed(self, uri=None, video_id=None): + """Retrieve a YouTubeVideoResponseFeed. + + Either a uri or a playlist_id must be provided. + + Args: + uri: An optional string representing the URI of the video response feed + that is to be retrieved. + video_id: An optional string representing the ID of the video whose + response feed is to be retrieved. + + Returns: + A YouTubeVideoResponseFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a video_id to the + GetYouTubeVideoResponseFeed() method. + """ + if uri is None and video_id is None: + raise YouTubeError('You must provide at least a uri or a video_id ' + 'to the GetYouTubeVideoResponseFeed() method') + elif video_id and not uri: + uri = '%s/%s/%s' % (YOUTUBE_VIDEO_URI, video_id, 'responses') + return self.Get( + uri, converter=gdata.youtube.YouTubeVideoResponseFeedFromString) + + def GetYouTubeVideoResponseEntry(self, uri): + """Retrieve a YouTubeVideoResponseEntry. + + Args: + uri: A string representing the URI of the video response entry that + is to be retrieved. + + Returns: + A YouTubeVideoResponseEntry if successfully retrieved. + """ + return self.Get( + uri, converter=gdata.youtube.YouTubeVideoResponseEntryFromString) + + def GetYouTubeSubscriptionFeed(self, uri=None, username='default'): + """Retrieve a YouTubeSubscriptionFeed. + + Either the uri of the feed or a username must be provided. + + Args: + uri: An optional string representing the URI of the feed that is to + be retrieved. + username: An optional string representing the username whose subscription + feed is to be retrieved. Defaults to the currently authenticted user. + + Returns: + A YouTubeVideoSubscriptionFeed if successfully retrieved. + """ + if uri is None: + uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, username, 'subscriptions') + return self.Get( + uri, converter=gdata.youtube.YouTubeSubscriptionFeedFromString) + + def GetYouTubeSubscriptionEntry(self, uri): + """Retrieve a YouTubeSubscriptionEntry. + + Args: + uri: A string representing the URI of the entry that is to be retrieved. + + Returns: + A YouTubeVideoSubscriptionEntry if successfully retrieved. + """ + return self.Get( + uri, converter=gdata.youtube.YouTubeSubscriptionEntryFromString) + + def GetYouTubeRelatedVideoFeed(self, uri=None, video_id=None): + """Retrieve a YouTubeRelatedVideoFeed. + + Either a uri for the feed or a video_id is required. + + Args: + uri: An optional string representing the URI of the feed that is to + be retrieved. + video_id: An optional string representing the ID of the video for which + to retrieve the related video feed. + + Returns: + A YouTubeRelatedVideoFeed if successfully retrieved. + + Raises: + YouTubeError: You must provide at least a uri or a video_id to the + GetYouTubeRelatedVideoFeed() method. + """ + if uri is None and video_id is None: + raise YouTubeError('You must provide at least a uri or a video_id ' + 'to the GetYouTubeRelatedVideoFeed() method') + elif video_id and not uri: + uri = '%s/%s/%s' % (YOUTUBE_VIDEO_URI, video_id, 'related') + return self.Get( + uri, converter=gdata.youtube.YouTubeVideoFeedFromString) + + def GetTopRatedVideoFeed(self): + """Retrieve the 'top_rated' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_TOP_RATED_URI) + + def GetMostViewedVideoFeed(self): + """Retrieve the 'most_viewed' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_VIEWED_URI) + + def GetRecentlyFeaturedVideoFeed(self): + """Retrieve the 'recently_featured' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_RECENTLY_FEATURED_URI) + + def GetWatchOnMobileVideoFeed(self): + """Retrieve the 'watch_on_mobile' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_WATCH_ON_MOBILE_URI) + + def GetTopFavoritesVideoFeed(self): + """Retrieve the 'top_favorites' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_TOP_FAVORITES_URI) + + def GetMostRecentVideoFeed(self): + """Retrieve the 'most_recent' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_RECENT_URI) + + def GetMostDiscussedVideoFeed(self): + """Retrieve the 'most_discussed' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_DISCUSSED_URI) + + def GetMostLinkedVideoFeed(self): + """Retrieve the 'most_linked' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_LINKED_URI) + + def GetMostRespondedVideoFeed(self): + """Retrieve the 'most_responded' standard video feed. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_RESPONDED_URI) + + def GetUserFavoritesFeed(self, username='default'): + """Retrieve the favorites feed for a given user. + + Args: + username: An optional string representing the username whose favorites + feed is to be retrieved. Defaults to the currently authenticated user. + + Returns: + A YouTubeVideoFeed if successfully retrieved. + """ + favorites_feed_uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, username, + 'favorites') + return self.GetYouTubeVideoFeed(favorites_feed_uri) + + def InsertVideoEntry(self, video_entry, filename_or_handle, + youtube_username='default', + content_type='video/quicktime'): + """Upload a new video to YouTube using the direct upload mechanism. + + Needs authentication. + + Args: + video_entry: The YouTubeVideoEntry to upload. + filename_or_handle: A file-like object or file name where the video + will be read from. + youtube_username: An optional string representing the username into whose + account this video is to be uploaded to. Defaults to the currently + authenticated user. + content_type: An optional string representing internet media type + (a.k.a. mime type) of the media object. Currently the YouTube API + supports these types: + o video/mpeg + o video/quicktime + o video/x-msvideo + o video/mp4 + o video/x-flv + + Returns: + The newly created YouTubeVideoEntry if successful. + + Raises: + AssertionError: video_entry must be a gdata.youtube.VideoEntry instance. + YouTubeError: An error occurred trying to read the video file provided. + gdata.service.RequestError: An error occurred trying to upload the video + to the API server. + """ + + # We need to perform a series of checks on the video_entry and on the + # file that we plan to upload, such as checking whether we have a valid + # video_entry and that the file is the correct type and readable, prior + # to performing the actual POST request. + + try: + assert(isinstance(video_entry, gdata.youtube.YouTubeVideoEntry)) + except AssertionError: + raise YouTubeError({'status':YOUTUBE_INVALID_ARGUMENT, + 'body':'`video_entry` must be a gdata.youtube.VideoEntry instance', + 'reason':'Found %s, not VideoEntry' % type(video_entry) + }) + #majtype, mintype = content_type.split('/') + # + #try: + # assert(mintype in YOUTUBE_SUPPORTED_UPLOAD_TYPES) + #except (ValueError, AssertionError): + # raise YouTubeError({'status':YOUTUBE_INVALID_CONTENT_TYPE, + # 'body':'This is not a valid content type: %s' % content_type, + # 'reason':'Accepted content types: %s' % + # ['video/%s' % (t) for t in YOUTUBE_SUPPORTED_UPLOAD_TYPES]}) + + if (isinstance(filename_or_handle, (str, unicode)) + and os.path.exists(filename_or_handle)): + mediasource = gdata.MediaSource() + mediasource.setFile(filename_or_handle, content_type) + elif hasattr(filename_or_handle, 'read'): + import StringIO + if hasattr(filename_or_handle, 'seek'): + filename_or_handle.seek(0) + file_handle = filename_or_handle + name = 'video' + if hasattr(filename_or_handle, 'name'): + name = filename_or_handle.name + mediasource = gdata.MediaSource(file_handle, content_type, + content_length=file_handle.len, file_name=name) + else: + raise YouTubeError({'status':YOUTUBE_INVALID_ARGUMENT, 'body': + '`filename_or_handle` must be a path name or a file-like object', + 'reason': ('Found %s, not path name or object ' + 'with a .read() method' % type(filename_or_handle))}) + upload_uri = '%s/%s/%s' % (YOUTUBE_UPLOAD_URI, youtube_username, + 'uploads') + self.additional_headers['Slug'] = mediasource.file_name + + # Using a nested try statement to retain Python 2.4 compatibility + try: + try: + return self.Post(video_entry, uri=upload_uri, media_source=mediasource, + converter=gdata.youtube.YouTubeVideoEntryFromString) + except gdata.service.RequestError, e: + raise YouTubeError(e.args[0]) + finally: + del(self.additional_headers['Slug']) + + def CheckUploadStatus(self, video_entry=None, video_id=None): + """Check upload status on a recently uploaded video entry. + + Needs authentication. Either video_entry or video_id must be provided. + + Args: + video_entry: An optional YouTubeVideoEntry whose upload status to check + video_id: An optional string representing the ID of the uploaded video + whose status is to be checked. + + Returns: + A tuple containing (video_upload_state, detailed_message) or None if + no status information is found. + + Raises: + YouTubeError: You must provide at least a video_entry or a video_id to the + CheckUploadStatus() method. + """ + if video_entry is None and video_id is None: + raise YouTubeError('You must provide at least a uri or a video_id ' + 'to the CheckUploadStatus() method') + elif video_id and not video_entry: + video_entry = self.GetYouTubeVideoEntry(video_id=video_id) + + control = video_entry.control + if control is not None: + draft = control.draft + if draft is not None: + if draft.text == 'yes': + yt_state = control.extension_elements[0] + if yt_state is not None: + state_value = yt_state.attributes['name'] + message = '' + if yt_state.text is not None: + message = yt_state.text + + return (state_value, message) + + def GetFormUploadToken(self, video_entry, uri=YOUTUBE_UPLOAD_TOKEN_URI): + """Receives a YouTube Token and a YouTube PostUrl from a YouTubeVideoEntry. + + Needs authentication. + + Args: + video_entry: The YouTubeVideoEntry to upload (meta-data only). + uri: An optional string representing the URI from where to fetch the + token information. Defaults to the YOUTUBE_UPLOADTOKEN_URI. + + Returns: + A tuple containing the URL to which to post your video file, along + with the youtube token that must be included with your upload in the + form of: (post_url, youtube_token). + """ + try: + response = self.Post(video_entry, uri) + except gdata.service.RequestError, e: + raise YouTubeError(e.args[0]) + + tree = ElementTree.fromstring(response) + + for child in tree: + if child.tag == 'url': + post_url = child.text + elif child.tag == 'token': + youtube_token = child.text + return (post_url, youtube_token) + + def UpdateVideoEntry(self, video_entry): + """Updates a video entry's meta-data. + + Needs authentication. + + Args: + video_entry: The YouTubeVideoEntry to update, containing updated + meta-data. + + Returns: + An updated YouTubeVideoEntry on success or None. + """ + for link in video_entry.link: + if link.rel == 'edit': + edit_uri = link.href + return self.Put(video_entry, uri=edit_uri, + converter=gdata.youtube.YouTubeVideoEntryFromString) + + def DeleteVideoEntry(self, video_entry): + """Deletes a video entry. + + Needs authentication. + + Args: + video_entry: The YouTubeVideoEntry to be deleted. + + Returns: + True if entry was deleted successfully. + """ + for link in video_entry.link: + if link.rel == 'edit': + edit_uri = link.href + return self.Delete(edit_uri) + + def AddRating(self, rating_value, video_entry): + """Add a rating to a video entry. + + Needs authentication. + + Args: + rating_value: The integer value for the rating (between 1 and 5). + video_entry: The YouTubeVideoEntry to be rated. + + Returns: + True if the rating was added successfully. + + Raises: + YouTubeError: rating_value must be between 1 and 5 in AddRating(). + """ + if rating_value < 1 or rating_value > 5: + raise YouTubeError('rating_value must be between 1 and 5 in AddRating()') + + entry = gdata.GDataEntry() + rating = gdata.youtube.Rating(min='1', max='5') + rating.extension_attributes['name'] = 'value' + rating.extension_attributes['value'] = str(rating_value) + entry.extension_elements.append(rating) + + for link in video_entry.link: + if link.rel == YOUTUBE_RATING_LINK_REL: + rating_uri = link.href + + return self.Post(entry, uri=rating_uri) + + def AddComment(self, comment_text, video_entry): + """Add a comment to a video entry. + + Needs authentication. Note that each comment that is posted must contain + the video entry that it is to be posted to. + + Args: + comment_text: A string representing the text of the comment. + video_entry: The YouTubeVideoEntry to be commented on. + + Returns: + True if the comment was added successfully. + """ + content = atom.Content(text=comment_text) + comment_entry = gdata.youtube.YouTubeVideoCommentEntry(content=content) + comment_post_uri = video_entry.comments.feed_link[0].href + + return self.Post(comment_entry, uri=comment_post_uri) + + def AddVideoResponse(self, video_id_to_respond_to, video_response): + """Add a video response. + + Needs authentication. + + Args: + video_id_to_respond_to: A string representing the ID of the video to be + responded to. + video_response: YouTubeVideoEntry to be posted as a response. + + Returns: + True if video response was posted successfully. + """ + post_uri = '%s/%s/%s' % (YOUTUBE_VIDEO_URI, video_id_to_respond_to, + 'responses') + return self.Post(video_response, uri=post_uri) + + def DeleteVideoResponse(self, video_id, response_video_id): + """Delete a video response. + + Needs authentication. + + Args: + video_id: A string representing the ID of video that contains the + response. + response_video_id: A string representing the ID of the video that was + posted as a response. + + Returns: + True if video response was deleted succcessfully. + """ + delete_uri = '%s/%s/%s/%s' % (YOUTUBE_VIDEO_URI, video_id, 'responses', + response_video_id) + return self.Delete(delete_uri) + + def AddComplaint(self, complaint_text, complaint_term, video_id): + """Add a complaint for a particular video entry. + + Needs authentication. + + Args: + complaint_text: A string representing the complaint text. + complaint_term: A string representing the complaint category term. + video_id: A string representing the ID of YouTubeVideoEntry to + complain about. + + Returns: + True if posted successfully. + + Raises: + YouTubeError: Your complaint_term is not valid. + """ + if complaint_term not in YOUTUBE_COMPLAINT_CATEGORY_TERMS: + raise YouTubeError('Your complaint_term is not valid') + + content = atom.Content(text=complaint_text) + category = atom.Category(term=complaint_term, + scheme=YOUTUBE_COMPLAINT_CATEGORY_SCHEME) + + complaint_entry = gdata.GDataEntry(content=content, category=[category]) + post_uri = '%s/%s/%s' % (YOUTUBE_VIDEO_URI, video_id, 'complaints') + + return self.Post(complaint_entry, post_uri) + + def AddVideoEntryToFavorites(self, video_entry, username='default'): + """Add a video entry to a users favorite feed. + + Needs authentication. + + Args: + video_entry: The YouTubeVideoEntry to add. + username: An optional string representing the username to whose favorite + feed you wish to add the entry. Defaults to the currently + authenticated user. + Returns: + The posted YouTubeVideoEntry if successfully posted. + """ + post_uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, username, 'favorites') + + return self.Post(video_entry, post_uri, + converter=gdata.youtube.YouTubeVideoEntryFromString) + + def DeleteVideoEntryFromFavorites(self, video_id, username='default'): + """Delete a video entry from the users favorite feed. + + Needs authentication. + + Args: + video_id: A string representing the ID of the video that is to be removed + username: An optional string representing the username of the user's + favorite feed. Defaults to the currently authenticated user. + + Returns: + True if entry was successfully deleted. + """ + edit_link = '%s/%s/%s/%s' % (YOUTUBE_USER_FEED_URI, username, 'favorites', + video_id) + return self.Delete(edit_link) + + def AddPlaylist(self, playlist_title, playlist_description, + playlist_private=None): + """Add a new playlist to the currently authenticated users account. + + Needs authentication. + + Args: + playlist_title: A string representing the title for the new playlist. + playlist_description: A string representing the description of the + playlist. + playlist_private: An optional boolean, set to True if the playlist is + to be private. + + Returns: + The YouTubePlaylistEntry if successfully posted. + """ + playlist_entry = gdata.youtube.YouTubePlaylistEntry( + title=atom.Title(text=playlist_title), + description=gdata.youtube.Description(text=playlist_description)) + if playlist_private: + playlist_entry.private = gdata.youtube.Private() + + playlist_post_uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, 'default', + 'playlists') + return self.Post(playlist_entry, playlist_post_uri, + converter=gdata.youtube.YouTubePlaylistEntryFromString) + + def UpdatePlaylist(self, playlist_id, new_playlist_title, + new_playlist_description, playlist_private=None, + username='default'): + """Update a playlist with new meta-data. + + Needs authentication. + + Args: + playlist_id: A string representing the ID of the playlist to be updated. + new_playlist_title: A string representing a new title for the playlist. + new_playlist_description: A string representing a new description for the + playlist. + playlist_private: An optional boolean, set to True if the playlist is + to be private. + username: An optional string representing the username whose playlist is + to be updated. Defaults to the currently authenticated user. + + Returns: + A YouTubePlaylistEntry if the update was successful. + """ + updated_playlist = gdata.youtube.YouTubePlaylistEntry( + title=atom.Title(text=new_playlist_title), + description=gdata.youtube.Description(text=new_playlist_description)) + if playlist_private: + updated_playlist.private = gdata.youtube.Private() + + playlist_put_uri = '%s/%s/playlists/%s' % (YOUTUBE_USER_FEED_URI, username, + playlist_id) + + return self.Put(updated_playlist, playlist_put_uri, + converter=gdata.youtube.YouTubePlaylistEntryFromString) + + def DeletePlaylist(self, playlist_uri): + """Delete a playlist from the currently authenticated users playlists. + + Needs authentication. + + Args: + playlist_uri: A string representing the URI of the playlist that is + to be deleted. + + Returns: + True if successfully deleted. + """ + return self.Delete(playlist_uri) + + def AddPlaylistVideoEntryToPlaylist( + self, playlist_uri, video_id, custom_video_title=None, + custom_video_description=None): + """Add a video entry to a playlist, optionally providing a custom title + and description. + + Needs authentication. + + Args: + playlist_uri: A string representing the URI of the playlist to which this + video entry is to be added. + video_id: A string representing the ID of the video entry to add. + custom_video_title: An optional string representing a custom title for + the video (only shown on the playlist). + custom_video_description: An optional string representing a custom + description for the video (only shown on the playlist). + + Returns: + A YouTubePlaylistVideoEntry if successfully posted. + """ + playlist_video_entry = gdata.youtube.YouTubePlaylistVideoEntry( + atom_id=atom.Id(text=video_id)) + if custom_video_title: + playlist_video_entry.title = atom.Title(text=custom_video_title) + if custom_video_description: + playlist_video_entry.description = gdata.youtube.Description( + text=custom_video_description) + + return self.Post(playlist_video_entry, playlist_uri, + converter=gdata.youtube.YouTubePlaylistVideoEntryFromString) + + def UpdatePlaylistVideoEntryMetaData( + self, playlist_uri, playlist_entry_id, new_video_title, + new_video_description, new_video_position): + """Update the meta data for a YouTubePlaylistVideoEntry. + + Needs authentication. + + Args: + playlist_uri: A string representing the URI of the playlist that contains + the entry to be updated. + playlist_entry_id: A string representing the ID of the entry to be + updated. + new_video_title: A string representing the new title for the video entry. + new_video_description: A string representing the new description for + the video entry. + new_video_position: An integer representing the new position on the + playlist for the video. + + Returns: + A YouTubePlaylistVideoEntry if the update was successful. + """ + playlist_video_entry = gdata.youtube.YouTubePlaylistVideoEntry( + title=atom.Title(text=new_video_title), + description=gdata.youtube.Description(text=new_video_description), + position=gdata.youtube.Position(text=str(new_video_position))) + + playlist_put_uri = playlist_uri + '/' + playlist_entry_id + + return self.Put(playlist_video_entry, playlist_put_uri, + converter=gdata.youtube.YouTubePlaylistVideoEntryFromString) + + def DeletePlaylistVideoEntry(self, playlist_uri, playlist_video_entry_id): + """Delete a playlist video entry from a playlist. + + Needs authentication. + + Args: + playlist_uri: A URI representing the playlist from which the playlist + video entry is to be removed from. + playlist_video_entry_id: A string representing id of the playlist video + entry that is to be removed. + + Returns: + True if entry was successfully deleted. + """ + delete_uri = '%s/%s' % (playlist_uri, playlist_video_entry_id) + return self.Delete(delete_uri) + + def AddSubscriptionToChannel(self, username_to_subscribe_to, + my_username = 'default'): + """Add a new channel subscription to the currently authenticated users + account. + + Needs authentication. + + Args: + username_to_subscribe_to: A string representing the username of the + channel to which we want to subscribe to. + my_username: An optional string representing the name of the user which + we want to subscribe. Defaults to currently authenticated user. + + Returns: + A new YouTubeSubscriptionEntry if successfully posted. + """ + subscription_category = atom.Category( + scheme=YOUTUBE_SUBSCRIPTION_CATEGORY_SCHEME, + term='channel') + subscription_username = gdata.youtube.Username( + text=username_to_subscribe_to) + + subscription_entry = gdata.youtube.YouTubeSubscriptionEntry( + category=subscription_category, + username=subscription_username) + + post_uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, my_username, + 'subscriptions') + + return self.Post(subscription_entry, post_uri, + converter=gdata.youtube.YouTubeSubscriptionEntryFromString) + + def AddSubscriptionToFavorites(self, username, my_username = 'default'): + """Add a new subscription to a users favorites to the currently + authenticated user's account. + + Needs authentication + + Args: + username: A string representing the username of the user's favorite feed + to subscribe to. + my_username: An optional string representing the username of the user + that is to be subscribed. Defaults to currently authenticated user. + + Returns: + A new YouTubeSubscriptionEntry if successful. + """ + subscription_category = atom.Category( + scheme=YOUTUBE_SUBSCRIPTION_CATEGORY_SCHEME, + term='favorites') + subscription_username = gdata.youtube.Username(text=username) + + subscription_entry = gdata.youtube.YouTubeSubscriptionEntry( + category=subscription_category, + username=subscription_username) + + post_uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, my_username, + 'subscriptions') + + return self.Post(subscription_entry, post_uri, + converter=gdata.youtube.YouTubeSubscriptionEntryFromString) + + def AddSubscriptionToQuery(self, query, my_username = 'default'): + """Add a new subscription to a specific keyword query to the currently + authenticated user's account. + + Needs authentication + + Args: + query: A string representing the keyword query to subscribe to. + my_username: An optional string representing the username of the user + that is to be subscribed. Defaults to currently authenticated user. + + Returns: + A new YouTubeSubscriptionEntry if successful. + """ + subscription_category = atom.Category( + scheme=YOUTUBE_SUBSCRIPTION_CATEGORY_SCHEME, + term='query') + subscription_query_string = gdata.youtube.QueryString(text=query) + + subscription_entry = gdata.youtube.YouTubeSubscriptionEntry( + category=subscription_category, + query_string=subscription_query_string) + + post_uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, my_username, + 'subscriptions') + + return self.Post(subscription_entry, post_uri, + converter=gdata.youtube.YouTubeSubscriptionEntryFromString) + + + + def DeleteSubscription(self, subscription_uri): + """Delete a subscription from the currently authenticated user's account. + + Needs authentication. + + Args: + subscription_uri: A string representing the URI of the subscription that + is to be deleted. + + Returns: + True if deleted successfully. + """ + return self.Delete(subscription_uri) + + def AddContact(self, contact_username, my_username='default'): + """Add a new contact to the currently authenticated user's contact feed. + + Needs authentication. + + Args: + contact_username: A string representing the username of the contact + that you wish to add. + my_username: An optional string representing the username to whose + contact the new contact is to be added. + + Returns: + A YouTubeContactEntry if added successfully. + """ + contact_category = atom.Category( + scheme = 'http://gdata.youtube.com/schemas/2007/contact.cat', + term = 'Friends') + contact_username = gdata.youtube.Username(text=contact_username) + contact_entry = gdata.youtube.YouTubeContactEntry( + category=contact_category, + username=contact_username) + + contact_post_uri = '%s/%s/%s' % (YOUTUBE_USER_FEED_URI, my_username, + 'contacts') + + return self.Post(contact_entry, contact_post_uri, + converter=gdata.youtube.YouTubeContactEntryFromString) + + def UpdateContact(self, contact_username, new_contact_status, + new_contact_category, my_username='default'): + """Update a contact, providing a new status and a new category. + + Needs authentication. + + Args: + contact_username: A string representing the username of the contact + that is to be updated. + new_contact_status: A string representing the new status of the contact. + This can either be set to 'accepted' or 'rejected'. + new_contact_category: A string representing the new category for the + contact, either 'Friends' or 'Family'. + my_username: An optional string representing the username of the user + whose contact feed we are modifying. Defaults to the currently + authenticated user. + + Returns: + A YouTubeContactEntry if updated succesfully. + + Raises: + YouTubeError: New contact status must be within the accepted values. Or + new contact category must be within the accepted categories. + """ + if new_contact_status not in YOUTUBE_CONTACT_STATUS: + raise YouTubeError('New contact status must be one of %s' % + (' '.join(YOUTUBE_CONTACT_STATUS))) + if new_contact_category not in YOUTUBE_CONTACT_CATEGORY: + raise YouTubeError('New contact category must be one of %s' % + (' '.join(YOUTUBE_CONTACT_CATEGORY))) + + contact_category = atom.Category( + scheme='http://gdata.youtube.com/schemas/2007/contact.cat', + term=new_contact_category) + + contact_status = gdata.youtube.Status(text=new_contact_status) + contact_entry = gdata.youtube.YouTubeContactEntry( + category=contact_category, + status=contact_status) + + contact_put_uri = '%s/%s/%s/%s' % (YOUTUBE_USER_FEED_URI, my_username, + 'contacts', contact_username) + + return self.Put(contact_entry, contact_put_uri, + converter=gdata.youtube.YouTubeContactEntryFromString) + + def DeleteContact(self, contact_username, my_username='default'): + """Delete a contact from a users contact feed. + + Needs authentication. + + Args: + contact_username: A string representing the username of the contact + that is to be deleted. + my_username: An optional string representing the username of the user's + contact feed from which to delete the contact. Defaults to the + currently authenticated user. + + Returns: + True if the contact was deleted successfully + """ + contact_edit_uri = '%s/%s/%s/%s' % (YOUTUBE_USER_FEED_URI, my_username, + 'contacts', contact_username) + return self.Delete(contact_edit_uri) + + def _GetDeveloperKey(self): + """Getter for Developer Key property. + + Returns: + If the developer key has been set, a string representing the developer key + is returned or None. + """ + if 'X-GData-Key' in self.additional_headers: + return self.additional_headers['X-GData-Key'][4:] + else: + return None + + def _SetDeveloperKey(self, developer_key): + """Setter for Developer Key property. + + Sets the developer key in the 'X-GData-Key' header. The actual value that + is set is 'key=' plus the developer_key that was passed. + """ + self.additional_headers['X-GData-Key'] = 'key=' + developer_key + + developer_key = property(_GetDeveloperKey, _SetDeveloperKey, + doc="""The Developer Key property""") + + def _GetClientId(self): + """Getter for Client Id property. + + Returns: + If the client_id has been set, a string representing it is returned + or None. + """ + if 'X-Gdata-Client' in self.additional_headers: + return self.additional_headers['X-Gdata-Client'] + else: + return None + + def _SetClientId(self, client_id): + """Setter for Client Id property. + + Sets the 'X-Gdata-Client' header. + """ + self.additional_headers['X-Gdata-Client'] = client_id + + client_id = property(_GetClientId, _SetClientId, + doc="""The ClientId property""") + + def Query(self, uri): + """Performs a query and returns a resulting feed or entry. + + Args: + uri: A string representing the URI of the feed that is to be queried. + + Returns: + On success, a tuple in the form: + (boolean succeeded=True, ElementTree._Element result) + On failure, a tuple in the form: + (boolean succeeded=False, {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server's response}) + """ + result = self.Get(uri) + return result + + def YouTubeQuery(self, query): + """Performs a YouTube specific query and returns a resulting feed or entry. + + Args: + query: A Query object or one if its sub-classes (YouTubeVideoQuery, + YouTubeUserQuery or YouTubePlaylistQuery). + + Returns: + Depending on the type of Query object submitted returns either a + YouTubeVideoFeed, a YouTubeUserFeed, a YouTubePlaylistFeed. If the + Query object provided was not YouTube-related, a tuple is returned. + On success the tuple will be in this form: + (boolean succeeded=True, ElementTree._Element result) + On failure, the tuple will be in this form: + (boolean succeeded=False, {'status': HTTP status code from server, + 'reason': HTTP reason from the server, + 'body': HTTP body of the server response}) + """ + result = self.Query(query.ToUri()) + if isinstance(query, YouTubeUserQuery): + return gdata.youtube.YouTubeUserFeedFromString(result.ToString()) + elif isinstance(query, YouTubePlaylistQuery): + return gdata.youtube.YouTubePlaylistFeedFromString(result.ToString()) + elif isinstance(query, YouTubeVideoQuery): + return gdata.youtube.YouTubeVideoFeedFromString(result.ToString()) + else: + return result + +class YouTubeVideoQuery(gdata.service.Query): + + """Subclasses gdata.service.Query to represent a YouTube Data API query. + + Attributes are set dynamically via properties. Properties correspond to + the standard Google Data API query parameters with YouTube Data API + extensions. Please refer to the API documentation for details. + + Attributes: + vq: The vq parameter, which is only supported for video feeds, specifies a + search query term. Refer to API documentation for further details. + orderby: The orderby parameter, which is only supported for video feeds, + specifies the value that will be used to sort videos in the search + result set. Valid values for this parameter are relevance, published, + viewCount and rating. + time: The time parameter, which is only available for the top_rated, + top_favorites, most_viewed, most_discussed, most_linked and + most_responded standard feeds, restricts the search to videos uploaded + within the specified time. Valid values for this parameter are today + (1 day), this_week (7 days), this_month (1 month) and all_time. + The default value for this parameter is all_time. + format: The format parameter specifies that videos must be available in a + particular video format. Refer to the API documentation for details. + racy: The racy parameter allows a search result set to include restricted + content as well as standard content. Valid values for this parameter + are include and exclude. By default, restricted content is excluded. + lr: The lr parameter restricts the search to videos that have a title, + description or keywords in a specific language. Valid values for the lr + parameter are ISO 639-1 two-letter language codes. + restriction: The restriction parameter identifies the IP address that + should be used to filter videos that can only be played in specific + countries. + location: A string of geo coordinates. Note that this is not used when the + search is performed but rather to filter the returned videos for ones + that match to the location entered. + feed: str (optional) The base URL which is the beginning of the query URL. + defaults to 'http://%s/feeds/videos' % (YOUTUBE_SERVER) + """ + + def __init__(self, video_id=None, feed_type=None, text_query=None, + params=None, categories=None, feed=None): + + if feed_type in YOUTUBE_STANDARDFEEDS and feed is None: + feed = 'http://%s/feeds/standardfeeds/%s' % (YOUTUBE_SERVER, feed_type) + elif (feed_type is 'responses' or feed_type is 'comments' and video_id + and feed is None): + feed = 'http://%s/feeds/videos/%s/%s' % (YOUTUBE_SERVER, video_id, + feed_type) + elif feed is None: + feed = 'http://%s/feeds/videos' % (YOUTUBE_SERVER) + + gdata.service.Query.__init__(self, feed, text_query=text_query, + params=params, categories=categories) + + def _GetVideoQuery(self): + if 'vq' in self: + return self['vq'] + else: + return None + + def _SetVideoQuery(self, val): + self['vq'] = val + + vq = property(_GetVideoQuery, _SetVideoQuery, + doc="""The video query (vq) query parameter""") + + def _GetOrderBy(self): + if 'orderby' in self: + return self['orderby'] + else: + return None + + def _SetOrderBy(self, val): + if val not in YOUTUBE_QUERY_VALID_ORDERBY_PARAMETERS: + if val.startswith('relevance_lang_') is False: + raise YouTubeError('OrderBy must be one of: %s ' % + ' '.join(YOUTUBE_QUERY_VALID_ORDERBY_PARAMETERS)) + self['orderby'] = val + + orderby = property(_GetOrderBy, _SetOrderBy, + doc="""The orderby query parameter""") + + def _GetTime(self): + if 'time' in self: + return self['time'] + else: + return None + + def _SetTime(self, val): + if val not in YOUTUBE_QUERY_VALID_TIME_PARAMETERS: + raise YouTubeError('Time must be one of: %s ' % + ' '.join(YOUTUBE_QUERY_VALID_TIME_PARAMETERS)) + self['time'] = val + + time = property(_GetTime, _SetTime, + doc="""The time query parameter""") + + def _GetFormat(self): + if 'format' in self: + return self['format'] + else: + return None + + def _SetFormat(self, val): + if val not in YOUTUBE_QUERY_VALID_FORMAT_PARAMETERS: + raise YouTubeError('Format must be one of: %s ' % + ' '.join(YOUTUBE_QUERY_VALID_FORMAT_PARAMETERS)) + self['format'] = val + + format = property(_GetFormat, _SetFormat, + doc="""The format query parameter""") + + def _GetRacy(self): + if 'racy' in self: + return self['racy'] + else: + return None + + def _SetRacy(self, val): + if val not in YOUTUBE_QUERY_VALID_RACY_PARAMETERS: + raise YouTubeError('Racy must be one of: %s ' % + ' '.join(YOUTUBE_QUERY_VALID_RACY_PARAMETERS)) + self['racy'] = val + + racy = property(_GetRacy, _SetRacy, + doc="""The racy query parameter""") + + def _GetLanguageRestriction(self): + if 'lr' in self: + return self['lr'] + else: + return None + + def _SetLanguageRestriction(self, val): + self['lr'] = val + + lr = property(_GetLanguageRestriction, _SetLanguageRestriction, + doc="""The lr (language restriction) query parameter""") + + def _GetIPRestriction(self): + if 'restriction' in self: + return self['restriction'] + else: + return None + + def _SetIPRestriction(self, val): + self['restriction'] = val + + restriction = property(_GetIPRestriction, _SetIPRestriction, + doc="""The restriction query parameter""") + + def _GetLocation(self): + if 'location' in self: + return self['location'] + else: + return None + + def _SetLocation(self, val): + self['location'] = val + + location = property(_GetLocation, _SetLocation, + doc="""The location query parameter""") + + + +class YouTubeUserQuery(YouTubeVideoQuery): + + """Subclasses YouTubeVideoQuery to perform user-specific queries. + + Attributes are set dynamically via properties. Properties correspond to + the standard Google Data API query parameters with YouTube Data API + extensions. + """ + + def __init__(self, username=None, feed_type=None, subscription_id=None, + text_query=None, params=None, categories=None): + + uploads_favorites_playlists = ('uploads', 'favorites', 'playlists') + + if feed_type is 'subscriptions' and subscription_id and username: + feed = "http://%s/feeds/users/%s/%s/%s" % (YOUTUBE_SERVER, username, + feed_type, subscription_id) + elif feed_type is 'subscriptions' and not subscription_id and username: + feed = "http://%s/feeds/users/%s/%s" % (YOUTUBE_SERVER, username, + feed_type) + elif feed_type in uploads_favorites_playlists: + feed = "http://%s/feeds/users/%s/%s" % (YOUTUBE_SERVER, username, + feed_type) + else: + feed = "http://%s/feeds/users" % (YOUTUBE_SERVER) + + YouTubeVideoQuery.__init__(self, feed=feed, text_query=text_query, + params=params, categories=categories) + + +class YouTubePlaylistQuery(YouTubeVideoQuery): + + """Subclasses YouTubeVideoQuery to perform playlist-specific queries. + + Attributes are set dynamically via properties. Properties correspond to + the standard Google Data API query parameters with YouTube Data API + extensions. + """ + + def __init__(self, playlist_id, text_query=None, params=None, + categories=None): + if playlist_id: + feed = "http://%s/feeds/playlists/%s" % (YOUTUBE_SERVER, playlist_id) + else: + feed = "http://%s/feeds/playlists" % (YOUTUBE_SERVER) + + YouTubeVideoQuery.__init__(self, feed=feed, text_query=text_query, + params=params, categories=categories) diff --git a/patches/projecthosting_patches.py b/patches/projecthosting_patches.py new file mode 100755 index 0000000..9e3c356 --- /dev/null +++ b/patches/projecthosting_patches.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +import sys +import re +import os.path + +# API docs: +# http://code.google.com/p/support/wiki/IssueTrackerAPIPython +import gdata.projecthosting.client +import gdata.projecthosting.data +import gdata.gauth +import gdata.client +import gdata.data +import atom.http_core +import atom.core + +class PatchBot(): + client = gdata.projecthosting.client.ProjectHostingClient() + + # you can use mewes for complete junk testing + #PROJECT_NAME = "mewes" + PROJECT_NAME = "lilypond" + + username = None + password = None + + def __init__(self): + # both of these bail if they fail + self.get_credentials() + #self.login() + + def get_credentials(self): + # TODO: can we use the coderview cookie for this? + #filename = os.path.expanduser("~/.codereview_upload_cookies") + filename = os.path.expanduser("~/.lilypond-project-hosting-login") + try: + login_data = open(filename).readlines() + self.username = login_data[0] + self.password = login_data[1] + except: + print "Could not find stored credentials" + print " %(filename)s" % locals() + print "Please enter loging details manually" + print + import getpass + print "Username (google account name):" + self.username = raw_input().strip() + self.password = getpass.getpass() + + def login(self): + try: + self.client.client_login( + self.username, self.password, + source='lilypond-patch-handler', service='code') + except: + print "Incorrect username or password" + sys.exit(1) + + + def update_issue(self, issue_id, description): + issue = self.client.update_issue( + self.PROJECT_NAME, + issue_id, + self.username, + comment = description, + labels = ["Patch-new"]) + return issue + + def get_patches(self): + query = gdata.projecthosting.client.Query( + canned_query='open', + label='Patch-Review') + feed = self.client.get_issues(self.PROJECT_NAME, + query=query) + return feed + + + def do_countdown(self): + issues = self.get_patches() + for i, issue in enumerate(issues.entry): + print i, '\t', issue.get_id(), '\t', issue.title.text + + +def test_countdown(): + patchy = PatchBot() + patchy.do_countdown() + +test_countdown() + diff --git a/auto-compile/rietveld-json.py b/patches/rietveld-json.py similarity index 100% rename from auto-compile/rietveld-json.py rename to patches/rietveld-json.py