././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631546865.6624212 lazr.restfulclient-0.14.4/0000755000175000017500000000000000000000000017135 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/COPYING.txt0000644000175000017500000001672500000000000021021 0ustar00cjwatsoncjwatson00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1525765273.0 lazr.restfulclient-0.14.4/HACKING.rst0000644000175000017500000000215100000000000020732 0ustar00cjwatsoncjwatson00000000000000.. This file is part of lazr.restfulclient. lazr.restfulclient is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 of the License. lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with lazr.restfulclient. If not, see . ============ Introduction ============ To run this project's tests, use `tox `. Getting help ------------ If you find bugs in this package, you can report them here: https://launchpad.net/lazr.restfulclient If you want to discuss this package, join the team and mailing list here: https://launchpad.net/~lazr-developers or send a message to: lazr-developers@lists.launchpad.net ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546802.0 lazr.restfulclient-0.14.4/NEWS.rst0000644000175000017500000002302600000000000020446 0ustar00cjwatsoncjwatson00000000000000=========================== NEWS for lazr.restfulclient =========================== 0.14.4 (2021-09-13) =================== - Drop support for Python < 2.6. - Adjust versioning strategy to avoid importing pkg_resources, which is slow in large environments. 0.14.3 (2020-01-27) =================== - Restore from_string, to_string, and __str__ methods of lazr.restfulclient.authorize.oauth.AccessToken, unintentionally removed in 0.14.0. 0.14.2 (2018-11-17) =================== - Fix compatibility with httplib2 0.12.0 for Python 3. [bug=1803754] - Really fix compatibility with httplib2 < 0.9. - Fix compatibility with httplib2 0.9 for Python 3. - Require httplib2 >= 0.7.7 for Python 3. 0.14.1 (2018-11-16) =================== - Add compatibility with httplib2 0.12.0. [bug=1803558] 0.14.0 (2018-05-08) =================== - Switch from buildout to tox. - Port from oauth to oauthlib. Some tests still need to use oauth until lazr.authentication is ported. [bug=1672458] - Use the distro module rather than platform.linux_distribution, since the latter is deprecated in Python 3.5 and will be removed in 3.7. [bug=1473577] 0.13.5 (2017-09-04) =================== - Fix bytes vs. unicode in json.loads calls. [bug=1403524] - Decode header before comparison. [bug=1414075] - Fix urllib unquote imports. [bug=1414055] - Fix urllib urlencode imports. [bug=1425609] - Tolerate httplib2 versions earlier than 0.9 again. - Fix handling of 304 responses with an empty body on Python 3. [bug=1714960] 0.13.4 (2014-12-05) =================== - Port to python3. - Support proxy settings from environment by default. 0.13.3 (2013-03-22) =================== - Fall back to httplib2's default certificate path if the Debian/Ubuntu one doesn't exist. The default bundle might work, but a path that doesn't exist is never going to. New httplib2 bundles contain the required CA certs. 0.13.2 (2012-12-06) =================== - lazr.restfulclient is almost exclusively used with launchpad.net, but httplib2's cert bundle doesn't include launchpad's CA. Therefore with the default setup launchpadlib doesn't work unless cert checking is disabled. This is mitigated by the fact that Ubuntu carries a patch to httplib2 to make it use the system CA certs. This release makes that the default approach in lazr.restfulclient so that launchpad.net can be used by anyone with the Debian/Ubuntu CA certs path (/etc/ssl/certs/ca-certificates.crt), regardless of whether they are using Ubuntu's patched version of httplib2. Any platforms that don't have that path remain broken. 0.13.1 (2012-09-26) =================== - Named POST operations may result in a resource moving to a new location. Detect the redirect and reload the resource from its new URL. 0.13.0 (2012-06-19) =================== - Add environment variable, LP_DISABLE_SSL_CERTIFICATE_VALIDATION, to disable SSL certificate checks. Most useful when testing against development servers. 0.12.3 (2012-05-17) =================== - Implement the mocked out authorizeRequest for the BasicHttpAuthorizer object. 0.12.2 (2012-04-16) =================== - Fix ServiceRoot.load() so that it properly handles relative URLs in a way that doesn't break subsequent API calls (bug 681767). 0.12.1 (2012-03-28) =================== - Made the cache safe for use by concurrent threads and processes. 0.12.0 (2011-06-30) =================== - Give a more useful AttributeError 0.11.2 (2011-02-03) =================== - The 'web_link' parameter now shows up in lp_attributes, not lp_entries. 0.11.1 (2010-11-04) =================== - Restored compatibility with Python 2.4. 0.11.0 (2010-10-28) =================== - Make it possibly to specify an "application name" separate from the OAuth consumer key. If present, the application name is used in the User-Agent header; otherwise, the OAuth consumer key is used. - Add a "system-wide consumer" which can be used to authorize a user's entire account to use a web service, rather than doing it one application at a time. 0.10.0 (2010-08-12) =================== - Add compatibility with lazr.restful 0.11.0 0.9.21 (2010-07-19) =================== - Ensure that all JSON representations are converted to Unicode. - Restore the old behavior of CollectionWithKeyBasedLookup, which is less efficient but easier to understand. That is, the following code will work as it did in 0.9.17, performing the lookup immediately and raising a KeyError if the object doesn't exist on the server side. service.collection['key'] The more efficient behavior (which doesn't perform the lookup until you actually need the object) is still available, but you have to write this code instead: service.collection('key') - Exceptional conditions will now raise an appropriate subclass of HTTPError instead of always raising HTTPError. - Credential files are now created as being user-readable only. (In launchpadlib, they were created using the default umask and then made user-readable with chmod.) 0.9.20 (2010-06-25) =================== - It's now possible to pass a relative URL (relative to the versioned service root) into load(). 0.9.19 (2010-06-21) =================== - When the representation of a resource, as retrieved from the server, is of a different type than expected, the server value now takes precedence. This means that, in rare situations, a resource may start out presumed to be of one type, and change its capabilities once its representation is fetched from the server. 0.9.18 (2010-06-16) =================== - Made it possible to avoid fetching a representation of every single object looked up from a CollectionWithKeyBasedLookup (by defining .collection_of on the class), potentially improving script performance. 0.9.17 (2010-05-10) =================== - Switched back to asking for compression using the standard Accept-Encoding header. Using the TE header has never worked in a real situation due to HTTP intermediaries. 0.9.16 (2010-05-03) =================== - If a server returns a 502 or 503 error code, lazr.restfulclient will retry its request a configurable number of times in hopes that the error is transient. - It's now possible to invoke lazr.restful destructor methods, with the lp_delete() method. 0.9.15 (2010-04-27) ==================== - Clients will no longer fetch a representation of a collection before invoking a named operation on the collection. 0.9.14 (2010-04-15) =================== - Clients now send a useful and somewhat customizable User-Agent string. - Added a workaround for a bug in httplib2. - Removed the software dependency on lazr.restful except when running the full test suite. (The standalone_test test suite tests basic functionality of lazr.restfulclient to make sure the code base doesn't fundamentally depend on lazr.restful.) 0.9.13 (2010-03-24) =================== - Removed some no-longer-needed compatibility code for buggy servers, and fixed the tests to work with the new release of simplejson. - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't strict enough. The maximum filename length is now 143 characters. 0.9.12 (2010-03-09) =================== - Fixed a bug that prevented a unicode string from being used as a cache filename. 0.9.11 (2010-02-11) =================== - If a lazr.restful web service publishes multiple versions, you can now specify which version to use in a separate constructor argument, rather than sticking it on to the end of the service root. - Filenames in the cache will never be longer than 150 characters, to avoid errors on eCryptfs filesystems. - Added a proof-of-concept test for OAuth-signed anonymous access. - Fixed comparisons of entries and hosted files with None. 0.9.10 (2009-10-23) =================== - lazr.restfulclient now requests the correct WADL media type. - Made HTTPError strings more verbose. - Implemented the equality operator for entry and hosted-file resources. - Resume setting the 'credentials' attribute on ServerRoot to avoid breaking compatibility with launchpadlib. 0.9.9 (2009-10-07) ================== - The WSGI authentication middleware has been moved from lazr.restful to the new lazr.authentication library, and lazr.restfulclient now uses the new library. 0.9.8 (2009-10-06) ================== - Added support for OAuth. 0.9.7 (2009-09-30) ================== - Added support for HTTP Basic Auth. 0.9.6 (2009-09-16) ================== - Made compatible with lazr.restful 0.9.6. 0.9.5 (2009-08-28) ================== - Removed debugging code. 0.9.4 (2009-08-26) ================== - Removed unnecessary build dependencies. - Updated tests for newer version of simplejson. - Made tests less fragile by cleaning up lazr.restful example filemanager between tests. - normalized output of simplejson to unicode. 0.9.3 (2009-08-05) ================== Removed a sys.path hack from setup.py. 0.9.2 (2009-07-16) ================== - Fields that can contain binary data are no longer run through simplejson.dumps(). - For fields that can take on a limited set of values, you can now get a list of possible values. 0.9.1 (2009-07-13) ================== - The client now knows to look for multipart/form-data representations and will create them as appropriate. The upshot of this is that you can now send binary data when invoking named operations that will accept binary data. 0.9 (2009-04-29) ================ - Initial public release ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631546865.6584213 lazr.restfulclient-0.14.4/PKG-INFO0000644000175000017500000003411400000000000020235 0ustar00cjwatsoncjwatson00000000000000Metadata-Version: 2.1 Name: lazr.restfulclient Version: 0.14.4 Summary: A programmable client library that takes advantage of the commonalities among Home-page: https://launchpad.net/lazr.restfulclient Maintainer: LAZR Developers Maintainer-email: lazr-developers@lists.launchpad.net License: LGPL v3 Download-URL: https://launchpad.net/lazr.restfulclient/+download Description: .. This file is part of lazr.restfulclient. lazr.restfulclient is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 of the License. lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with lazr.restfulclient. If not, see . LAZR restfulclient ****************** A programmable client library that takes advantage of the commonalities among lazr.restful web services to provide added functionality on top of wadllib. Please see https://dev.launchpad.net/LazrStyleGuide and https://dev.launchpad.net/Hacking for how to develop in this package. =========================== NEWS for lazr.restfulclient =========================== 0.14.4 (2021-09-13) =================== - Drop support for Python < 2.6. - Adjust versioning strategy to avoid importing pkg_resources, which is slow in large environments. 0.14.3 (2020-01-27) =================== - Restore from_string, to_string, and __str__ methods of lazr.restfulclient.authorize.oauth.AccessToken, unintentionally removed in 0.14.0. 0.14.2 (2018-11-17) =================== - Fix compatibility with httplib2 0.12.0 for Python 3. [bug=1803754] - Really fix compatibility with httplib2 < 0.9. - Fix compatibility with httplib2 0.9 for Python 3. - Require httplib2 >= 0.7.7 for Python 3. 0.14.1 (2018-11-16) =================== - Add compatibility with httplib2 0.12.0. [bug=1803558] 0.14.0 (2018-05-08) =================== - Switch from buildout to tox. - Port from oauth to oauthlib. Some tests still need to use oauth until lazr.authentication is ported. [bug=1672458] - Use the distro module rather than platform.linux_distribution, since the latter is deprecated in Python 3.5 and will be removed in 3.7. [bug=1473577] 0.13.5 (2017-09-04) =================== - Fix bytes vs. unicode in json.loads calls. [bug=1403524] - Decode header before comparison. [bug=1414075] - Fix urllib unquote imports. [bug=1414055] - Fix urllib urlencode imports. [bug=1425609] - Tolerate httplib2 versions earlier than 0.9 again. - Fix handling of 304 responses with an empty body on Python 3. [bug=1714960] 0.13.4 (2014-12-05) =================== - Port to python3. - Support proxy settings from environment by default. 0.13.3 (2013-03-22) =================== - Fall back to httplib2's default certificate path if the Debian/Ubuntu one doesn't exist. The default bundle might work, but a path that doesn't exist is never going to. New httplib2 bundles contain the required CA certs. 0.13.2 (2012-12-06) =================== - lazr.restfulclient is almost exclusively used with launchpad.net, but httplib2's cert bundle doesn't include launchpad's CA. Therefore with the default setup launchpadlib doesn't work unless cert checking is disabled. This is mitigated by the fact that Ubuntu carries a patch to httplib2 to make it use the system CA certs. This release makes that the default approach in lazr.restfulclient so that launchpad.net can be used by anyone with the Debian/Ubuntu CA certs path (/etc/ssl/certs/ca-certificates.crt), regardless of whether they are using Ubuntu's patched version of httplib2. Any platforms that don't have that path remain broken. 0.13.1 (2012-09-26) =================== - Named POST operations may result in a resource moving to a new location. Detect the redirect and reload the resource from its new URL. 0.13.0 (2012-06-19) =================== - Add environment variable, LP_DISABLE_SSL_CERTIFICATE_VALIDATION, to disable SSL certificate checks. Most useful when testing against development servers. 0.12.3 (2012-05-17) =================== - Implement the mocked out authorizeRequest for the BasicHttpAuthorizer object. 0.12.2 (2012-04-16) =================== - Fix ServiceRoot.load() so that it properly handles relative URLs in a way that doesn't break subsequent API calls (bug 681767). 0.12.1 (2012-03-28) =================== - Made the cache safe for use by concurrent threads and processes. 0.12.0 (2011-06-30) =================== - Give a more useful AttributeError 0.11.2 (2011-02-03) =================== - The 'web_link' parameter now shows up in lp_attributes, not lp_entries. 0.11.1 (2010-11-04) =================== - Restored compatibility with Python 2.4. 0.11.0 (2010-10-28) =================== - Make it possibly to specify an "application name" separate from the OAuth consumer key. If present, the application name is used in the User-Agent header; otherwise, the OAuth consumer key is used. - Add a "system-wide consumer" which can be used to authorize a user's entire account to use a web service, rather than doing it one application at a time. 0.10.0 (2010-08-12) =================== - Add compatibility with lazr.restful 0.11.0 0.9.21 (2010-07-19) =================== - Ensure that all JSON representations are converted to Unicode. - Restore the old behavior of CollectionWithKeyBasedLookup, which is less efficient but easier to understand. That is, the following code will work as it did in 0.9.17, performing the lookup immediately and raising a KeyError if the object doesn't exist on the server side. service.collection['key'] The more efficient behavior (which doesn't perform the lookup until you actually need the object) is still available, but you have to write this code instead: service.collection('key') - Exceptional conditions will now raise an appropriate subclass of HTTPError instead of always raising HTTPError. - Credential files are now created as being user-readable only. (In launchpadlib, they were created using the default umask and then made user-readable with chmod.) 0.9.20 (2010-06-25) =================== - It's now possible to pass a relative URL (relative to the versioned service root) into load(). 0.9.19 (2010-06-21) =================== - When the representation of a resource, as retrieved from the server, is of a different type than expected, the server value now takes precedence. This means that, in rare situations, a resource may start out presumed to be of one type, and change its capabilities once its representation is fetched from the server. 0.9.18 (2010-06-16) =================== - Made it possible to avoid fetching a representation of every single object looked up from a CollectionWithKeyBasedLookup (by defining .collection_of on the class), potentially improving script performance. 0.9.17 (2010-05-10) =================== - Switched back to asking for compression using the standard Accept-Encoding header. Using the TE header has never worked in a real situation due to HTTP intermediaries. 0.9.16 (2010-05-03) =================== - If a server returns a 502 or 503 error code, lazr.restfulclient will retry its request a configurable number of times in hopes that the error is transient. - It's now possible to invoke lazr.restful destructor methods, with the lp_delete() method. 0.9.15 (2010-04-27) ==================== - Clients will no longer fetch a representation of a collection before invoking a named operation on the collection. 0.9.14 (2010-04-15) =================== - Clients now send a useful and somewhat customizable User-Agent string. - Added a workaround for a bug in httplib2. - Removed the software dependency on lazr.restful except when running the full test suite. (The standalone_test test suite tests basic functionality of lazr.restfulclient to make sure the code base doesn't fundamentally depend on lazr.restful.) 0.9.13 (2010-03-24) =================== - Removed some no-longer-needed compatibility code for buggy servers, and fixed the tests to work with the new release of simplejson. - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't strict enough. The maximum filename length is now 143 characters. 0.9.12 (2010-03-09) =================== - Fixed a bug that prevented a unicode string from being used as a cache filename. 0.9.11 (2010-02-11) =================== - If a lazr.restful web service publishes multiple versions, you can now specify which version to use in a separate constructor argument, rather than sticking it on to the end of the service root. - Filenames in the cache will never be longer than 150 characters, to avoid errors on eCryptfs filesystems. - Added a proof-of-concept test for OAuth-signed anonymous access. - Fixed comparisons of entries and hosted files with None. 0.9.10 (2009-10-23) =================== - lazr.restfulclient now requests the correct WADL media type. - Made HTTPError strings more verbose. - Implemented the equality operator for entry and hosted-file resources. - Resume setting the 'credentials' attribute on ServerRoot to avoid breaking compatibility with launchpadlib. 0.9.9 (2009-10-07) ================== - The WSGI authentication middleware has been moved from lazr.restful to the new lazr.authentication library, and lazr.restfulclient now uses the new library. 0.9.8 (2009-10-06) ================== - Added support for OAuth. 0.9.7 (2009-09-30) ================== - Added support for HTTP Basic Auth. 0.9.6 (2009-09-16) ================== - Made compatible with lazr.restful 0.9.6. 0.9.5 (2009-08-28) ================== - Removed debugging code. 0.9.4 (2009-08-26) ================== - Removed unnecessary build dependencies. - Updated tests for newer version of simplejson. - Made tests less fragile by cleaning up lazr.restful example filemanager between tests. - normalized output of simplejson to unicode. 0.9.3 (2009-08-05) ================== Removed a sys.path hack from setup.py. 0.9.2 (2009-07-16) ================== - Fields that can contain binary data are no longer run through simplejson.dumps(). - For fields that can take on a limited set of values, you can now get a list of possible values. 0.9.1 (2009-07-13) ================== - The client now knows to look for multipart/form-data representations and will create them as appropriate. The upshot of this is that you can now send binary data when invoking named operations that will accept binary data. 0.9 (2009-04-29) ================ - Initial public release Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Provides-Extra: docs Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1525767394.0 lazr.restfulclient-0.14.4/README.rst0000644000175000017500000000164100000000000020626 0ustar00cjwatsoncjwatson00000000000000A programmable client library that takes advantage of the commonalities among lazr.restful web services to provide added functionality on top of wadllib. For detailed documentation, please see: https://lazr-restfulclient.readthedocs.io/ .. This file is part of lazr.restfulclient. lazr.restfulclient is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 of the License. lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with lazr.restfulclient. If not, see . ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631546865.6624212 lazr.restfulclient-0.14.4/setup.cfg0000644000175000017500000000004600000000000020756 0ustar00cjwatsoncjwatson00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546776.0 lazr.restfulclient-0.14.4/setup.py0000644000175000017500000000565000000000000020655 0ustar00cjwatsoncjwatson00000000000000#!/usr/bin/env python # Copyright 2009-2018 Canonical Ltd. All rights reserved. # # This file is part of lazr.restfulclient # # lazr.restfulclient is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 of the License. # # lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . from setuptools import setup, find_packages # generic helpers primarily for the long_description def generate(*docname_or_string): marker = '.. pypi description ends here' res = [] for value in docname_or_string: if value.endswith('.rst'): with open(value) as f: value = f.read() idx = value.find(marker) if idx >= 0: value = value[:idx] res.append(value) if not value.endswith('\n'): res.append('') return '\n'.join(res) # end generic helpers tests_require = [ 'fixtures>=1.3.0', 'lazr.authentication', 'lazr.restful>=0.11.0', 'mock; python_version < "3"', 'oauth', 'testtools', 'wsgi_intercept', 'zope.testrunner', ] setup( name='lazr.restfulclient', version='0.14.4', namespace_packages=['lazr'], packages=find_packages('src'), package_dir={'':'src'}, include_package_data=True, zip_safe=False, maintainer='LAZR Developers', maintainer_email='lazr-developers@lists.launchpad.net', description=open('README.rst').readline().strip(), long_description=generate( 'src/lazr/restfulclient/docs/index.rst', 'NEWS.rst'), license='LGPL v3', install_requires=[ 'distro', 'httplib2; python_version < "3"', 'httplib2>=0.7.7; python_version >= "3"', 'importlib-metadata; python_version < "3.8"', 'oauthlib', 'setuptools', 'six', 'wadllib>=1.1.4', ], url='https://launchpad.net/lazr.restfulclient', download_url= 'https://launchpad.net/lazr.restfulclient/+download', classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", ], tests_require=tests_require, extras_require=dict( docs=['Sphinx'], test=tests_require, ), test_suite='lazr.restfulclient.tests', ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631546865.6144211 lazr.restfulclient-0.14.4/src/0000755000175000017500000000000000000000000017724 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631546865.6144211 lazr.restfulclient-0.14.4/src/lazr/0000755000175000017500000000000000000000000020674 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/src/lazr/__init__.py0000644000175000017500000000163700000000000023014 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009 Canonical Ltd. All rights reserved. # # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 of the License. # # lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . # this is a namespace package try: import pkg_resources pkg_resources.declare_namespace(__name__) except ImportError: import pkgutil __path__ = pkgutil.extend_path(__path__, __name__) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631546865.6264212 lazr.restfulclient-0.14.4/src/lazr/restfulclient/0000755000175000017500000000000000000000000023557 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546776.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/__init__.py0000644000175000017500000000163500000000000025675 0ustar00cjwatsoncjwatson00000000000000# Copyright 2008 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # lazr.restfulclient is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with lazr.restfulclient. If not, see # . try: import importlib.metadata as importlib_metadata except ImportError: import importlib_metadata __version__ = importlib_metadata.version("lazr.restfulclient") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1611142027.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/_browser.py0000644000175000017500000004501700000000000025762 0ustar00cjwatsoncjwatson00000000000000# Copyright 2008,2012 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # lazr.restfulclient is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with lazr.restfulclient. If not, see # . """Browser object to make requests of lazr.restful web services. The `Browser` class does some massage of HTTP requests and responses, and handles custom caches. It is not part of the public lazr.restfulclient API. (But maybe it should be?) """ __metaclass__ = type __all__ = [ 'Browser', 'RestfulHttp', 'ssl_certificate_validation_disabled', ] import atexit import errno from hashlib import md5 from io import BytesIO from json import dumps import os import re import shutil import sys import tempfile # Import sleep directly into the module so we can monkey-patch it # during a test. from time import sleep from httplib2 import ( Http, urlnorm, ) try: from httplib2 import proxy_info_from_environment except ImportError: from httplib2 import ProxyInfo proxy_info_from_environment = ProxyInfo.from_environment try: # Python 3. from urllib.parse import urlencode except ImportError: from urllib import urlencode from wadllib.application import Application from lazr.uri import URI from lazr.restfulclient.errors import error_for, HTTPError from lazr.restfulclient._json import DatetimeJSONEncoder if bytes is str: # Python 2 unicode_type = unicode str_types = basestring else: unicode_type = str str_types = str # A drop-in replacement for httplib2's safename. Substantially borrowed # from httplib2, but its cache name format changed in 0.12.0 and we want to # stick with the previous version. re_url_scheme = re.compile(br'^\w+://') re_url_scheme_s = re.compile(r'^\w+://') re_slash = re.compile(br'[?/:|]+') def safename(filename): """Return a filename suitable for the cache. Strips dangerous and common characters to create a filename we can use to store the cache in. """ try: if isinstance(filename, bytes): filename_match = filename.decode('utf-8') else: filename_match = filename if re_url_scheme_s.match(filename_match): if isinstance(filename, bytes): filename = filename.decode('utf-8') filename = filename.encode('idna') else: filename = filename.encode('idna') except UnicodeError: pass if isinstance(filename, unicode_type): filename = filename.encode('utf-8') filemd5 = md5(filename).hexdigest() filename = re_url_scheme.sub(b"", filename) filename = re_slash.sub(b",", filename) # This is the part that we changed. In stock httplib2, the # filename is trimmed if it's longer than 200 characters, and then # a comma and a 32-character md5 sum are appended. This causes # problems on eCryptfs filesystems, where the maximum safe # filename length is closer to 143 characters. # # We take a (user-hackable) maximum filename length from # RestfulHttp and subtract 33 characters to make room for the comma # and the md5 sum. # # See: # http://code.google.com/p/httplib2/issues/detail?id=92 # https://bugs.launchpad.net/bugs/344878 # https://bugs.launchpad.net/bugs/545197 maximum_filename_length = RestfulHttp.maximum_cache_filename_length maximum_length_before_md5_sum = maximum_filename_length - 32 - 1 if len(filename) > maximum_length_before_md5_sum: filename = filename[:maximum_length_before_md5_sum] return ",".join((filename.decode('utf-8'), filemd5)) def ssl_certificate_validation_disabled(): """Whether the user has disabled SSL certificate connection. Some testing servers have broken certificates. Rather than raising an error, we allow an environment variable, ``LP_DISABLE_SSL_CERTIFICATE_VALIDATION`` to disable the check. """ return bool( os.environ.get('LP_DISABLE_SSL_CERTIFICATE_VALIDATION', False)) if os.path.exists('/etc/ssl/certs/ca-certificates.crt'): SYSTEM_CA_CERTS = '/etc/ssl/certs/ca-certificates.crt' else: from httplib2 import CA_CERTS as SYSTEM_CA_CERTS class RestfulHttp(Http): """An Http subclass with some custom behavior. This Http client uses the TE header instead of the Accept-Encoding header to ask for compressed representations. It also knows how to react when its cache is a MultipleRepresentationCache. """ maximum_cache_filename_length = 143 def __init__(self, authorizer=None, cache=None, timeout=None, proxy_info=proxy_info_from_environment): cert_disabled = ssl_certificate_validation_disabled() super(RestfulHttp, self).__init__( cache, timeout, proxy_info, disable_ssl_certificate_validation=cert_disabled, ca_certs=SYSTEM_CA_CERTS) self.authorizer = authorizer if self.authorizer is not None: self.authorizer.authorizeSession(self) def _request(self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey): """Use the authorizer to authorize an outgoing request.""" if 'authorization' in headers: # There's an authorization header left over from a # previous request that resulted in a redirect. Resources # protected by OAuth or HTTP Digest must send a distinct # Authorization header with each request, to prevent # playback attacks. Remove the Authorization header and # start again. del headers['authorization'] if self.authorizer is not None: self.authorizer.authorizeRequest( absolute_uri, method, body, headers) return super(RestfulHttp, self)._request( conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey) def _getCachedHeader(self, uri, header): """Retrieve a cached value for an HTTP header.""" if isinstance(self.cache, MultipleRepresentationCache): return self.cache._getCachedHeader(uri, header) return None class AtomicFileCache(object): """A FileCache that can be shared by multiple processes. Based on a patch found at . """ TEMPFILE_PREFIX = ".temp" def __init__(self, cache, safe=safename): """Construct an ``AtomicFileCache``. :param cache: The directory to use as a cache. :param safe: A function that takes a key and returns a name that's safe to use as a filename. The key must never return a string that begins with ``TEMPFILE_PREFIX``. By default uses ``safename``. """ self._cache_dir = os.path.normpath(cache) self._get_safe_name = safe try: os.makedirs(self._cache_dir) except OSError as e: if e.errno != errno.EEXIST: raise def _get_key_path(self, key): """Return the path on disk where ``key`` is stored.""" safe_key = self._get_safe_name(key) if safe_key.startswith(self.TEMPFILE_PREFIX): # If the cache key starts with the tempfile prefix, then it's # possible that it will clash with a temporary file that we # create. raise ValueError( "Cache key cannot start with '%s'" % self.TEMPFILE_PREFIX) return os.path.join(self._cache_dir, safe_key) def get(self, key): """Get the value of ``key`` if set. This behaves slightly differently to ``FileCache`` in that if ``set()`` fails to store a key, this ``get()`` will behave as if that key were never set whereas ``FileCache`` returns the empty string. :param key: The key to retrieve. Must be either bytes or unicode text. :return: The value of ``key`` if set, None otherwise. """ cache_full_path = self._get_key_path(key) try: f = open(cache_full_path, 'rb') try: return f.read() finally: f.close() except (IOError, OSError) as e: if e.errno != errno.ENOENT: raise def set(self, key, value): """Set ``key`` to ``value``. :param key: The key to set. Must be either bytes or unicode text. :param value: The value to set ``key`` to. Must be bytes. """ # Open a temporary file handle, path_name = tempfile.mkstemp( prefix=self.TEMPFILE_PREFIX, dir=self._cache_dir) f = os.fdopen(handle, 'wb') f.write(value) f.close() cache_full_path = self._get_key_path(key) # And rename atomically (on POSIX at least) if sys.platform == 'win32' and os.path.exists(cache_full_path): os.unlink(cache_full_path) os.rename(path_name, cache_full_path) def delete(self, key): """Delete ``key`` from the cache. If ``key`` has not already been set then has no effect. :param key: The key to delete. Must be either bytes or unicode text. """ cache_full_path = self._get_key_path(key) try: os.remove(cache_full_path) except OSError as e: if e.errno != errno.ENOENT: raise class MultipleRepresentationCache(AtomicFileCache): """A cache that can hold different representations of the same resource. If a resource has two representations with two media types, FileCache will only store the most recently fetched representation. This cache can keep track of multiple representations of the same resource. This class works on the assumption that outside calling code sets an instance's request_media_type attribute to the value of the 'Accept' header before initiating the request. This class is very much not thread-safe, but FileCache isn't thread-safe anyway. """ def __init__(self, cache): """Tell FileCache to call append_media_type when generating keys.""" super(MultipleRepresentationCache, self).__init__( cache, self.append_media_type) self.request_media_type = None def append_media_type(self, key): """Append the request media type to the cache key. This ensures that representations of the same resource will be cached separately, so long as they're served as different media types. """ if self.request_media_type is not None: key = key + '-' + self.request_media_type return safename(key) def _getCachedHeader(self, uri, header): """Retrieve a cached value for an HTTP header.""" (scheme, authority, request_uri, cachekey) = urlnorm(uri) cached_value = self.get(cachekey) header_start = header + ':' if not isinstance(header_start, bytes): header_start = header_start.encode('utf-8') if cached_value is not None: for line in BytesIO(cached_value): if line.startswith(header_start): return line[len(header_start):].strip() return None class Browser: """A class for making calls to lazr.restful web services.""" NOT_MODIFIED = object() MAX_RETRIES = 6 def __init__(self, service_root, credentials, cache=None, timeout=None, proxy_info=None, user_agent=None, max_retries=MAX_RETRIES): """Initialize, possibly creating a cache. If no cache is provided, a temporary directory will be used as a cache. The temporary directory will be automatically removed when the Python process exits. """ if cache is None: cache = tempfile.mkdtemp() atexit.register(shutil.rmtree, cache) if isinstance(cache, str_types): cache = MultipleRepresentationCache(cache) self._connection = service_root.httpFactory( credentials, cache, timeout, proxy_info) self.user_agent = user_agent self.max_retries = max_retries def _request_and_retry(self, url, method, body, headers): for retry_count in range(0, self.max_retries+1): response, content = self._connection.request( url, method=method, body=body, headers=headers) if (response.status in [502, 503] and retry_count < self.max_retries): # The server returned a 502 or 503. Sleep for 0, 1, 2, # 4, 8, 16, ... seconds and try again. sleep_for = int(2**(retry_count-1)) sleep(sleep_for) else: break # Either the request succeeded or we gave up. return response, content def _request(self, url, data=None, method='GET', media_type='application/json', extra_headers=None): """Create an authenticated request object.""" # If the user is trying to get data that has been redacted, # give a helpful message. if url == "tag:launchpad.net:2008:redacted": raise ValueError("You tried to access a resource that you " "don't have the server-side permission to see.") # Add extra headers for the request. headers = {'Accept': media_type} if self.user_agent is not None: headers['User-Agent'] = self.user_agent if isinstance(self._connection.cache, MultipleRepresentationCache): self._connection.cache.request_media_type = media_type if extra_headers is not None: headers.update(extra_headers) response, content = self._request_and_retry( str(url), method=method, body=data, headers=headers) if response.status == 304: # The resource didn't change. if content == b'': if ('If-None-Match' in headers or 'If-Modified-Since' in headers): # The caller made a conditional request, and the # condition failed. Rather than send an empty # representation, which might be misinterpreted, # send a special object that will let the calling code know # that the resource was not modified. return response, self.NOT_MODIFIED else: # The caller didn't make a conditional request, # but the response code is 304 and there's no # content. The only way to handle this is to raise # an error. # # We don't use error_for() here because 304 is not # normally considered an error condition. raise HTTPError(response, content) else: # XXX leonardr 2010/04/12 bug=httplib2#97 # # Why is this check here? Why would there ever be any # content when the response code is 304? It's because of # an httplib2 bug that sometimes sets a 304 response # code when caching retrieved documents. When the # cached document is retrieved, we get a 304 response # code and a full representation. # # Since the cache lookup succeeded, the 'real' # response code is 200. This code undoes the bad # behavior in httplib2. response.status = 200 return response, content # Turn non-2xx responses into appropriate HTTPError subclasses. error = error_for(response, content) if error is not None: raise error return response, content def get(self, resource_or_uri, headers=None, return_response=False): """GET a representation of the given resource or URI.""" if isinstance(resource_or_uri, (str_types, URI)): url = resource_or_uri else: method = resource_or_uri.get_method('get') url = method.build_request_url() response, content = self._request(url, extra_headers=headers) if return_response: return (response, content) return content def get_wadl_application(self, url): """GET a WADL representation of the resource at the requested url.""" wadl_type = 'application/vnd.sun.wadl+xml' response, content = self._request(url, media_type=wadl_type) url = str(url) if not isinstance(content, bytes): content = content.encode('utf-8') return Application(url, content) def post(self, url, method_name, **kws): """POST a request to the web service.""" kws['ws.op'] = method_name data = urlencode(kws) return self._request(url, data, 'POST') def put(self, url, representation, media_type, headers=None): """PUT the given representation to the URL.""" extra_headers = {'Content-Type': media_type} if headers is not None: extra_headers.update(headers) return self._request( url, representation, 'PUT', extra_headers=extra_headers) def delete(self, url): """DELETE the resource at the given URL.""" self._request(url, method='DELETE') return None def patch(self, url, representation, headers=None): """PATCH the object at url with the updated representation.""" extra_headers = {'Content-Type': 'application/json'} if headers is not None: extra_headers.update(headers) # httplib2 doesn't know about the PATCH method, so we need to # do some work ourselves. Pull any cached value of "ETag" out # and use it as the value for "If-Match". cached_etag = self._connection._getCachedHeader(str(url), 'etag') if cached_etag is not None and not self._connection.ignore_etag: # http://www.w3.org/1999/04/Editing/ headers['If-Match'] = cached_etag return self._request( url, dumps(representation, cls=DatetimeJSONEncoder), 'PATCH', extra_headers=extra_headers) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1611142027.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/_json.py0000644000175000017500000000221700000000000025243 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public License # as published by the Free Software Foundation, version 3 of the # License. # # lazr.restfulclient is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with lazr.restfulclient. If not, see # . """Classes for working with JSON.""" __metaclass__ = type __all__ = ['DatetimeJSONEncoder'] import datetime from json import JSONEncoder class DatetimeJSONEncoder(JSONEncoder): """A JSON encoder that understands datetime objects. Datetime objects are formatted according to ISO 1601. """ def default(self, obj): if isinstance(obj, datetime.datetime): return obj.isoformat() return JSONEncoder.default(self, obj) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1631546865.6264212 lazr.restfulclient-0.14.4/src/lazr/restfulclient/authorize/0000755000175000017500000000000000000000000025571 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/authorize/__init__.py0000644000175000017500000000620100000000000027701 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # lazr.restfulclient is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with lazr.restfulclient. If not, see # . """Classes to authorize lazr.restfulclient with various web services. This module includes an authorizer classes for HTTP Basic Auth, as well as a base-class authorizer that does nothing. A set of classes for authorizing with OAuth is located in the 'oauth' module. """ __metaclass__ = type __all__ = [ 'BasicHttpAuthorizer', 'HttpAuthorizer', ] import base64 class HttpAuthorizer: """Handles authentication for HTTP requests. There are two ways to authenticate. The authorize_session() method is called once when the client is initialized. This works for authentication methods like Basic Auth. The authorize_request is called for every HTTP request, which is useful for authentication methods like Digest and OAuth. The base class is a null authorizer which does not perform any authentication at all. """ def authorizeSession(self, client): """Set up credentials for the entire session.""" pass def authorizeRequest(self, absolute_uri, method, body, headers): """Set up credentials for a single request. This probably involves setting the Authentication header. """ pass @property def user_agent_params(self): """Any parameters necessary to identify this user agent. By default this is an empty dict (because authentication details don't contain any information about the application making the request), but when a resource is protected by OAuth, the OAuth consumer name is part of the user agent. """ return {} class BasicHttpAuthorizer(HttpAuthorizer): """Handles authentication for services that use HTTP Basic Auth.""" def __init__(self, username, password): """Constructor. :param username: User to send as authorization for all requests. :param password: Password to send as authorization for all requests. """ self.username = username self.password = password def authorizeRequest(self, absolute_uri, method, body, headers): """Set up credentials for a single request. This sets the authorization header with the username/password. """ headers['authorization'] = 'Basic ' + base64.b64encode( "%s:%s" % (self.username, self.password)).strip() def authorizeSession(self, client): client.add_credentials(self.username, self.password) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580131530.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/authorize/oauth.py0000644000175000017500000002412600000000000027270 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009-2018 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # lazr.restfulclient is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with lazr.restfulclient. If not, see # . """OAuth classes for use with lazr.restfulclient.""" try: # Python 3, SafeConfigParser was renamed to just ConfigParser. from configparser import ConfigParser as SafeConfigParser except ImportError: from ConfigParser import SafeConfigParser import os import platform import stat import socket from oauthlib import oauth1 import six from six.moves.urllib.parse import ( parse_qs, urlencode, ) from lazr.restfulclient.authorize import HttpAuthorizer from lazr.restfulclient.errors import CredentialsFileError __metaclass__ = type __all__ = [ 'AccessToken', 'Consumer', 'OAuthAuthorizer', 'SystemWideConsumer', ] CREDENTIALS_FILE_VERSION = '1' # For compatibility, Consumer and AccessToken are defined using terminology # from the older oauth library rather than the newer oauthlib. class Consumer: """An OAuth consumer (application).""" def __init__(self, key, secret='', application_name=None): """Initialize :param key: The OAuth consumer key :param secret: The OAuth consumer secret. Don't use this. It's a misfeature, and lazr.restful doesn't expect it. :param application_name: An application name, if different from the consumer key. If present, this will be used in the User-Agent header. """ self.key = key self.secret = secret self.application_name = application_name class AccessToken: """An OAuth access token.""" def __init__(self, key, secret='', context=None): self.key = key self.secret = secret self.context = context def to_string(self): return urlencode([ ("oauth_token_secret", self.secret), ("oauth_token", self.key), ]) __str__ = to_string @classmethod def from_string(cls, s): params = parse_qs(s, keep_blank_values=False) key = params["oauth_token"][0] secret = params["oauth_token_secret"][0] return cls(key, secret) class TruthyString(six.text_type): """A Unicode string which is always true.""" def __bool__(self): return True __nonzero__ = __bool__ class SystemWideConsumer(Consumer): """A consumer associated with the logged-in user rather than an app. This can be used to share a single OAuth token among multiple desktop applications. The OAuth consumer key will be derived from system information (platform and hostname). """ KEY_FORMAT = "System-wide: %s (%s)" def __init__(self, application_name, secret=''): """Constructor. :param application_name: An application name. This will be used in the User-Agent header. :param secret: The OAuth consumer secret. Don't use this. It's a misfeature, and lazr.restful doesn't expect it. """ super(SystemWideConsumer, self).__init__( self.consumer_key, secret, application_name) @property def consumer_key(self): """The system-wide OAuth consumer key for this computer. This key identifies the platform and the computer's hostname. It does not identify the active user. """ try: import distro distname = distro.name() except Exception: # This can happen due to various kinds of failures with the data # sources used by the distro module. distname = '' if distname == '': distname = platform.system() # (eg. "Windows") return self.KEY_FORMAT % (distname, socket.gethostname()) class OAuthAuthorizer(HttpAuthorizer): """A client that signs every outgoing request with OAuth credentials.""" def __init__(self, consumer_name=None, consumer_secret='', access_token=None, oauth_realm="OAuth", application_name=None): self.consumer = None if consumer_name is not None: self.consumer = Consumer( consumer_name, consumer_secret, application_name) self.access_token = access_token self.oauth_realm = oauth_realm @property def user_agent_params(self): """Any information necessary to identify this user agent. In this case, the OAuth consumer name. """ params = {} if self.consumer is None: return params params['oauth_consumer'] = self.consumer.key if self.consumer.application_name is not None: params['application'] = self.consumer.application_name return params def load(self, readable_file): """Load credentials from a file-like object. This overrides the consumer and access token given in the constructor and replaces them with the values read from the file. :param readable_file: A file-like object to read the credentials from :type readable_file: Any object supporting the file-like `read()` method """ # Attempt to load the access token from the file. parser = SafeConfigParser() reader = getattr(parser, 'read_file', parser.readfp) reader(readable_file) # Check the version number and extract the access token and # secret. Then convert these to the appropriate instances. if not parser.has_section(CREDENTIALS_FILE_VERSION): raise CredentialsFileError('No configuration for version %s' % CREDENTIALS_FILE_VERSION) consumer_key = parser.get( CREDENTIALS_FILE_VERSION, 'consumer_key') consumer_secret = parser.get( CREDENTIALS_FILE_VERSION, 'consumer_secret') self.consumer = Consumer(consumer_key, consumer_secret) access_token = parser.get( CREDENTIALS_FILE_VERSION, 'access_token') access_secret = parser.get( CREDENTIALS_FILE_VERSION, 'access_secret') self.access_token = AccessToken(access_token, access_secret) @classmethod def load_from_path(cls, path): """Convenience method for loading credentials from a file. Open the file, create the Credentials and load from the file, and finally close the file and return the newly created Credentials instance. :param path: In which file the credential file should be saved. :type path: string :return: The loaded Credentials instance. :rtype: `Credentials` """ credentials = cls() credentials_file = open(path, 'r') credentials.load(credentials_file) credentials_file.close() return credentials def save(self, writable_file): """Write the credentials to the file-like object. :param writable_file: A file-like object to write the credentials to :type writable_file: Any object supporting the file-like `write()` method :raise CredentialsFileError: when there is either no consumer or no access token """ if self.consumer is None: raise CredentialsFileError('No consumer') if self.access_token is None: raise CredentialsFileError('No access token') parser = SafeConfigParser() parser.add_section(CREDENTIALS_FILE_VERSION) parser.set(CREDENTIALS_FILE_VERSION, 'consumer_key', self.consumer.key) parser.set(CREDENTIALS_FILE_VERSION, 'consumer_secret', self.consumer.secret) parser.set(CREDENTIALS_FILE_VERSION, 'access_token', self.access_token.key) parser.set(CREDENTIALS_FILE_VERSION, 'access_secret', self.access_token.secret) parser.write(writable_file) def save_to_path(self, path): """Convenience method for saving credentials to a file. Create the file, call self.save(), and close the file. Existing files are overwritten. The resulting file will be readable and writable only by the user. :param path: In which file the credential file should be saved. :type path: string """ credentials_file = os.fdopen( os.open(path, (os.O_CREAT | os.O_TRUNC | os.O_WRONLY), (stat.S_IREAD | stat.S_IWRITE)), 'w') self.save(credentials_file) credentials_file.close() def authorizeRequest(self, absolute_uri, method, body, headers): """Sign a request with OAuth credentials.""" client = oauth1.Client( self.consumer.key, client_secret=self.consumer.secret, resource_owner_key=TruthyString(self.access_token.key or ''), resource_owner_secret=self.access_token.secret, signature_method=oauth1.SIGNATURE_PLAINTEXT, realm=self.oauth_realm) # The older oauth library (which may still be used on the server) # requires the oauth_token parameter to be present and will fail # authentication if it isn't. This hack forces it to be present # even if its value is the empty string. client.resource_owner_key = TruthyString(client.resource_owner_key) _, signed_headers, _ = client.sign(absolute_uri) for key, value in signed_headers.items(): # client.sign returns Unicode headers; convert these to native # strings. if six.PY2: key = key.encode('UTF-8') value = value.encode('UTF-8') headers[key] = value ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1631546865.654421 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/0000755000175000017500000000000000000000000024507 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1525765273.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/Makefile0000644000175000017500000000114600000000000026151 0ustar00cjwatsoncjwatson00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = lazrrestfulclient SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546802.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/NEWS.rst0000644000175000017500000002302600000000000026020 0ustar00cjwatsoncjwatson00000000000000=========================== NEWS for lazr.restfulclient =========================== 0.14.4 (2021-09-13) =================== - Drop support for Python < 2.6. - Adjust versioning strategy to avoid importing pkg_resources, which is slow in large environments. 0.14.3 (2020-01-27) =================== - Restore from_string, to_string, and __str__ methods of lazr.restfulclient.authorize.oauth.AccessToken, unintentionally removed in 0.14.0. 0.14.2 (2018-11-17) =================== - Fix compatibility with httplib2 0.12.0 for Python 3. [bug=1803754] - Really fix compatibility with httplib2 < 0.9. - Fix compatibility with httplib2 0.9 for Python 3. - Require httplib2 >= 0.7.7 for Python 3. 0.14.1 (2018-11-16) =================== - Add compatibility with httplib2 0.12.0. [bug=1803558] 0.14.0 (2018-05-08) =================== - Switch from buildout to tox. - Port from oauth to oauthlib. Some tests still need to use oauth until lazr.authentication is ported. [bug=1672458] - Use the distro module rather than platform.linux_distribution, since the latter is deprecated in Python 3.5 and will be removed in 3.7. [bug=1473577] 0.13.5 (2017-09-04) =================== - Fix bytes vs. unicode in json.loads calls. [bug=1403524] - Decode header before comparison. [bug=1414075] - Fix urllib unquote imports. [bug=1414055] - Fix urllib urlencode imports. [bug=1425609] - Tolerate httplib2 versions earlier than 0.9 again. - Fix handling of 304 responses with an empty body on Python 3. [bug=1714960] 0.13.4 (2014-12-05) =================== - Port to python3. - Support proxy settings from environment by default. 0.13.3 (2013-03-22) =================== - Fall back to httplib2's default certificate path if the Debian/Ubuntu one doesn't exist. The default bundle might work, but a path that doesn't exist is never going to. New httplib2 bundles contain the required CA certs. 0.13.2 (2012-12-06) =================== - lazr.restfulclient is almost exclusively used with launchpad.net, but httplib2's cert bundle doesn't include launchpad's CA. Therefore with the default setup launchpadlib doesn't work unless cert checking is disabled. This is mitigated by the fact that Ubuntu carries a patch to httplib2 to make it use the system CA certs. This release makes that the default approach in lazr.restfulclient so that launchpad.net can be used by anyone with the Debian/Ubuntu CA certs path (/etc/ssl/certs/ca-certificates.crt), regardless of whether they are using Ubuntu's patched version of httplib2. Any platforms that don't have that path remain broken. 0.13.1 (2012-09-26) =================== - Named POST operations may result in a resource moving to a new location. Detect the redirect and reload the resource from its new URL. 0.13.0 (2012-06-19) =================== - Add environment variable, LP_DISABLE_SSL_CERTIFICATE_VALIDATION, to disable SSL certificate checks. Most useful when testing against development servers. 0.12.3 (2012-05-17) =================== - Implement the mocked out authorizeRequest for the BasicHttpAuthorizer object. 0.12.2 (2012-04-16) =================== - Fix ServiceRoot.load() so that it properly handles relative URLs in a way that doesn't break subsequent API calls (bug 681767). 0.12.1 (2012-03-28) =================== - Made the cache safe for use by concurrent threads and processes. 0.12.0 (2011-06-30) =================== - Give a more useful AttributeError 0.11.2 (2011-02-03) =================== - The 'web_link' parameter now shows up in lp_attributes, not lp_entries. 0.11.1 (2010-11-04) =================== - Restored compatibility with Python 2.4. 0.11.0 (2010-10-28) =================== - Make it possibly to specify an "application name" separate from the OAuth consumer key. If present, the application name is used in the User-Agent header; otherwise, the OAuth consumer key is used. - Add a "system-wide consumer" which can be used to authorize a user's entire account to use a web service, rather than doing it one application at a time. 0.10.0 (2010-08-12) =================== - Add compatibility with lazr.restful 0.11.0 0.9.21 (2010-07-19) =================== - Ensure that all JSON representations are converted to Unicode. - Restore the old behavior of CollectionWithKeyBasedLookup, which is less efficient but easier to understand. That is, the following code will work as it did in 0.9.17, performing the lookup immediately and raising a KeyError if the object doesn't exist on the server side. service.collection['key'] The more efficient behavior (which doesn't perform the lookup until you actually need the object) is still available, but you have to write this code instead: service.collection('key') - Exceptional conditions will now raise an appropriate subclass of HTTPError instead of always raising HTTPError. - Credential files are now created as being user-readable only. (In launchpadlib, they were created using the default umask and then made user-readable with chmod.) 0.9.20 (2010-06-25) =================== - It's now possible to pass a relative URL (relative to the versioned service root) into load(). 0.9.19 (2010-06-21) =================== - When the representation of a resource, as retrieved from the server, is of a different type than expected, the server value now takes precedence. This means that, in rare situations, a resource may start out presumed to be of one type, and change its capabilities once its representation is fetched from the server. 0.9.18 (2010-06-16) =================== - Made it possible to avoid fetching a representation of every single object looked up from a CollectionWithKeyBasedLookup (by defining .collection_of on the class), potentially improving script performance. 0.9.17 (2010-05-10) =================== - Switched back to asking for compression using the standard Accept-Encoding header. Using the TE header has never worked in a real situation due to HTTP intermediaries. 0.9.16 (2010-05-03) =================== - If a server returns a 502 or 503 error code, lazr.restfulclient will retry its request a configurable number of times in hopes that the error is transient. - It's now possible to invoke lazr.restful destructor methods, with the lp_delete() method. 0.9.15 (2010-04-27) ==================== - Clients will no longer fetch a representation of a collection before invoking a named operation on the collection. 0.9.14 (2010-04-15) =================== - Clients now send a useful and somewhat customizable User-Agent string. - Added a workaround for a bug in httplib2. - Removed the software dependency on lazr.restful except when running the full test suite. (The standalone_test test suite tests basic functionality of lazr.restfulclient to make sure the code base doesn't fundamentally depend on lazr.restful.) 0.9.13 (2010-03-24) =================== - Removed some no-longer-needed compatibility code for buggy servers, and fixed the tests to work with the new release of simplejson. - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't strict enough. The maximum filename length is now 143 characters. 0.9.12 (2010-03-09) =================== - Fixed a bug that prevented a unicode string from being used as a cache filename. 0.9.11 (2010-02-11) =================== - If a lazr.restful web service publishes multiple versions, you can now specify which version to use in a separate constructor argument, rather than sticking it on to the end of the service root. - Filenames in the cache will never be longer than 150 characters, to avoid errors on eCryptfs filesystems. - Added a proof-of-concept test for OAuth-signed anonymous access. - Fixed comparisons of entries and hosted files with None. 0.9.10 (2009-10-23) =================== - lazr.restfulclient now requests the correct WADL media type. - Made HTTPError strings more verbose. - Implemented the equality operator for entry and hosted-file resources. - Resume setting the 'credentials' attribute on ServerRoot to avoid breaking compatibility with launchpadlib. 0.9.9 (2009-10-07) ================== - The WSGI authentication middleware has been moved from lazr.restful to the new lazr.authentication library, and lazr.restfulclient now uses the new library. 0.9.8 (2009-10-06) ================== - Added support for OAuth. 0.9.7 (2009-09-30) ================== - Added support for HTTP Basic Auth. 0.9.6 (2009-09-16) ================== - Made compatible with lazr.restful 0.9.6. 0.9.5 (2009-08-28) ================== - Removed debugging code. 0.9.4 (2009-08-26) ================== - Removed unnecessary build dependencies. - Updated tests for newer version of simplejson. - Made tests less fragile by cleaning up lazr.restful example filemanager between tests. - normalized output of simplejson to unicode. 0.9.3 (2009-08-05) ================== Removed a sys.path hack from setup.py. 0.9.2 (2009-07-16) ================== - Fields that can contain binary data are no longer run through simplejson.dumps(). - For fields that can take on a limited set of values, you can now get a list of possible values. 0.9.1 (2009-07-13) ================== - The client now knows to look for multipart/form-data representations and will create them as appropriate. The upshot of this is that you can now send binary data when invoking named operations that will accept binary data. 0.9 (2009-04-29) ================ - Initial public release ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1524828521.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/authorizer.standalone.rst0000644000175000017500000002347000000000000031572 0ustar00cjwatsoncjwatson00000000000000Authorizers =========== Authorizers are objects that encapsulate knowledge about a particular web service's authentication scheme. lazr.restfulclient includes authorizers for common HTTP authentication schemes. The BasicHttpAuthorizer ----------------------- This authorizer handles HTTP Basic Auth. To test it, we'll create a fake web service that serves some dummy WADL. >>> import pkg_resources >>> wadl_string = pkg_resources.resource_string( ... 'wadllib.tests.data', 'launchpad-wadl.xml') >>> responses = { 'application/vnd.sun.wadl+xml' : wadl_string, ... 'application/json' : '{}' } >>> def dummy_application(environ, start_response): ... media_type = environ['HTTP_ACCEPT'] ... content = responses[media_type] ... start_response( ... '200', [('Content-type', media_type)]) ... return [content] The WADL file will be protected with HTTP Basic Auth. To access it, you'll need to provide a username of "user" and a password of "password". >>> def authenticate(username, password): ... """Accepts "user/password", rejects everything else. ... ... :return: The username, if the credentials are valid. ... None, otherwise. ... """ ... if username == "user" and password == "password": ... return username ... return None >>> from lazr.authentication.wsgi import BasicAuthMiddleware >>> def protected_application(): ... return BasicAuthMiddleware( ... dummy_application, authenticate_with=authenticate) Finally, we'll set up a WSGI intercept so that we can test the web service by making HTTP requests to http://api.launchpad.dev/. (This is the hostname mentioned in the WADL file.) >>> import wsgi_intercept >>> from wsgi_intercept.httplib2_intercept import install >>> install() >>> wsgi_intercept.add_wsgi_intercept( ... 'api.launchpad.dev', 80, protected_application) With no HttpAuthorizer, a ServiceRoot can't get access to the web service. >>> from lazr.restfulclient.resource import ServiceRoot >>> client = ServiceRoot(None, "http://api.launchpad.dev/") Traceback (most recent call last): ... Unauthorized: HTTP Error 401: Unauthorized ... We can't get access if the authorizer doesn't have the right credentials. >>> from lazr.restfulclient.authorize import BasicHttpAuthorizer >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword") >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/") Traceback (most recent call last): ... Unauthorized: HTTP Error 401: Unauthorized ... If we provide the right credentials, we can retrieve the WADL. We'll still get an exception, because our fake web service is too fake for ServiceRoot--its 'service root' resource doesn't match the WADL--but we're able to make HTTP requests without getting 401 errors. Note that the HTTP request includes the User-Agent header, but that that header contains no special information about the authorization method. This will change when the authorization method is OAuth. >>> import httplib2 >>> httplib2.debuglevel = 1 >>> authorizer = BasicHttpAuthorizer("user", "password") >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") send: 'GET / ...user-agent: lazr.restfulclient ...' ... The BasicHttpAuthorizer allows you to adds proper basic auth headers to the request, when asked to, using the username and password information it already knows about. >>> headers = {} >>> authorizer.authorizeRequest('/', 'GET', '', headers) >>> headers.get('authorization') 'Basic dXNlcjpwYXNzd29yZA==' Teardown. >>> httplib2.debuglevel = 0 >>> _ = wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80) The OAuthAuthorizer ------------------- This authorizer handles OAuth authorization. To test it, we'll protect the dummy application with a piece of OAuth middleware. The middleware will accept only one consumer/token combination, though it will also allow anonymous access: if you pass in an empty token and secret, you'll get a lower level of access. >>> from oauth.oauth import OAuthConsumer, OAuthToken >>> valid_consumer = OAuthConsumer("consumer", '') >>> valid_token = OAuthToken("token", "secret") >>> empty_token = OAuthToken("", "") Our authenticate() implementation checks against the one valid consumer and token. >>> def authenticate(consumer, token, parameters): ... """Accepts the valid consumer and token, rejects everything else. ... ... :return: The consumer, if the credentials are valid. ... None, otherwise. ... """ ... if token.key == '' and token.secret == '': ... # Anonymous access. ... return consumer ... if consumer == valid_consumer and token == valid_token: ... return consumer ... return None Our data store helps the middleware look up consumer and token objects from the information provided in a signed OAuth request. >>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore >>> class AnonymousAccessDataStore(SimpleOAuthDataStore): ... """A data store that will accept any consumer.""" ... def lookup_consumer(self, consumer): ... """If there's no matching consumer, just create one. ... ... This will let anonymous requests succeed with any ... consumer key.""" ... consumer = super( ... AnonymousAccessDataStore, self).lookup_consumer( ... consumer) ... if consumer is None: ... consumer = OAuthConsumer(consumer, '') ... return consumer >>> data_store = AnonymousAccessDataStore( ... {valid_consumer.key : valid_consumer}, ... {valid_token.key : valid_token, ... empty_token.key : empty_token}) Now we're ready to protect the dummy_application with OAuthMiddleware, using our authenticate() implementation and our data store. >>> from lazr.authentication.wsgi import OAuthMiddleware >>> def protected_application(): ... return OAuthMiddleware( ... dummy_application, realm="OAuth test", ... authenticate_with=authenticate, data_store=data_store) >>> wsgi_intercept.add_wsgi_intercept( ... 'api.launchpad.dev', 80, protected_application) Let's try out some clients. As you'd expect, you can't get through the middleware with no HTTPAuthorizer at all. >>> from lazr.restfulclient.authorize.oauth import OAuthAuthorizer >>> client = ServiceRoot(None, "http://api.launchpad.dev/") Traceback (most recent call last): ... Unauthorized: HTTP Error 401: Unauthorized ... Invalid credentials are also no help. >>> authorizer = OAuthAuthorizer( ... valid_consumer.key, access_token=OAuthToken("invalid", "token")) >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") Traceback (most recent call last): ... Unauthorized: HTTP Error 401: Unauthorized ... But valid credentials work fine (again, up to the point at which lazr.restfulclient runs against the limits of this simple web service). Note that the User-Agent header mentions the consumer key. >>> httplib2.debuglevel = 1 >>> authorizer = OAuthAuthorizer( ... valid_consumer.key, access_token=valid_token) >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") send: 'GET /...user-agent: lazr.restfulclient...; oauth_consumer="consumer"...' ... If the OAuthAuthorizer is created with an application name as well as a consumer key, the application name is mentioned in the User-Agent header as well. >>> authorizer = OAuthAuthorizer( ... valid_consumer.key, access_token=valid_token, ... application_name="the app") >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") send: 'GET /...user-agent: lazr.restfulclient...; application="the app"; oauth_consumer="consumer"...' ... >>> httplib2.debuglevel = 0 It's even possible to get anonymous access by providing an empty access token. >>> authorizer = OAuthAuthorizer( ... valid_consumer.key, access_token=empty_token) >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") Because of the way the AnonymousAccessDataStore (defined earlier in the test) works, you can even get anonymous access by specifying an OAuth consumer that's not in the server-side list of valid consumers. >>> authorizer = OAuthAuthorizer( ... "random consumer", access_token=empty_token) >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") A ServiceRoot object has a 'credentials' attribute which contains the Authorizer used to authorize outgoing requests. >>> from lazr.restfulclient.resource import ServiceRoot >>> root = ServiceRoot(authorizer, "http://api.launchpad.dev/") >>> root.credentials If you try to provide credentials with an unrecognized OAuth consumer, you'll get an error--even if the credentials are valid. The data store used in this test only lets unrecognized OAuth consumers through when they request anonymous access. >>> authorizer = OAuthAuthorizer( ... 'random consumer', access_token=valid_token) >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") Traceback (most recent call last): ... Unauthorized: HTTP Error 401: Unauthorized ... >>> authorizer = OAuthAuthorizer( ... 'random consumer', access_token=OAuthToken("invalid", "token")) >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") Traceback (most recent call last): ... Unauthorized: HTTP Error 401: Unauthorized ... Teardown. >>> _ = wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1524828521.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/caching.rst0000644000175000017500000002173500000000000026645 0ustar00cjwatsoncjwatson00000000000000******* Caching ******* lazr.restfulclient automatically caches the responses to its requests in a temporary directory. >>> import httplib2 >>> httplib2.debuglevel = 1 >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service_with_cache = CookbookWebServiceClient() send: 'GET /1.0/ ... reply: ...200... ... header: Content-Type: application/vnd.sun.wadl+xml ... send: 'GET /1.0/ ... reply: ...200... ... header: Content-Type: application/json ... >>> print service_with_cache.recipes[4].instructions send: 'GET /1.0/recipes/4 ... reply: ...200... ... Preheat oven to... The second and subsequent times you request some object, it's likely that lazr.restfulclient will make a conditional HTTP GET request instead of a normal request. The HTTP response code will be 304 instead of 200, and lazr.restfulclient will use the cached representation of the object. >>> print service_with_cache.recipes[4].instructions send: 'GET /1.0/recipes/4 ... reply: ...304... ... Preheat oven to... This is true even if you initially got the object as part of a collection. >>> recipes = service_with_cache.recipes[:10] send: ... reply: ...200... >>> first_recipe = recipes[0] >>> first_recipe.lp_refresh() send: ... reply: ...304... Note that if you get an object as part of a collection and then get it some other way, a conditional GET request will *not* be made. This is a shortcoming of the library. >>> service_with_cache.recipes[first_recipe.id] send: ... reply: ...200... The default lazr.restfulclient cache directory is a temporary directory that's deleted when the Python process ends. (If the process is killed, the directory will stick around in /tmp.) It's much more efficient to keep a cache directory across multiple uses of lazr.restfulclient. You can provide a cache directory name as argument when creating a Service object. This directory will fill up with cached HTTP responses, and since it's a directory you control it will persist across lazr.restfulclient sessions. >>> import tempfile >>> tempdir = tempfile.mkdtemp() >>> first_service = CookbookWebServiceClient(cache=tempdir) send: 'GET /1.0/ ... reply: ...200... ... send: 'GET /1.0/ ... reply: ...200... ... >>> print first_service.recipes[4].instructions send: 'GET /1.0/recipes/4 ... reply: ...200... ... Preheat oven to... This will save you a *lot* of time in subsequent sessions, because you'll be able to use cached versions of the initial (very expensive) documents. A new client will not re-request the service root at all. >>> second_service = CookbookWebServiceClient(cache=unicode(tempdir)) You'll also be able to make conditional requests for many resources and avoid transferring their full representations. >>> print second_service.recipes[4].instructions send: 'GET /1.0/recipes/4 ... reply: ...304... ... Preheat oven to... Of course, if you ever need to clear the cache directory, you'll have to do it yourself. Cleanup. >>> import shutil >>> shutil.rmtree(tempdir) Cache expiration ---------------- The '1.0' version of the example web service, which we've been using up til now, sets a long cache expiry time for the service root. That's why we were able to create a second client that didn't request the service root at all--just fetched the representations from its cache. The 'devel' version of the example web service sets a cache expiry time of two seconds. Let's see what that looks like on the client side. >>> tempdir = tempfile.mkdtemp() >>> first_service = CookbookWebServiceClient( ... cache=tempdir, version='devel') send: 'GET /devel/ ... reply: ...200... ... send: 'GET /devel/ ... reply: ...200... ... Now let's wait for three seconds to make sure the representations become stale. >>> from time import sleep >>> sleep(3) When the representations are stale, a new client makes *conditional* requests for the representations. If the conditions fail (as they do here), the cached representations are considered to have been refreshed, just as if the server had sent them again. >>> second_service = CookbookWebServiceClient( ... cache=tempdir, version='devel') send: 'GET /devel/ ... reply: ...304... ... send: 'GET /devel/ ... reply: ...304... ... Let's quickly create another client before the representation grows stale again. >>> second_service = CookbookWebServiceClient( ... cache=tempdir, version='devel') When the representations are not stale, a new client does not make any HTTP requests at all--it fetches representations direct from the cache. Cleanup. >>> httplib2.debuglevel = 0 >>> shutil.rmtree(tempdir) Cache filenames --------------- lazr.restfulclient caches HTTP repsonses in individual files named after the URL accessed. This is behavior derived from httplib2, but lazr.restfulclient does two things differently from httplib2. To see these two things, let's set up a client that uses a temporary directory as a cache file. The directory starts out empty. >>> from os import listdir >>> tempdir = tempfile.mkdtemp() >>> len(listdir(tempdir)) 0 As soon as we create a client object, though, lazr.restfulclient fetches a JSON and a WADL representation of the service root, and caches them individually. >>> service = CookbookWebServiceClient(cache=tempdir) >>> cache_contents = listdir(tempdir) >>> for file in sorted(cache_contents): ... print file cookbooks.dev...application,json... cookbooks.dev...vnd.sun.wadl+xml... This is the first difference between lazr.restfulclient's caching and httplib2's. httplib2 would store all requests for the service root in a filename based solely on the URL. This effectively limits httplib2 to a single representation of a given resource: the WADL representation would be overwritten with the JSON representation. lazr.restfulclient incorporates the media type in the cache filename, so that WADL and JSON representations are stored separately. The second difference has to do with filename length limits. httplib2 caps filenames at about 240 characters so that cache files can be stored on filesystems with 255-character filename length limits. For compatibility with eCryptfs filesystems, lazr.restfulclient goes further, and caps filenames at 143 characters. To test out the limit, let's create a cookbook with an incredibly long name. >>> long_name = ( ... "This cookbook name is amazingly long; so long that it will " ... "surely be truncated when it is incorporated into a file " ... "name for the cache. The cache file will contain a cached " ... "HTTP respone containing a JSON representation of of this " ... "cookbook, whose name, I repeat, is very long indeed.") >>> len(long_name) 281 >>> import datetime >>> date = datetime.datetime(1994, 1, 1) >>> book = service.cookbooks.create( ... name=long_name, cuisine="General", copyright_date=date, ... price=10.22, last_printing=date) lazr.restfulclient automatically fetched a JSON representation of the new cookbook, so it's already present in the cache. Because a cookbook's URL incorporates its name, and this cookbook's name is incredibly long, it must have been truncated to fit on disk. >>> [cookbook_cache_filename] = [file for file in listdir(tempdir) ... if 'amazingly' in file] Indeed, the filename has been truncated to fit in the rough 143-character safety limit for eCryptfs filesystems. >>> len(cookbook_cache_filename) 143 Despite the truncation, some of the useful information from the cookbook's name makes it into the filename, making it easy to find when manually crawling through the cache directory. >>> print cookbook_cache_filename cookbooks.dev...This%20cookbook%20name%20is%20amazingly%20long... To avoid conflicts caused by truncation, the filename always ends with an MD5 sum derived from the untruncated URL. Let's create a second cookbook whose name differs from the first cookbook only at the end. >>> longer_name = long_name + ": The Sequel" >>> book = service.cookbooks.create( ... name=longer_name, cuisine="General", copyright_date=date, ... price=10.22, last_printing=date) This cookbook's URL is identical to the first cookbook's URL for far longer than 143 characters. But since the truncated filename incorporates an MD5 sum based on the full URL, the two cookbooks are cached in separate files. >>> [file1, file2] = [file for file in listdir(tempdir) ... if 'amazingly' in file] The filenames are identical up to the last 32 characters, which is where the MD5 sum begins. But because the MD5 sums are different, they are not completely identical. >>> file1[:-32] == file2[:-32] True >>> file1 == file2 False Cleanup. >>> import shutil >>> shutil.rmtree(tempdir) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/collections.rst0000644000175000017500000001100200000000000027551 0ustar00cjwatsoncjwatson00000000000000*********** Collections *********** lazr.restful makes collections of data available through Pythonic mechanisms like slices. >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service = CookbookWebServiceClient() You can iterate through all the items in a collection. >>> names = sorted([recipe.dish.name for recipe in service.recipes]) >>> len(names) 5 >>> names [u'Baked beans', ..., u'Roast chicken'] But it's almost always better to slice them. >>> sorted([recipe.dish.name for recipe in service.recipes[:2]]) [u'Roast chicken', u'Roast chicken'] You can get a slice of any collection, so long as you provide start and end points keyed to the beginning of the list. You can't key a slice to the end of the list because it might be expensive to calculate how big the list is. This set-up code creates a regular Python list of all recipes on the site, for comparison with a lazr.restful Collection object representing the same list. >>> all_recipes = [recipe for recipe in service.recipes] >>> recipes = service.recipes Calling len() on the Collection object makes sure that the first page of representations is cached, which forces this test to test an optimization. >>> ignored = len(recipes) These tests demonstrate that slicing the collection resource gives the same results as collecting all the entries in the collection, and slicing an ordinary list. >>> def slices_match(slice): ... """Slice two lists of recipes, then make sure they're the same.""" ... list1 = recipes[slice] ... list2 = all_recipes[slice] ... if len(list1) != len(list2): ... raise ("Lists are different sizes: %d vs. %d" % ... (len(list1), len(list2))) ... for index in range(0, len(list1)): ... if list1[index].id != list2[index].id: ... raise ("%s doesn't match %s in position %d" % ... (list1[index].id, list2[index].id, index)) ... return True >>> slices_match(slice(3)) True >>> slices_match(slice(50)) True >>> slices_match(slice(1,2)) True >>> slices_match(slice(2,21)) True >>> slices_match(slice(2,21,3)) True >>> slices_match(slice(0, 200)) True >>> slices_match(slice(30, 200)) True >>> slices_match(slice(60, 100)) True >>> recipes[5:] Traceback (most recent call last): ... ValueError: Collection slices must have a definite, nonnegative end point. >>> recipes[10:-1] Traceback (most recent call last): ... ValueError: Collection slices must have a definite, nonnegative end point. >>> recipes[-1:] Traceback (most recent call last): ... ValueError: Collection slices must have a nonnegative start point. >>> recipes[:] Traceback (most recent call last): ... ValueError: Collection slices must have a definite, nonnegative end point. You can slice a collection that's the return value of a named operation. >>> e_recipes = service.cookbooks.find_recipes(search='e') >>> len(e_recipes[1:3]) 2 You can also access individual items in this collection by index. >>> print e_recipes[1].dish.name Foies de voilaille en aspic >>> e_recipes[1000] Traceback (most recent call last): ... IndexError: list index out of range When are representations fetched? ================================= To avoid unnecessary HTTP requests, a representation of a collection is fetched at the last possible moment. Let's see what that means. >>> import httplib2 >>> httplib2.debuglevel = 1 >>> service = CookbookWebServiceClient() send: ... ... Just accessing a top-level collection doesn't trigger an HTTP request. >>> recipes = service.recipes >>> dishes = service.dishes >>> cookbooks = service.cookbooks Getting the length of the collection, or any entry from the collection, triggers an HTTP request. >>> len(recipes) send: 'GET /1.0/recipes... ... >>> dish = dishes[1] send: 'GET /1.0/dishes... ... Invoking a named operation will also trigger an HTTP request. >>> cookbooks.find_recipes(search="foo") send: ... ... Scoped collections work the same way: just getting a reference to the collection doesn't trigger an HTTP request. >>> recipes = dish.recipes But getting any information about the collection triggers an HTTP request. >>> len(recipes) send: 'GET /1.0/dishes/.../recipes ... ... Cleanup. >>> httplib2.debuglevel = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1525766393.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/entries.rst0000644000175000017500000005017500000000000026722 0ustar00cjwatsoncjwatson00000000000000****************** Entry manipulation ****************** Objects available through the web interface, such as cookbooks, have a readable interface which is available through direct attribute access. >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service = CookbookWebServiceClient() >>> recipe = service.recipes[1] >>> print recipe.instructions You can always judge... These objects may have a number of attributes, as well as associated entries and collections. >>> cookbook = recipe.cookbook >>> print cookbook.name Mastering the Art of French Cooking >>> len(cookbook.recipes) 2 The lp_* introspection methods let you know what you can do with an object. You can also use dir(), but it'll be cluttered with all sorts of other stuff. >>> sorted(dir(cookbook)) [..., 'confirmed', 'copyright_date', 'cover', ... 'find_recipes', ..., 'recipes', ...] >>> sorted(cookbook.lp_attributes) ['confirmed', 'copyright_date', ..., 'resource_type_link', ..., 'self_link', 'web_link'] >>> sorted(cookbook.lp_entries) ['cover'] >>> sorted(cookbook.lp_collections) ['recipes'] >>> sorted(cookbook.lp_operations) ['find_recipe_for', 'find_recipes', 'make_more_interesting', 'replace_cover'] Some attributes can only take on certain values. The lp_values_for method will show you these values. >>> sorted(cookbook.lp_values_for('cuisine')) ['American', 'Dessert', u'Fran\xe7aise', 'General', 'Vegetarian'] Some attributes don't have a predefined list of acceptable values. For them, lp_values_for() returns None. >>> print cookbook.lp_values_for('copyright_date') None Some of these attributes can be changed. For example, a client can change a recipe's preparation instructions. When changing attribute values though, the changes are not pushed to the web service until the entry is explicitly saved. This allows the client to batch the changes over the wire for efficiency. >>> recipe.instructions = 'Modified instructions' >>> print service.recipes[1].instructions You can always judge... Once the changes are saved though, they are propagated to the web service. >>> recipe.lp_save() >>> print service.recipes[1].instructions Modified instructions An entry object is a normal Python object like any other. Attributes of an entry, like 'cuisine' or 'cookbook', are available as attributes on the resource, and may be set. Random strings that are not attributes of the entry cannot be set or read as Python attributes. >>> recipe.instructions = 'Different instructions' >>> recipe.is_great = True Traceback (most recent call last): ... AttributeError: 'Entry' object has no attribute 'is_great' >>> recipe.is_great Traceback (most recent call last): ... AttributeError: http://cookbooks.dev/1.0/recipes/1 object has no attribute 'is_great' The client can set more than one attribute on an entry at a time: they'll all be changed when the entry is saved. >>> cookbook.cuisine u'Fran\xe7aise' >>> cookbook.description u'' >>> cookbook.cuisine = 'Dessert' >>> cookbook.description = "A new description" >>> cookbook.lp_save() >>> cookbook = service.recipes[1].cookbook >>> print cookbook.cuisine Dessert >>> print cookbook.description A new description Some of an entry's attributes may take other resources as values. >>> old_cookbook = recipe.cookbook >>> other_cookbook = service.cookbooks['Everyday Greens'] >>> print other_cookbook.name Everyday Greens >>> recipe.cookbook = other_cookbook >>> recipe.lp_save() >>> print recipe.cookbook.name Everyday Greens >>> recipe.cookbook = old_cookbook >>> recipe.lp_save() Refreshing data --------------- Here are two objects representing recipe #1. We'll fetch a representation for the first object right away... >>> recipe_copy = service.recipes[1] >>> print recipe_copy.instructions Different instructions ...but retrieve the second object in a way that doesn't fetch its representation. >>> recipe_copy_2 = service.recipes(1) An entry is automatically refreshed after saving. >>> recipe.instructions = 'Even newer instructions' >>> recipe.lp_save() >>> print recipe.instructions Even newer instructions If an old object representing that entry already has a representation, it will still show the old data. >>> print recipe_copy.instructions Different instructions If an old object representing that entry doesn't have a representation yet, it will show the new data. >>> print recipe_copy_2.instructions Even newer instructions You can also refresh a resource object manually. >>> recipe_copy.lp_refresh() >>> print recipe_copy.instructions Even newer instructions Bookmarking an entry -------------------- You can get an entry's URL from the 'self_link' attribute, save the URL for a while, and retrieve the entry later using the load() function. >>> bookmark = recipe.self_link >>> new_recipe = service.load(bookmark) >>> print new_recipe.dish.name Roast chicken You can bookmark a URI relative to the version of the web service currently in use. >>> cookbooks = service.load("cookbooks") >>> assert isinstance(cookbooks._wadl_resource.url, basestring) >>> print cookbooks._wadl_resource.url http://cookbooks.dev/1.0/cookbooks >>> print cookbooks['The Joy of Cooking'].self_link http://cookbooks.dev/1.0/cookbooks/The%20Joy%20of%20Cooking >>> cookbook = service.load("/cookbooks/The%20Joy%20of%20Cooking") >>> assert isinstance(cookbook._wadl_resource.url, basestring) >>> print cookbook._wadl_resource.url http://cookbooks.dev/1.0/cookbooks/The%20Joy%20of%20Cooking >>> print cookbook.self_link http://cookbooks.dev/1.0/cookbooks/The%20Joy%20of%20Cooking >>> service_root = service.load("") >>> assert isinstance(service_root._wadl_resource.url, basestring) >>> print service_root._wadl_resource.url http://cookbooks.dev/1.0/ >>> print service_root.cookbooks['The Joy of Cooking'].name The Joy of Cooking But you can't provide the web service version and bookmark a URI relative to the service root. >>> cookbooks = service.load("/1.0/cookbooks") Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... (That code attempts to load http://cookbooks.dev/1.0/1.0/cookbooks, which doesn't exist.) You can't bookmark an absolute or relative URI that has nothing to do with the web service. >>> bookmark = 'http://cookbooks.dev/' >>> service.load(bookmark) Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... >>> service.load("/no-such-url") Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... You can't bookmark the return value of a named operation. This is not really desirable, but that's how things work right now. >>> url_without_type = ('http://cookbooks.dev/1.0/cookbooks' + ... '?ws.op=find_recipes&search=a') >>> service.load(url_without_type) Traceback (most recent call last): ... ValueError: Couldn't determine the resource type of... Moving an entry --------------- Some entries will move to different URLs when a client changes their data attributes. For instance, a cookbook's URL is determined by its name. >>> cookbook = service.cookbooks['The Joy of Cooking'] >>> print cookbook.name The Joy of Cooking >>> old_link = cookbook.self_link >>> print old_link http://cookbooks.dev/1.0/cookbooks/The%20Joy%20of%20Cooking >>> cookbook.name = "Another Name" >>> cookbook.lp_save() Change the name, and you change the URL. >>> new_link = cookbook.self_link >>> print new_link http://cookbooks.dev/1.0/cookbooks/Another%20Name Old bookmarks won't work anymore. >>> print service.load(old_link) Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... >>> print service.load(new_link).name Another Name Under the covers though, a refresh of the original object has been retrieved from the web service, so it's safe to continue using, and changing it. >>> cookbook.description = u'This cookbook was renamed' >>> cookbook.lp_save() >>> print service.load(new_link).description This cookbook was renamed It's just as easy to move this cookbook back to the old name. >>> cookbook.name = 'The Joy of Cooking' >>> cookbook.lp_save() Now the old bookmark works again, and the new bookmark no longer works. >>> print service.load(old_link).name The Joy of Cooking >>> print service.load(new_link) Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... Validation ---------- Some attributes are subject to validation. For instance, a cookbook's cuisine is limited to one of a few selections. >>> from lazr.restfulclient.errors import HTTPError >>> def print_error_on_save(entry): ... try: ... entry.lp_save() ... except HTTPError, error: ... for line in sorted(error.content.splitlines()): ... print line.decode("utf-8") ... else: ... print 'Did not get expected HTTPError!' >>> cookbook.cuisine = 'No such cuisine' >>> print_error_on_save(cookbook) cuisine: Invalid value "No such cuisine". Acceptable values are: ... >>> cookbook.cuisine = 'General' Some attributes can't be modified at all. >>> cookbook.copyright_date = None >>> print_error_on_save(cookbook) copyright_date: You tried to modify a read-only attribute. If the client tries to save an entry that has more than one problem, it will get back an error message listing all the problems. >>> cookbook.cuisine = 'No such cuisine' >>> print_error_on_save(cookbook) copyright_date: You tried to modify a read-only attribute. cuisine: Invalid value "No such cuisine". Acceptable values are: ... Server-side data massage ------------------------ Send bad data and your request will be rejected. But if you send data that's not quite what the server is expecting, the server may accept it while tweaking it. This means that the state of your object after you call lp_save() may be slightly different from the object before you called lp_save(). >>> cookbook.lp_refresh() >>> cookbook.description = " Some extraneous whitespace " >>> cookbook.lp_save() >>> cookbook.description u'Some extraneous whitespace' Data types ---------- Incoming data is serialized from JSON, and all the JSON data types appear to the end-user as native Python data types. But there's no standard serialization for JSON dates, so those are handled separately. From the perspective of the end-user, date and date-time fields always look like Python datetime objects or None. >>> cookbook.copyright_date datetime.datetime(1995, 1, 1,...) >>> from datetime import datetime >>> cookbook.last_printing = datetime(2009, 1, 1) >>> cookbook.lp_save() Avoiding conflicts ================== lazr.restful and lazr.restfulclient work together to try to avoid situations where one person unknowingly overwrites another's work. Here, two different clients are interested in the same lazr.restful object. >>> first_client = CookbookWebServiceClient() >>> first_cookbook = first_client.load(cookbook.self_link) >>> first_description = first_cookbook.description >>> second_client = CookbookWebServiceClient() >>> second_cookbook = second_client.load(cookbook.self_link) >>> second_cookbook.description == first_description True The first client decides to change the description. >>> first_cookbook.description = 'A description.' >>> first_cookbook.lp_save() The second client tries to make a conflicting change, but the server detects that the second client doesn't have the latest information, and rejects the request. >>> second_cookbook.description = 'A conflicting description.' >>> second_cookbook.lp_save() Traceback (most recent call last): ... PreconditionFailed: HTTP Error 412: Precondition Failed ... Now the second client has a chance to look at the changes that were made, before making their own changes. >>> second_cookbook.lp_refresh() >>> print second_cookbook.description A description. >>> second_cookbook.description = 'A conflicting description.' >>> second_cookbook.lp_save() Conflict detection works even when you operate on an object you retrieved from a collection. >>> first_cookbook = first_client.cookbooks[:10][0] >>> second_cookbook = second_client.cookbooks[:10][0] >>> first_cookbook.name == second_cookbook.name True >>> first_cookbook.description = "A description" >>> first_cookbook.lp_save() >>> second_cookbook.description = "A conflicting description" >>> second_cookbook.lp_save() Traceback (most recent call last): ... PreconditionFailed: HTTP Error 412: Precondition Failed ... >>> second_cookbook.lp_refresh() >>> print second_cookbook.description A description >>> second_cookbook.description = "A conflicting description" >>> second_cookbook.lp_save() >>> first_cookbook.lp_refresh() >>> print first_cookbook.description A conflicting description Comparing entries ----------------- Two entries are equal if they represent the same state of the same server-side resource. >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service = CookbookWebServiceClient() What does this mean? Well, two distinct objects that represent the same resource are equal. >>> recipe = service.recipes[1] >>> recipe_2 = service.load(recipe.self_link) >>> recipe is recipe_2 False >>> recipe == recipe_2 True >>> recipe != recipe_2 False Two totally different entries are not equal. >>> another_recipe = service.recipes[2] >>> recipe == another_recipe False An entry can be compared to None, but the comparison never succeeds. >>> recipe == None False If one entry represents the current state of the server, and the other is out of date or has client-side modifications, they will not be considered equal. Here, 'recipe' has been modified and 'recipe_2' represents the current state of the server. >>> recipe.instructions = "Modified for equality testing." >>> recipe == recipe_2 False After a save, 'recipe' is up to date, and 'recipe_2' is out of date. >>> recipe.lp_save() >>> recipe == recipe_2 False Refreshing 'recipe_2' brings it up to date, and equality succeeds again. >>> recipe_2.lp_refresh() >>> recipe == recipe_2 True If you make the *exact same* client-side modifications to two objects representing the same resource, the objects will be considered equal. >>> recipe.instructions = "Modified again." >>> recipe_2.instructions = recipe.instructions >>> recipe == recipe_2 True If you then save one of the objects, they will stop being equal, because the saved object has a new ETag. >>> recipe.lp_save() >>> recipe == recipe_2 False Server-side permissions ----------------------- The server may hide some data from you because you lack the permission to see it. To avoid objects that are mysteriously missing fields, the server will serve a special "redacted" value that lets you know you don't have permission to see the data. >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service = CookbookWebServiceClient() >>> cookbook = service.recipes[1].cookbook >>> print cookbook.confirmed tag:launchpad.net:2008:redacted If you try to make an HTTP request for the "redacted" value (usually by following a link that you don't know is redacted), you'll get a helpful error. >>> service.load("tag:launchpad.net:2008:redacted") Traceback (most recent call last): ... ValueError: You tried to access a resource that you don't have the server-side permission to see. Deleting an entry ================= Some entries can be deleted with the lp_delete method. Before demonstrating this, let's acquire the underlying data model objects so that we can restore the entry later. This is a bit of a hack, but it's a lot less work than any alternative. >>> from lazr.restful.example.base.interfaces import IRecipeSet >>> from zope.component import getUtility >>> recipe_set = getUtility(IRecipeSet) >>> underlying_recipe = recipe_set.get(6) >>> underlying_cookbook = underlying_recipe.cookbook Now let's delete the entry. >>> recipe = service.recipes[6] >>> print recipe.lp_delete() None A deleted entry no longer exists. >>> recipe.lp_refresh() Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... Some entries can't be deleted. >>> cookbook.lp_delete() Traceback (most recent call last): ... MethodNotAllowed: HTTP Error 405: Method Not Allowed ... Cleanup: restore the deleted recipe. >>> recipe_set.recipes.append(underlying_recipe) >>> underlying_cookbook.recipes.append(underlying_recipe) When are representations fetched? ================================= To avoid unnecessary HTTP requests, a representation of an entry is fetched at the last possible moment. Let's see what that means. >>> import httplib2 >>> httplib2.debuglevel = 1 >>> service = CookbookWebServiceClient() send: ... ... Here's an entry we got from a lookup operation on a top-level collection. The default top-level lookup operation fetches a representation of an entry immediately so as to immediately signal errors. >>> recipe = service.recipes[1] send: 'GET /1.0/recipes/1 ...' ... But there's also a lookup operation that only triggers an HTTP request when we try to get some data from the entry: >>> recipe1 = service.recipes(1) This gives a recipe object, because CookbookWebServiceClient happens to know that the 'recipes' collection contains recipe objects. Here's the dish associated with that original recipe entry. Traversing from one entry to another causes an HTTP request for the first entry (the recipe). Without this HTTP request, there's no way to know the URL of the dish. >>> dish = recipe1.dish send: 'GET /1.0/recipes/1 ...' ... Note that this request is a request for the _recipe_, not the dish. We don't need to know anything about the dish yet. And now that we have a representation of the recipe, we can traverse from the recipe to its cookbook without making another request. >>> cookbook = recipe1.cookbook Accessing any information about an entry we've traversed to _will_ cause an HTTP request. >>> print dish.name send: 'GET /1.0/dishes/Roast%20chicken ...' ... Roast chicken Invoking a named operation also causes one (and only one) HTTP request. >>> recipes = cookbook.find_recipes(search="foo") send: 'GET /1.0/cookbooks/...ws.op=find_recipes...' ... Even dereferencing an entry from another entry and then invoking a named operation causes only one HTTP request. >>> recipes = recipe1.cookbook.find_recipes(search="bar") send: 'GET /1.0/cookbooks/...ws.op=find_recipes...' ... In all cases we are able to delay HTTP requests until the moment we need data that can only be found by making those HTTP requests (even if, as in the first example, that data is "does this object exist?). If it turns out we never need that data, we've eliminated a request entirely. If CookbookWebServiceClient didn't know that the 'recipes' collection contained recipe objects, then doing a lookup on that collection *would* trigger an HTTP request. There'd simply be no other way to know what kind of object was at the other end of the URL. >>> from lazr.restfulclient.tests.example import RecipeSet >>> old_collection_of = RecipeSet.collection_of >>> RecipeSet.collection_of = None >>> recipe1 = service.recipes[1] send: 'GET /1.0/recipes/1 ...' ... On the plus side, at least accessing this object's properties doesn't require _another_ HTTP request. >>> print recipe1.instructions Modified again. Cleanup. >>> RecipeSet.collection_of = old_collection_of >>> httplib2.debuglevel = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/hosted-files.rst0000644000175000017500000001137500000000000027636 0ustar00cjwatsoncjwatson00000000000000************ Hosted files ************ Some resources published by lazr.restful are externally hosted files that can have binary representations. lazr.restfulclient gives you access to these resources. >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service = CookbookWebServiceClient() An example of a hosted binary file is the cover of a cookbook. "Everyday Greens" starts off with no cover. >>> greens = service.cookbooks['Everyday Greens'] >>> cover = greens.cover >>> sorted(dir(cover)) [..., 'open'] >>> cover.open() Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... You can open a hosted file for write access and write to it as though it were a file on disk. >>> image = "Pretend this is an image." >>> len(image) 25 >>> file_handle = cover.open("w", "image/png", "a-cover.png") >>> file_handle.content_type 'image/png' >>> file_handle.filename 'a-cover.png' >>> print file_handle.last_modified None >>> file_handle.write(image) >>> file_handle.close() Once it exists on the server, you can open a hosted file for read access and read it. >>> file_handle = cover.open() >>> file_handle.content_type 'image/png' >>> file_handle.filename '0' >>> last_modified = file_handle.last_modified >>> last_modified is None False >>> len(file_handle.read()) 25 Note that the filename is '0', not 'a-cover.png'. The filename from the server is implementation-dependent and may not have anything to do with the filename the client sent. If the server implementation uses lazr.librarian, it will serve files with the originally uploaded filename, but the example web service uses its own, simpler implementation which serves the file's ID as the filename. Modifying a file will change its 'last_modified' attribute. >>> file_handle = cover.open("w", "image/png", "another-cover.png") >>> file_handle.write(image) >>> file_handle.close() >>> file_handle = cover.open() >>> file_handle.filename '1' >>> last_modified_2 = file_handle.last_modified >>> last_modified == last_modified_2 False Once a file exists, it can be deleted. >>> cover.delete() >>> cover.open() Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... Comparing hosted files ---------------------- Two hosted file objects are the same if they point to the same server-side resource. >>> cover = service.cookbooks['Everyday Greens'].cover >>> cover_2 = service.cookbooks['Everyday Greens'].cover >>> cover == cover_2 True >>> other_cover = service.cookbooks['The Joy of Cooking'].cover >>> cover == other_cover False A hosted file can be compared to None, but the comparison never succeeds. >>> cover == None False Error handling -------------- The only access modes supported are 'r' and 'w'. >>> cover.open("r+") Traceback (most recent call last): ... ValueError: Invalid mode. Supported modes are: r, w When opening a file for write access, you must specify the content_type argument. >>> cover.open("w") Traceback (most recent call last): ... ValueError: Files opened for write access must specify content_type. >>> cover.open("w", "image/png") Traceback (most recent call last): ... ValueError: Files opened for write access must specify filename. When opening a file for read access, you must *not* specify the content_type or filename arguments--they come from the server. >>> cover.open("r", "image/png") Traceback (most recent call last): ... ValueError: Files opened for read access can't specify content_type. >>> cover.open("r", filename="foo.png") Traceback (most recent call last): ... ValueError: Files opened for read access can't specify filename. Caching ------- Hosted file resources implement the normal server-side caching mechanism. >>> file_handle = cover.open("w", "image/png", "image.png") >>> file_handle.write(image) >>> file_handle.close() >>> import httplib2 >>> httplib2.debuglevel = 1 >>> service = CookbookWebServiceClient() send: ... >>> cover = service.cookbooks['Everyday Greens'].cover send: ... The first request for a file retrieves the file from the server. >>> len(cover.open().read()) send: ... reply: '...303 See Other... reply: '...200 Ok... 25 The second request retrieves the file from the cache. >>> len(cover.open().read()) send: ... reply: '...303 See Other... reply: '...304 Not Modified... 25 Finally, some cleanup code that deletes the cover. >>> cover.delete() send: 'DELETE... reply: '...200... >>> httplib2.debuglevel = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1572870896.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/index.rst0000644000175000017500000000275400000000000026360 0ustar00cjwatsoncjwatson00000000000000.. This file is part of lazr.restfulclient. lazr.restfulclient is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 of the License. lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with lazr.restfulclient. If not, see . LAZR restfulclient ****************** A programmable client library that takes advantage of the commonalities among lazr.restful web services to provide added functionality on top of wadllib. Please see https://dev.launchpad.net/LazrStyleGuide and https://dev.launchpad.net/Hacking for how to develop in this package. .. pypi description ends here .. toctree:: :glob: toplevel collections entries operations hosted-files caching authorizer.standalone retry.standalone NEWS .. _Sphinx: http://sphinx.pocoo.org/ .. _Table of contents: http://sphinx.pocoo.org/concepts.html#the-toc-tree Importable ========== The lazr.restfulclient package is importable, and has a version number. >>> import lazr.restfulclient >>> print 'VERSION:', lazr.restfulclient.__version__ VERSION: ... ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1524828521.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/operations.rst0000644000175000017500000001164700000000000027435 0ustar00cjwatsoncjwatson00000000000000**************** Named operations **************** Entries and collections support named operations: one-off functionality that's been given a name and a set of parameters. >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service = CookbookWebServiceClient() Arguments to named operations are automatically converted to JSON for transmission over the wire. Here's a named operation that takes a boolean argument. >>> [recipe for recipe in service.cookbooks.find_recipes( ... search="Chicken", vegetarian=True)] [] Strings that happen to be numbers are handled properly. Here, if "1.234" were converted into a number at any point in the chain, the 'search' operation on the server wouldn't know how to handle it and the request would fail. >>> [people for people in service.cookbooks.find_recipes(search="1.234")] [] Counts ------ Named operations that return a collection return either a link that lazr.restfulclient can follow to get the size of the collection or (under some circumstances) the length itself. The len() function hides this indirection from the end-user. >>> results = service.cookbooks.find_recipes(search="Chicken") >>> print len(results) 0 Special data types ------------------ lazr.restfulclient uses some data types that don't directly correspond to JSON data types. These data types can be used in named operations. For instance, a named operation can take a date or datetime object as one of its arguments. >>> import datetime >>> date = datetime.datetime(1994, 1, 1) >>> cookbook = service.cookbooks.create( ... name="New cookbook", cuisine="General", ... copyright_date=date, price=1.23, last_printing=date) >>> print cookbook.name New cookbook A named operation can take an entry as one of its arguments. lazr.restfulclient lets you pass in the actual entry as the argument value. >>> dish = service.recipes[1].dish >>> cookbook = service.recipes[1].cookbook >>> print cookbook.find_recipe_for(dish=dish) http://cookbooks.dev/1.0/recipes/1 A named operation can take binary data as one of its arguments. >>> cookbook.replace_cover(cover="\x00\xe2\xe3") >>> cookbook.cover.open().read() '\x00\xe2\xe3' A named operation that returns a null value corresponds to a Python value of None. >>> dish = service.recipes[4].dish >>> print cookbook.find_recipe_for(dish=dish) None A named operation may change the resource's location so we get a 301 response with the new URL. >>> from urllib import quote >>> from lazr.restful.testing.webservice import WebServiceCaller >>> webservice = WebServiceCaller(domain='cookbooks.dev') >>> url = quote("/cookbooks/New cookbook") >>> print webservice.named_post(url, 'make_more_interesting') HTTP/1.1 301 Moved Permanently ... Location: http://cookbooks.dev/devel/cookbooks/The%20New%20New%20cookbook ... JSON-encoding ------------- lazr.restfulclient encodes most arguments (even string arguments) as JSON before sending them over the wire. This way, a named operation that takes a string argument can take a string that looks like a JSON object without causing confusion. >>> cookbooks = service.cookbooks.find_for_cuisine(cuisine="General") >>> len([cookbook for cookbook in cookbooks]) > 0 True >>> cookbook = service.cookbooks.create( ... name="null", cuisine="General", ... copyright_date=date, price=1.23, last_printing=date) >>> cookbook.name u'null' >>> cookbook = service.cookbooks.create( ... name="4.56", cuisine="General", ... copyright_date=date, price=1.23, last_printing=date) >>> cookbook.name u'4.56' >>> cookbook = service.cookbooks.create( ... name='"foo"', cuisine="General", ... copyright_date=date, price=1.23, last_printing=date) >>> cookbook.name u'"foo"' A named operation that takes a non-string object (such as a float) will not accept a string that's the JSON representation of the object. >>> try: ... service.cookbooks.create( ... name="Yet another 1.23 cookbook", cuisine="General", ... copyright_date=date, last_printing=date, price="1.23") ... except Exception, e: ... print e.content price: got 'unicode', expected float, int: u'1.23' Named operations on collections don't fetch the collections ----------------------------------------------------------- If you invoke a named operation on a collection, the only HTTP request made is the one for the named operation. You don't have to get a representation of the collection to invoke the operation. >>> import httplib2 >>> httplib2.debuglevel = 1 >>> service = CookbookWebServiceClient() send: ... ... >>> print service.cookbooks.find_recipes( ... search="Chicken", vegetarian=True) send: 'GET /1.0/cookbooks?...vegetarian=true...' ... Cleanup. >>> httplib2.debuglevel = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1524828521.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/retry.standalone.rst0000644000175000017500000000775000000000000030546 0ustar00cjwatsoncjwatson00000000000000Retry requests on server error ****************************** If lazr.restfulclient talks to a server that sends out a server-side error with status codes 502 or 503, the client will wait a few seconds and try the request again. Eventually it will give up and escalate the error code in the form of an exception. To test this, let's simulate a lazr.restful server prone to transient errors using a WSGI application. >>> import pkg_resources >>> wadl_string = pkg_resources.resource_string( ... 'wadllib.tests.data', 'launchpad-wadl.xml') >>> representations = { 'application/vnd.sun.wadl+xml' : wadl_string, ... 'application/json' : '{}' } This application will cause one request to fail for every item in its BROKEN_RESPONSES list. >>> BROKEN_RESPONSES = [] >>> def broken_application(environ, start_response): ... if len(BROKEN_RESPONSES) > 0: ... start_response(str(BROKEN_RESPONSES.pop()), ... [('Content-type', 'text/plain')]) ... return ["Sorry, I'm still broken."] ... else: ... media_type = environ['HTTP_ACCEPT'] ... content = representations[media_type] ... start_response( ... '200', [('Content-type', media_type)]) ... return [content] >>> def make_broken_application(): ... return broken_application >>> import wsgi_intercept >>> wsgi_intercept.add_wsgi_intercept( ... 'api.launchpad.dev', 80, make_broken_application) >>> BROKEN_RESPONSES = [] >>> from wsgi_intercept.httplib2_intercept import install >>> install() Here's a fake implementation of time.sleep() so that this test doesn't take a really long time to run, and so we can visualize sleep() being called as lazr.restfulclient retries over and over again. >>> def fake_sleep(time): ... print "sleep(%s) called" % time >>> import lazr.restfulclient._browser >>> old_sleep = lazr.restfulclient._browser.sleep >>> lazr.restfulclient._browser.sleep = fake_sleep As it starts out, the application isn't broken at all. >>> from lazr.restfulclient.resource import ServiceRoot >>> client = ServiceRoot(None, "http://api.launchpad.dev/") Let's queue up one broken response. The client will sleep once and try again. >>> BROKEN_RESPONSES = [502] >>> client = ServiceRoot(None, "http://api.launchpad.dev/") sleep(0) called Now the application will fail six times and then start working. >>> BROKEN_RESPONSES = [502, 503, 502, 503, 502, 503] >>> client = ServiceRoot(None, "http://api.launchpad.dev/") sleep(0) called sleep(1) called sleep(2) called sleep(4) called sleep(8) called sleep(16) called Now the application will fail seven times and then start working. But the client will give up before then--it will only retry the request six times. >>> BROKEN_RESPONSES = [502, 503, 502, 503, 502, 503, 502] >>> client = ServiceRoot(None, "http://api.launchpad.dev/") Traceback (most recent call last): ... ServerError: HTTP Error 502: ... By increasing the 'max_retries' constructor argument, we can make the application try more than six times, and eventually succeed. >>> BROKEN_RESPONSES = [502, 503, 502, 503, 502, 503, 502] >>> client = ServiceRoot(None, "http://api.launchpad.dev/", ... max_retries=10) sleep(0) called sleep(1) called sleep(2) called sleep(4) called sleep(8) called sleep(16) called sleep(32) called Now the application will fail once and then give a 400 error. The client will not retry in hopes that the 400 error will go away--400 is a client error. >>> BROKEN_RESPONSES = [502, 400] >>> client = ServiceRoot(None, "http://api.launchpad.dev/") Traceback (most recent call last): ... BadRequest: HTTP Error 400: ... Teardown. >>> _ = wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80) >>> lazr.restfulclient._browser.sleep = old_sleep ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/docs/toplevel.rst0000644000175000017500000001456700000000000027110 0ustar00cjwatsoncjwatson00000000000000***************** Top-level objects ***************** Every web service has a top-level "root" object. >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient >>> service = CookbookWebServiceClient() The root object provides access to service-wide objects. >>> sorted(service.lp_entries) ['featured_cookbook'] >>> sorted(service.lp_collections) ['cookbooks', 'dishes', 'recipes'] Top-level entries ================= You can access a top-level entry through attribute access. >>> print service.featured_cookbook.name Mastering the Art of French Cooking Top-level collections ===================== You can access a top-level collection through attribute access. >>> len(service.dishes) 3 Specific top-level collections may support key-based lookups. For instance, the recipe collection does lookups by recipe ID. This is custom code written for this specific web service, and it won't work in general. >>> print service.recipes[1].dish.name Roast chicken Looking up an object in a top-level collection triggers an HTTP request, and if the object does not exist on the server, a KeyError is raised. >>> import httplib2 >>> httplib2.debuglevel = 1 >>> debug_service = CookbookWebServiceClient() send: 'GET ...' ... >>> recipe = debug_service.recipes[4] send: 'GET /1.0/recipes/4 ...' ... >>> recipe = debug_service.recipes[1000] Traceback (most recent call last): ... KeyError: 1000 If you want to look up an object without triggering an HTTP request, you can use parentheses instead of square brackets. >>> recipe = debug_service.recipes(4) >>> nonexistent_recipe = debug_service.recipes(1000) >>> sorted(recipe.lp_attributes) ['http_etag', 'id', 'instructions', ...] The HTTP request, and any potential error, happens when you try to access one of the object's properties. >>> print recipe.instructions send: 'GET /1.0/recipes/4 ...' ... Preheat oven to... >>> print nonexistent_recipe.instructions Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found ... This is useful if you plan to invoke a named operation on the object instead of accessing its properties--this can save you an HTTP request and speed up your applicaiton. Now, let's imagine that a top-level collection is misconfigured. We know that the top-level collection of recipes contains objects whose resource type is 'recipe'. But let's tell lazr.restfulclient that that collection contains objects of type 'cookbook'. >>> from lazr.restfulclient.tests.example import RecipeSet >>> print RecipeSet.collection_of recipe >>> RecipeSet.collection_of = 'cookbook' Looking up an object will give you something that presents the interface of a cookbook. >>> not_really_a_cookbook = debug_service.recipes(2) >>> sorted(not_really_a_cookbook.lp_attributes) ['confirmed', 'copyright_date', 'cuisine'...] But once you try to access one of the object's properties, and the HTTP request is made... >>> print not_really_a_cookbook.resource_type_link send: 'GET /1.0/recipes/2 ...' ... http://cookbooks.dev/1.0/#recipe ...the server serves a recipe, and so the client-side object takes on the properties of a recipe. You can only fool lazr.restfulclient up to the point where it has real data to look at. >>> sorted(not_really_a_cookbook.lp_attributes) ['http_etag', 'id', 'instructions', ...] >>> print not_really_a_cookbook.instructions Draw, singe, stuff, and truss... This isn't just a defense mechanism: it's a useful feature when a top-level collection contains mixed subclasses of some superclass. For instance, the launchpadlib library defines the 'people' collection as containing 'team' objects, even though it also contains 'person' objects, which expose a subset of a team's functionality. All objects looked up in that collection start out as team objects, but once an object's data is fetched, if it turns out to actually be a person, it switches from the "team" interface to the "people" interface. (This bit of hackery is necessary because WADL doesn't have an inheritance mechanism.) If you try to access a property based on a resource type the object doesn't really implement, you'll get an error. >>> not_really_a_cookbook = debug_service.recipes(3) >>> sorted(not_really_a_cookbook.lp_attributes) ['confirmed', 'copyright_date', 'cuisine'...] >>> not_really_a_cookbook.cuisine Traceback (most recent call last): ... AttributeError: http://cookbooks.dev/1.0/recipes/3 object has no attribute 'cuisine' Cleanup. >>> httplib2.debuglevel = 0 >>> RecipeSet.collection_of = 'recipe' Versioning ========== By passing in a 'version' argument to the client constructor, you can access different versions of the web service. >>> print service.recipes[1].self_link http://cookbooks.dev/1.0/recipes/1 >>> devel_service = CookbookWebServiceClient(version="devel") >>> print devel_service.recipes[1].self_link http://cookbooks.dev/devel/recipes/1 You can also forgo the 'version' argument and pass in a service root that incorporates a version string. >>> devel_service = CookbookWebServiceClient( ... service_root="http://cookbooks.dev/devel/", version=None) >>> print devel_service.recipes[1].self_link http://cookbooks.dev/devel/recipes/1 Error reporting =============== If there's an error communicating with the server, lazr.restfulclient raises HTTPError or an appropriate subclass. The error might be a client-side error (maybe you tried to access something that doesn't exist) or a server-side error (maybe the server crashed due to a bug). The string representation of the error should have enough information to help you figure out what happened. This example demonstrates NotFound, the HTTPError subclass used when the server sends a 404 error For detailed information about the different HTTPError subclasses, see tests/test_error.py. >>> from lazr.restfulclient.errors import HTTPError >>> try: ... service.load("http://cookbooks.dev/") ... except Exception, e: ... pass >>> raise e Traceback (most recent call last): ... NotFound: HTTP Error 404: Not Found Response headers: --- ... content-type: text/plain ... --- Response body: --- ... --- >>> print isinstance(e, HTTPError) True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/errors.py0000644000175000017500000001024200000000000025444 0ustar00cjwatsoncjwatson00000000000000# Copyright 2008 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # lazr.restfulclient is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with lazr.restfulclient. If not, see # . """lazr.restfulclient errors.""" __metaclass__ = type __all__ = [ 'BadRequest', 'Conflict', 'ClientError', 'CredentialsError', 'CredentialsFileError', 'HTTPError', 'MethodNotAllowed', 'NotFound', 'PreconditionFailed', 'RestfulError', 'ResponseError', 'ServerError', 'Unauthorized', 'UnexpectedResponseError', ] class RestfulError(Exception): """Base error for the lazr.restfulclient API library.""" class CredentialsError(RestfulError): """Base credentials/authentication error.""" class CredentialsFileError(CredentialsError): """Error in credentials file.""" class ResponseError(RestfulError): """Error in response.""" def __init__(self, response, content): RestfulError.__init__(self) self.response = response self.content = content class UnexpectedResponseError(ResponseError): """An unexpected response was received.""" def __str__(self): return '%s: %s' % (self.response.status, self.response.reason) class HTTPError(ResponseError): """An HTTP non-2xx response code was received.""" def __str__(self): """Show the error code, response headers, and response body.""" headers = "\n".join(["%s: %s" % pair for pair in sorted(self.response.items())]) return ("HTTP Error %s: %s\n" "Response headers:\n---\n%s\n---\n" "Response body:\n---\n%s\n---\n") % ( self.response.status, self.response.reason, headers, self.content) class ClientError(HTTPError): """An exception representing a client-side error.""" class Unauthorized(ClientError): """An exception representing an authentication failure.""" class NotFound(ClientError): """An exception representing a nonexistent resource.""" class MethodNotAllowed(ClientError): """An exception raised when you use an unsupported HTTP method. This is most likely because you tried to delete a resource that can't be deleted. """ class BadRequest(ClientError): """An exception representing a problem with a client request.""" class Conflict(ClientError): """An exception representing a conflict with another client.""" class PreconditionFailed(ClientError): """An exception representing the failure of a conditional PUT/PATCH. The most likely explanation is that another client changed this object while you were working on it, and your version of the object is now out of date. """ class ServerError(HTTPError): """An exception representing a server-side error.""" def error_for(response, content): """Turn an HTTP response into an HTTPError subclass. :return: None if the response code is 1xx, 2xx or 3xx. Otherwise, an instance of an appropriate HTTPError subclass (or HTTPError if nothing else is appropriate. """ http_errors_by_status_code = { 400 : BadRequest, 401 : Unauthorized, 404 : NotFound, 405 : MethodNotAllowed, 409 : Conflict, 412 : PreconditionFailed, } if response.status // 100 <= 3: # 1xx, 2xx and 3xx are not considered errors. return None else: cls = http_errors_by_status_code.get(response.status, HTTPError) if cls is HTTPError: if response.status // 100 == 5: cls = ServerError elif response.status // 100 == 4: cls = ClientError return cls(response, content) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1611142027.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/resource.py0000644000175000017500000013213600000000000025766 0ustar00cjwatsoncjwatson00000000000000# Copyright 2008 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public License # as published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # lazr.restfulclient is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with lazr.restfulclient. If not, see # . """Common support for web service resources.""" __metaclass__ = type __all__ = [ 'Collection', 'CollectionWithKeyBasedLookup', 'Entry', 'NamedOperation', 'Resource', 'ServiceRoot', ] from email.message import Message from io import BytesIO from json import dumps, loads try: # Python 3. from urllib.parse import urljoin, urlparse, parse_qs, unquote, urlencode except ImportError: from urlparse import urljoin, urlparse, parse_qs from urllib import unquote, urlencode import sys if sys.version_info[0] >= 3: text_type = str binary_type = bytes else: text_type = unicode binary_type = str from lazr.uri import URI from wadllib.application import Resource as WadlResource from lazr.restfulclient import __version__ from lazr.restfulclient._browser import Browser, RestfulHttp from lazr.restfulclient._json import DatetimeJSONEncoder from lazr.restfulclient.errors import HTTPError missing = object() class HeaderDictionary: """A dictionary that bridges httplib2's and wadllib's expectations. httplib2 expects all header dictionary access to give lowercase header names. wadllib expects to access the header exactly as it's specified in the WADL file, which means the official HTTP header name. This class transforms keys to lowercase before doing a lookup on the underlying dictionary. That way wadllib can pass in the official header name and httplib2 will get the lowercased name. """ def __init__(self, wrapped_dictionary): self.wrapped_dictionary = wrapped_dictionary def get(self, key, default=None): """Retrieve a value, converting the key to lowercase.""" return self.wrapped_dictionary.get(key.lower()) def __getitem__(self, key): """Retrieve a value, converting the key to lowercase.""" value = self.get(key, missing) if value is missing: raise KeyError(key) return value class RestfulBase: """Base class for classes that know about lazr.restful services.""" JSON_MEDIA_TYPE = 'application/json' def _transform_resources_to_links(self, dictionary): new_dictionary = {} for key, value in dictionary.items(): if isinstance(value, Resource): value = value.self_link new_dictionary[self._get_external_param_name(key)] = value return new_dictionary def _get_external_param_name(self, param_name): """Turn a lazr.restful name into something to be sent over HTTP. For resources this may involve sticking '_link' or '_collection_link' on the end of the parameter name. For arguments to named operations, the parameter name is returned as is. """ return param_name class Resource(RestfulBase): """Base class for lazr.restful HTTP resources.""" def __init__(self, root, wadl_resource): """Initialize with respect to a wadllib Resource object.""" if root is None: # This _is_ the root. root = self # These values need to be put directly into __dict__ to avoid # calling __setattr__, which would cause an infinite recursion. self.__dict__['_root'] = root self.__dict__['_wadl_resource'] = wadl_resource FIND_COLLECTIONS = object() FIND_ENTRIES = object() FIND_ATTRIBUTES = object() @property def lp_collections(self): """Name the collections this resource links to.""" return self._get_parameter_names(self.FIND_COLLECTIONS) @property def lp_entries(self): """Name the entries this resource links to.""" return self._get_parameter_names(self.FIND_ENTRIES) @property def lp_attributes(self): """Name this resource's scalar attributes.""" return self._get_parameter_names(self.FIND_ATTRIBUTES) @property def lp_operations(self): """Name all of this resource's custom operations.""" # This library distinguishes between named operations by the # value they give for ws.op, not by their WADL names or IDs. names = [] for method in self._wadl_resource.method_iter: name = method.name.lower() if name == 'get': params = method.request.params(['query', 'plain']) elif name == 'post': for media_type in ['application/x-www-form-urlencoded', 'multipart/form-data']: definition = method.request.get_representation_definition( media_type) if definition is not None: definition = definition.resolve_definition() break params = definition.params(self._wadl_resource) for param in params: if param.name == 'ws.op': names.append(param.fixed_value) break return names @property def __members__(self): """A hook into dir() that returns web service-derived members.""" return self._get_parameter_names( self.FIND_COLLECTIONS, self.FIND_ENTRIES, self.FIND_ATTRIBUTES) __methods__ = lp_operations def _get_parameter_names(self, *kinds): """Retrieve some subset of the resource's parameters.""" names = [] for parameter in self._wadl_resource.parameters( self.JSON_MEDIA_TYPE): name = parameter.name link = parameter.link if (name != 'self_link' and link is not None and link.can_follow): # This is a link to a resource with a WADL # description. Since this is a lazr.restful web # service, we know it's either an entry or a # collection, and that its name ends with '_link' or # '_collection_link', respectively. # # self_link is a special case. 'obj.self' will always # work, but it's not useful. 'obj.self_link' is # useful, so we advertise the scalar value instead. if name.endswith('_collection_link'): # It's a link to a collection. if self.FIND_COLLECTIONS in kinds: names.append(name[:-16]) else: # It's a link to an entry. if self.FIND_ENTRIES in kinds: names.append(name[:-5]) else: # There are three possibilities. This is not a link at # all, it's a link to a resource not described by # WADL, or it's the 'self_link'. Either way, # lazr.restfulclient should treat this parameter as a # scalar attribute. if self.FIND_ATTRIBUTES in kinds: names.append(name) return names def lp_has_parameter(self, param_name): """Does this resource have a parameter with the given name?""" return self._get_external_param_name(param_name) is not None def lp_get_parameter(self, param_name): """Get the value of one of the resource's parameters. :return: A scalar value if the parameter is not a link. A new Resource object, whose resource is bound to a representation, if the parameter is a link. """ self._ensure_representation() for suffix in ['_link', '_collection_link']: param = self._wadl_resource.get_parameter( param_name + suffix) if param is not None: try: param.get_value() except KeyError: # The parameter could have been present, but isn't. # Try the next parameter. continue if param.get_value() is None: # This parameter is a link to another object, but # there's no other object. Return None rather than # chasing down the nonexistent other object. return None linked_resource = param.linked_resource return self._create_bound_resource( self._root, linked_resource, param_name=param.name) param = self._wadl_resource.get_parameter(param_name) if param is None: raise KeyError("No such parameter: %s" % param_name) return param.get_value() def lp_get_named_operation(self, operation_name): """Get a custom operation with the given name. :return: A NamedOperation instance that can be called with appropriate arguments to invoke the operation. """ params = {'ws.op': operation_name} method = self._wadl_resource.get_method('get', query_params=params) if method is None: method = self._wadl_resource.get_method( 'post', representation_params=params) if method is None: raise KeyError("No operation with name: %s" % operation_name) return NamedOperation(self._root, self, method) @classmethod def _create_bound_resource( cls, root, resource, representation=None, representation_media_type='application/json', representation_needs_processing=True, representation_definition=None, param_name=None): """Create a lazr.restful Resource subclass from a wadllib Resource. :param resource: The wadllib Resource to wrap. :param representation: A previously fetched representation of this resource, to be reused. If not provided, this method will act just like the Resource constructor. :param representation_media_type: The media type of any previously fetched representation. :param representation_needs_processing: Set to False if the 'representation' parameter should be used as is. :param representation_definition: A wadllib RepresentationDefinition object describing the structure of this representation. Used in cases when the representation isn't the result of sending a standard GET to the resource. :param param_name: The name of the link that was followed to get to this resource. :return: An instance of the appropriate lazr.restful Resource subclass. """ # We happen to know that all lazr.restful resource types are # defined in a single document. Turn the resource's type_url # into an anchor into that document: this is its resource # type. Then look up a client-side class that corresponds to # the resource type. type_url = resource.type_url resource_type = urlparse(type_url)[-1] default = Entry if (type_url.endswith('-page') or (param_name is not None and param_name.endswith('_collection_link'))): default = Collection r_class = root.RESOURCE_TYPE_CLASSES.get(resource_type, default) if representation is not None: # We've been given a representation. Bind the resource # immediately. resource = resource.bind( representation, representation_media_type, representation_needs_processing, representation_definition=representation_definition) else: # We'll fetch a representation and bind the resource when # necessary. pass return r_class(root, resource) def lp_refresh(self, new_url=None, etag=None): """Update this resource's representation.""" if new_url is not None: self._wadl_resource._url = new_url headers = {} if etag is not None: headers['If-None-Match'] = etag representation = self._root._browser.get( self._wadl_resource, headers=headers) if representation == self._root._browser.NOT_MODIFIED: # The entry wasn't modified. No need to do anything. return # __setattr__ assumes we're setting an attribute of the resource, # so we manipulate __dict__ directly. self.__dict__['_wadl_resource'] = self._wadl_resource.bind( representation, self.JSON_MEDIA_TYPE) def __getattr__(self, attr): """Try to retrive a named operation or parameter of the given name.""" try: return self.lp_get_named_operation(attr) except KeyError: pass try: return self.lp_get_parameter(attr) except KeyError: raise AttributeError("%s object has no attribute '%s'" % (self, attr)) def lp_values_for(self, param_name): """Find the set of possible values for a parameter.""" parameter = self._wadl_resource.get_parameter( param_name, self.JSON_MEDIA_TYPE) options = parameter.options if len(options) > 0: return [option.value for option in options] return None def _get_external_param_name(self, param_name): """What's this parameter's name in the underlying representation?""" for suffix in ['_link', '_collection_link', '']: name = param_name + suffix if self._wadl_resource.get_parameter(name): return name return None def _ensure_representation(self): """Make sure this resource has a representation fetched.""" if self._wadl_resource.representation is None: # Get a representation of the linked resource. representation = self._root._browser.get(self._wadl_resource) if isinstance(representation, binary_type): representation = representation.decode('utf-8') representation = loads(representation) # In rare cases, the resource type served by the # server conflicts with the type the client thought # this resource had. When this happens, the server # value takes precedence. # # XXX This should probably be moved into a hook method # defined by Entry, since it's not relevant to other # resource types. if isinstance(representation, dict): type_link = representation['resource_type_link'] if (type_link is not None and type_link != self._wadl_resource.type_url): resource_type = self._root._wadl.get_resource_type( type_link) self._wadl_resource.tag = resource_type.tag self.__dict__['_wadl_resource'] = self._wadl_resource.bind( representation, self.JSON_MEDIA_TYPE, representation_needs_processing=False) def __ne__(self, other): """Inequality operator.""" return not self == other class ScalarValue(Resource): """A resource representing a single scalar value.""" @property def value(self): """Return the scalar value.""" self._ensure_representation() return self._wadl_resource.representation class HostedFile(Resource): """A resource representing a file managed by a lazr.restful service.""" def open(self, mode='r', content_type=None, filename=None): """Open the file on the server for read or write access.""" if mode in ('r', 'w'): return HostedFileBuffer(self, mode, content_type, filename) else: raise ValueError("Invalid mode. Supported modes are: r, w") def delete(self): """Delete the file from the server.""" self._root._browser.delete(self._wadl_resource.url) def _get_parameter_names(self, *kinds): """HostedFile objects define no web service parameters.""" return [] def __eq__(self, other): """Equality comparison. Two hosted files are the same if they have the same URL. There is no need to check the contents because the only way to retrieve or modify the hosted file contents is to open a filehandle, which goes direct to the server. """ return (other is not None and self._wadl_resource.url == other._wadl_resource.url) class ServiceRoot(Resource): """Entry point to the service. Subclass this for a service-specific client. :ivar credentials: The credentials instance used to access Launchpad. """ # Custom subclasses of Resource to use when # instantiating resources of a certain WADL type. RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile, 'ScalarValue': ScalarValue} def __init__(self, authorizer, service_root, cache=None, timeout=None, proxy_info=None, version=None, base_client_name='', max_retries=Browser.MAX_RETRIES): """Root access to a lazr.restful API. :param credentials: The credentials used to access the service. :param service_root: The URL to the root of the web service. :type service_root: string """ if version is not None: if service_root[-1] != '/': service_root += '/' service_root += str(version) if service_root[-1] != '/': service_root += '/' self._root_uri = URI(service_root) # Set up data necessary to calculate the User-Agent header. self._base_client_name = base_client_name # Get the WADL definition. self.credentials = authorizer self._browser = Browser( self, authorizer, cache, timeout, proxy_info, self._user_agent, max_retries) self._wadl = self._browser.get_wadl_application(self._root_uri) # Get the root resource. root_resource = self._wadl.get_resource_by_path('') bound_root = root_resource.bind( self._browser.get(root_resource), 'application/json') super(ServiceRoot, self).__init__(None, bound_root) @property def _user_agent(self): """The value for the User-Agent header. This will be something like: launchpadlib 1.6.1, lazr.restfulclient 1.0.0; application=apport That is, a string describing lazr.restfulclient and an optional custom client built on top, and parameters containing any authorization-specific information that identifies the user agent (such as the application name). """ base_portion = "lazr.restfulclient %s" % __version__ if self._base_client_name != '': base_portion = self._base_client_name + ' (' + base_portion + ')' message = Message() message['User-Agent'] = base_portion if self.credentials is not None: user_agent_params = self.credentials.user_agent_params for key in sorted(user_agent_params): value = user_agent_params[key] message.set_param(key, value, 'User-Agent') return message['User-Agent'] def httpFactory(self, authorizer, cache, timeout, proxy_info): return RestfulHttp(authorizer, cache, timeout, proxy_info) def load(self, url): """Load a resource given its URL.""" parsed = urlparse(url) if parsed.scheme == '': # This is a relative URL. Make it absolute by joining # it with the service root resource. if url[:1] == '/': url = url[1:] url = str(self._root_uri.append(url)) document = self._browser.get(url) if isinstance(document, binary_type): document = document.decode('utf-8') try: representation = loads(document) except ValueError: raise ValueError("%s doesn't serve a JSON document." % url) type_link = representation.get("resource_type_link") if type_link is None: raise ValueError("Couldn't determine the resource type of %s." % url) resource_type = self._root._wadl.get_resource_type(type_link) wadl_resource = WadlResource(self._root._wadl, url, resource_type.tag) return self._create_bound_resource( self._root, wadl_resource, representation, 'application/json', representation_needs_processing=False) class NamedOperation(RestfulBase): """A class for a named operation to be invoked with GET or POST.""" def __init__(self, root, resource, wadl_method): """Initialize with respect to a WADL Method object""" self.root = root self.resource = resource self.wadl_method = wadl_method def __call__(self, *args, **kwargs): """Invoke the method and process the result.""" if len(args) > 0: raise TypeError('Method must be called with keyword args.') http_method = self.wadl_method.name args = self._transform_resources_to_links(kwargs) request = self.wadl_method.request if http_method in ('get', 'head', 'delete'): params = request.query_params else: definition = request.get_representation_definition( 'multipart/form-data') if definition is None: definition = request.get_representation_definition( 'application/x-www-form-urlencoded') assert definition is not None, ( "A POST named operation must define a multipart or " "form-urlencoded request representation." ) params = definition.params(self.resource._wadl_resource) send_as_is_params = set([param.name for param in params if param.type == 'binary' or len(param.options) > 0]) for key, value in args.items(): # Certain parameter values should not be JSON-encoded: # binary parameters (because they can't be JSON-encoded) # and option values (because JSON-encoding them will screw # up wadllib's parameter validation). The option value thing # is a little hacky, but it's the best solution for now. if key not in send_as_is_params: args[key] = dumps(value, cls=DatetimeJSONEncoder) if http_method in ('get', 'head', 'delete'): url = self.wadl_method.build_request_url(**args) in_representation = '' extra_headers = {} else: url = self.wadl_method.build_request_url() (media_type, in_representation) = self.wadl_method.build_representation( **args) extra_headers = {'Content-type': media_type} # Pass uppercase method names to httplib2, as that is what it works # with. If you pass a lowercase method name to httplib then it doesn't # consider it to be a GET, PUT, etc., and so will do things like not # cache. Wadl Methods return their method lower cased, which is how it # is compared in this method, but httplib2 expects the opposite, hence # the .upper() call. response, content = self.root._browser._request( url, in_representation, http_method.upper(), extra_headers=extra_headers) if response.status == 201: return self._handle_201_response(url, response, content) else: if http_method == 'post': # The method call probably modified this resource in # an unknown way. If it moved to a new location, reload it or # else just refresh its representation. if response.status == 301: url = response['location'] response, content = self.root._browser._request(url) else: self.resource.lp_refresh() return self._handle_200_response(url, response, content) def _handle_201_response(self, url, response, content): """Handle the creation of a new resource by fetching it.""" wadl_response = self.wadl_method.response.bind( HeaderDictionary(response)) wadl_parameter = wadl_response.get_parameter('Location') wadl_resource = wadl_parameter.linked_resource # Fetch a representation of the new resource. response, content = self.root._browser._request( wadl_resource.url) # Return an instance of the appropriate lazr.restful # Resource subclass. return Resource._create_bound_resource( self.root, wadl_resource, content, response['content-type']) def _handle_200_response(self, url, response, content): """Process the return value of an operation.""" content_type = response['content-type'] # Process the returned content, assuming we know how. response_definition = self.wadl_method.response representation_definition = ( response_definition.get_representation_definition( content_type)) if representation_definition is None: # The operation returned a document with nothing # special about it. if content_type == self.JSON_MEDIA_TYPE: if isinstance(content, binary_type): content = content.decode('utf-8') return loads(content) # We don't know how to process the content. return content # The operation returned a representation of some # resource. Instantiate a Resource object for it. if isinstance(content, binary_type): content = content.decode('utf-8') document = loads(content) if document is None: # The operation returned a null value. return document if "self_link" in document and "resource_type_link" in document: # The operation returned an entry. Use the self_link and # resource_type_link of the entry representation to build # a Resource object of the appropriate type. That way this # object will support all of the right named operations. url = document["self_link"] resource_type = self.root._wadl.get_resource_type( document["resource_type_link"]) wadl_resource = WadlResource(self.root._wadl, url, resource_type.tag) else: # The operation returned a collection. It's probably an ad # hoc collection that doesn't correspond to any resource # type. Instantiate it as a resource backed by the # representation type defined in the return value, instead # of a resource type tag. representation_definition = ( representation_definition.resolve_definition()) wadl_resource = WadlResource( self.root._wadl, url, representation_definition.tag) return Resource._create_bound_resource( self.root, wadl_resource, document, content_type, representation_needs_processing=False, representation_definition=representation_definition) def _get_external_param_name(self, param_name): """Named operation parameter names are sent as is.""" return param_name class Entry(Resource): """A class for an entry-type resource that can be updated with PATCH.""" def __init__(self, root, wadl_resource): super(Entry, self).__init__(root, wadl_resource) # Initialize this here in a semi-magical way so as to stop a # particular infinite loop that would follow. Setting # self._dirty_attributes would call __setattr__(), which would # turn around immediately and get self._dirty_attributes. If # this latter was not in the instance dictionary, that would # end up calling __getattr__(), which would again reference # self._dirty_attributes. This is where the infloop would # occur. Poking this directly into self.__dict__ means that # the check for self._dirty_attributes won't call __getattr__(), # breaking the cycle. self.__dict__['_dirty_attributes'] = {} super(Entry, self).__init__(root, wadl_resource) def __repr__(self): """Return the WADL resource type and the URL to the resource.""" return '<%s at %s>' % ( URI(self.resource_type_link).fragment, self.self_link) def lp_delete(self): """Delete the resource.""" return self._root._browser.delete(URI(self.self_link)) def __str__(self): """Return the URL to the resource.""" return self.self_link def __getattr__(self, name): """Try to retrive a parameter of the given name.""" if name != '_dirty_attributes': if name in self._dirty_attributes: return self._dirty_attributes[name] return super(Entry, self).__getattr__(name) def __setattr__(self, name, value): """Set the parameter of the given name.""" if not self.lp_has_parameter(name): raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) self._dirty_attributes[name] = value def __eq__(self, other): """Equality operator. Two entries are the same if their self_link and http_etag attributes are the same, and if their dirty attribute dicts contain the same values. """ return ( other is not None and self.self_link == other.self_link and self.http_etag == other.http_etag and self._dirty_attributes == other._dirty_attributes) def lp_refresh(self, new_url=None): """Update this resource's representation.""" etag = getattr(self, 'http_etag', None) super(Entry, self).lp_refresh(new_url, etag) self._dirty_attributes.clear() def lp_save(self): """Save changes to the entry.""" representation = self._transform_resources_to_links( self._dirty_attributes) # If the entry contains an ETag, set the If-Match header # to that value. headers = {} etag = getattr(self, 'http_etag', None) if etag is not None: headers['If-Match'] = etag # PATCH the new representation to the 'self' link. It's possible that # this will cause the object to be permanently moved. Catch that # exception and refresh our representation. response, content = self._root._browser.patch( URI(self.self_link), representation, headers) if response.status == 301: self.lp_refresh(response['location']) self._dirty_attributes.clear() content_type = response['content-type'] if response.status == 209 and content_type == self.JSON_MEDIA_TYPE: # The server sent back a new representation of the object. # Use it in preference to the existing representation. if isinstance(content, binary_type): content = content.decode('utf-8') new_representation = loads(content) self._wadl_resource.representation = new_representation self._wadl_resource.media_type = content_type class Collection(Resource): """A collection-type resource that supports pagination.""" def __init__(self, root, wadl_resource): """Create a collection object.""" super(Collection, self).__init__(root, wadl_resource) def __len__(self): """The number of items in the collection. :return: length of the collection :rtype: int """ total_size = self.total_size if isinstance(total_size, int): # The size was a number present in the collection # representation. return total_size elif isinstance(total_size, ScalarValue): # The size was linked to from the collection representation, # not directly present. return total_size.value else: raise TypeError('collection size is not available') def __iter__(self): """Iterate over the items in the collection. :return: iterator :rtype: sequence of `Entry` """ self._ensure_representation() current_page = self._wadl_resource.representation while True: for resource in self._convert_dicts_to_entries( current_page.get('entries', {})): yield resource next_link = current_page.get('next_collection_link') if next_link is None: break next_get = self._root._browser.get(URI(next_link)) if isinstance(next_get, binary_type): next_get = next_get.decode('utf-8') current_page = loads(next_get) def __getitem__(self, key): """Look up a slice, or a subordinate resource by index. To discourage situations where a lazr.restful client fetches all of an enormous list, all collection slices must have a definitive end point. For performance reasons, all collection slices must be indexed from the start of the list rather than the end. """ if isinstance(key, slice): return self._get_slice(key) else: # Look up a single item by its position in the list. found_slice = self._get_slice(slice(key, key + 1)) if len(found_slice) != 1: raise IndexError("list index out of range") return found_slice[0] def _get_slice(self, slice): """Retrieve a slice of a collection.""" start = slice.start or 0 stop = slice.stop if start < 0: raise ValueError("Collection slices must have a nonnegative " "start point.") if stop < 0: raise ValueError("Collection slices must have a definite, " "nonnegative end point.") existing_representation = self._wadl_resource.representation if (existing_representation is not None and start < len(existing_representation['entries'])): # An optimization: the first page of entries has already # been loaded. This can happen if this collection is the # return value of a named operation, or if the client did # something like check the length of the collection. # # Either way, we've already made an HTTP request and # gotten some entries back. The client has requested a # slice that includes some of the entries we already have. # In the best case, we can fulfil the slice immediately, # without making another HTTP request. # # Even if we can't fulfil the entire slice, we can get one # or more objects from the first page and then have fewer # objects to retrieve from the server later. This saves us # time and bandwidth, and it might let us save a whole # HTTP request. entry_page = existing_representation['entries'] first_page_size = len(entry_page) entry_dicts = entry_page[start:stop] page_url = existing_representation.get('next_collection_link') else: # No part of this collection has been loaded yet, or the # slice starts beyond the part that has been loaded. We'll # use our secret knowledge of lazr.restful to set a value for # the ws.start variable. That way we start reading entries # from the first one we want. first_page_size = None entry_dicts = [] page_url = self._with_url_query_variable_set( self._wadl_resource.url, 'ws.start', start) desired_size = stop - start more_needed = desired_size - len(entry_dicts) # Iterate over pages until we have the correct number of entries. while more_needed > 0 and page_url is not None: page_get = self._root._browser.get(page_url) if isinstance(page_get, binary_type): page_get = page_get.decode('utf-8') representation = loads(page_get) current_page_entries = representation['entries'] entry_dicts += current_page_entries[:more_needed] more_needed = desired_size - len(entry_dicts) page_url = representation.get('next_collection_link') if page_url is None: # We've gotten the entire collection; there are no # more entries. break if first_page_size is None: first_page_size = len(current_page_entries) if more_needed > 0 and more_needed < first_page_size: # An optimization: it's likely that we need less than # a full page of entries, because the number we need # is less than the size of the first page we got. # Instead of requesting a full-sized page, we'll # request only the number of entries we think we'll # need. If we're wrong, there's no problem; we'll just # keep looping. page_url = self._with_url_query_variable_set( page_url, 'ws.size', more_needed) if slice.step is not None: entry_dicts = entry_dicts[::slice.step] # Convert entry_dicts into a list of Entry objects. return [resource for resource in self._convert_dicts_to_entries(entry_dicts)] def _convert_dicts_to_entries(self, entries): """Convert dictionaries describing entries to Entry objects. The dictionaries come from the 'entries' field of the JSON dictionary you get when you GET a page of a collection. Each dictionary is the same as you'd get if you sent a GET request to the corresponding entry resource. So each of these dictionaries can be treated as a preprocessed representation of an entry resource, and turned into an Entry instance. :yield: A sequence of Entry instances. """ for entry_dict in entries: resource_url = entry_dict['self_link'] resource_type_link = entry_dict['resource_type_link'] wadl_application = self._wadl_resource.application resource_type = wadl_application.get_resource_type( resource_type_link) resource = WadlResource( self._wadl_resource.application, resource_url, resource_type.tag) yield Resource._create_bound_resource( self._root, resource, entry_dict, self.JSON_MEDIA_TYPE, False) def _with_url_query_variable_set(self, url, variable, new_value): """A helper method to set a query variable in a URL.""" uri = URI(url) if uri.query is None: params = {} else: params = parse_qs(uri.query) params[variable] = str(new_value) uri.query = urlencode(params, True) return str(uri) class CollectionWithKeyBasedLookup(Collection): """A collection-type resource that supports key-based lookup. This collection can be sliced, but any single index passed into __getitem__ will be treated as a custom lookup key. """ def __getitem__(self, key): """Look up a slice, or a subordinate resource by unique ID.""" if isinstance(key, slice): return super(CollectionWithKeyBasedLookup, self).__getitem__(key) try: url = self._get_url_from_id(key) except NotImplementedError: raise TypeError("unsubscriptable object") if url is None: raise KeyError(key) shim_resource = self(key) try: shim_resource._ensure_representation() except HTTPError as e: if e.response.status == 404: raise KeyError(key) else: raise return shim_resource def __call__(self, key): """Retrieve a member from this collection without looking it up.""" try: url = self._get_url_from_id(key) except NotImplementedError: raise TypeError("unsubscriptable object") if url is None: raise ValueError(key) if self.collection_of is not None: # We know what kind of resource is at the other end of the # URL. There's no need to actually fetch that URL until # the user demands it. If the user is invoking a named # operation on this object rather than fetching its data, # this will save us one round trip. representation = None resource_type_link = urljoin( self._root._wadl.markup_url, '#' + self.collection_of) else: # We don't know what kind of resource this is. Either the # subclass wasn't programmed with this knowledge, or # there's simply no way to tell without going to the # server, because the collection contains more than one # kind of resource. The only way to know for sure is to # retrieve a representation of the resource and see how # the resource describes itself. try: url_get = self._root._browser.get(url) if isinstance(url_get, binary_type): url_get = url_get.decode('utf-8') representation = loads(url_get) except HTTPError as error: # There's no resource corresponding to the given ID. if error.response.status == 404: raise KeyError(key) raise # We know that every lazr.restful resource has a # 'resource_type_link' in its representation. resource_type_link = representation['resource_type_link'] resource = WadlResource(self._root._wadl, url, resource_type_link) return self._create_bound_resource( self._root, resource, representation=representation, representation_needs_processing=False) # If provided, this should be a string designating the ID of a # resource_type from a specific service's WADL file. collection_of = None def _get_url_from_id(self, key): """Transform the unique ID of an object into its URL.""" raise NotImplementedError() class HostedFileBuffer(BytesIO): """The contents of a file hosted by a lazr.restful service.""" def __init__(self, hosted_file, mode, content_type=None, filename=None): self.url = hosted_file._wadl_resource.url if mode == 'r': if content_type is not None: raise ValueError("Files opened for read access can't " "specify content_type.") if filename is not None: raise ValueError("Files opened for read access can't " "specify filename.") response, value = hosted_file._root._browser.get( self.url, return_response=True) content_type = response['content-type'] last_modified = response.get('last-modified') # The Content-Location header contains the URL of the file # hosted by the web service. We happen to know that the # final component of the URL is the name of the uploaded # file. content_location = response['content-location'] path = urlparse(content_location)[2] filename = unquote(path.split("/")[-1]) elif mode == 'w': value = '' if content_type is None: raise ValueError("Files opened for write access must " "specify content_type.") if filename is None: raise ValueError("Files opened for write access must " "specify filename.") last_modified = None else: raise ValueError("Invalid mode. Supported modes are: r, w") self.hosted_file = hosted_file self.mode = mode self.content_type = content_type self.filename = filename self.last_modified = last_modified BytesIO.__init__(self, value) def close(self): if self.mode == 'w': disposition = 'attachment; filename="%s"' % self.filename self.hosted_file._root._browser.put( self.url, self.getvalue(), self.content_type, {'Content-Disposition': disposition}) BytesIO.close(self) def write(self, b): BytesIO.write(self, b) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1631546865.654421 lazr.restfulclient-0.14.4/src/lazr/restfulclient/tests/0000755000175000017500000000000000000000000024721 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1338454969.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/tests/__init__.py0000644000175000017500000000136500000000000027037 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009 Canonical Ltd. All rights reserved. # # This file is part of lazr.restfulclient # # lazr.restfulclient is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 of the License. # # lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . "The lazr.restfulclient tests." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1418306485.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/tests/example.py0000644000175000017500000000435700000000000026737 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009 Canonical Ltd. All rights reserved. # # This file is part of lazr.restfulclient # # lazr.restfulclient is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 of the License. # # lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . "Test client for the lazr.restful example web service." __metaclass__ = type __all__ = [ 'CookbookWebServiceClient', ] try: # Python 3. from urllib.parse import quote except ImportError: from urllib import quote from lazr.restfulclient.resource import ( CollectionWithKeyBasedLookup, ServiceRoot) class CookbookSet(CollectionWithKeyBasedLookup): """A custom subclass capable of cookbook lookup by cookbook name.""" def _get_url_from_id(self, id): """Transform a cookbook name into the URL to a cookbook resource.""" return (str(self._root._root_uri.ensureSlash()) + 'cookbooks/' + quote(str(id))) collection_of = "cookbook" class RecipeSet(CollectionWithKeyBasedLookup): """A custom subclass capable of recipe lookup by recipe ID.""" def _get_url_from_id(self, id): """Transform a recipe ID into the URL to a recipe resource.""" return str(self._root._root_uri.ensureSlash()) + 'recipes/' + str(id) collection_of = "recipe" class CookbookWebServiceClient(ServiceRoot): RESOURCE_TYPE_CLASSES = dict(ServiceRoot.RESOURCE_TYPE_CLASSES) RESOURCE_TYPE_CLASSES['recipes'] = RecipeSet RESOURCE_TYPE_CLASSES['cookbooks'] = CookbookSet DEFAULT_SERVICE_ROOT = "http://cookbooks.dev/" DEFAULT_VERSION = "1.0" def __init__(self, service_root=DEFAULT_SERVICE_ROOT, version=DEFAULT_VERSION, cache=None): super(CookbookWebServiceClient, self).__init__( None, service_root, cache=cache, version=version) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1542458218.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/tests/test_atomicfilecache.py0000644000175000017500000001370400000000000031437 0ustar00cjwatsoncjwatson00000000000000# Copyright 2012 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public License # as published by the Free Software Foundation, version 3 of the # License. # # lazr.restfulclient is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . """Tests for the atomic file cache.""" __metaclass__ = type import shutil import tempfile import unittest import sys PY3 = sys.version_info[0] >= 3 if PY3: binary_type = bytes else: binary_type = str import httplib2 from lazr.restfulclient._browser import ( AtomicFileCache, safename) class TestFileCacheInterface(unittest.TestCase): """Tests for ``AtomicFileCache``.""" file_cache_factory = httplib2.FileCache unicode_bytes = b'pa\xc9\xaa\xce\xb8\xc9\x99n' unicode_text = unicode_bytes.decode('utf-8') def setUp(self): super(TestFileCacheInterface, self).setUp() self.cache_dir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.cache_dir) super(TestFileCacheInterface, self).tearDown() def make_file_cache(self): """Make a FileCache-like object to be tested.""" return self.file_cache_factory(self.cache_dir, safename) def test_get_non_existent_key(self): # get() returns None if the key does not exist. cache = self.make_file_cache() self.assertIs(None, cache.get('nonexistent')) def test_set_key(self): # A key set with set() can be got by get(). cache = self.make_file_cache() cache.set('key', b'value') self.assertEqual(b'value', cache.get('key')) def test_set_twice_overrides(self): # Setting a key again overrides the value. cache = self.make_file_cache() cache.set('key', b'value') cache.set('key', b'new-value') self.assertEqual(b'new-value', cache.get('key')) def test_delete_absent_key(self): # Deleting a key that's not there does nothing. cache = self.make_file_cache() cache.delete('nonexistent') self.assertIs(None, cache.get('nonexistent')) def test_delete_key(self): # A key once set can be deleted. Further attempts to get that key # return None. cache = self.make_file_cache() cache.set('key', b'value') cache.delete('key') self.assertIs(None, cache.get('key')) def test_get_non_string_key(self): # get() raises TypeError if asked to get a non-string key. cache = self.make_file_cache() self.assertRaises(TypeError, cache.get, 42) def test_delete_non_string_key(self): # delete() raises TypeError if asked to delete a non-string key. cache = self.make_file_cache() self.assertRaises(TypeError, cache.delete, 42) def test_set_non_string_key(self): # set() raises TypeError if asked to set a non-string key. cache = self.make_file_cache() self.assertRaises(TypeError, cache.set, 42, 'the answer') def test_set_non_string_value(self): # set() raises TypeError if asked to set a key to a non-string value. # Attempts to retrieve that value return the empty string. This is # probably a bug in httplib2.FileCache. cache = self.make_file_cache() self.assertRaises(TypeError, cache.set, 'answer', 42) self.assertEqual(b'', cache.get('answer')) def test_get_unicode(self): # get() can retrieve unicode keys. cache = self.make_file_cache() self.assertIs(None, cache.get(self.unicode_text)) def test_set_unicode_keys(self): cache = self.make_file_cache() cache.set(self.unicode_text, b'value') self.assertEqual(b'value', cache.get(self.unicode_text)) def test_set_unicode_value(self): # set() cannot store unicode values. Values must be bytes. cache = self.make_file_cache() error = TypeError if PY3 else UnicodeEncodeError self.assertRaises( error, cache.set, 'key', self.unicode_text) def test_delete_unicode(self): # delete() can remove unicode keys. cache = self.make_file_cache() cache.set(self.unicode_text, b'value') cache.delete(self.unicode_text) self.assertIs(None, cache.get(self.unicode_text)) class TestAtomicFileCache(TestFileCacheInterface): """Tests for ``AtomicFileCache``.""" file_cache_factory = AtomicFileCache @staticmethod def prefix_safename(x): if isinstance(x, binary_type): x = x.decode('utf-8') return AtomicFileCache.TEMPFILE_PREFIX + x def test_set_non_string_value(self): # set() raises TypeError if asked to set a key to a non-string value. # Attempts to retrieve that value act is if it were never set. # # Note: This behaviour differs from httplib2.FileCache. cache = self.make_file_cache() self.assertRaises(TypeError, cache.set, 'answer', 42) self.assertIs(None, cache.get('answer')) # Implementation-specific tests follow. def test_bad_safename_get(self): safename = self.prefix_safename cache = AtomicFileCache(self.cache_dir, safename) self.assertRaises(ValueError, cache.get, 'key') def test_bad_safename_set(self): safename = self.prefix_safename cache = AtomicFileCache(self.cache_dir, safename) self.assertRaises(ValueError, cache.set, 'key', b'value') def test_bad_safename_delete(self): safename = self.prefix_safename cache = AtomicFileCache(self.cache_dir, safename) self.assertRaises(ValueError, cache.delete, 'key') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1525765273.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/tests/test_docs.py0000644000175000017500000000574300000000000027273 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009 Canonical Ltd. All rights reserved. # # This file is part of lazr.restfulclient # # lazr.restfulclient is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 of the License. # # lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . "Test harness for doctests." # pylint: disable-msg=E0611,W0142 __metaclass__ = type __all__ = [ 'load_tests', ] import atexit import doctest import os from pkg_resources import ( resource_filename, resource_exists, resource_listdir, cleanup_resources) import wsgi_intercept from wsgi_intercept.httplib2_intercept import install, uninstall # We avoid importing anything from lazr.restful into the module level, # so that standalone_tests() can run without any support from # lazr.restful. DOCTEST_FLAGS = ( doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF) def setUp(test): from lazr.restful.example.base.tests.test_integration import WSGILayer install() wsgi_intercept.add_wsgi_intercept( 'cookbooks.dev', 80, WSGILayer.make_application) def tearDown(test): from lazr.restful.example.base.interfaces import IFileManager from zope.component import getUtility uninstall() file_manager = getUtility(IFileManager) file_manager.files = {} file_manager.counter = 0 def find_doctests(suffix, ignore_suffix=None): """Find doctests matching a certain suffix.""" doctest_files = [] # Match doctests against the suffix. if resource_exists('lazr.restfulclient', 'docs'): for name in resource_listdir('lazr.restfulclient', 'docs'): if ignore_suffix is not None and name.endswith(ignore_suffix): continue if name.endswith(suffix): doctest_files.append( os.path.abspath( resource_filename( 'lazr.restfulclient', 'docs/%s' % name))) return doctest_files def load_tests(loader, tests, pattern): """Load all the doctests.""" from lazr.restful.example.base.tests.test_integration import WSGILayer atexit.register(cleanup_resources) restful_suite = doctest.DocFileSuite( *find_doctests('.rst', ignore_suffix='.standalone.rst'), module_relative=False, optionflags=DOCTEST_FLAGS, setUp=setUp, tearDown=tearDown) restful_suite.layer = WSGILayer tests.addTest(restful_suite) tests.addTest(doctest.DocFileSuite( *find_doctests('.standalone.rst'), module_relative=False, optionflags=DOCTEST_FLAGS)) return tests ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1428523705.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/tests/test_error.py0000644000175000017500000000642300000000000027470 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public License # as published by the Free Software Foundation, version 3 of the # License. # # lazr.restfulclient is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . """Tests for the error_for helper function.""" __metaclass__ = type import unittest from lazr.restfulclient.errors import ( ClientError, Conflict, MethodNotAllowed, NotFound, PreconditionFailed, ResponseError, ServerError, Unauthorized, error_for) class DummyRequest(object): """Just enough of a request to fool error_for().""" def __init__(self, status): self.status = status class TestErrorFor(unittest.TestCase): def error_for_status(self, status, expected_error, content=''): """Make sure error_for returns the right HTTPError subclass.""" request = DummyRequest(status) error = error_for(request, content) if expected_error is None: self.assertIsNone(error) else: self.assertTrue(isinstance(error, expected_error)) self.assertEqual(content, error.content) def test_no_error_for_2xx(self): """Make sure a 2xx response code yields no error.""" for status in (200, 201, 209, 299): self.error_for_status(status, None) def test_no_error_for_3xx(self): """Make sure a 3xx response code yields no error.""" for status in (301, 302, 303, 304, 399): self.error_for_status(status, None) def test_error_for_400(self): """Make sure a 400 response code yields ResponseError.""" self.error_for_status(400, ResponseError, "error message") def test_error_for_401(self): """Make sure a 401 response code yields Unauthorized.""" self.error_for_status(401, Unauthorized, "error message") def test_error_for_404(self): """Make sure a 404 response code yields Not Found.""" self.error_for_status(404, NotFound, "error message") def test_error_for_405(self): """Make sure a 405 response code yields MethodNotAllowed.""" self.error_for_status(405, MethodNotAllowed, "error message") def test_error_for_409(self): """Make sure a 409 response code yields Conflict.""" self.error_for_status(409, Conflict, "error message") def test_error_for_412(self): """Make sure a 412 response code yields PreconditionFailed.""" self.error_for_status(412, PreconditionFailed, "error message") def test_error_for_4xx(self): """Make sure an unrexognized 4xx response code yields ClientError.""" self.error_for_status(499, ClientError, "error message") def test_no_error_for_5xx(self): """Make sure a 5xx response codes yields ServerError.""" for status in (500, 502, 503, 599): self.error_for_status(status, ServerError) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1580131530.0 lazr.restfulclient-0.14.4/src/lazr/restfulclient/tests/test_oauth.py0000644000175000017500000001260700000000000027460 0ustar00cjwatsoncjwatson00000000000000# Copyright 2009-2018 Canonical Ltd. # This file is part of lazr.restfulclient. # # lazr.restfulclient is free software: you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public License # as published by the Free Software Foundation, version 3 of the # License. # # lazr.restfulclient is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with lazr.restfulclient. If not, see . """Tests for the OAuth-aware classes.""" __metaclass__ = type import os import os.path import stat import unittest from fixtures import ( MockPatch, TempDir, ) from testtools import TestCase from lazr.restfulclient.authorize.oauth import ( AccessToken, Consumer, OAuthAuthorizer, SystemWideConsumer, ) class TestConsumer(TestCase): def test_data_fields(self): consumer = Consumer("key", "secret", "application") self.assertEqual(consumer.key, "key") self.assertEqual(consumer.secret, "secret") self.assertEqual(consumer.application_name, "application") def test_default_application_name(self): # Application name defaults to None consumer = Consumer("key", "secret") self.assertEqual(consumer.application_name, None) class TestAccessToken(TestCase): def test_data_fields(self): access_token = AccessToken("key", "secret", "context") self.assertEqual(access_token.key, "key") self.assertEqual(access_token.secret, "secret") self.assertEqual(access_token.context, "context") def test_default_context(self): # Context defaults to None. access_token = AccessToken("key", "secret") self.assertIsNone(access_token.context) def test___str__(self): access_token = AccessToken("lock&key", "secret=password") self.assertEqual( "oauth_token_secret=secret%3Dpassword&oauth_token=lock%26key", str(access_token)) def test_from_string(self): access_token = AccessToken.from_string( "oauth_token_secret=secret%3Dpassword&oauth_token=lock%26key") self.assertEqual(access_token.key, "lock&key") self.assertEqual(access_token.secret, "secret=password") class TestSystemWideConsumer(TestCase): def test_useful_distro_name(self): # If distro.name returns a useful string, as it does on Ubuntu, # we'll use the first string for the system type. self.useFixture(MockPatch('distro.name', return_value='Fooix')) self.useFixture(MockPatch('platform.system', return_value='FooOS')) self.useFixture(MockPatch('socket.gethostname', return_value='foo')) consumer = SystemWideConsumer("app name") self.assertEqual( consumer.key, 'System-wide: Fooix (foo)') def test_empty_distro_name(self): # If distro.name returns an empty string, as it does on Windows and # Mac OS X, we fall back to the result of platform.system(). self.useFixture(MockPatch('distro.name', return_value='')) self.useFixture(MockPatch('platform.system', return_value='BarOS')) self.useFixture(MockPatch('socket.gethostname', return_value='bar')) consumer = SystemWideConsumer("app name") self.assertEqual( consumer.key, 'System-wide: BarOS (bar)') def test_broken_distro_name(self): # If distro.name raises an exception, we fall back to the result of # platform.system(). self.useFixture( MockPatch('distro.name', side_effect=Exception('Oh noes!'))) self.useFixture(MockPatch('platform.system', return_value='BazOS')) self.useFixture(MockPatch('socket.gethostname', return_value='baz')) consumer = SystemWideConsumer("app name") self.assertEqual( consumer.key, 'System-wide: BazOS (baz)') class TestOAuthAuthorizer(TestCase): """Test for the OAuth Authorizer.""" def test_save_to_and_load_from__path(self): # Credentials can be saved to and loaded from a file using # save_to_path() and load_from_path(). temp_dir = self.useFixture(TempDir()).path credentials_path = os.path.join(temp_dir, 'credentials') credentials = OAuthAuthorizer( 'consumer.key', consumer_secret='consumer.secret', access_token=AccessToken('access.key', 'access.secret')) credentials.save_to_path(credentials_path) self.assertTrue(os.path.exists(credentials_path)) # Make sure the file is readable and writable by the user, but # not by anyone else. self.assertEqual(stat.S_IMODE(os.stat(credentials_path).st_mode), stat.S_IREAD | stat.S_IWRITE) loaded_credentials = OAuthAuthorizer.load_from_path(credentials_path) self.assertEqual(loaded_credentials.consumer.key, 'consumer.key') self.assertEqual( loaded_credentials.consumer.secret, 'consumer.secret') self.assertEqual( loaded_credentials.access_token.key, 'access.key') self.assertEqual( loaded_credentials.access_token.secret, 'access.secret') def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1631546865.618421 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/0000755000175000017500000000000000000000000025250 5ustar00cjwatsoncjwatson00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546865.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/PKG-INFO0000644000175000017500000003411400000000000026350 0ustar00cjwatsoncjwatson00000000000000Metadata-Version: 2.1 Name: lazr.restfulclient Version: 0.14.4 Summary: A programmable client library that takes advantage of the commonalities among Home-page: https://launchpad.net/lazr.restfulclient Maintainer: LAZR Developers Maintainer-email: lazr-developers@lists.launchpad.net License: LGPL v3 Download-URL: https://launchpad.net/lazr.restfulclient/+download Description: .. This file is part of lazr.restfulclient. lazr.restfulclient is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 of the License. lazr.restfulclient is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with lazr.restfulclient. If not, see . LAZR restfulclient ****************** A programmable client library that takes advantage of the commonalities among lazr.restful web services to provide added functionality on top of wadllib. Please see https://dev.launchpad.net/LazrStyleGuide and https://dev.launchpad.net/Hacking for how to develop in this package. =========================== NEWS for lazr.restfulclient =========================== 0.14.4 (2021-09-13) =================== - Drop support for Python < 2.6. - Adjust versioning strategy to avoid importing pkg_resources, which is slow in large environments. 0.14.3 (2020-01-27) =================== - Restore from_string, to_string, and __str__ methods of lazr.restfulclient.authorize.oauth.AccessToken, unintentionally removed in 0.14.0. 0.14.2 (2018-11-17) =================== - Fix compatibility with httplib2 0.12.0 for Python 3. [bug=1803754] - Really fix compatibility with httplib2 < 0.9. - Fix compatibility with httplib2 0.9 for Python 3. - Require httplib2 >= 0.7.7 for Python 3. 0.14.1 (2018-11-16) =================== - Add compatibility with httplib2 0.12.0. [bug=1803558] 0.14.0 (2018-05-08) =================== - Switch from buildout to tox. - Port from oauth to oauthlib. Some tests still need to use oauth until lazr.authentication is ported. [bug=1672458] - Use the distro module rather than platform.linux_distribution, since the latter is deprecated in Python 3.5 and will be removed in 3.7. [bug=1473577] 0.13.5 (2017-09-04) =================== - Fix bytes vs. unicode in json.loads calls. [bug=1403524] - Decode header before comparison. [bug=1414075] - Fix urllib unquote imports. [bug=1414055] - Fix urllib urlencode imports. [bug=1425609] - Tolerate httplib2 versions earlier than 0.9 again. - Fix handling of 304 responses with an empty body on Python 3. [bug=1714960] 0.13.4 (2014-12-05) =================== - Port to python3. - Support proxy settings from environment by default. 0.13.3 (2013-03-22) =================== - Fall back to httplib2's default certificate path if the Debian/Ubuntu one doesn't exist. The default bundle might work, but a path that doesn't exist is never going to. New httplib2 bundles contain the required CA certs. 0.13.2 (2012-12-06) =================== - lazr.restfulclient is almost exclusively used with launchpad.net, but httplib2's cert bundle doesn't include launchpad's CA. Therefore with the default setup launchpadlib doesn't work unless cert checking is disabled. This is mitigated by the fact that Ubuntu carries a patch to httplib2 to make it use the system CA certs. This release makes that the default approach in lazr.restfulclient so that launchpad.net can be used by anyone with the Debian/Ubuntu CA certs path (/etc/ssl/certs/ca-certificates.crt), regardless of whether they are using Ubuntu's patched version of httplib2. Any platforms that don't have that path remain broken. 0.13.1 (2012-09-26) =================== - Named POST operations may result in a resource moving to a new location. Detect the redirect and reload the resource from its new URL. 0.13.0 (2012-06-19) =================== - Add environment variable, LP_DISABLE_SSL_CERTIFICATE_VALIDATION, to disable SSL certificate checks. Most useful when testing against development servers. 0.12.3 (2012-05-17) =================== - Implement the mocked out authorizeRequest for the BasicHttpAuthorizer object. 0.12.2 (2012-04-16) =================== - Fix ServiceRoot.load() so that it properly handles relative URLs in a way that doesn't break subsequent API calls (bug 681767). 0.12.1 (2012-03-28) =================== - Made the cache safe for use by concurrent threads and processes. 0.12.0 (2011-06-30) =================== - Give a more useful AttributeError 0.11.2 (2011-02-03) =================== - The 'web_link' parameter now shows up in lp_attributes, not lp_entries. 0.11.1 (2010-11-04) =================== - Restored compatibility with Python 2.4. 0.11.0 (2010-10-28) =================== - Make it possibly to specify an "application name" separate from the OAuth consumer key. If present, the application name is used in the User-Agent header; otherwise, the OAuth consumer key is used. - Add a "system-wide consumer" which can be used to authorize a user's entire account to use a web service, rather than doing it one application at a time. 0.10.0 (2010-08-12) =================== - Add compatibility with lazr.restful 0.11.0 0.9.21 (2010-07-19) =================== - Ensure that all JSON representations are converted to Unicode. - Restore the old behavior of CollectionWithKeyBasedLookup, which is less efficient but easier to understand. That is, the following code will work as it did in 0.9.17, performing the lookup immediately and raising a KeyError if the object doesn't exist on the server side. service.collection['key'] The more efficient behavior (which doesn't perform the lookup until you actually need the object) is still available, but you have to write this code instead: service.collection('key') - Exceptional conditions will now raise an appropriate subclass of HTTPError instead of always raising HTTPError. - Credential files are now created as being user-readable only. (In launchpadlib, they were created using the default umask and then made user-readable with chmod.) 0.9.20 (2010-06-25) =================== - It's now possible to pass a relative URL (relative to the versioned service root) into load(). 0.9.19 (2010-06-21) =================== - When the representation of a resource, as retrieved from the server, is of a different type than expected, the server value now takes precedence. This means that, in rare situations, a resource may start out presumed to be of one type, and change its capabilities once its representation is fetched from the server. 0.9.18 (2010-06-16) =================== - Made it possible to avoid fetching a representation of every single object looked up from a CollectionWithKeyBasedLookup (by defining .collection_of on the class), potentially improving script performance. 0.9.17 (2010-05-10) =================== - Switched back to asking for compression using the standard Accept-Encoding header. Using the TE header has never worked in a real situation due to HTTP intermediaries. 0.9.16 (2010-05-03) =================== - If a server returns a 502 or 503 error code, lazr.restfulclient will retry its request a configurable number of times in hopes that the error is transient. - It's now possible to invoke lazr.restful destructor methods, with the lp_delete() method. 0.9.15 (2010-04-27) ==================== - Clients will no longer fetch a representation of a collection before invoking a named operation on the collection. 0.9.14 (2010-04-15) =================== - Clients now send a useful and somewhat customizable User-Agent string. - Added a workaround for a bug in httplib2. - Removed the software dependency on lazr.restful except when running the full test suite. (The standalone_test test suite tests basic functionality of lazr.restfulclient to make sure the code base doesn't fundamentally depend on lazr.restful.) 0.9.13 (2010-03-24) =================== - Removed some no-longer-needed compatibility code for buggy servers, and fixed the tests to work with the new release of simplejson. - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't strict enough. The maximum filename length is now 143 characters. 0.9.12 (2010-03-09) =================== - Fixed a bug that prevented a unicode string from being used as a cache filename. 0.9.11 (2010-02-11) =================== - If a lazr.restful web service publishes multiple versions, you can now specify which version to use in a separate constructor argument, rather than sticking it on to the end of the service root. - Filenames in the cache will never be longer than 150 characters, to avoid errors on eCryptfs filesystems. - Added a proof-of-concept test for OAuth-signed anonymous access. - Fixed comparisons of entries and hosted files with None. 0.9.10 (2009-10-23) =================== - lazr.restfulclient now requests the correct WADL media type. - Made HTTPError strings more verbose. - Implemented the equality operator for entry and hosted-file resources. - Resume setting the 'credentials' attribute on ServerRoot to avoid breaking compatibility with launchpadlib. 0.9.9 (2009-10-07) ================== - The WSGI authentication middleware has been moved from lazr.restful to the new lazr.authentication library, and lazr.restfulclient now uses the new library. 0.9.8 (2009-10-06) ================== - Added support for OAuth. 0.9.7 (2009-09-30) ================== - Added support for HTTP Basic Auth. 0.9.6 (2009-09-16) ================== - Made compatible with lazr.restful 0.9.6. 0.9.5 (2009-08-28) ================== - Removed debugging code. 0.9.4 (2009-08-26) ================== - Removed unnecessary build dependencies. - Updated tests for newer version of simplejson. - Made tests less fragile by cleaning up lazr.restful example filemanager between tests. - normalized output of simplejson to unicode. 0.9.3 (2009-08-05) ================== Removed a sys.path hack from setup.py. 0.9.2 (2009-07-16) ================== - Fields that can contain binary data are no longer run through simplejson.dumps(). - For fields that can take on a limited set of values, you can now get a list of possible values. 0.9.1 (2009-07-13) ================== - The client now knows to look for multipart/form-data representations and will create them as appropriate. The upshot of this is that you can now send binary data when invoking named operations that will accept binary data. 0.9 (2009-04-29) ================ - Initial public release Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Provides-Extra: docs Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546865.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/SOURCES.txt0000644000175000017500000000262600000000000027142 0ustar00cjwatsoncjwatson00000000000000COPYING.txt HACKING.rst NEWS.rst README.rst setup.py src/lazr/__init__.py src/lazr.restfulclient.egg-info/PKG-INFO src/lazr.restfulclient.egg-info/SOURCES.txt src/lazr.restfulclient.egg-info/dependency_links.txt src/lazr.restfulclient.egg-info/namespace_packages.txt src/lazr.restfulclient.egg-info/not-zip-safe src/lazr.restfulclient.egg-info/pbr.json src/lazr.restfulclient.egg-info/requires.txt src/lazr.restfulclient.egg-info/top_level.txt src/lazr/restfulclient/__init__.py src/lazr/restfulclient/_browser.py src/lazr/restfulclient/_json.py src/lazr/restfulclient/errors.py src/lazr/restfulclient/resource.py src/lazr/restfulclient/authorize/__init__.py src/lazr/restfulclient/authorize/oauth.py src/lazr/restfulclient/docs/Makefile src/lazr/restfulclient/docs/NEWS.rst src/lazr/restfulclient/docs/authorizer.standalone.rst src/lazr/restfulclient/docs/caching.rst src/lazr/restfulclient/docs/collections.rst src/lazr/restfulclient/docs/entries.rst src/lazr/restfulclient/docs/hosted-files.rst src/lazr/restfulclient/docs/index.rst src/lazr/restfulclient/docs/operations.rst src/lazr/restfulclient/docs/retry.standalone.rst src/lazr/restfulclient/docs/toplevel.rst src/lazr/restfulclient/tests/__init__.py src/lazr/restfulclient/tests/example.py src/lazr/restfulclient/tests/test_atomicfilecache.py src/lazr/restfulclient/tests/test_docs.py src/lazr/restfulclient/tests/test_error.py src/lazr/restfulclient/tests/test_oauth.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546865.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/dependency_links.txt0000644000175000017500000000000100000000000031316 0ustar00cjwatsoncjwatson00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546865.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/namespace_packages.txt0000644000175000017500000000000500000000000031576 0ustar00cjwatsoncjwatson00000000000000lazr ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1428522477.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/not-zip-safe0000644000175000017500000000000100000000000027476 0ustar00cjwatsoncjwatson00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1436317052.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/pbr.json0000664000175000017500000000005700000000000026732 0ustar00cjwatsoncjwatson00000000000000{"is_release": false, "git_version": "b278e66"}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546865.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/requires.txt0000644000175000017500000000051100000000000027645 0ustar00cjwatsoncjwatson00000000000000distro oauthlib setuptools six wadllib>=1.1.4 [:python_version < "3"] httplib2 [:python_version < "3.8"] importlib-metadata [:python_version >= "3"] httplib2>=0.7.7 [docs] Sphinx [test] fixtures>=1.3.0 lazr.authentication lazr.restful>=0.11.0 oauth testtools wsgi_intercept zope.testrunner [test:python_version < "3"] mock ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1631546865.0 lazr.restfulclient-0.14.4/src/lazr.restfulclient.egg-info/top_level.txt0000644000175000017500000000000500000000000027775 0ustar00cjwatsoncjwatson00000000000000lazr