├── .gitignore ├── .hgignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.rst ├── THANKS ├── TODO.txt ├── bootstrap.py ├── buildout.cfg ├── debian ├── changelog ├── clean ├── compat ├── control ├── copyright ├── python-restkit.preinst ├── pyversions ├── rules ├── source │ └── format └── watch ├── doc ├── Makefile ├── NOTICE ├── _static │ ├── blitzer │ │ ├── images │ │ │ ├── ui-bg_diagonals-thick_75_f3d8d8_40x40.png │ │ │ ├── ui-bg_dots-small_65_a6a6a6_2x2.png │ │ │ ├── ui-bg_flat_0_333333_40x100.png │ │ │ ├── ui-bg_flat_65_ffffff_40x100.png │ │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ │ ├── ui-bg_glass_55_fbf8ee_1x400.png │ │ │ ├── ui-bg_highlight-hard_100_eeeeee_1x100.png │ │ │ ├── ui-bg_highlight-hard_100_f6f6f6_1x100.png │ │ │ ├── ui-bg_highlight-soft_15_cc0000_1x100.png │ │ │ ├── ui-icons_004276_256x240.png │ │ │ ├── ui-icons_cc0000_256x240.png │ │ │ └── ui-icons_ffffff_256x240.png │ │ └── jquery-ui-custom.css │ ├── restkit.css │ └── screenshot_client.png ├── _templates │ └── layout.html ├── api.rst ├── api │ ├── client.rst │ ├── conn.rst │ ├── datastructure.rst │ ├── errors.rst │ ├── filters.rst │ ├── forms.rst │ ├── modules.rst │ ├── oauth2.rst │ ├── resource.rst │ ├── restkit.contrib.rst │ ├── restkit.rst │ ├── session.rst │ ├── tee.rst │ ├── util.rst │ ├── version.rst │ └── wrappers.rst ├── authentication.rst ├── client.rst ├── conf.py ├── ghp-import ├── green.rst ├── index.rst ├── installation.rst ├── make.bat ├── news.rst ├── pool.rst ├── resource.rst ├── shell.rst ├── sitemap_config.xml ├── sitemap_gen.py ├── sphinxtogithub.py ├── streaming.rst └── wsgi_proxy.rst ├── examples ├── couchdbproxy.py ├── proxy.ini ├── test_eventlet.py ├── test_gevent.py └── test_threads.py ├── main ├── requirements.txt ├── requirements_dev.txt ├── restkit ├── __init__.py ├── client.py ├── conn.py ├── contrib │ ├── __init__.py │ ├── console.py │ ├── ipython_shell.py │ ├── webob_api.py │ ├── webob_helper.py │ └── wsgi_proxy.py ├── datastructures.py ├── errors.py ├── filters.py ├── forms.py ├── oauth2.py ├── resource.py ├── session.py ├── tee.py ├── util.py ├── version.py └── wrappers.py ├── scripts └── restcli ├── setup.cfg ├── setup.py ├── tests ├── 004-test-client.py ├── 005-test-resource.py ├── 006-test-webob.py ├── 007-test-util.py ├── 008-test-request.py ├── 009-test-oauth_filter.py ├── 010-test-proxies.py ├── 1M ├── __init__.py ├── _server_test.py ├── t.py └── treq.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.swp 3 | *.pyc 4 | *#* 5 | build 6 | dist 7 | setuptools-* 8 | .svn/* 9 | .DS_Store 10 | *.so 11 | restkit.egg-info 12 | nohup.out 13 | *.orig 14 | *.rej 15 | *~ 16 | *.o 17 | *.pyc 18 | *.pyo 19 | tests/*.err 20 | *.swp 21 | store/* 22 | *.DS_Store 23 | *.beam 24 | *.log 25 | restkit.egg-info 26 | dist/ 27 | doc/_build/ 28 | distribute-* 29 | .coverage 30 | .tox 31 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | bin 4 | dist 5 | eggs 6 | parts 7 | var/ 8 | develop-eggs 9 | docs/_build/ 10 | docs/modules/ 11 | .installed.cfg 12 | *.egg-info 13 | *.swp 14 | *.pyc 15 | *.pyo 16 | *.log 17 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.6 5 | - 2.7 6 | 7 | install: 8 | - pip install -r requirements_dev.txt --use-mirrors 9 | - python setup.py install 10 | 11 | script: python setup.py test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2008-2013 (c) Benoît Chesneau 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include NOTICE 2 | include LICENSE 3 | include README.rst 4 | include TODO.txt 5 | include THANKS 6 | include requirements.txt 7 | include bootstrap.py 8 | include buildout.cfg 9 | recursive-include debian * 10 | recursive-include examples * 11 | recursive-include tests * 12 | recursive-include doc * 13 | recursive-include scripts * 14 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Restkit 2 | ------- 3 | 4 | 2008-2013 (c) Benoît Chesneau 5 | 6 | Restkit is released under the MIT license. See the LICENSE file for the complete license. 7 | 8 | oauth2: 9 | ------ 10 | 11 | oauth2 is under MIT license. 12 | 13 | Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | About 2 | ----- 3 | 4 | Restkit is an HTTP resource kit for `Python `_. It allows 5 | you to easily access to HTTP resource and build objects around it. It's the 6 | base of `couchdbkit `_ a Python `CouchDB 7 | `_ framework. 8 | 9 | Restkit is a full HTTP client using pure socket calls and its own HTTP parser. 10 | It's not based on httplib or urllib2. 11 | 12 | Features 13 | -------- 14 | 15 | - Full compatible HTTP client for HTTP 1.0 and 1.1 16 | - Threadsafe 17 | - Use pure socket calls and its own HTTP parser (It's not based on httplib or urllib2) 18 | - Map HTTP resources to Python objects 19 | - **Read** and **Send** on the fly 20 | - Reuses connections 21 | - `Eventlet `_ and `Gevent `_ support 22 | - Support `Chunked transfer encoding`_ in both ways. 23 | - Support `Basic Authentification`_ and `OAuth`_. 24 | - Multipart forms and url-encoded forms 25 | - Streaming support 26 | - Proxy handling 27 | - HTTP Filters, you can hook requests in responses with your own callback 28 | - Compatible with Python 2.x (>= 2.6) 29 | 30 | Documentation 31 | ------------- 32 | 33 | http://restkit.readthedocs.org 34 | 35 | 36 | Installation 37 | ------------ 38 | 39 | restkit requires Python 2.x superior to 2.6 (Python 3 support is coming soon) 40 | 41 | To install restkit using pip you must make sure you have a 42 | recent version of distribute installed:: 43 | 44 | $ curl -O http://python-distribute.org/distribute_setup.py 45 | $ sudo python distribute_setup.py 46 | $ easy_install pip 47 | 48 | 49 | To install from source, run the following command:: 50 | 51 | $ git clone https://github.com/benoitc/restkit.git 52 | $ cd restkit 53 | $ pip install -r requirements.txt 54 | $ python setup.py install 55 | 56 | From pypi:: 57 | 58 | $ pip install restkit 59 | 60 | License 61 | ------- 62 | 63 | restkit is available under the MIT license. 64 | 65 | .. _Chunked transfer encoding: http://en.wikipedia.org/wiki/Chunked_transfer_encoding 66 | .. _Basic Authentification: http://www.ietf.org/rfc/rfc2617.txt 67 | .. _OAuth: http://oauth.net/ 68 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | Restkit THANKS 2 | ===================== 3 | 4 | A number of people have contributed to Restkit by reporting problems, 5 | suggesting improvements or submitting changes. Some of these people are: 6 | 7 | Gael Pasgrimaud 8 | Andrew Wilkinson 9 | Xavier Grangier 10 | Benoit Calvez 11 | Marc Abramowitz 12 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - port to python 3 2 | - refactor the client to simplify the code 3 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Corporation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Bootstrap a buildout-based project 15 | 16 | Simply run this script in a directory containing a buildout.cfg. 17 | The script accepts buildout command-line options, so you can 18 | use the -c option to specify an alternate configuration file. 19 | 20 | $Id$ 21 | """ 22 | 23 | import os, shutil, sys, tempfile, urllib2 24 | from optparse import OptionParser 25 | 26 | tmpeggs = tempfile.mkdtemp() 27 | 28 | is_jython = sys.platform.startswith('java') 29 | 30 | # parsing arguments 31 | parser = OptionParser() 32 | parser.add_option("-v", "--version", dest="version", 33 | help="use a specific zc.buildout version") 34 | parser.add_option("-d", "--distribute", 35 | action="store_true", dest="distribute", default=False, 36 | help="Use Distribute rather than Setuptools.") 37 | 38 | parser.add_option("-c", None, action="store", dest="config_file", 39 | help=("Specify the path to the buildout configuration " 40 | "file to be used.")) 41 | 42 | options, args = parser.parse_args() 43 | 44 | # if -c was provided, we push it back into args for buildout' main function 45 | if options.config_file is not None: 46 | args += ['-c', options.config_file] 47 | 48 | if options.version is not None: 49 | VERSION = '==%s' % options.version 50 | else: 51 | VERSION = '' 52 | 53 | USE_DISTRIBUTE = options.distribute 54 | args = args + ['bootstrap'] 55 | 56 | to_reload = False 57 | try: 58 | import pkg_resources 59 | if not hasattr(pkg_resources, '_distribute'): 60 | to_reload = True 61 | raise ImportError 62 | except ImportError: 63 | ez = {} 64 | if USE_DISTRIBUTE: 65 | exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py' 66 | ).read() in ez 67 | ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True) 68 | else: 69 | exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' 70 | ).read() in ez 71 | ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) 72 | 73 | if to_reload: 74 | reload(pkg_resources) 75 | else: 76 | import pkg_resources 77 | 78 | if sys.platform == 'win32': 79 | def quote(c): 80 | if ' ' in c: 81 | return '"%s"' % c # work around spawn lamosity on windows 82 | else: 83 | return c 84 | else: 85 | def quote (c): 86 | return c 87 | 88 | cmd = 'from setuptools.command.easy_install import main; main()' 89 | ws = pkg_resources.working_set 90 | 91 | if USE_DISTRIBUTE: 92 | requirement = 'distribute' 93 | else: 94 | requirement = 'setuptools' 95 | 96 | if is_jython: 97 | import subprocess 98 | 99 | assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', 100 | quote(tmpeggs), 'zc.buildout' + VERSION], 101 | env=dict(os.environ, 102 | PYTHONPATH= 103 | ws.find(pkg_resources.Requirement.parse(requirement)).location 104 | ), 105 | ).wait() == 0 106 | 107 | else: 108 | assert os.spawnle( 109 | os.P_WAIT, sys.executable, quote (sys.executable), 110 | '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION, 111 | dict(os.environ, 112 | PYTHONPATH= 113 | ws.find(pkg_resources.Requirement.parse(requirement)).location 114 | ), 115 | ) == 0 116 | 117 | ws.add_entry(tmpeggs) 118 | ws.require('zc.buildout' + VERSION) 119 | import zc.buildout.buildout 120 | zc.buildout.buildout.main(args) 121 | shutil.rmtree(tmpeggs) 122 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | newest = false 3 | parts = eggs 4 | develop = . 5 | 6 | [eggs] 7 | recipe = zc.recipe.egg 8 | eggs = 9 | pastescript 10 | ipython 11 | http_parser 12 | restkit 13 | webob 14 | nose 15 | coverage 16 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | restkit (3.3.0-1) karmic; urgency=low 2 | 3 | * bump version 4 | 5 | -- Benoit Chesneau Mon, 20 Jun 2011 17:25:00 +0100 6 | 7 | restkit (3.2.3-1) karmic; urgency=low 8 | 9 | * bump version 10 | 11 | -- Benoit Chesneau Tue, 05 Apr 2011 21:12:00 +0100 12 | 13 | restkit (3.2.2-1) karmic; urgency=low 14 | 15 | * bump version 16 | 17 | -- Benoit Chesneau Tue, 05 Apr 2011 10:47:00 +0100 18 | 19 | restkit (3.2.1-1) karmic; urgency=low 20 | 21 | * bump version 22 | 23 | -- Benoit Chesneau Thu, 22 MAr 2011 10:19:00 +0100 24 | 25 | restkit (3.2.0-1) karmic; urgency=low 26 | 27 | * bump version 28 | 29 | -- Benoit Chesneau Thu, 18 Feb 2011 19:18:00 +0100 30 | 31 | restkit (3.1.0-1) karmic; urgency=low 32 | 33 | * bump version 34 | 35 | -- Benoit Chesneau Thu, 11 Feb 2011 21:37:00 +0100 36 | 37 | restkit (3.0.4-1) karmic; urgency=low 38 | 39 | * bump version 40 | 41 | -- Benoit Chesneau Thu, 07 Feb 2011 17:03:00 +0100 42 | 43 | restkit (3.0.3-1) karmic; urgency=low 44 | 45 | * bump version 46 | 47 | -- Benoit Chesneau Thu, 07 Feb 2011 11:09:00 +0100 48 | 49 | restkit (3.0.2-1) karmic; urgency=low 50 | 51 | * bump version 52 | 53 | -- Benoit Chesneau Thu, 03 Feb 2011 21:33:00 +0100 54 | 55 | restkit (3.0-1) karmic; urgency=low 56 | 57 | * bump version 58 | 59 | -- Benoit Chesneau Thu, 03 Feb 2011 13:43:00 +0100 60 | 61 | restkit (2.3.3-1) karmic; urgency=low 62 | 63 | * bump version 64 | 65 | -- Benoit Chesneau Wed, 17 Dec 2010 01:43:00 +0100 66 | 67 | restkit (2.3.2-1) karmic; urgency=low 68 | 69 | * bump version 70 | 71 | -- Benoit Chesneau Wed, 15 Dec 2010 23:00:00 +0100 72 | 73 | restkit (2.3.1-1) karmic; urgency=low 74 | 75 | * bump version 76 | 77 | -- Benoit Chesneau Thu, 26 Nov 2010 08:31:00 +0100 78 | 79 | restkit (2.3.0-1) karmic; urgency=low 80 | 81 | * bump version 82 | 83 | -- Benoit Chesneau Thu, 25 Nov 2010 14:39:00 +0100 84 | 85 | restkit (2.2.2-1) karmic; urgency=low 86 | 87 | * bump version 88 | 89 | -- Benoit Chesneau Thu, 16 Oct 2010 17:30:00 +0100 90 | 91 | restkit (2.2.0-1) karmic; urgency=low 92 | 93 | * bump version 94 | 95 | -- Benoit Chesneau Thu, 14 Sep 2010 20:50:00 +0100 96 | 97 | restkit (2.1.7-1) karmic; urgency=low 98 | 99 | * bump version 100 | 101 | -- Benoit Chesneau Thu, 06 Sep 2010 03:51:00 -0700 102 | 103 | restkit (2.1.5-1) karmic; urgency=low 104 | 105 | * Fix NoMoreData error on 0 Content-Length 106 | * Fix oauth issue. 107 | 108 | -- Benoit Chesneau Thu, 02 Sep 2010 22:51:00 +0100 109 | 110 | restkit (2.1.5-1) karmic; urgency=low 111 | 112 | * Bump releaase 113 | 114 | -- Benoit Chesneau Fri, 27 Aug 2010 04:16:00 +0100 115 | 116 | restkit (2.1.2-1) karmic; urgency=low 117 | 118 | * Fix multiple headers in filters on_request 119 | 120 | -- Benoit Chesneau Tue, 10 Aug 2010 22:53:00 +0100 121 | 122 | restkit (2.1.1-1) karmic; urgency=low 123 | 124 | * New release 125 | 126 | -- Benoit Chesneau Thu, 05 Aug 2010 18:38:00 +0100 127 | 128 | restkit (0.9.4-1) karmic; urgency=low 129 | 130 | * Initial release 131 | 132 | -- Benoit Chesneau Mon, 22 Feb 2010 08:06:28 +0100 133 | -------------------------------------------------------------------------------- /debian/clean: -------------------------------------------------------------------------------- 1 | restkit.egg-info/* 2 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: restkit 2 | Section: python 3 | Priority: optional 4 | Maintainer: Benoit Chesneau 5 | Build-Depends: debhelper (>= 7), python-support, python-setuptools 6 | Standards-Version: 3.9.0.0 7 | Homepage: http://benoitc.github.com/restkit 8 | 9 | Package: python-restkit 10 | Architecture: all 11 | Depends: ${python:Depends}, ${shlibs:Depends}, ${misc:Depends}, 12 | python-http-parser(>=0.6.0) 13 | Provides: ${python:Provides} 14 | Description: Python REST kit 15 | An HTTP resource kit for Python 16 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | 2008-2010 (c) Benoît Chesneau 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /debian/python-restkit.preinst: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | # This was added by stdeb to workaround Debian #479852. In a nutshell, 6 | # pycentral does not remove normally remove its symlinks on an 7 | # upgrade. Since we're using python-support, however, those symlinks 8 | # will be broken. This tells python-central to clean up any symlinks. 9 | if [ -e /var/lib/dpkg/info/python-restkit.list ] && which pycentral >/dev/null 2>&1 10 | then 11 | pycentral pkgremove python-restkit 12 | fi 13 | 14 | #DEBHELPER# 15 | -------------------------------------------------------------------------------- /debian/pyversions: -------------------------------------------------------------------------------- 1 | 2.5- 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | # export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | # Example watch control file for uscan 2 | # Rename this file to "watch" and then you can run the "uscan" command 3 | # to check for upstream updates and more. 4 | # See uscan(1) for format 5 | 6 | # Compulsory line, this is a version 3 file 7 | version=3 8 | 9 | # Uncomment to examine a Webpage 10 | # 11 | #http://www.example.com/downloads.php python-couchdbkit-(.*)\.tar\.gz 12 | opts=dversionmangle=s/\+dfsg$// \ 13 | http://pypi.python.org/packages/source/c/restkit/restkit-(.*).tar.gz 14 | # http://github.com/benoitc/couchdbkit/downloads/ /benoitc/couchdbkit/tarball/([0-9].*) 15 | 16 | # Uncomment to examine a Webserver directory 17 | #http://www.example.com/pub/python-couchdbkit-(.*)\.tar\.gz 18 | 19 | # Uncommment to examine a FTP server 20 | #ftp://ftp.example.com/pub/python-couchdbkit-(.*)\.tar\.gz debian uupdate 21 | 22 | # Uncomment to find new files on sourceforge, for devscripts >= 2.9 23 | # http://sf.net/python-couchdbkit/python-couchdbkit-(.*)\.tar\.gz 24 | 25 | # Uncomment to find new files on GooglePages 26 | # http://example.googlepages.com/foo.html python-couchdbkit-(.*)\.tar\.gz 27 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | GHPIMPORT = ${PWD}/ghp-import 10 | SPHINXTOGITHUB = ${PWD}/sphinxtogithub.py 11 | EPYDOC = epydoc 12 | SITEMAPGEN = ${PWD}/sitemap_gen.py 13 | 14 | # Internal variables. 15 | PAPEROPT_a4 = -D latex_paper_size=a4 16 | PAPEROPT_letter = -D latex_paper_size=letter 17 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 18 | 19 | 20 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 21 | 22 | help: 23 | @echo "Please use \`make ' where is one of" 24 | @echo " html to make standalone HTML files" 25 | @echo " dirhtml to make HTML files named index.html in directories" 26 | @echo " pickle to make pickle files" 27 | @echo " json to make JSON files" 28 | @echo " htmlhelp to make HTML files and a HTML help project" 29 | @echo " qthelp to make HTML files and a qthelp project" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " changes to make an overview of all changed/added/deprecated items" 32 | @echo " linkcheck to check all external links for integrity" 33 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 34 | 35 | clean: 36 | -rm -rf $(BUILDDIR)/* 37 | 38 | html: clean 39 | @mkdir -p api 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | ${SITEMAPGEN} --config=sitemap_config.xml 42 | 43 | @echo 44 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 45 | 46 | github: 47 | @echo "Send to github" 48 | $(GHPIMPORT) -p $(BUILDDIR)/html 49 | 50 | dirhtml: 51 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 52 | @echo 53 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 54 | 55 | pickle: 56 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 57 | @echo 58 | @echo "Build finished; now you can process the pickle files." 59 | 60 | json: 61 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 62 | @echo 63 | @echo "Build finished; now you can process the JSON files." 64 | 65 | htmlhelp: 66 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 67 | @echo 68 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 69 | ".hhp project file in $(BUILDDIR)/htmlhelp." 70 | 71 | qthelp: 72 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 73 | @echo 74 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 75 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 76 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/restkit.qhcp" 77 | @echo "To view the help file:" 78 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/restkit.qhc" 79 | 80 | latex: 81 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 82 | @echo 83 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 84 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 85 | "run these through (pdf)latex." 86 | 87 | changes: 88 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 89 | @echo 90 | @echo "The overview file is in $(BUILDDIR)/changes." 91 | 92 | linkcheck: 93 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 94 | @echo 95 | @echo "Link check complete; look for any errors in the above output " \ 96 | "or in $(BUILDDIR)/linkcheck/output.txt." 97 | 98 | doctest: 99 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 100 | @echo "Testing of doctests in the sources finished, look at the " \ 101 | "results in $(BUILDDIR)/doctest/output.txt." 102 | -------------------------------------------------------------------------------- /doc/NOTICE: -------------------------------------------------------------------------------- 1 | Restkit 2 | ------- 3 | 4 | 2008-2011 (c) Benoît Chesneau 5 | 6 | Restkit documentation is released under the MIT license. 7 | See the LICENSE file for the complete license. 8 | 9 | 10 | sphinx-to-github extension 11 | -------------------------- 12 | 13 | Under BSD license : 14 | 15 | Copyright (c) 2009, Michael Jones 16 | All rights reserved. 17 | 18 | Redistribution and use in source and binary forms, with or without modification, 19 | are permitted provided that the following conditions are met: 20 | 21 | * Redistributions of source code must retain the above copyright notice, 22 | this list of conditions and the following disclaimer. 23 | * Redistributions in binary form must reproduce the above copyright notice, 24 | this list of conditions and the following disclaimer in the documentation 25 | and/or other materials provided with the distribution. 26 | * The names of its contributors may not be used to endorse or promote 27 | products derived from this software without specific prior written 28 | permission. 29 | 30 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 31 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 32 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 33 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 34 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 35 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 36 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 37 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 39 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | 41 | 42 | ghp-import 43 | ---------- 44 | 45 | Under Tumbolia license 46 | 47 | Tumbolia Public License 48 | 49 | Copyright 2010, Paul Davis 50 | 51 | Copying and distribution of this file, with or without modification, are 52 | permitted in any medium without royalty provided the copyright notice and this 53 | notice are preserved. 54 | 55 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 56 | 57 | 0. opan saurce LOL 58 | 59 | 60 | sitemap_gen 61 | ----------- 62 | 63 | Under BSD License : 64 | 65 | Copyright (c) 2004, 2005, Google Inc. 66 | All rights reserved. 67 | 68 | Redistribution and use in source and binary forms, with or without 69 | modification, are permitted provided that the following conditions 70 | are met: 71 | 72 | * Redistributions of source code must retain the above copyright 73 | notice, this list of conditions and the following disclaimer. 74 | 75 | * Redistributions in binary form must reproduce the above 76 | copyright notice, this list of conditions and the following 77 | disclaimer in the documentation and/or other materials provided 78 | with the distribution. 79 | 80 | * Neither the name of Google Inc. nor the names of its contributors 81 | may be used to endorse or promote products derived from this 82 | software without specific prior written permission. 83 | 84 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 85 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 86 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 87 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 88 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 89 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 90 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 91 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 92 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 93 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 94 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 95 | -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_diagonals-thick_75_f3d8d8_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_diagonals-thick_75_f3d8d8_40x40.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_dots-small_65_a6a6a6_2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_dots-small_65_a6a6a6_2x2.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_flat_0_333333_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_flat_0_333333_40x100.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_flat_65_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_flat_65_ffffff_40x100.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_glass_55_fbf8ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_glass_55_fbf8ee_1x400.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_highlight-hard_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_highlight-hard_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_highlight-hard_100_f6f6f6_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_highlight-hard_100_f6f6f6_1x100.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-bg_highlight-soft_15_cc0000_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-bg_highlight-soft_15_cc0000_1x100.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-icons_004276_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-icons_004276_256x240.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /doc/_static/blitzer/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/blitzer/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /doc/_static/restkit.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | background: #f9f9f9; 4 | } 5 | 6 | p { 7 | font-size: 1em; 8 | } 9 | 10 | a { 11 | color: #e21a1a; 12 | } 13 | 14 | a:hover { 15 | color: #000; 16 | } 17 | 18 | h1 { 19 | font-size: 1.3em; 20 | border-bottom: 1px solid #ccc; 21 | padding-bottom: 0.3em; 22 | margin-top: 1em; 23 | margin-bottom: 1.5em; 24 | color: #000; 25 | } 26 | 27 | h2 { 28 | font-size: 1.1em; 29 | margin: 2em 0 0 0; 30 | color: #000; 31 | padding: 0; 32 | } 33 | 34 | div.container { 35 | display: block; 36 | width: 41em; 37 | margin: 0 auto; 38 | } 39 | 40 | h1.logo { 41 | border: none; 42 | margin: 0; 43 | float: left; 44 | top: 25px; 45 | } 46 | 47 | h1.logo a { 48 | text-decoration: none; 49 | display: block; 50 | padding: 0.3em; 51 | text-decoration: none; 52 | background: #000; 53 | color: #fff; 54 | border: none; 55 | border-radius: 5px; 56 | -moz-border-radius: 5px; 57 | -webkit-border-radius: 5px; 58 | } 59 | #header { 60 | display: block; 61 | } 62 | 63 | #header:after,#header:after { 64 | content: "\0020"; 65 | display: block; 66 | height: 0; 67 | clear: both; 68 | visibility: hidden; 69 | overflow: hidden; 70 | } 71 | 72 | #links { 73 | color: #4E4E4E; 74 | top: 25px; 75 | } 76 | 77 | #links a { 78 | font-weight: 700; 79 | text-decoration: none; 80 | } 81 | 82 | #header #links { 83 | text-align: right; 84 | font-size: .8em; 85 | margin-top: 0.5em; 86 | position: relative; 87 | top: 0; 88 | right: 0; 89 | } 90 | 91 | #menu { 92 | position: relative; 93 | clear: both; 94 | display: block; 95 | width: 100%; 96 | } 97 | 98 | ul#actions{ 99 | list-style: none; 100 | display: block; 101 | float: left; 102 | width: 60%; 103 | text-indent: 0; 104 | margin-left: 0; 105 | padding: 0; 106 | } 107 | 108 | ul#actions li { 109 | display: block; 110 | float: left; 111 | background: #e21a1a; 112 | 113 | border-radius: 5px; 114 | -moz-border-radius: 5px; 115 | -webkit-border-radius: 5px; 116 | margin: 0 .3em 0 0; 117 | padding: .2em; 118 | } 119 | 120 | ul#actions li a { 121 | color: #fff; 122 | text-decoration: None; 123 | } 124 | 125 | 126 | div.highlight { 127 | background: #000; 128 | border-radius: 5px; 129 | -moz-border-radius: 5px; 130 | -webkit-border-radius: 5px; 131 | padding: .2em; 132 | 133 | 134 | } 135 | 136 | 137 | div.highlight pre { 138 | overflow-x: hidden; 139 | } 140 | #footer { 141 | border-top: 1px solid #ccc; 142 | clear: both; 143 | display: block; 144 | width: 100%; 145 | margin-top: 3em; 146 | padding-top: 1em; 147 | text-align: center; 148 | font-size: 0.8em; 149 | } 150 | 151 | #footer a { 152 | color: #444; 153 | } 154 | 155 | /* cse */ 156 | 157 | #cse { 158 | display: block; 159 | width: 250px; 160 | font-size: 1em; 161 | padding: 0; 162 | background: #f9f9f9; 163 | float: right; 164 | padding: .2em; 165 | } 166 | #cse form { 167 | margin: 0; 168 | padding: 0.7em 0; 169 | } 170 | 171 | #cse input[type="text"] { 172 | border-radius: 5px; 173 | -moz-border-radius: 5px; 174 | -webkit-border-radius: 5px; 175 | border: 1px solid #e21a1a; 176 | font-size: 1em; 177 | width: 150px; 178 | } -------------------------------------------------------------------------------- /doc/_static/screenshot_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/doc/_static/screenshot_client.png -------------------------------------------------------------------------------- /doc/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% set script_files = [] %} 2 | {% extends "!layout.html" %} 3 | 4 | {% block extrahead %} 5 | 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | {% endblock %} 25 | 26 | {% block header %} 27 |
28 | 60 | 61 | {% endblock %} 62 | 63 | {%- block relbar1 %}{% endblock %} 64 | 65 | 66 | {% block footer %} 67 |
68 |
69 | 73 | 74 | {% endblock %} 75 | 76 | {%- block relbar2 %}{% endblock %} 77 | -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | restkit API 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | api/restkit 8 | api/client 9 | api/conn 10 | api/datastructures 11 | api/errors 12 | api/filters 13 | api/forms 14 | api/oauth2 15 | api/resource 16 | api/wrappers 17 | api/restkit.contrib 18 | -------------------------------------------------------------------------------- /doc/api/client.rst: -------------------------------------------------------------------------------- 1 | :mod:`client` Module 2 | -------------------- 3 | 4 | .. automodule:: restkit.client 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/conn.rst: -------------------------------------------------------------------------------- 1 | :mod:`conn` Module 2 | ------------------ 3 | 4 | .. automodule:: restkit.conn 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/datastructure.rst: -------------------------------------------------------------------------------- 1 | :mod:`datastructures` Module 2 | ---------------------------- 3 | 4 | .. automodule:: restkit.datastructures 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/errors.rst: -------------------------------------------------------------------------------- 1 | :mod:`errors` Module 2 | -------------------- 3 | 4 | .. automodule:: restkit.errors 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/filters.rst: -------------------------------------------------------------------------------- 1 | :mod:`filters` Module 2 | --------------------- 3 | 4 | .. automodule:: restkit.filters 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/forms.rst: -------------------------------------------------------------------------------- 1 | :mod:`forms` Module 2 | ------------------- 3 | 4 | .. automodule:: restkit.forms 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/modules.rst: -------------------------------------------------------------------------------- 1 | restkit 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | restkit 8 | -------------------------------------------------------------------------------- /doc/api/oauth2.rst: -------------------------------------------------------------------------------- 1 | :mod:`oauth2` Module 2 | -------------------- 3 | 4 | .. automodule:: restkit.oauth2 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/resource.rst: -------------------------------------------------------------------------------- 1 | :mod:`resource` Module 2 | ---------------------- 3 | 4 | .. automodule:: restkit.resource 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/restkit.contrib.rst: -------------------------------------------------------------------------------- 1 | contrib Package 2 | =============== 3 | 4 | :mod:`console` Module 5 | --------------------- 6 | 7 | .. automodule:: restkit.contrib.console 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`ipython_shell` Module 13 | --------------------------- 14 | 15 | .. automodule:: restkit.contrib.ipython_shell 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`webob_api` Module 21 | ----------------------- 22 | 23 | .. automodule:: restkit.contrib.webob_api 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`webob_helper` Module 29 | -------------------------- 30 | 31 | .. automodule:: restkit.contrib.webob_helper 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`wsgi_proxy` Module 37 | ------------------------ 38 | 39 | .. automodule:: restkit.contrib.wsgi_proxy 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | -------------------------------------------------------------------------------- /doc/api/restkit.rst: -------------------------------------------------------------------------------- 1 | restkit Package 2 | =============== 3 | 4 | .. automodule:: restkit 5 | :members: request, set_logging 6 | -------------------------------------------------------------------------------- /doc/api/session.rst: -------------------------------------------------------------------------------- 1 | :mod:`session` Module 2 | --------------------- 3 | 4 | .. automodule:: restkit.session 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/tee.rst: -------------------------------------------------------------------------------- 1 | :mod:`tee` Module 2 | ----------------- 3 | 4 | .. automodule:: restkit.tee 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/util.rst: -------------------------------------------------------------------------------- 1 | :mod:`util` Module 2 | ------------------ 3 | 4 | .. automodule:: restkit.util 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/version.rst: -------------------------------------------------------------------------------- 1 | :mod:`version` Module 2 | --------------------- 3 | 4 | .. automodule:: restkit.version 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/api/wrappers.rst: -------------------------------------------------------------------------------- 1 | :mod:`wrappers` Module 2 | ---------------------- 3 | 4 | .. automodule:: restkit.wrappers 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | Restkit support for now `basic authentication`_ and `OAuth`_. But any 5 | other authentication schema can easily be added using http filters. 6 | 7 | Basic authentication 8 | -------------------- 9 | 10 | Basic authentication is managed by the object :api:`restkit.filters.BasicAuth`. It's handled automatically in :api:`restkit.request` function and in :api:`restkit.resource.Resource` object if `basic_auth_url` property is True. 11 | 12 | To use `basic authentication` in a `Resource object` you can do:: 13 | 14 | from restkit import Resource, BasicAuth 15 | 16 | auth = BasicAuth("username", "password") 17 | r = Resource("http://friendpaste.com", filters=[auth]) 18 | 19 | Or simply use an authentication url:: 20 | 21 | r = Resource("http://username:password@friendpaste.com") 22 | 23 | OAuth 24 | ----- 25 | 26 | Restkit OAuth is based on `simplegeo python-oauth2 module `_ So you don't need other installation to use OAuth (you can also simply use :api:`restkit.oauth2` module in your applications). 27 | 28 | The OAuth filter :api:`restkit.oauth2.filter.OAuthFilter` allow you to associate a consumer per resource (path). Initalize Oauth filter with:: 29 | 30 | path, consumer, token, signaturemethod) 31 | 32 | `token` and `method signature` are optionnals. Consumer should be an instance of :api:`restkit.oauth2.Consumer`, token an instance of :api:`restkit.oauth2.Token` signature method an instance of :api:`oauth2.SignatureMethod` (:api:`restkit.oauth2.Token` is only needed for three-legged requests. 33 | 34 | The filter is appleid if the path match. It allows you to maintain different authorization per path. A wildcard at the indicate to the filter to match all path behind. 35 | 36 | Example the rule `/some/resource/*` will match `/some/resource/other` and `/some/resource/other2`, while the rule `/some/resource` will only match the path `/some/resource`. 37 | 38 | Simple client example: 39 | ~~~~~~~~~~~~~~~~~~~~~~ 40 | 41 | :: 42 | 43 | from restkit import OAuthFilter, request 44 | import restkit.oauth2 as oauth 45 | 46 | # Create your consumer with the proper key/secret. 47 | consumer = oauth.Consumer(key="your-twitter-consumer-key", 48 | secret="your-twitter-consumer-secret") 49 | 50 | # Request token URL for Twitter. 51 | request_token_url = "http://twitter.com/oauth/request_token" 52 | 53 | # Create our filter. 54 | auth = oauth.OAuthFilter('*', consumer) 55 | 56 | # The request. 57 | resp = request(request_token_url, filters=[auth]) 58 | print resp.body_string() 59 | 60 | 61 | If you want to add OAuth to your `TwitterSearch` resource:: 62 | 63 | # Create your consumer with the proper key/secret. 64 | consumer = oauth.Consumer(key="your-twitter-consumer-key", 65 | secret="your-twitter-consumer-secret") 66 | 67 | # Create our filter. 68 | client = oauth.OAuthfilter('*', consumer) 69 | 70 | s = TwitterSearch(filters=[client]) 71 | 72 | Twitter Three-legged OAuth Example: 73 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 74 | 75 | Below is an example from `python-oauth2 `_ of how one would go through a three-legged OAuth flow to gain access to protected resources on Twitter. This is a simple CLI script, but can be easily translated to a web application:: 76 | 77 | import urlparse 78 | 79 | from restkit import request 80 | from restkit.filters import OAuthFilter 81 | import restkit.util.oauth2 as oauth 82 | 83 | consumer_key = 'my_key_from_twitter' 84 | consumer_secret = 'my_secret_from_twitter' 85 | 86 | request_token_url = 'http://twitter.com/oauth/request_token' 87 | access_token_url = 'http://twitter.com/oauth/access_token' 88 | authorize_url = 'http://twitter.com/oauth/authorize' 89 | 90 | consumer = oauth.Consumer(consumer_key, consumer_secret) 91 | 92 | auth = OAuthFilter('*', consumer) 93 | 94 | # Step 1: Get a request token. This is a temporary token that is used for 95 | # having the user authorize an access token and to sign the request to obtain 96 | # said access token. 97 | 98 | 99 | 100 | resp = request(request_token_url, filters=[auth]) 101 | if resp.status_int != 200: 102 | raise Exception("Invalid response %s." % resp.status_code) 103 | 104 | request_token = dict(urlparse.parse_qsl(resp.body_string())) 105 | 106 | print "Request Token:" 107 | print " - oauth_token = %s" % request_token['oauth_token'] 108 | print " - oauth_token_secret = %s" % request_token['oauth_token_secret'] 109 | print 110 | 111 | # Step 2: Redirect to the provider. Since this is a CLI script we do not 112 | # redirect. In a web application you would redirect the user to the URL 113 | # below. 114 | 115 | print "Go to the following link in your browser:" 116 | print "%s?oauth_token=%s" % (authorize_url, request_token['oauth_token']) 117 | print 118 | 119 | # After the user has granted access to you, the consumer, the provider will 120 | # redirect you to whatever URL you have told them to redirect to. You can 121 | # usually define this in the oauth_callback argument as well. 122 | accepted = 'n' 123 | while accepted.lower() == 'n': 124 | accepted = raw_input('Have you authorized me? (y/n) ') 125 | oauth_verifier = raw_input('What is the PIN? ') 126 | 127 | # Step 3: Once the consumer has redirected the user back to the oauth_callback 128 | # URL you can request the access token the user has approved. You use the 129 | # request token to sign this request. After this is done you throw away the 130 | # request token and use the access token returned. You should store this 131 | # access token somewhere safe, like a database, for future use. 132 | token = oauth.Token(request_token['oauth_token'], 133 | request_token['oauth_token_secret']) 134 | token.set_verifier(oauth_verifier) 135 | 136 | auth = OAuthFilter("*", consumer, token) 137 | 138 | resp = request(access_token_url, "POST", filters=[auth]) 139 | access_token = dict(urlparse.parse_qsl(resp.body_string())) 140 | 141 | print "Access Token:" 142 | print " - oauth_token = %s" % access_token['oauth_token'] 143 | print " - oauth_token_secret = %s" % access_token['oauth_token_secret'] 144 | print 145 | print "You may now access protected resources using the access tokens above." 146 | print 147 | 148 | 149 | 150 | .. _basic authentication: http://www.ietf.org/rfc/rfc2617.txt 151 | .. _OAuth: http://oauth.net/ -------------------------------------------------------------------------------- /doc/client.rst: -------------------------------------------------------------------------------- 1 | Command Line 2 | ============ 3 | 4 | Restkit integrate a simple HTTP client in command line named `restcli` allowing you to perform requests. 5 | 6 | .. image:: _static/screenshot_client.png 7 | :width: 90% 8 | 9 | Usage:: 10 | 11 | $ restcli --help 12 | Usage: 'restcli [options] url [METHOD] [filename]' 13 | 14 | Options: 15 | -H HEADERS, --header=HEADERS 16 | http string header in the form of Key:Value. For 17 | example: "Accept: application/json" 18 | -X METHOD, --request=METHOD 19 | http request method 20 | --follow-redirect 21 | -S, --server-response 22 | print server response 23 | -p, --prettify Prettify display 24 | --log-level=LOG_LEVEL 25 | Log level below which to silence messages. [info] 26 | -i FILE, --input=FILE 27 | the name of the file to read from 28 | -o OUTPUT, --output=OUTPUT 29 | the name of the file to write to 30 | --version show program's version number and exit 31 | -h, --help show this help message and exit 32 | 33 | To have better prettyfication, make sure you have `pygments `_, `tidy `_ and `simplejson `_ (or python2.6) installed. They may be already installed on your machine. 34 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import sys, os 7 | import restkit 8 | 9 | CURDIR = os.path.abspath(os.path.dirname(__file__)) 10 | sys.path.append(os.path.join(CURDIR, '..', '..')) 11 | sys.path.append(os.path.join(CURDIR, '..')) 12 | sys.path.append(os.path.join(CURDIR, '.')) 13 | 14 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 15 | 16 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinxtogithub'] 17 | 18 | templates_path = ['_templates'] 19 | 20 | source_suffix = '.rst' 21 | master_doc = 'index' 22 | 23 | project = u'restkit' 24 | copyright = u'2008-2013 Benoît Chesneau ' 25 | 26 | version = restkit.__version__ 27 | release = version 28 | 29 | 30 | exclude_trees = ['_build'] 31 | 32 | if on_rtd: 33 | pygments_style = 'sphinx' 34 | html_theme = 'default' 35 | else: 36 | pygments_style = 'fruity' 37 | html_theme = 'basic' 38 | html_theme_path = [""] 39 | 40 | 41 | html_static_path = ['_static'] 42 | 43 | htmlhelp_basename = 'restkitdoc' 44 | 45 | latex_documents = [ 46 | ('index', 'restkit.tex', u'restkit Documentation', 47 | u'Benoît Chesneau', 'manual'), 48 | ] 49 | -------------------------------------------------------------------------------- /doc/ghp-import: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # This file is part of the ghp-import package released under 4 | # the Tumbolia Public License. See the LICENSE file for more 5 | # information. 6 | 7 | import optparse as op 8 | import os 9 | import subprocess as sp 10 | import time 11 | 12 | __usage__ = "%prog [OPTIONS] DIRECTORY" 13 | 14 | def is_repo(d): 15 | if not os.path.isdir(d): 16 | return False 17 | if not os.path.isdir(os.path.join(d, 'objects')): 18 | return False 19 | if not os.path.isdir(os.path.join(d, 'refs')): 20 | return False 21 | 22 | headref = os.path.join(d, 'HEAD') 23 | if os.path.isfile(headref): 24 | return True 25 | if os.path.islinke(headref) and os.readlink(headref).startswith("refs"): 26 | return True 27 | return False 28 | 29 | def find_repo(path): 30 | if is_repo(path): 31 | return True 32 | if is_repo(os.path.join(path, '.git')): 33 | return True 34 | (parent, ignore) = os.path.split(path) 35 | if parent == path: 36 | return False 37 | return find_repo(parent) 38 | 39 | def try_rebase(remote): 40 | cmd = ['git', 'rev-list', '--max-count=1', 'origin/gh-pages'] 41 | p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) 42 | (rev, ignore) = p.communicate() 43 | if p.wait() != 0: 44 | return True 45 | cmd = ['git', 'update-ref', 'refs/heads/gh-pages', rev.strip()] 46 | if sp.call(cmd) != 0: 47 | return False 48 | return True 49 | 50 | def get_config(key): 51 | p = sp.Popen(['git', 'config', key], stdin=sp.PIPE, stdout=sp.PIPE) 52 | (value, stderr) = p.communicate() 53 | return value.strip() 54 | 55 | def get_prev_commit(): 56 | cmd = ['git', 'rev-list', '--max-count=1', 'gh-pages'] 57 | p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) 58 | (rev, ignore) = p.communicate() 59 | if p.wait() != 0: 60 | return None 61 | return rev.strip() 62 | 63 | def make_when(timestamp=None): 64 | if timestamp is None: 65 | timestamp = int(time.time()) 66 | currtz = "%+05d" % (time.timezone / 36) # / 3600 * 100 67 | return "%s %s" % (timestamp, currtz) 68 | 69 | def start_commit(pipe, message): 70 | username = get_config("user.name") 71 | email = get_config("user.email") 72 | pipe.stdin.write('commit refs/heads/gh-pages\n') 73 | pipe.stdin.write('committer %s <%s> %s\n' % (username, email, make_when())) 74 | pipe.stdin.write('data %d\n%s\n' % (len(message), message)) 75 | head = get_prev_commit() 76 | if head: 77 | pipe.stdin.write('from %s\n' % head) 78 | pipe.stdin.write('deleteall\n') 79 | 80 | def add_file(pipe, srcpath, tgtpath): 81 | pipe.stdin.write('M 100644 inline %s\n' % tgtpath) 82 | with open(srcpath) as handle: 83 | data = handle.read() 84 | pipe.stdin.write('data %d\n' % len(data)) 85 | pipe.stdin.write(data) 86 | pipe.stdin.write('\n') 87 | 88 | def run_import(srcdir, message): 89 | cmd = ['git', 'fast-import', '--date-format=raw', '--quiet'] 90 | pipe = sp.Popen(cmd, stdin=sp.PIPE) 91 | start_commit(pipe, message) 92 | for path, dnames, fnames in os.walk(srcdir): 93 | for fn in fnames: 94 | fpath = os.path.join(path, fn) 95 | add_file(pipe, fpath, os.path.relpath(fpath, start=srcdir)) 96 | pipe.stdin.write('\n') 97 | pipe.stdin.close() 98 | if pipe.wait() != 0: 99 | print "Failed to process commit." 100 | 101 | def options(): 102 | return [ 103 | op.make_option('-m', dest='mesg', default='Update documentation', 104 | help='The commit message to use on the gh-pages branch.'), 105 | op.make_option('-p', dest='push', default=False, action='store_true', 106 | help='Push the branch to origin/gh-pages after committing.'), 107 | op.make_option('-r', dest='remote', default='origin', 108 | help='The name of the remote to push to. [%default]') 109 | ] 110 | 111 | def main(): 112 | parser = op.OptionParser(usage=__usage__, option_list=options()) 113 | opts, args = parser.parse_args() 114 | 115 | if len(args) == 0: 116 | parser.error("No import directory specified.") 117 | 118 | if len(args) > 1: 119 | parser.error("Unknown arguments specified: %s" % ', '.join(args[1:])) 120 | 121 | if not os.path.isdir(args[0]): 122 | parser.error("Not a directory: %s" % args[0]) 123 | 124 | if not find_repo(os.getcwd()): 125 | parser.error("No Git repository found.") 126 | 127 | if not try_rebase(opts.remote): 128 | parser.error("Failed to rebase gh-pages branch.") 129 | 130 | run_import(args[0], opts.mesg) 131 | 132 | if opts.push: 133 | sp.check_call(['git', 'push', opts.remote, 'gh-pages']) 134 | 135 | if __name__ == '__main__': 136 | main() 137 | 138 | -------------------------------------------------------------------------------- /doc/green.rst: -------------------------------------------------------------------------------- 1 | Usage with Eventlet and Gevent 2 | ============================== 3 | 4 | Restkit can be used with `eventlet`_ or `gevent`_ and provide specific 5 | connection manager to manage iddle connections for them. 6 | 7 | Use it with gevent: 8 | ------------------- 9 | 10 | Here is a quick crawler example using Gevent:: 11 | 12 | import timeit 13 | 14 | # patch python to use replace replace functions and classes with 15 | # cooperative ones 16 | from gevent import monkey; monkey.patch_all() 17 | 18 | import gevent 19 | from restkit import * 20 | from socketpool import ConnectionPool 21 | 22 | # set a pool with a gevent packend 23 | pool = ConnectionPool(factory=Connection, backend="gevent") 24 | 25 | urls = [ 26 | "http://yahoo.fr", 27 | "http://google.com", 28 | "http://friendpaste.com", 29 | "http://benoitc.io", 30 | "http://couchdb.apache.org"] 31 | 32 | allurls = [] 33 | for i in range(10): 34 | allurls.extend(urls) 35 | 36 | def fetch(u): 37 | r = request(u, follow_redirect=True, pool=pool) 38 | print "RESULT: %s: %s (%s)" % (u, r.status, len(r.body_string())) 39 | 40 | def extract(): 41 | 42 | jobs = [gevent.spawn(fetch, url) for url in allurls] 43 | gevent.joinall(jobs) 44 | 45 | t = timeit.Timer(stmt=extract) 46 | print "%.2f s" % t.timeit(number=1) 47 | 48 | .. NOTE: 49 | 50 | You have to set the pool in the main thread so it can be used 51 | everywhere in your application. 52 | 53 | You can also set a global pool and use it transparently in your 54 | application:: 55 | 56 | from restkit.session import set_session 57 | set_session("gevent") 58 | 59 | Use it with eventlet: 60 | --------------------- 61 | 62 | Same exemple as above but using eventlet:: 63 | 64 | import timeit 65 | 66 | # patch python 67 | import eventlet 68 | eventlet.monkey_patch() 69 | 70 | from restkit import * 71 | from socketpool import ConnectionPool 72 | 73 | # set a pool with a gevent packend 74 | pool = ConnectionPool(factory=Connection, backend="eventlet") 75 | 76 | epool = eventlet.GreenPool() 77 | 78 | urls = [ 79 | "http://yahoo.fr", 80 | "http://google.com", 81 | "http://friendpaste.com", 82 | "http://benoitc.io", 83 | "http://couchdb.apache.org"] 84 | 85 | allurls = [] 86 | for i in range(10): 87 | allurls.extend(urls) 88 | 89 | def fetch(u): 90 | r = request(u, follow_redirect=True, pool=pool) 91 | print "RESULT: %s: %s (%s)" % (u, r.status, len(r.body_string())) 92 | 93 | def extract(): 94 | for url in allurls: 95 | epool.spawn_n(fetch, url) 96 | epool.waitall() 97 | 98 | t = timeit.Timer(stmt=extract) 99 | print "%.2f s" % t.timeit(number=1) 100 | 101 | 102 | .. _eventlet: http://eventlet.net 103 | .. _gevent: http://gevent.org 104 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. RESTKit documentation master file, created by 2 | sphinx-quickstart on Fri Feb 26 23:09:27 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to RESTKit's documentation! 7 | =================================== 8 | 9 | Restkit is an HTTP resource kit for `Python `_. It allows you to easily access to HTTP resource and build objects around it. It's the base of `couchdbkit `_ a Python `CouchDB `_ framework. 10 | 11 | You can simply use :func:`restkit.request` function to do any HTTP requests. 12 | 13 | Usage example, get a friendpaste paste:: 14 | 15 | >>> from restkit import request 16 | >>> r = request('http://friendpaste.com/1ZSEoJeOarc3ULexzWOk5Y_633433316631/raw') 17 | >>> r.body_string() 18 | 'welcome to friendpaste' 19 | >>> r.headers 20 | {'status': '200 OK', 'transfer-encoding': 'chunked', 'set-cookie': 21 | 'FRIENDPASTE_SID=b581975029d689119d3e89416e4c2f6480a65d96; expires=Sun, 22 | 14-Mar-2010 03:29:31 GMT; Max-Age=1209600; Path=/', 'server': 'nginx/0.7.62', 23 | 'connection': 'keep-alive', 'date': 'Sun, 28 Feb 2010 03:29:31 GMT', 24 | 'content-type': 'text/plain'} 25 | 26 | of from a resource: 27 | 28 | >>> from restkit import Resource 29 | >>> res = Resource('http://friendpaste.com') 30 | >>> r = res.get('/1ZSEoJeOarc3ULexzWOk5Y_633433316631/raw') 31 | >>> r.body_string() 32 | 'welcome to friendpaste' 33 | 34 | but you can do more like building object mapping HTTP resources, .... 35 | 36 | .. note:: 37 | restkit source code is hosted on `Github `_ 38 | 39 | Features 40 | -------- 41 | 42 | - Full compatible HTTP client for HTTP 1.0 and 1.1 43 | - Threadsafe 44 | - Use pure socket calls and its own HTTP parser (It's not based on httplib or urllib2) 45 | - Map HTTP resources to Python objects 46 | - **Read** and **Send** on the fly 47 | - Reuses connections 48 | - `Eventlet `_ and `Gevent `_ support 49 | - Support `Chunked transfer encoding`_ in both ways. 50 | - Support `Basic Authentification`_ and `OAuth`_. 51 | - Multipart forms and url-encoded forms 52 | - Streaming support 53 | - Proxy handling 54 | - HTTP Filters, you can hook requests in responses with your own callback 55 | - Compatible with Python 2.x (>= 2.6) 56 | 57 | 58 | Content 59 | ------- 60 | 61 | .. toctree:: 62 | :maxdepth: 2 63 | 64 | installation 65 | resource 66 | pool 67 | authentication 68 | streaming 69 | green 70 | client 71 | shell 72 | wsgi_proxy 73 | 74 | 75 | API 76 | --- 77 | 78 | The changelog is available :ref:`here ` . 79 | 80 | .. toctree:: 81 | :maxdepth: 2 82 | 83 | api 84 | 85 | .. _Chunked transfer encoding: http://en.wikipedia.org/wiki/Chunked_transfer_encoding 86 | .. _Basic Authentification: http://www.ietf.org/rfc/rfc2617.txt 87 | .. _OAuth: http://oauth.net/ 88 | -------------------------------------------------------------------------------- /doc/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | Requirements 7 | ------------ 8 | 9 | - **Python 2.6 or newer** (Python 3.x will be supported soon) 10 | - setuptools >= 0.6c6 11 | - nosetests (for the test suite only) 12 | 13 | Installation 14 | ------------ 15 | 16 | To install restkit using pip you must make sure you have a 17 | recent version of distribute installed:: 18 | 19 | $ curl -O http://python-distribute.org/distribute_setup.py 20 | $ sudo python distribute_setup.py 21 | $ easy_install pip 22 | 23 | To install or upgrade to the latest released version of restkit:: 24 | 25 | $ pip install -r requirements.txt 26 | $ pip install restkit 27 | 28 | 29 | Note: if you get an error on MacOSX try to install with the following 30 | arguments:: 31 | 32 | $ env ARCHFLAGS="-arch i386 -arch x86_64" pip install http-parser 33 | 34 | Installation from source 35 | ------------------------ 36 | 37 | You can install Restkit from source as simply as you would install any 38 | other Python package. Restkit uses setuptools which will automatically 39 | fetch all dependencies (including setuptools itself). 40 | 41 | Get a Copy 42 | ++++++++++ 43 | 44 | You can download a tarball of the latest sources from `GitHub Downloads`_ or fetch them with git_:: 45 | 46 | $ git clone git://github.com/benoitc/restkit.git 47 | 48 | .. _`GitHub Downloads`: http://github.com/benoitc/restkit/downloads 49 | .. _git: http://git-scm.com/ 50 | 51 | Installation 52 | ++++++++++++ 53 | 54 | :: 55 | 56 | $ python setup.py install 57 | 58 | 59 | Note: If you don't use setuptools or distribute, make sure http-parser 60 | is installed first. 61 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set BUILDDIR=_build 7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 8 | if NOT "%PAPER%" == "" ( 9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 10 | ) 11 | 12 | if "%1" == "" goto help 13 | 14 | if "%1" == "help" ( 15 | :help 16 | echo.Please use `make ^` where ^ is one of 17 | echo. html to make standalone HTML files 18 | echo. dirhtml to make HTML files named index.html in directories 19 | echo. pickle to make pickle files 20 | echo. json to make JSON files 21 | echo. htmlhelp to make HTML files and a HTML help project 22 | echo. qthelp to make HTML files and a qthelp project 23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 24 | echo. changes to make an overview over all changed/added/deprecated items 25 | echo. linkcheck to check all external links for integrity 26 | echo. doctest to run all doctests embedded in the documentation if enabled 27 | goto end 28 | ) 29 | 30 | if "%1" == "clean" ( 31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 32 | del /q /s %BUILDDIR%\* 33 | goto end 34 | ) 35 | 36 | if "%1" == "html" ( 37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 38 | echo. 39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 40 | goto end 41 | ) 42 | 43 | if "%1" == "dirhtml" ( 44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 47 | goto end 48 | ) 49 | 50 | if "%1" == "pickle" ( 51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 52 | echo. 53 | echo.Build finished; now you can process the pickle files. 54 | goto end 55 | ) 56 | 57 | if "%1" == "json" ( 58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 59 | echo. 60 | echo.Build finished; now you can process the JSON files. 61 | goto end 62 | ) 63 | 64 | if "%1" == "htmlhelp" ( 65 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 66 | echo. 67 | echo.Build finished; now you can run HTML Help Workshop with the ^ 68 | .hhp project file in %BUILDDIR%/htmlhelp. 69 | goto end 70 | ) 71 | 72 | if "%1" == "qthelp" ( 73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 74 | echo. 75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 76 | .qhcp project file in %BUILDDIR%/qthelp, like this: 77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\restkit.qhcp 78 | echo.To view the help file: 79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\restkit.ghc 80 | goto end 81 | ) 82 | 83 | if "%1" == "latex" ( 84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 85 | echo. 86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 87 | goto end 88 | ) 89 | 90 | if "%1" == "changes" ( 91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 92 | echo. 93 | echo.The overview file is in %BUILDDIR%/changes. 94 | goto end 95 | ) 96 | 97 | if "%1" == "linkcheck" ( 98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 99 | echo. 100 | echo.Link check complete; look for any errors in the above output ^ 101 | or in %BUILDDIR%/linkcheck/output.txt. 102 | goto end 103 | ) 104 | 105 | if "%1" == "doctest" ( 106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 107 | echo. 108 | echo.Testing of doctests in the sources finished, look at the ^ 109 | results in %BUILDDIR%/doctest/output.txt. 110 | goto end 111 | ) 112 | 113 | :end 114 | -------------------------------------------------------------------------------- /doc/pool.rst: -------------------------------------------------------------------------------- 1 | Reuses connections 2 | ================== 3 | 4 | Reusing connections is good. Restkit can maintain http 5 | connections for you and reuse them if the server allows it. To do that restkit 6 | uses the `socketpool module 7 | `_ :: 8 | 9 | from restkit import * 10 | from socketpool import ConnectionPool 11 | 12 | pool = ConnectionPool(factory=Connection) 13 | 14 | r = request("http://someurl", pool=pool) 15 | 16 | .. NOTE:: 17 | 18 | By default, restkit uses a generic session object that is globally available. 19 | You can change its settings by using the 20 | **restkit.sesssion.set_session** function. 21 | 22 | Restkit also provides a Pool that works with `eventlet `_ or `gevent `_. 23 | 24 | Example of usage with Gevent:: 25 | 26 | from restkit import * 27 | from socketpool import ConnectionPool 28 | 29 | # set a pool with a gevent packend 30 | pool = ConnectionPool(factory=Connection, backend="gevent") 31 | 32 | Replace **gevent** by **eventlet** for eventlet support. 33 | -------------------------------------------------------------------------------- /doc/resource.rst: -------------------------------------------------------------------------------- 1 | Build resource object 2 | ===================== 3 | 4 | Building a resource object is easy using :class:`restkit.resource.Resource` class. You just need too inherit this object and add your methods. `Couchdbkit `_ is using restkit to access to `CouchDB `_. A resource object is an Python object associated to an URI. You can use `get`, `post`, `put`, `delete` or `head` method just like you do a request. 5 | 6 | Create a simple Twitter Search resource 7 | +++++++++++++++++++++++++++++++++++++++ 8 | 9 | We use `simplejson `_ to handle deserialisation of data. 10 | 11 | Here is the snippet:: 12 | 13 | from restkit import Resource 14 | 15 | try: 16 | import simplejson as json 17 | except ImportError: 18 | import json # py2.6 only 19 | 20 | class TwitterSearch(Resource): 21 | 22 | def __init__(self, **kwargs): 23 | search_url = "http://search.twitter.com" 24 | super(TwitterSearch, self).__init__(search_url, follow_redirect=True, 25 | max_follow_redirect=10, **kwargs) 26 | 27 | def search(self, query): 28 | return self.get('search.json', q=query) 29 | 30 | def request(self, *args, **kwargs): 31 | resp = super(TwitterSearch, self).request(*args, **kwargs) 32 | return json.loads(resp.body_string()) 33 | 34 | if __name__ == "__main__": 35 | s = TwitterSearch() 36 | print s.search("gunicorn") 37 | -------------------------------------------------------------------------------- /doc/shell.rst: -------------------------------------------------------------------------------- 1 | restkit shell 2 | ============= 3 | 4 | restkit come with a IPython based shell to help you to debug your http apps. Just run:: 5 | 6 | $ restkit --shell http://benoitc.github.com/restkit/ 7 | 8 | HTTP Methods 9 | ------------ 10 | :: 11 | >>> delete([req|url|path_info]) # send a HTTP delete 12 | >>> get([req|url|path_info], **query_string) # send a HTTP get 13 | >>> head([req|url|path_info], **query_string) # send a HTTP head 14 | >>> post([req|url|path_info], [Stream()|**query_string_body]) # send a HTTP post 15 | >>> put([req|url|path_info], stream) # send a HTTP put 16 | 17 | 18 | Helpers 19 | ------- 20 | :: 21 | 22 | >>> req # request to play with. By default http methods will use this one 23 | 24 | 25 | >>> stream # Stream() instance if you specified a -i in command line 26 | None 27 | 28 | >>> ctypes # Content-Types helper with headers properties 29 | 33 | 34 | restkit shell 1.2.1 35 | 1) restcli$ 36 | 37 | 38 | Here is a sample session:: 39 | 40 | 1) restcli$ req 41 | ----------> req() 42 | GET /restkit/ HTTP/1.0 43 | Host: benoitc.github.com 44 | 2) restcli$ get() 45 | 200 OK 46 | Content-Length: 10476 47 | Accept-Ranges: bytes 48 | Expires: Sat, 03 Apr 2010 12:25:09 GMT 49 | Server: nginx/0.7.61 50 | Last-Modified: Mon, 08 Mar 2010 07:53:16 GMT 51 | Connection: keep-alive 52 | Cache-Control: max-age=86400 53 | Date: Fri, 02 Apr 2010 12:25:09 GMT 54 | Content-Type: text/html 55 | 2) 56 | 3) restcli$ resp.status 57 | 3) '200 OK' 58 | 4) restcli$ put() 59 | 405 Not Allowed 60 | Date: Fri, 02 Apr 2010 12:25:28 GMT 61 | Content-Length: 173 62 | Content-Type: text/html 63 | Connection: keep-alive 64 | Server: nginx/0.7.61 65 | 66 | 67 | 405 Not Allowed 68 | 69 |

405 Not Allowed

70 |
nginx/0.7.61
71 | 72 | 73 | 74 | 4) 75 | 5) restcli$ resp.status 76 | 5) '405 Not Allowed' 77 | 6) restcli$ req.path_info = '/restkit/api/index.html' 78 | 7) restcli$ get 79 | ----------> get() 80 | 200 OK 81 | Content-Length: 10476 82 | Accept-Ranges: bytes 83 | Expires: Sat, 03 Apr 2010 12:26:18 GMT 84 | Server: nginx/0.7.61 85 | Last-Modified: Mon, 08 Mar 2010 07:53:16 GMT 86 | Connection: keep-alive 87 | Cache-Control: max-age=86400 88 | Date: Fri, 02 Apr 2010 12:26:18 GMT 89 | Content-Type: text/html 90 | 7) 91 | 8) restcli$ get('/restkit') 92 | 301 Moved Permanently 93 | Location: http://benoitc.github.com/restkit/ 94 | 95 | 96 | 301 Moved Permanently 97 | 98 |

301 Moved Permanently

99 |
nginx/0.7.61
100 | 101 | 102 | 103 | 8) 104 | 9) restcli$ resp.location 105 | 9) 'http://benoitc.github.com/restkit/' 106 | 107 | -------------------------------------------------------------------------------- /doc/sitemap_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /doc/streaming.rst: -------------------------------------------------------------------------------- 1 | Stream your content 2 | =================== 3 | 4 | With Restkit you can easily stream your content to and from a server. 5 | 6 | Stream to 7 | --------- 8 | 9 | To stream a content to a server, pass to your request a file (or file-like object) or an iterator as `body`. If you use an iterator or a file-like object and Restkit can't determine its size (by reading `Content-Length` header or fetching the size of the file), sending will be chunked and Restkit add `Transfer-Encoding: chunked` header to the list of headers. 10 | 11 | Here is a quick snippet with a file:: 12 | 13 | from restkit import request 14 | 15 | with open("/some/file", "r") as f: 16 | request("/some/url", 'POST', body=f) 17 | 18 | Here restkit will put the file size in `Content-Length` header. Another example with an iterator:: 19 | 20 | from restkit import request 21 | 22 | myiterator = ['line 1', 'line 2'] 23 | request("/some/url", 'POST', body=myiterator) 24 | 25 | Sending will be chunked. If you want to send without TE: chunked, you need to add the `Content-Length` header:: 26 | 27 | request("/some/url", 'POST', body=myiterator, 28 | headers={'content-Length': 12}) 29 | 30 | Stream from 31 | ----------- 32 | 33 | Each requests return a :class:`restkit.wrappers.Response` object. If you want to receive the content in a streaming fashion you just have to use the `body_stream` member of the response. You can `iter` on it or just use as a file-like object (read, readline, readlines, ...). 34 | 35 | **Attention**: Since 2.0, response.body are just streamed and aren't persistent. In previous version, the implementation may cause problem with memory or storage usage. 36 | 37 | Quick snippet with iteration:: 38 | 39 | import os 40 | from restkit import request 41 | import tempfile 42 | 43 | r = request("http://e-engura.com/images/logo.gif") 44 | fd, fname = tempfile.mkstemp(suffix='.gif') 45 | 46 | with r.body_stream() as body: 47 | with os.fdopen(fd, "wb") as f: 48 | for block in body: 49 | f.write(block) 50 | 51 | Or if you just want to read:: 52 | 53 | with r.body_stream() as body: 54 | with os.fdopen(fd, "wb") as f: 55 | while True: 56 | data = body.read(1024) 57 | if not data: 58 | break 59 | f.write(data) 60 | 61 | Tee input 62 | --------- 63 | 64 | While with body_stream you can only consume the input until the end, you 65 | may want to reuse this body later in your application. For that, restkit 66 | since the 3.0 version offer the `tee` method. It copy response input to 67 | standard output or a file if length > sock.MAX_BODY. When all the input 68 | has been read, connection is released:: 69 | 70 | from restkit import request 71 | import tempfile 72 | 73 | r = request("http://e-engura.com/images/logo.gif") 74 | fd, fname = tempfile.mkstemp(suffix='.gif') 75 | fd1, fname1 = tempfile.mkstemp(suffix='.gif') 76 | 77 | body = t.tee() 78 | # save first file 79 | with os.fdopen(fd, "wb") as f: 80 | for chunk in body: f.write(chunk) 81 | 82 | # reset 83 | body.seek(0) 84 | # save second file. 85 | with os.fdopen(fd1, "wb") as f: 86 | for chunk in body: f.write(chunk) 87 | 88 | 89 | -------------------------------------------------------------------------------- /doc/wsgi_proxy.rst: -------------------------------------------------------------------------------- 1 | wsgi_proxy 2 | ---------- 3 | 4 | Restkit version 1.2 introduced a WSGI proxy extension written by `Gael 5 | Pasgrimaud `_. This extension proxies WSGI requests to a 6 | remote server. 7 | 8 | Here is a quick example. You can read full post `here 9 | `_ 10 | . 11 | 12 | In this example, we create a simple proxy for `CouchDB `_. We 13 | use `webob `_ and `gunicorn 14 | `_ to launch it:: 15 | 16 | import urlparse 17 | 18 | from webob import Request 19 | from restkit.conn import TConnectionManager 20 | from restkit.ext.wsgi_proxy import HostProxy 21 | 22 | mgr = TConnectionManager(nb_connections=10) 23 | proxy = HostProxy("http://127.0.0.1:5984", pool=mgr) 24 | 25 | 26 | def application(environ, start_response): 27 | req = Request(environ) 28 | if 'RAW_URI' in req.environ: 29 | # gunicorn so we can use real path non encoded 30 | u = urlparse.urlparse(req.environ['RAW_URI']) 31 | req.environ['PATH_INFO'] = u.path 32 | 33 | # do smth like adding oauth headers .. 34 | resp = req.get_response(proxy) 35 | 36 | # rewrite response 37 | # do auth ... 38 | return resp(environ, start_response) 39 | 40 | 41 | And then launch your application:: 42 | 43 | gunicorn -w 12 -a "egg:gunicorn#eventlet" couchdbproxy:application 44 | 45 | 46 | And access to your couchdb at `http://127.0.0.1:8000`. 47 | 48 | You can also use a Paste configuration:: 49 | 50 | [app:proxy] 51 | use = egg:restkit#host_proxy 52 | uri = http://www.example.com/example_db 53 | strip_script_name = false 54 | allowed_methods = get 55 | 56 | Here is a more advanced example to show how to use the Proxy class to build a 57 | distributed proxy. `/a/db` will proxify `http://a.mypool.org/db`:: 58 | 59 | import urlparse 60 | 61 | from webob import Request 62 | from restkit.conn import TConnectionManager 63 | from restkit.ext.wsgi_proxy import Proxy 64 | 65 | mgr = TConnectionManager(nb_connections=10) 66 | 67 | proxy = Proxy(pool=mgr, strip_script_name=True) 68 | 69 | 70 | def application(environ, start_response): 71 | req = Request(environ).copy() 72 | req.path_info_pop() 73 | req.environ['SERVER_NAME'] = '%s.mypool.org:80' % req.script_name.strip('/') 74 | resp = req.get_response(Proxy) 75 | return resp(environ, start_response) 76 | 77 | -------------------------------------------------------------------------------- /examples/couchdbproxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import urlparse 7 | 8 | from webob import Request 9 | from restkit.contrib.wsgi_proxy import HostProxy 10 | 11 | import restkit 12 | from restkit.conn import Connection 13 | from socketpool import ConnectionPool 14 | 15 | restkit.set_logging("debug") 16 | 17 | pool = ConnectionPool(factory=Connection, max_size=10, backend="thread") 18 | proxy = HostProxy("http://127.0.0.1:5984", pool=pool) 19 | 20 | 21 | def application(environ, start_response): 22 | req = Request(environ) 23 | if 'RAW_URI' in req.environ: 24 | # gunicorn so we use real path non encoded 25 | u = urlparse.urlparse(req.environ['RAW_URI']) 26 | req.environ['PATH_INFO'] = u.path 27 | 28 | # do smth like adding oauth headers .. 29 | resp = req.get_response(proxy) 30 | 31 | # rewrite response 32 | # do auth ... 33 | return resp(environ, start_response) 34 | -------------------------------------------------------------------------------- /examples/proxy.ini: -------------------------------------------------------------------------------- 1 | [server:main] 2 | use = egg:Paste#http 3 | port = 4969 4 | 5 | [app:main] 6 | use = egg:Paste#urlmap 7 | /couchdb = couchdb 8 | / = proxy 9 | 10 | [app:couchdb] 11 | use = egg:restkit#couchdb_proxy 12 | db_name = 13 | 14 | [app:proxy] 15 | use = egg:restkit#host_proxy 16 | uri = http://benoitc.github.com/restkit/ 17 | max_connections=50 18 | -------------------------------------------------------------------------------- /examples/test_eventlet.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | 3 | import eventlet 4 | eventlet.monkey_patch() 5 | 6 | from restkit import * 7 | from restkit.conn import Connection 8 | from socketpool import ConnectionPool 9 | 10 | #set_logging("debug") 11 | 12 | pool = ConnectionPool(factory=Connection, backend="eventlet") 13 | 14 | epool = eventlet.GreenPool() 15 | 16 | urls = [ 17 | "http://refuge.io", 18 | "http://gunicorn.org", 19 | "http://friendpaste.com", 20 | "http://benoitc.io", 21 | "http://couchdb.apache.org"] 22 | 23 | allurls = [] 24 | for i in range(10): 25 | allurls.extend(urls) 26 | 27 | def fetch(u): 28 | r = request(u, follow_redirect=True, pool=pool) 29 | print "RESULT: %s: %s (%s)" % (u, r.status, len(r.body_string())) 30 | 31 | def extract(): 32 | for url in allurls: 33 | epool.spawn_n(fetch, url) 34 | epool.waitall() 35 | 36 | t = timeit.Timer(stmt=extract) 37 | print "%.2f s" % t.timeit(number=1) 38 | -------------------------------------------------------------------------------- /examples/test_gevent.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | 3 | from gevent import monkey; monkey.patch_all() 4 | import gevent 5 | 6 | from restkit import * 7 | from restkit.conn import Connection 8 | from socketpool import ConnectionPool 9 | 10 | #set_logging("debug") 11 | 12 | pool = ConnectionPool(factory=Connection, backend="gevent") 13 | 14 | urls = [ 15 | "http://refuge.io", 16 | "http://gunicorn.org", 17 | "http://friendpaste.com", 18 | "http://benoitc.io", 19 | "http://couchdb.apache.org"] 20 | 21 | allurls = [] 22 | for i in range(10): 23 | allurls.extend(urls) 24 | 25 | def fetch(u): 26 | r = request(u, follow_redirect=True, pool=pool) 27 | print "RESULT: %s: %s (%s)" % (u, r.status, len(r.body_string())) 28 | 29 | def extract(): 30 | 31 | jobs = [gevent.spawn(fetch, url) for url in allurls] 32 | gevent.joinall(jobs) 33 | 34 | t = timeit.Timer(stmt=extract) 35 | print "%.2f s" % t.timeit(number=1) 36 | -------------------------------------------------------------------------------- /examples/test_threads.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import timeit 3 | from restkit import * 4 | 5 | #set_logging("debug") 6 | 7 | urls = [ 8 | "http://refuge.io", 9 | "http://gunicorn.org", 10 | "http://friendpaste.com", 11 | "http://benoitc.io", 12 | "http://couchdb.apache.org"] 13 | 14 | allurls = [] 15 | for i in range(10): 16 | allurls.extend(urls) 17 | 18 | def fetch(u): 19 | r = request(u, follow_redirect=True) 20 | print "RESULT: %s: %s (%s)" % (u, r.status, len(r.body_string())) 21 | 22 | def spawn(u): 23 | t = threading.Thread(target=fetch, args=[u]) 24 | t.daemon = True 25 | t.start() 26 | return t 27 | 28 | def extract(): 29 | threads = [spawn(u) for u in allurls] 30 | [t.join() for t in threads] 31 | 32 | t = timeit.Timer(stmt=extract) 33 | print "%.2f s" % t.timeit(number=1) 34 | -------------------------------------------------------------------------------- /main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/main -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | http-parser>=0.8.3 2 | socketpool>=0.5.3 3 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | http-parser>=0.8.3 2 | socketpool>=0.5.3 3 | nose 4 | webob 5 | sphinx 6 | ipython 7 | -------------------------------------------------------------------------------- /restkit/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from restkit.version import version_info, __version__ 7 | 8 | try: 9 | from restkit.conn import Connection 10 | from restkit.errors import ResourceNotFound, Unauthorized, RequestFailed,\ 11 | RedirectLimit, RequestError, InvalidUrl, ResponseError, ProxyError, \ 12 | ResourceError, ResourceGone 13 | from restkit.client import Client, MAX_FOLLOW_REDIRECTS 14 | from restkit.wrappers import Request, Response, ClientResponse 15 | from restkit.resource import Resource 16 | from restkit.filters import BasicAuth, OAuthFilter 17 | except ImportError: 18 | import traceback 19 | traceback.print_exc() 20 | 21 | import urlparse 22 | import logging 23 | 24 | LOG_LEVELS = { 25 | "critical": logging.CRITICAL, 26 | "error": logging.ERROR, 27 | "warning": logging.WARNING, 28 | "info": logging.INFO, 29 | "debug": logging.DEBUG 30 | } 31 | 32 | def set_logging(level, handler=None): 33 | """ 34 | Set level of logging, and choose where to display/save logs 35 | (file or standard output). 36 | """ 37 | if not handler: 38 | handler = logging.StreamHandler() 39 | 40 | loglevel = LOG_LEVELS.get(level, logging.INFO) 41 | logger = logging.getLogger('restkit') 42 | logger.setLevel(loglevel) 43 | format = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s" 44 | datefmt = r"%Y-%m-%d %H:%M:%S" 45 | 46 | handler.setFormatter(logging.Formatter(format, datefmt)) 47 | logger.addHandler(handler) 48 | 49 | 50 | def request(url, method='GET', body=None, headers=None, **kwargs): 51 | """Quick shortcut method to pass a request 52 | 53 | Request parameters 54 | ------------------ 55 | 56 | - **url**: str, url string 57 | - **method**: str, by default GET. http verbs 58 | - **body**: the body, could be a string, an iterator or a file-like object 59 | - **headers**: dict or list of tupple, http headers 60 | 61 | Client parameters 62 | ----------------- 63 | 64 | - **follow_redirect**: follow redirection, by default False 65 | - **max_follow_redirect**: number of redirections available 66 | - **filters** http filters to pass 67 | - **decompress**: allows the client to decompress the response body 68 | - ** max_status_line_garbage**: defines the maximum number of ignorable 69 | lines before we expect a HTTP response's status line. With HTTP/1.1 70 | persistent connections, the problem arises that broken scripts could 71 | return a wrong Content-Length (there are more bytes sent than 72 | specified). Unfortunately, in some cases, this cannot be detected after 73 | the bad response, but only before the next one. So the client is abble 74 | to skip bad lines using this limit. 0 disable garbage collection, None 75 | means unlimited number of tries. 76 | - **max_header_count**: determines the maximum HTTP header count allowed. 77 | by default no limit. 78 | - manager: the manager to use. By default we use the global one. 79 | - **response_class**: the response class to use 80 | - **timeout**: the default timeout of the connection (SO_TIMEOUT) 81 | - **max_tries**: the number of tries before we give up a connection 82 | - **wait_tries**: number of time we wait between each tries. 83 | - **ssl_args**: ssl named arguments, See 84 | http://docs.python.org/library/ssl.html informations 85 | """ 86 | 87 | # detect credentials from url 88 | u = urlparse.urlparse(url) 89 | if u.username is not None: 90 | password = u.password or "" 91 | filters = kwargs.get('filters') or [] 92 | url = urlparse.urlunparse((u.scheme, u.netloc.split("@")[-1], 93 | u.path, u.params, u.query, u.fragment)) 94 | filters.append(BasicAuth(u.username, password)) 95 | 96 | kwargs['filters'] = filters 97 | 98 | http_client = Client(**kwargs) 99 | return http_client.request(url, method=method, body=body, 100 | headers=headers) 101 | -------------------------------------------------------------------------------- /restkit/conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import logging 7 | import random 8 | import select 9 | import socket 10 | import ssl 11 | import time 12 | import cStringIO 13 | 14 | from socketpool import Connector 15 | from socketpool.util import is_connected 16 | 17 | CHUNK_SIZE = 16 * 1024 18 | MAX_BODY = 1024 * 112 19 | DNS_TIMEOUT = 60 20 | 21 | 22 | class Connection(Connector): 23 | 24 | def __init__(self, host, port, backend_mod=None, pool=None, 25 | is_ssl=False, extra_headers=[], proxy_pieces=None, timeout=None, 26 | **ssl_args): 27 | 28 | # connect the socket, if we are using an SSL connection, we wrap 29 | # the socket. 30 | self._s = backend_mod.Socket(socket.AF_INET, socket.SOCK_STREAM) 31 | if timeout is not None: 32 | self._s.settimeout(timeout) 33 | self._s.connect((host, port)) 34 | if proxy_pieces: 35 | self._s.sendall(proxy_pieces) 36 | response = cStringIO.StringIO() 37 | while response.getvalue()[-4:] != '\r\n\r\n': 38 | response.write(self._s.recv(1)) 39 | response.close() 40 | if is_ssl: 41 | self._s = ssl.wrap_socket(self._s, **ssl_args) 42 | 43 | self.extra_headers = extra_headers 44 | self.is_ssl = is_ssl 45 | self.backend_mod = backend_mod 46 | self.host = host 47 | self.port = port 48 | self._connected = True 49 | self._life = time.time() - random.randint(0, 10) 50 | self._pool = pool 51 | self._released = False 52 | 53 | def matches(self, **match_options): 54 | target_host = match_options.get('host') 55 | target_port = match_options.get('port') 56 | return target_host == self.host and target_port == self.port 57 | 58 | def is_connected(self): 59 | if self._connected: 60 | return is_connected(self._s) 61 | return False 62 | 63 | def handle_exception(self, exception): 64 | raise 65 | 66 | def get_lifetime(self): 67 | return self._life 68 | 69 | def invalidate(self): 70 | self.close() 71 | self._connected = False 72 | self._life = -1 73 | 74 | def release(self, should_close=False): 75 | if self._pool is not None: 76 | if self._connected: 77 | if should_close: 78 | self.invalidate() 79 | self._pool.release_connection(self) 80 | else: 81 | self._pool = None 82 | elif self._connected: 83 | self.invalidate() 84 | 85 | def close(self): 86 | if not self._s or not hasattr(self._s, "close"): 87 | return 88 | try: 89 | self._s.close() 90 | except: 91 | pass 92 | 93 | def socket(self): 94 | return self._s 95 | 96 | def send_chunk(self, data): 97 | chunk = "".join(("%X\r\n" % len(data), data, "\r\n")) 98 | self._s.sendall(chunk) 99 | 100 | def send(self, data, chunked=False): 101 | if chunked: 102 | return self.send_chunk(data) 103 | 104 | return self._s.sendall(data) 105 | 106 | def sendlines(self, lines, chunked=False): 107 | for line in list(lines): 108 | self.send(line, chunked=chunked) 109 | 110 | 111 | # TODO: add support for sendfile api 112 | def sendfile(self, data, chunked=False): 113 | """ send a data from a FileObject """ 114 | 115 | if hasattr(data, 'seek'): 116 | data.seek(0) 117 | 118 | while True: 119 | binarydata = data.read(CHUNK_SIZE) 120 | if binarydata == '': 121 | break 122 | self.send(binarydata, chunked=chunked) 123 | 124 | 125 | def recv(self, size=1024): 126 | return self._s.recv(size) 127 | -------------------------------------------------------------------------------- /restkit/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/restkit/contrib/__init__.py -------------------------------------------------------------------------------- /restkit/contrib/console.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from __future__ import with_statement 7 | import os 8 | import optparse as op 9 | import sys 10 | 11 | # import pygments if here 12 | try: 13 | import pygments 14 | from pygments.lexers import get_lexer_for_mimetype 15 | from pygments.formatters import TerminalFormatter 16 | except ImportError: 17 | pygments = False 18 | 19 | # import json 20 | try: 21 | import simplejson as json 22 | except ImportError: 23 | try: 24 | import json 25 | except ImportError: 26 | json = False 27 | 28 | from restkit import __version__, request, set_logging 29 | from restkit.util import popen3, locate_program 30 | 31 | __usage__ = "'%prog [options] url [METHOD] [filename]'" 32 | 33 | 34 | pretties = { 35 | 'application/json': 'text/javascript', 36 | 'text/plain': 'text/javascript' 37 | } 38 | 39 | def external(cmd, data): 40 | try: 41 | (child_stdin, child_stdout, child_stderr) = popen3(cmd) 42 | err = child_stderr.read() 43 | if err: 44 | return data 45 | return child_stdout.read() 46 | except: 47 | return data 48 | 49 | def indent_xml(data): 50 | tidy_cmd = locate_program("tidy") 51 | if tidy_cmd: 52 | cmd = " ".join([tidy_cmd, '-qi', '-wrap', '70', '-utf8', data]) 53 | return external(cmd, data) 54 | return data 55 | 56 | def indent_json(data): 57 | if not json: 58 | return data 59 | info = json.loads(data) 60 | return json.dumps(info, indent=2, sort_keys=True) 61 | 62 | 63 | common_indent = { 64 | 'application/json': indent_json, 65 | 'text/html': indent_xml, 66 | 'text/xml': indent_xml, 67 | 'application/xhtml+xml': indent_xml, 68 | 'application/xml': indent_xml, 69 | 'image/svg+xml': indent_xml, 70 | 'application/rss+xml': indent_xml, 71 | 'application/atom+xml': indent_xml, 72 | 'application/xsl+xml': indent_xml, 73 | 'application/xslt+xml': indent_xml 74 | } 75 | 76 | def indent(mimetype, data): 77 | if mimetype in common_indent: 78 | return common_indent[mimetype](data) 79 | return data 80 | 81 | def prettify(response, cli=True): 82 | if not pygments or not 'content-type' in response.headers: 83 | return response.body_string() 84 | 85 | ctype = response.headers['content-type'] 86 | try: 87 | mimetype, encoding = ctype.split(";") 88 | except ValueError: 89 | mimetype = ctype.split(";")[0] 90 | 91 | # indent body 92 | body = indent(mimetype, response.body_string()) 93 | 94 | # get pygments mimetype 95 | mimetype = pretties.get(mimetype, mimetype) 96 | 97 | try: 98 | lexer = get_lexer_for_mimetype(mimetype) 99 | body = pygments.highlight(body, lexer, TerminalFormatter()) 100 | return body 101 | except: 102 | return body 103 | 104 | def as_bool(value): 105 | if value.lower() in ('true', '1'): 106 | return True 107 | return False 108 | 109 | def update_defaults(defaults): 110 | config = os.path.expanduser('~/.restcli') 111 | if os.path.isfile(config): 112 | for line in open(config): 113 | key, value = line.split('=', 1) 114 | key = key.lower().strip() 115 | key = key.replace('-', '_') 116 | if key.startswith('header'): 117 | key = 'headers' 118 | value = value.strip() 119 | if key in defaults: 120 | default = defaults[key] 121 | if default in (True, False): 122 | value = as_bool(value) 123 | elif isinstance(default, list): 124 | default.append(value) 125 | value = default 126 | defaults[key] = value 127 | 128 | def options(): 129 | """ build command lines options """ 130 | 131 | defaults = dict( 132 | headers=[], 133 | request='GET', 134 | follow_redirect=False, 135 | server_response=False, 136 | prettify=False, 137 | log_level=None, 138 | input=None, 139 | output=None, 140 | ) 141 | update_defaults(defaults) 142 | 143 | def opt_args(option, *help): 144 | help = ' '.join(help) 145 | help = help.strip() 146 | default = defaults.get(option) 147 | if default is not None: 148 | help += ' Default to %r.' % default 149 | return dict(default=defaults.get(option), help=help) 150 | 151 | return [ 152 | op.make_option('-H', '--header', action='append', dest='headers', 153 | **opt_args('headers', 154 | 'HTTP string header in the form of Key:Value. ', 155 | 'For example: "Accept: application/json".')), 156 | op.make_option('-X', '--request', action='store', dest='method', 157 | **opt_args('request', 'HTTP request method.')), 158 | op.make_option('--follow-redirect', action='store_true', 159 | dest='follow_redirect', **opt_args('follow_redirect')), 160 | op.make_option('-S', '--server-response', action='store_true', 161 | dest='server_response', 162 | **opt_args('server_response', 'Print server response.')), 163 | op.make_option('-p', '--prettify', dest="prettify", action='store_true', 164 | **opt_args('prettify', "Prettify display.")), 165 | op.make_option('--log-level', dest="log_level", 166 | **opt_args('log_level', 167 | "Log level below which to silence messages.")), 168 | op.make_option('-i', '--input', action='store', dest='input', 169 | metavar='FILE', 170 | **opt_args('input', 'The name of the file to read from.')), 171 | op.make_option('-o', '--output', action='store', dest='output', 172 | **opt_args('output', 'The name of the file to write to.')), 173 | op.make_option('--shell', action='store_true', dest='shell', 174 | help='Open a IPython shell'), 175 | ] 176 | 177 | def main(): 178 | """ function to manage restkit command line """ 179 | parser = op.OptionParser(usage=__usage__, option_list=options(), 180 | version="%prog " + __version__) 181 | 182 | opts, args = parser.parse_args() 183 | args_len = len(args) 184 | 185 | if opts.shell: 186 | try: 187 | from restkit.contrib import ipython_shell as shell 188 | shell.main(options=opts, *args) 189 | except Exception, e: 190 | print >>sys.stderr, str(e) 191 | sys.exit(1) 192 | return 193 | 194 | if args_len < 1: 195 | return parser.error('incorrect number of arguments') 196 | 197 | if opts.log_level is not None: 198 | set_logging(opts.log_level) 199 | 200 | body = None 201 | headers = [] 202 | if opts.input: 203 | if opts.input == '-': 204 | body = sys.stdin.read() 205 | headers.append(("Content-Length", str(len(body)))) 206 | else: 207 | fname = os.path.normpath(os.path.join(os.getcwd(),opts.input)) 208 | body = open(fname, 'r') 209 | 210 | if opts.headers: 211 | for header in opts.headers: 212 | try: 213 | k, v = header.split(':') 214 | headers.append((k, v)) 215 | except ValueError: 216 | pass 217 | 218 | 219 | try: 220 | if len(args) == 2: 221 | if args[1] == "-" and not opts.input: 222 | body = sys.stdin.read() 223 | headers.append(("Content-Length", str(len(body)))) 224 | 225 | if not opts.method and opts.input: 226 | method = 'POST' 227 | else: 228 | method=opts.method.upper() 229 | 230 | resp = request(args[0], method=method, body=body, 231 | headers=headers, follow_redirect=opts.follow_redirect) 232 | 233 | if opts.output and opts.output != '-': 234 | with open(opts.output, 'wb') as f: 235 | if opts.server_response: 236 | f.write("Server response from %s:\n" % resp.final_url) 237 | for k, v in resp.headerslist: 238 | f.write( "%s: %s" % (k, v)) 239 | else: 240 | with resp.body_stream() as body: 241 | for block in body: 242 | f.write(block) 243 | else: 244 | if opts.server_response: 245 | if opts.prettify: 246 | print "\n\033[0m\033[95mServer response from %s:\n\033[0m" % ( 247 | resp.final_url) 248 | for k, v in resp.headerslist: 249 | print "\033[94m%s\033[0m: %s" % (k, v) 250 | print "\033[0m" 251 | else: 252 | print "Server response from %s:\n" % (resp.final_url) 253 | for k, v in resp.headerslist: 254 | print "%s: %s" % (k, v) 255 | print "" 256 | 257 | if opts.output == '-': 258 | if opts.prettify: 259 | print prettify(resp) 260 | else: 261 | print resp.body_string() 262 | else: 263 | if opts.prettify: 264 | print prettify(resp) 265 | else: 266 | print resp.body_string() 267 | 268 | except Exception, e: 269 | sys.stderr.write("An error happened: %s" % str(e)) 270 | sys.stderr.flush() 271 | sys.exit(1) 272 | 273 | sys.exit(0) 274 | 275 | -------------------------------------------------------------------------------- /restkit/contrib/ipython_shell.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from StringIO import StringIO 7 | import urlparse 8 | 9 | try: 10 | from IPython.config.loader import Config 11 | from IPython.frontend.terminal.embed import InteractiveShellEmbed 12 | except ImportError: 13 | raise ImportError('IPython (http://pypi.python.org/pypi/ipython) >=0.11' +\ 14 | 'is required.') 15 | 16 | try: 17 | import webob 18 | except ImportError: 19 | raise ImportError('webob (http://pythonpaste.org/webob/) is required.') 20 | 21 | from webob import Response as BaseResponse 22 | 23 | from restkit import __version__ 24 | from restkit.contrib.console import common_indent, json 25 | from restkit.contrib.webob_api import Request as BaseRequest 26 | 27 | 28 | class Stream(StringIO): 29 | def __repr__(self): 30 | return '' % self.len 31 | 32 | 33 | class JSON(Stream): 34 | def __init__(self, value): 35 | self.__value = value 36 | if json: 37 | Stream.__init__(self, json.dumps(value)) 38 | else: 39 | Stream.__init__(self, value) 40 | def __repr__(self): 41 | return '' % self.__value 42 | 43 | 44 | class Response(BaseResponse): 45 | def __str__(self, skip_body=True): 46 | if self.content_length < 200 and skip_body: 47 | skip_body = False 48 | return BaseResponse.__str__(self, skip_body=skip_body) 49 | def __call__(self): 50 | print self 51 | 52 | 53 | class Request(BaseRequest): 54 | ResponseClass = Response 55 | def get_response(self, *args, **kwargs): 56 | url = self.url 57 | stream = None 58 | for a in args: 59 | if isinstance(a, Stream): 60 | stream = a 61 | a.seek(0) 62 | continue 63 | elif isinstance(a, basestring): 64 | if a.startswith('http'): 65 | url = a 66 | elif a.startswith('/'): 67 | url = a 68 | 69 | self.set_url(url) 70 | 71 | if stream: 72 | self.body_file = stream 73 | self.content_length = stream.len 74 | if self.method == 'GET' and kwargs: 75 | for k, v in kwargs.items(): 76 | self.GET[k] = v 77 | elif self.method == 'POST' and kwargs: 78 | for k, v in kwargs.items(): 79 | self.GET[k] = v 80 | return BaseRequest.get_response(self) 81 | 82 | def __str__(self, skip_body=True): 83 | if self.content_length < 200 and skip_body: 84 | skip_body = False 85 | return BaseRequest.__str__(self, skip_body=skip_body) 86 | 87 | def __call__(self): 88 | print self 89 | 90 | 91 | class ContentTypes(object): 92 | _values = {} 93 | def __repr__(self): 94 | return '<%s(%s)>' % (self.__class__.__name__, sorted(self._values)) 95 | def __str__(self): 96 | return '\n'.join(['%-20.20s: %s' % h for h in \ 97 | sorted(self._value.items())]) 98 | 99 | 100 | ctypes = ContentTypes() 101 | for k in common_indent: 102 | attr = k.replace('/', '_').replace('+', '_') 103 | ctypes._values[attr] = attr 104 | ctypes.__dict__[attr] = k 105 | del k, attr 106 | 107 | 108 | class RestShell(InteractiveShellEmbed): 109 | def __init__(self, user_ns={}): 110 | 111 | cfg = Config() 112 | shell_config = cfg.InteractiveShellEmbed 113 | shell_config.prompt_in1 = '\C_Blue\#) \C_Greenrestcli\$ ' 114 | 115 | super(RestShell, self).__init__(config = cfg, 116 | banner1= 'restkit shell %s' % __version__, 117 | exit_msg="quit restcli shell", user_ns=user_ns) 118 | 119 | 120 | class ShellClient(object): 121 | methods = dict( 122 | get='[req|url|path_info], **query_string', 123 | post='[req|url|path_info], [Stream()|**query_string_body]', 124 | head='[req|url|path_info], **query_string', 125 | put='[req|url|path_info], stream', 126 | delete='[req|url|path_info]') 127 | 128 | def __init__(self, url='/', options=None, **kwargs): 129 | self.options = options 130 | self.url = url or '/' 131 | self.ns = {} 132 | self.shell = RestShell(user_ns=self.ns) 133 | self.update_ns(self.ns) 134 | self.help() 135 | self.shell(header='', global_ns={}, local_ns={}) 136 | 137 | def update_ns(self, ns): 138 | for k in self.methods: 139 | ns[k] = self.request_meth(k) 140 | stream = None 141 | headers = {} 142 | if self.options: 143 | if self.options.input: 144 | stream = Stream(open(self.options.input).read()) 145 | if self.options.headers: 146 | for header in self.options.headers: 147 | try: 148 | k, v = header.split(':') 149 | headers.append((k, v)) 150 | except ValueError: 151 | pass 152 | req = Request.blank('/') 153 | req._client = self 154 | del req.content_type 155 | if stream: 156 | req.body_file = stream 157 | 158 | req.headers = headers 159 | req.set_url(self.url) 160 | ns.update( 161 | Request=Request, 162 | Response=Response, 163 | Stream=Stream, 164 | req=req, 165 | stream=stream, 166 | ctypes=ctypes, 167 | ) 168 | if json: 169 | ns['JSON'] = JSON 170 | 171 | def request_meth(self, k): 172 | def req(*args, **kwargs): 173 | resp = self.request(k.upper(), *args, **kwargs) 174 | self.shell.user_ns.update(dict(resp=resp)) 175 | 176 | print resp 177 | return resp 178 | req.func_name = k 179 | req.__name__ = k 180 | req.__doc__ = """send a HTTP %s""" % k.upper() 181 | return req 182 | 183 | def request(self, meth, *args, **kwargs): 184 | """forward to restkit.request""" 185 | req = None 186 | for a in args: 187 | if isinstance(a, Request): 188 | req = a 189 | args = [a for a in args if a is not req] 190 | break 191 | if req is None: 192 | req = self.shell.user_ns.get('req') 193 | if not isinstance(req, Request): 194 | req = Request.blank('/') 195 | del req.content_type 196 | req.method = meth 197 | 198 | req.set_url(self.url) 199 | resp = req.get_response(*args, **kwargs) 200 | self.url = req.url 201 | return resp 202 | 203 | def help(self): 204 | ns = self.ns.copy() 205 | methods = '' 206 | for k in sorted(self.methods): 207 | args = self.methods[k] 208 | doc = ' >>> %s(%s)' % (k, args) 209 | methods += '%-65.65s # send a HTTP %s\n' % (doc, k) 210 | ns['methods'] = methods 211 | print HELP.strip() % ns 212 | print '' 213 | 214 | def __repr__(self): 215 | return '' 216 | 217 | 218 | def main(*args, **kwargs): 219 | for a in args: 220 | if a.startswith('http://'): 221 | kwargs['url'] = a 222 | ShellClient(**kwargs) 223 | 224 | 225 | HELP = """ 226 | restkit shell 227 | ============= 228 | 229 | HTTP Methods 230 | ------------ 231 | 232 | %(methods)s 233 | Helpers 234 | ------- 235 | 236 | >>> req # request to play with. By default http methods will use this one 237 | %(req)r 238 | 239 | >>> stream # Stream() instance if you specified a -i in command line 240 | %(stream)r 241 | 242 | >>> ctypes # Content-Types helper with headers properties 243 | %(ctypes)r 244 | """ 245 | 246 | if __name__ == '__main__': 247 | import sys 248 | main(*sys.argv[1:]) 249 | -------------------------------------------------------------------------------- /restkit/contrib/webob_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 - 3 | # 4 | # This file is part of restkit released under the MIT license. 5 | # See the NOTICE for more information. 6 | 7 | import base64 8 | from StringIO import StringIO 9 | import urlparse 10 | import urllib 11 | 12 | try: 13 | from webob import Request as BaseRequest 14 | except ImportError: 15 | raise ImportError('WebOb (http://pypi.python.org/pypi/WebOb) is required') 16 | 17 | from .wsgi_proxy import Proxy 18 | 19 | __doc__ = '''Subclasses of webob.Request who use restkit to get a 20 | webob.Response via restkit.ext.wsgi_proxy.Proxy. 21 | 22 | Example:: 23 | 24 | >>> req = Request.blank('http://pypi.python.org/pypi/restkit') 25 | >>> resp = req.get_response() 26 | >>> print resp #doctest: +ELLIPSIS 27 | 200 OK 28 | Date: ... 29 | Transfer-Encoding: chunked 30 | Content-Type: text/html; charset=utf-8 31 | Server: Apache/2... 32 | 33 | 34 | ... 35 | 36 | 37 | ''' 38 | 39 | PROXY = Proxy(allowed_methods=['GET', 'POST', 'HEAD', 'DELETE', 'PUT', 'PURGE']) 40 | 41 | class Method(property): 42 | def __init__(self, name): 43 | self.name = name 44 | def __get__(self, instance, klass): 45 | if not instance: 46 | return self 47 | instance.method = self.name.upper() 48 | def req(*args, **kwargs): 49 | return instance.get_response(*args, **kwargs) 50 | return req 51 | 52 | 53 | class Request(BaseRequest): 54 | get = Method('get') 55 | post = Method('post') 56 | put = Method('put') 57 | head = Method('head') 58 | delete = Method('delete') 59 | 60 | def get_response(self): 61 | if self.content_length < 0: 62 | self.content_length = 0 63 | if self.method in ('DELETE', 'GET'): 64 | self.body = '' 65 | elif self.method == 'POST' and self.POST: 66 | body = urllib.urlencode(self.POST.copy()) 67 | stream = StringIO(body) 68 | stream.seek(0) 69 | self.body_file = stream 70 | self.content_length = stream.len 71 | if 'form' not in self.content_type: 72 | self.content_type = 'application/x-www-form-urlencoded' 73 | self.server_name = self.host 74 | return BaseRequest.get_response(self, PROXY) 75 | 76 | __call__ = get_response 77 | 78 | def set_url(self, url): 79 | 80 | path = url.lstrip('/') 81 | 82 | if url.startswith("http://") or url.startswith("https://"): 83 | u = urlparse.urlsplit(url) 84 | if u.username is not None: 85 | password = u.password or "" 86 | encode = base64.b64encode("%s:%s" % (u.username, password)) 87 | self.headers['Authorization'] = 'Basic %s' % encode 88 | 89 | self.scheme = u.scheme, 90 | self.host = u.netloc.split("@")[-1] 91 | self.path_info = u.path or "/" 92 | self.query_string = u.query 93 | url = urlparse.urlunsplit((u.scheme, u.netloc.split("@")[-1], 94 | u.path, u.query, u.fragment)) 95 | else: 96 | 97 | if '?' in path: 98 | path, self.query_string = path.split('?', 1) 99 | self.path_info = '/' + path 100 | 101 | 102 | url = self.url 103 | self.scheme, self.host, self.path_info = urlparse.urlparse(url)[0:3] 104 | 105 | -------------------------------------------------------------------------------- /restkit/contrib/webob_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | import webob.exc 8 | 9 | from restkit import errors 10 | 11 | class WebobResourceError(webob.exc.WSGIHTTPException): 12 | """ 13 | Wrapper to return webob exceptions instead of restkit errors. Usefull 14 | for those who want to build `WSGI `_ applications 15 | speaking directly to others via HTTP. 16 | 17 | To do it place somewhere in your application the function 18 | `wrap_exceptions`:: 19 | 20 | wrap_exceptions() 21 | 22 | It will automatically replace restkit errors by webob exceptions. 23 | """ 24 | 25 | def __init__(self, msg=None, http_code=None, response=None): 26 | webob.exc.WSGIHTTPException.__init__(self) 27 | 28 | http_code = http_code or 500 29 | klass = webob.exc.status_map[http_code] 30 | self.code = http_code 31 | self.title = klass.title 32 | self.status = '%s %s' % (self.code, self.title) 33 | self.explanation = msg 34 | self.response = response 35 | # default params 36 | self.msg = msg 37 | 38 | def _status_int__get(self): 39 | """ 40 | The status as an integer 41 | """ 42 | return int(self.status.split()[0]) 43 | def _status_int__set(self, value): 44 | self.status = value 45 | status_int = property(_status_int__get, _status_int__set, 46 | doc=_status_int__get.__doc__) 47 | 48 | def _get_message(self): 49 | return self.explanation 50 | def _set_message(self, msg): 51 | self.explanation = msg or '' 52 | message = property(_get_message, _set_message) 53 | 54 | webob_exceptions = False 55 | def wrap_exceptions(): 56 | """ wrap restkit exception to return WebBob exceptions""" 57 | global webob_exceptions 58 | if webob_exceptions: return 59 | errors.ResourceError = WebobResourceError 60 | webob_exceptions = True 61 | 62 | -------------------------------------------------------------------------------- /restkit/contrib/wsgi_proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import urlparse 7 | 8 | try: 9 | from cStringIO import StringIO 10 | except ImportError: 11 | from StringIO import StringIO 12 | 13 | from restkit.client import Client 14 | from restkit.conn import MAX_BODY 15 | from restkit.util import rewrite_location 16 | 17 | ALLOWED_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] 18 | 19 | BLOCK_SIZE = 4096 * 16 20 | 21 | WEBOB_ERROR = ("Content-Length is set to -1. This usually mean that WebOb has " 22 | "already parsed the content body. You should set the Content-Length " 23 | "header to the correct value before forwarding your request to the " 24 | "proxy: ``req.content_length = str(len(req.body));`` " 25 | "req.get_response(proxy)") 26 | 27 | class Proxy(object): 28 | """A proxy wich redirect the request to SERVER_NAME:SERVER_PORT 29 | and send HTTP_HOST header""" 30 | 31 | def __init__(self, manager=None, allowed_methods=ALLOWED_METHODS, 32 | strip_script_name=True, **kwargs): 33 | self.allowed_methods = allowed_methods 34 | self.strip_script_name = strip_script_name 35 | self.client = Client(**kwargs) 36 | 37 | def extract_uri(self, environ): 38 | port = None 39 | scheme = environ['wsgi.url_scheme'] 40 | if 'SERVER_NAME' in environ: 41 | host = environ['SERVER_NAME'] 42 | else: 43 | host = environ['HTTP_HOST'] 44 | if ':' in host: 45 | host, port = host.split(':') 46 | 47 | if not port: 48 | if 'SERVER_PORT' in environ: 49 | port = environ['SERVER_PORT'] 50 | else: 51 | port = scheme == 'https' and '443' or '80' 52 | 53 | uri = '%s://%s:%s' % (scheme, host, port) 54 | return uri 55 | 56 | def __call__(self, environ, start_response): 57 | method = environ['REQUEST_METHOD'] 58 | if method not in self.allowed_methods: 59 | start_response('403 Forbidden', ()) 60 | return [''] 61 | 62 | if self.strip_script_name: 63 | path_info = '' 64 | else: 65 | path_info = environ['SCRIPT_NAME'] 66 | path_info += environ['PATH_INFO'] 67 | 68 | query_string = environ['QUERY_STRING'] 69 | if query_string: 70 | path_info += '?' + query_string 71 | 72 | host_uri = self.extract_uri(environ) 73 | uri = host_uri + path_info 74 | 75 | new_headers = {} 76 | for k, v in environ.items(): 77 | if k.startswith('HTTP_'): 78 | k = k[5:].replace('_', '-').title() 79 | new_headers[k] = v 80 | 81 | 82 | ctype = environ.get("CONTENT_TYPE") 83 | if ctype and ctype is not None: 84 | new_headers['Content-Type'] = ctype 85 | 86 | clen = environ.get('CONTENT_LENGTH') 87 | te = environ.get('transfer-encoding', '').lower() 88 | if not clen and te != 'chunked': 89 | new_headers['transfer-encoding'] = 'chunked' 90 | elif clen: 91 | new_headers['Content-Length'] = clen 92 | 93 | if new_headers.get('Content-Length', '0') == '-1': 94 | raise ValueError(WEBOB_ERROR) 95 | 96 | response = self.client.request(uri, method, body=environ['wsgi.input'], 97 | headers=new_headers) 98 | 99 | if 'location' in response: 100 | if self.strip_script_name: 101 | prefix_path = environ['SCRIPT_NAME'] 102 | 103 | new_location = rewrite_location(host_uri, response.location, 104 | prefix_path=prefix_path) 105 | 106 | headers = [] 107 | for k, v in response.headerslist: 108 | if k.lower() == 'location': 109 | v = new_location 110 | headers.append((k, v)) 111 | else: 112 | headers = response.headerslist 113 | 114 | start_response(response.status, headers) 115 | 116 | if method == "HEAD": 117 | return StringIO() 118 | 119 | return response.tee() 120 | 121 | class TransparentProxy(Proxy): 122 | """A proxy based on HTTP_HOST environ variable""" 123 | 124 | def extract_uri(self, environ): 125 | port = None 126 | scheme = environ['wsgi.url_scheme'] 127 | host = environ['HTTP_HOST'] 128 | if ':' in host: 129 | host, port = host.split(':') 130 | 131 | if not port: 132 | port = scheme == 'https' and '443' or '80' 133 | 134 | uri = '%s://%s:%s' % (scheme, host, port) 135 | return uri 136 | 137 | 138 | class HostProxy(Proxy): 139 | """A proxy to redirect all request to a specific uri""" 140 | 141 | def __init__(self, uri, **kwargs): 142 | super(HostProxy, self).__init__(**kwargs) 143 | self.uri = uri.rstrip('/') 144 | self.scheme, self.net_loc = urlparse.urlparse(self.uri)[0:2] 145 | 146 | def extract_uri(self, environ): 147 | environ['HTTP_HOST'] = self.net_loc 148 | return self.uri 149 | 150 | def get_config(local_config): 151 | """parse paste config""" 152 | config = {} 153 | allowed_methods = local_config.get('allowed_methods', None) 154 | if allowed_methods: 155 | config['allowed_methods'] = [m.upper() for m in allowed_methods.split()] 156 | strip_script_name = local_config.get('strip_script_name', 'true') 157 | if strip_script_name.lower() in ('false', '0'): 158 | config['strip_script_name'] = False 159 | config['max_connections'] = int(local_config.get('max_connections', '5')) 160 | return config 161 | 162 | def make_proxy(global_config, **local_config): 163 | """TransparentProxy entry_point""" 164 | config = get_config(local_config) 165 | return TransparentProxy(**config) 166 | 167 | def make_host_proxy(global_config, uri=None, **local_config): 168 | """HostProxy entry_point""" 169 | uri = uri.rstrip('/') 170 | config = get_config(local_config) 171 | return HostProxy(uri, **config) 172 | -------------------------------------------------------------------------------- /restkit/datastructures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | try: 7 | from UserDict import DictMixin 8 | except ImportError: 9 | from collections import MutableMapping as DictMixin 10 | 11 | 12 | class MultiDict(DictMixin): 13 | 14 | """ 15 | An ordered dictionary that can have multiple values for each key. 16 | Adds the methods getall, getone, mixed and extend and add to the normal 17 | dictionary interface. 18 | """ 19 | 20 | def __init__(self, *args, **kw): 21 | if len(args) > 1: 22 | raise TypeError("MultiDict can only be called with one positional argument") 23 | if args: 24 | if isinstance(args[0], MultiDict): 25 | items = args[0]._items 26 | elif hasattr(args[0], 'iteritems'): 27 | items = list(args[0].iteritems()) 28 | elif hasattr(args[0], 'items'): 29 | items = args[0].items() 30 | else: 31 | items = list(args[0]) 32 | self._items = items 33 | else: 34 | self._items = [] 35 | if kw: 36 | self._items.extend(kw.iteritems()) 37 | 38 | @classmethod 39 | def from_fieldstorage(cls, fs): 40 | """ 41 | Create a dict from a cgi.FieldStorage instance 42 | """ 43 | obj = cls() 44 | # fs.list can be None when there's nothing to parse 45 | for field in fs.list or (): 46 | if field.filename: 47 | obj.add(field.name, field) 48 | else: 49 | obj.add(field.name, field.value) 50 | return obj 51 | 52 | def __getitem__(self, key): 53 | for k, v in reversed(self._items): 54 | if k == key: 55 | return v 56 | raise KeyError(key) 57 | 58 | def __setitem__(self, key, value): 59 | try: 60 | del self[key] 61 | except KeyError: 62 | pass 63 | self._items.append((key, value)) 64 | 65 | def add(self, key, value): 66 | """ 67 | Add the key and value, not overwriting any previous value. 68 | """ 69 | self._items.append((key, value)) 70 | 71 | def getall(self, key): 72 | """ 73 | Return a list of all values matching the key (may be an empty list) 74 | """ 75 | return [v for k, v in self._items if k == key] 76 | 77 | def iget(self, key): 78 | """like get but case insensitive """ 79 | lkey = key.lower() 80 | for k, v in self._items: 81 | if k.lower() == lkey: 82 | return v 83 | return None 84 | 85 | def getone(self, key): 86 | """ 87 | Get one value matching the key, raising a KeyError if multiple 88 | values were found. 89 | """ 90 | v = self.getall(key) 91 | if not v: 92 | raise KeyError('Key not found: %r' % key) 93 | if len(v) > 1: 94 | raise KeyError('Multiple values match %r: %r' % (key, v)) 95 | return v[0] 96 | 97 | def mixed(self): 98 | """ 99 | Returns a dictionary where the values are either single 100 | values, or a list of values when a key/value appears more than 101 | once in this dictionary. This is similar to the kind of 102 | dictionary often used to represent the variables in a web 103 | request. 104 | """ 105 | result = {} 106 | multi = {} 107 | for key, value in self.iteritems(): 108 | if key in result: 109 | # We do this to not clobber any lists that are 110 | # *actual* values in this dictionary: 111 | if key in multi: 112 | result[key].append(value) 113 | else: 114 | result[key] = [result[key], value] 115 | multi[key] = None 116 | else: 117 | result[key] = value 118 | return result 119 | 120 | def dict_of_lists(self): 121 | """ 122 | Returns a dictionary where each key is associated with a list of values. 123 | """ 124 | r = {} 125 | for key, val in self.iteritems(): 126 | r.setdefault(key, []).append(val) 127 | return r 128 | 129 | def __delitem__(self, key): 130 | items = self._items 131 | found = False 132 | for i in range(len(items)-1, -1, -1): 133 | if items[i][0] == key: 134 | del items[i] 135 | found = True 136 | if not found: 137 | raise KeyError(key) 138 | 139 | def __contains__(self, key): 140 | for k, v in self._items: 141 | if k == key: 142 | return True 143 | return False 144 | 145 | has_key = __contains__ 146 | 147 | def clear(self): 148 | self._items = [] 149 | 150 | def copy(self): 151 | return self.__class__(self) 152 | 153 | def setdefault(self, key, default=None): 154 | for k, v in self._items: 155 | if key == k: 156 | return v 157 | self._items.append((key, default)) 158 | return default 159 | 160 | def pop(self, key, *args): 161 | if len(args) > 1: 162 | raise TypeError, "pop expected at most 2 arguments, got "\ 163 | + repr(1 + len(args)) 164 | for i in range(len(self._items)): 165 | if self._items[i][0] == key: 166 | v = self._items[i][1] 167 | del self._items[i] 168 | return v 169 | if args: 170 | return args[0] 171 | else: 172 | raise KeyError(key) 173 | 174 | def ipop(self, key, *args): 175 | """ like pop but case insensitive """ 176 | if len(args) > 1: 177 | raise TypeError, "pop expected at most 2 arguments, got "\ 178 | + repr(1 + len(args)) 179 | 180 | lkey = key.lower() 181 | for i, item in enumerate(self._items): 182 | if item[0].lower() == lkey: 183 | v = self._items[i][1] 184 | del self._items[i] 185 | return v 186 | if args: 187 | return args[0] 188 | else: 189 | raise KeyError(key) 190 | 191 | def popitem(self): 192 | return self._items.pop() 193 | 194 | def extend(self, other=None, **kwargs): 195 | if other is None: 196 | pass 197 | elif hasattr(other, 'items'): 198 | self._items.extend(other.items()) 199 | elif hasattr(other, 'keys'): 200 | for k in other.keys(): 201 | self._items.append((k, other[k])) 202 | else: 203 | for k, v in other: 204 | self._items.append((k, v)) 205 | if kwargs: 206 | self.update(kwargs) 207 | 208 | def __repr__(self): 209 | items = ', '.join(['(%r, %r)' % v for v in self.iteritems()]) 210 | return '%s([%s])' % (self.__class__.__name__, items) 211 | 212 | def __len__(self): 213 | return len(self._items) 214 | 215 | ## 216 | ## All the iteration: 217 | ## 218 | 219 | def keys(self): 220 | return [k for k, v in self._items] 221 | 222 | def iterkeys(self): 223 | for k, v in self._items: 224 | yield k 225 | 226 | __iter__ = iterkeys 227 | 228 | def items(self): 229 | return self._items[:] 230 | 231 | def iteritems(self): 232 | return iter(self._items) 233 | 234 | def values(self): 235 | return [v for k, v in self._items] 236 | 237 | def itervalues(self): 238 | for k, v in self._items: 239 | yield v 240 | 241 | 242 | -------------------------------------------------------------------------------- /restkit/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | """ 7 | exception classes. 8 | """ 9 | 10 | class ResourceError(Exception): 11 | """ default error class """ 12 | 13 | status_int = None 14 | 15 | def __init__(self, msg=None, http_code=None, response=None): 16 | self.msg = msg or '' 17 | self.status_int = http_code or self.status_int 18 | self.response = response 19 | Exception.__init__(self) 20 | 21 | def _get_message(self): 22 | return self.msg 23 | def _set_message(self, msg): 24 | self.msg = msg or '' 25 | message = property(_get_message, _set_message) 26 | 27 | def __str__(self): 28 | if self.msg: 29 | return self.msg 30 | try: 31 | return str(self.__dict__) 32 | except (NameError, ValueError, KeyError), e: 33 | return 'Unprintable exception %s: %s' \ 34 | % (self.__class__.__name__, str(e)) 35 | 36 | 37 | class ResourceNotFound(ResourceError): 38 | """Exception raised when no resource was found at the given url. 39 | """ 40 | status_int = 404 41 | 42 | class Unauthorized(ResourceError): 43 | """Exception raised when an authorization is required to access to 44 | the resource specified. 45 | """ 46 | 47 | class ResourceGone(ResourceError): 48 | """ 49 | http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.11 50 | """ 51 | status_int = 410 52 | 53 | class RequestFailed(ResourceError): 54 | """Exception raised when an unexpected HTTP error is received in response 55 | to a request. 56 | 57 | 58 | The request failed, meaning the remote HTTP server returned a code 59 | other than success, unauthorized, or NotFound. 60 | 61 | The exception message attempts to extract the error 62 | 63 | You can get the status code by e.status_int, or see anything about the 64 | response via e.response. For example, the entire result body (which is 65 | probably an HTML error page) is e.response.body. 66 | """ 67 | 68 | class RedirectLimit(Exception): 69 | """Exception raised when the redirection limit is reached.""" 70 | 71 | class RequestError(Exception): 72 | """Exception raised when a request is malformed""" 73 | 74 | class RequestTimeout(Exception): 75 | """ Exception raised on socket timeout """ 76 | 77 | class InvalidUrl(Exception): 78 | """ 79 | Not a valid url for use with this software. 80 | """ 81 | 82 | class ResponseError(Exception): 83 | """ Error raised while getting response or decompressing response stream""" 84 | 85 | 86 | class ProxyError(Exception): 87 | """ raised when proxy error happend""" 88 | 89 | class BadStatusLine(Exception): 90 | """ Exception returned by the parser when the status line is invalid""" 91 | pass 92 | 93 | class ParserError(Exception): 94 | """ Generic exception returned by the parser """ 95 | pass 96 | 97 | class UnexpectedEOF(Exception): 98 | """ exception raised when remote closed the connection """ 99 | 100 | class AlreadyRead(Exception): 101 | """ raised when a response have already been read """ 102 | 103 | class ProxyError(Exception): 104 | pass 105 | 106 | ############################# 107 | # HTTP parser errors 108 | ############################# 109 | 110 | class ParseException(Exception): 111 | pass 112 | 113 | class NoMoreData(ParseException): 114 | def __init__(self, buf=None): 115 | self.buf = buf 116 | def __str__(self): 117 | return "No more data after: %r" % self.buf 118 | 119 | class InvalidRequestLine(ParseException): 120 | def __init__(self, req): 121 | self.req = req 122 | self.code = 400 123 | 124 | def __str__(self): 125 | return "Invalid HTTP request line: %r" % self.req 126 | 127 | class InvalidRequestMethod(ParseException): 128 | def __init__(self, method): 129 | self.method = method 130 | 131 | def __str__(self): 132 | return "Invalid HTTP method: %r" % self.method 133 | 134 | class InvalidHTTPVersion(ParseException): 135 | def __init__(self, version): 136 | self.version = version 137 | 138 | def __str__(self): 139 | return "Invalid HTTP Version: %s" % self.version 140 | 141 | class InvalidHTTPStatus(ParseException): 142 | def __init__(self, status): 143 | self.status = status 144 | 145 | def __str__(self): 146 | return "Invalid HTTP Status: %s" % self.status 147 | 148 | class InvalidHeader(ParseException): 149 | def __init__(self, hdr): 150 | self.hdr = hdr 151 | 152 | def __str__(self): 153 | return "Invalid HTTP Header: %r" % self.hdr 154 | 155 | class InvalidHeaderName(ParseException): 156 | def __init__(self, hdr): 157 | self.hdr = hdr 158 | 159 | def __str__(self): 160 | return "Invalid HTTP header name: %r" % self.hdr 161 | 162 | class InvalidChunkSize(ParseException): 163 | def __init__(self, data): 164 | self.data = data 165 | 166 | def __str__(self): 167 | return "Invalid chunk size: %r" % self.data 168 | 169 | class ChunkMissingTerminator(ParseException): 170 | def __init__(self, term): 171 | self.term = term 172 | 173 | def __str__(self): 174 | return "Invalid chunk terminator is not '\\r\\n': %r" % self.term 175 | 176 | class HeaderLimit(ParseException): 177 | """ exception raised when we gore more headers than 178 | max_header_count 179 | """ 180 | -------------------------------------------------------------------------------- /restkit/filters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import base64 7 | import re 8 | try: 9 | from urlparse import parse_qsl 10 | except ImportError: 11 | from cgi import parse_qsl 12 | from urlparse import urlunparse 13 | 14 | from restkit.oauth2 import Request, SignatureMethod_HMAC_SHA1 15 | 16 | class BasicAuth(object): 17 | """ Simple filter to manage basic authentification""" 18 | 19 | def __init__(self, username, password): 20 | self.credentials = (username, password) 21 | 22 | def on_request(self, request): 23 | encode = base64.b64encode("%s:%s" % self.credentials) 24 | request.headers['Authorization'] = 'Basic %s' % encode 25 | 26 | def validate_consumer(consumer): 27 | """ validate a consumer agains oauth2.Consumer object """ 28 | if not hasattr(consumer, "key"): 29 | raise ValueError("Invalid consumer.") 30 | return consumer 31 | 32 | def validate_token(token): 33 | """ validate a token agains oauth2.Token object """ 34 | if token is not None and not hasattr(token, "key"): 35 | raise ValueError("Invalid token.") 36 | return token 37 | 38 | 39 | class OAuthFilter(object): 40 | """ oauth filter """ 41 | 42 | def __init__(self, path, consumer, token=None, method=None, 43 | realm=""): 44 | """ Init OAuthFilter 45 | 46 | :param path: path or regexp. * mean all path on wicth oauth can be 47 | applied. 48 | :param consumer: oauth consumer, instance of oauth2.Consumer 49 | :param token: oauth token, instance of oauth2.Token 50 | :param method: oauth signature method 51 | 52 | token and method signature are optionnals. Consumer should be an 53 | instance of `oauth2.Consumer`, token an instance of `oauth2.Toke` 54 | signature method an instance of `oauth2.SignatureMethod`. 55 | 56 | """ 57 | 58 | if path.endswith('*'): 59 | self.match = re.compile("%s.*" % path.rsplit('*', 1)[0]) 60 | else: 61 | self.match = re.compile("%s$" % path) 62 | self.consumer = validate_consumer(consumer) 63 | self.token = validate_token(token) 64 | self.method = method or SignatureMethod_HMAC_SHA1() 65 | self.realm = realm 66 | 67 | def on_path(self, request): 68 | path = request.parsed_url.path or "/" 69 | return (self.match.match(path) is not None) 70 | 71 | def on_request(self, request): 72 | if not self.on_path(request): 73 | return 74 | 75 | params = {} 76 | form = False 77 | parsed_url = request.parsed_url 78 | 79 | if request.body and request.body is not None: 80 | ctype = request.headers.iget('content-type') 81 | if ctype is not None and \ 82 | ctype.startswith('application/x-www-form-urlencoded'): 83 | # we are in a form try to get oauth params from here 84 | form = True 85 | params = dict(parse_qsl(request.body)) 86 | 87 | # update params from quey parameters 88 | params.update(parse_qsl(parsed_url.query)) 89 | 90 | raw_url = urlunparse((parsed_url.scheme, parsed_url.netloc, 91 | parsed_url.path, '', '', '')) 92 | 93 | oauth_req = Request.from_consumer_and_token(self.consumer, 94 | token=self.token, http_method=request.method, 95 | http_url=raw_url, parameters=params, 96 | is_form_encoded=form) 97 | 98 | oauth_req.sign_request(self.method, self.consumer, self.token) 99 | 100 | if form: 101 | request.body = oauth_req.to_postdata() 102 | 103 | request.headers['Content-Length'] = len(request.body) 104 | elif request.method in ('GET', 'HEAD'): 105 | request.original_url = request.url 106 | request.url = oauth_req.to_url() 107 | else: 108 | oauth_headers = oauth_req.to_header(realm=self.realm) 109 | request.headers.update(oauth_headers) 110 | -------------------------------------------------------------------------------- /restkit/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | import mimetypes 8 | import os 9 | import re 10 | import urllib 11 | 12 | 13 | from restkit.util import to_bytestring, url_quote, url_encode 14 | 15 | MIME_BOUNDARY = 'END_OF_PART' 16 | CRLF = '\r\n' 17 | 18 | def form_encode(obj, charset="utf8"): 19 | encoded = url_encode(obj, charset=charset) 20 | return to_bytestring(encoded) 21 | 22 | 23 | class BoundaryItem(object): 24 | def __init__(self, name, value, fname=None, filetype=None, filesize=None, 25 | quote=url_quote): 26 | self.quote = quote 27 | self.name = quote(name) 28 | if value is not None and not hasattr(value, 'read'): 29 | value = self.encode_unreadable_value(value) 30 | self.size = len(value) 31 | self.value = value 32 | if fname is not None: 33 | if isinstance(fname, unicode): 34 | fname = fname.encode("utf-8").encode("string_escape").replace('"', '\\"') 35 | else: 36 | fname = fname.encode("string_escape").replace('"', '\\"') 37 | self.fname = fname 38 | if filetype is not None: 39 | filetype = to_bytestring(filetype) 40 | self.filetype = filetype 41 | 42 | if isinstance(value, file) and filesize is None: 43 | try: 44 | value.flush() 45 | except IOError: 46 | pass 47 | self.size = int(os.fstat(value.fileno())[6]) 48 | 49 | self._encoded_hdr = None 50 | self._encoded_bdr = None 51 | 52 | def encode_hdr(self, boundary): 53 | """Returns the header of the encoding of this parameter""" 54 | if not self._encoded_hdr or self._encoded_bdr != boundary: 55 | boundary = self.quote(boundary) 56 | self._encoded_bdr = boundary 57 | headers = ["--%s" % boundary] 58 | if self.fname: 59 | disposition = 'form-data; name="%s"; filename="%s"' % (self.name, 60 | self.fname) 61 | else: 62 | disposition = 'form-data; name="%s"' % self.name 63 | headers.append("Content-Disposition: %s" % disposition) 64 | if self.filetype: 65 | filetype = self.filetype 66 | else: 67 | filetype = "text/plain; charset=utf-8" 68 | headers.append("Content-Type: %s" % filetype) 69 | headers.append("Content-Length: %i" % self.size) 70 | headers.append("") 71 | headers.append("") 72 | self._encoded_hdr = CRLF.join(headers) 73 | return self._encoded_hdr 74 | 75 | def encode(self, boundary): 76 | """Returns the string encoding of this parameter""" 77 | value = self.value 78 | if re.search("^--%s$" % re.escape(boundary), value, re.M): 79 | raise ValueError("boundary found in encoded string") 80 | 81 | return "%s%s%s" % (self.encode_hdr(boundary), value, CRLF) 82 | 83 | def iter_encode(self, boundary, blocksize=16384): 84 | if not hasattr(self.value, "read"): 85 | yield self.encode(boundary) 86 | else: 87 | yield self.encode_hdr(boundary) 88 | while True: 89 | block = self.value.read(blocksize) 90 | if not block: 91 | yield CRLF 92 | return 93 | yield block 94 | 95 | def encode_unreadable_value(self, value): 96 | return value 97 | 98 | 99 | class MultipartForm(object): 100 | def __init__(self, params, boundary, headers, bitem_cls=BoundaryItem, 101 | quote=url_quote): 102 | self.boundary = boundary 103 | self.tboundary = "--%s--%s" % (boundary, CRLF) 104 | self.boundaries = [] 105 | self._clen = headers.get('Content-Length') 106 | 107 | if hasattr(params, 'items'): 108 | params = params.items() 109 | 110 | for param in params: 111 | name, value = param 112 | if hasattr(value, "read"): 113 | fname = getattr(value, 'name') 114 | if fname is not None: 115 | filetype = ';'.join(filter(None, mimetypes.guess_type(fname))) 116 | else: 117 | filetype = None 118 | if not isinstance(value, file) and self._clen is None: 119 | value = value.read() 120 | 121 | boundary = bitem_cls(name, value, fname, filetype, quote=quote) 122 | self.boundaries.append(boundary) 123 | elif isinstance(value, list): 124 | for v in value: 125 | boundary = bitem_cls(name, v, quote=quote) 126 | self.boundaries.append(boundary) 127 | else: 128 | boundary = bitem_cls(name, value, quote=quote) 129 | self.boundaries.append(boundary) 130 | 131 | def get_size(self, recalc=False): 132 | if self._clen is None or recalc: 133 | self._clen = 0 134 | for boundary in self.boundaries: 135 | self._clen += boundary.size 136 | self._clen += len(boundary.encode_hdr(self.boundary)) 137 | self._clen += len(CRLF) 138 | self._clen += len(self.tboundary) 139 | return int(self._clen) 140 | 141 | def __iter__(self): 142 | for boundary in self.boundaries: 143 | for block in boundary.iter_encode(self.boundary): 144 | yield block 145 | yield self.tboundary 146 | 147 | 148 | def multipart_form_encode(params, headers, boundary, quote=url_quote): 149 | """Creates a tuple with MultipartForm instance as body and dict as headers 150 | 151 | params 152 | dict with fields for the body 153 | 154 | headers 155 | dict with fields for the header 156 | 157 | boundary 158 | string to use as boundary 159 | 160 | quote (default: url_quote) 161 | some callable expecting a string an returning a string. Use for quoting of 162 | boundary and form-data keys (names). 163 | """ 164 | headers = headers or {} 165 | boundary = quote(boundary) 166 | body = MultipartForm(params, boundary, headers, quote=quote) 167 | headers['Content-Type'] = "multipart/form-data; boundary=%s" % boundary 168 | headers['Content-Length'] = str(body.get_size()) 169 | return body, headers 170 | -------------------------------------------------------------------------------- /restkit/resource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | """ 8 | restkit.resource 9 | ~~~~~~~~~~~~~~~~ 10 | 11 | This module provide a common interface for all HTTP request. 12 | """ 13 | from copy import copy 14 | import urlparse 15 | 16 | from restkit.errors import ResourceNotFound, Unauthorized, \ 17 | RequestFailed, ResourceGone 18 | from restkit.client import Client 19 | from restkit.filters import BasicAuth 20 | from restkit import util 21 | from restkit.wrappers import Response 22 | 23 | class Resource(object): 24 | """A class that can be instantiated for access to a RESTful resource, 25 | including authentication. 26 | """ 27 | 28 | charset = 'utf-8' 29 | encode_keys = True 30 | safe = "/:" 31 | basic_auth_url = True 32 | response_class = Response 33 | 34 | def __init__(self, uri, **client_opts): 35 | """Constructor for a `Resource` object. 36 | 37 | Resource represent an HTTP resource. 38 | 39 | - uri: str, full uri to the server. 40 | - client_opts: `restkit.client.Client` Options 41 | """ 42 | client_opts = client_opts or {} 43 | 44 | self.initial = dict( 45 | uri = uri, 46 | client_opts = client_opts.copy() 47 | ) 48 | 49 | # set default response_class 50 | if self.response_class is not None and \ 51 | not 'response_class' in client_opts: 52 | client_opts['response_class'] = self.response_class 53 | 54 | self.filters = client_opts.get('filters') or [] 55 | self.uri = uri 56 | if self.basic_auth_url: 57 | # detect credentials from url 58 | u = urlparse.urlparse(uri) 59 | if u.username: 60 | password = u.password or "" 61 | 62 | # add filters 63 | filters = copy(self.filters) 64 | filters.append(BasicAuth(u.username, password)) 65 | client_opts['filters'] = filters 66 | 67 | # update uri 68 | self.uri = urlparse.urlunparse((u.scheme, u.netloc.split("@")[-1], 69 | u.path, u.params, u.query, u.fragment)) 70 | 71 | self.client_opts = client_opts 72 | self.client = Client(**self.client_opts) 73 | 74 | def __repr__(self): 75 | return '<%s %s>' % (self.__class__.__name__, self.uri) 76 | 77 | def clone(self): 78 | """if you want to add a path to resource uri, you can do: 79 | 80 | .. code-block:: python 81 | 82 | resr2 = res.clone() 83 | 84 | """ 85 | obj = self.__class__(self.initial['uri'], 86 | **self.initial['client_opts']) 87 | return obj 88 | 89 | def __call__(self, path): 90 | """if you want to add a path to resource uri, you can do: 91 | 92 | .. code-block:: python 93 | 94 | Resource("/path").get() 95 | """ 96 | 97 | uri = self.initial['uri'] 98 | 99 | new_uri = util.make_uri(uri, path, charset=self.charset, 100 | safe=self.safe, encode_keys=self.encode_keys) 101 | 102 | obj = type(self)(new_uri, **self.initial['client_opts']) 103 | return obj 104 | 105 | def get(self, path=None, headers=None, params_dict=None, **params): 106 | """ HTTP GET 107 | 108 | - path: string additionnal path to the uri 109 | - headers: dict, optionnal headers that will 110 | be added to HTTP request. 111 | - params: Optionnal parameterss added to the request. 112 | """ 113 | return self.request("GET", path=path, headers=headers, 114 | params_dict=params_dict, **params) 115 | 116 | def head(self, path=None, headers=None, params_dict=None, **params): 117 | """ HTTP HEAD 118 | 119 | see GET for params description. 120 | """ 121 | return self.request("HEAD", path=path, headers=headers, 122 | params_dict=params_dict, **params) 123 | 124 | def delete(self, path=None, headers=None, params_dict=None, **params): 125 | """ HTTP DELETE 126 | 127 | see GET for params description. 128 | """ 129 | return self.request("DELETE", path=path, headers=headers, 130 | params_dict=params_dict, **params) 131 | 132 | def post(self, path=None, payload=None, headers=None, 133 | params_dict=None, **params): 134 | """ HTTP POST 135 | 136 | - payload: string passed to the body of the request 137 | - path: string additionnal path to the uri 138 | - headers: dict, optionnal headers that will 139 | be added to HTTP request. 140 | - params: Optionnal parameterss added to the request 141 | """ 142 | 143 | return self.request("POST", path=path, payload=payload, 144 | headers=headers, params_dict=params_dict, **params) 145 | 146 | def put(self, path=None, payload=None, headers=None, 147 | params_dict=None, **params): 148 | """ HTTP PUT 149 | 150 | see POST for params description. 151 | """ 152 | return self.request("PUT", path=path, payload=payload, 153 | headers=headers, params_dict=params_dict, **params) 154 | 155 | def make_params(self, params): 156 | return params or {} 157 | 158 | def make_headers(self, headers): 159 | return headers or [] 160 | 161 | def unauthorized(self, response): 162 | return True 163 | 164 | def request(self, method, path=None, payload=None, headers=None, 165 | params_dict=None, **params): 166 | """ HTTP request 167 | 168 | This method may be the only one you want to override when 169 | subclassing `restkit.rest.Resource`. 170 | 171 | - payload: string or File object passed to the body of the request 172 | - path: string additionnal path to the uri 173 | - headers: dict, optionnal headers that will 174 | be added to HTTP request. 175 | :params_dict: Options parameters added to the request as a dict 176 | - params: Optionnal parameterss added to the request 177 | """ 178 | 179 | params = params or {} 180 | params.update(params_dict or {}) 181 | 182 | while True: 183 | uri = util.make_uri(self.uri, path, charset=self.charset, 184 | safe=self.safe, encode_keys=self.encode_keys, 185 | **self.make_params(params)) 186 | 187 | # make request 188 | 189 | resp = self.client.request(uri, method=method, body=payload, 190 | headers=self.make_headers(headers)) 191 | 192 | if resp is None: 193 | # race condition 194 | raise ValueError("Unkown error: response object is None") 195 | 196 | if resp.status_int >= 400: 197 | if resp.status_int == 404: 198 | raise ResourceNotFound(resp.body_string(), 199 | response=resp) 200 | elif resp.status_int in (401, 403): 201 | if self.unauthorized(resp): 202 | raise Unauthorized(resp.body_string(), 203 | http_code=resp.status_int, 204 | response=resp) 205 | elif resp.status_int == 410: 206 | raise ResourceGone(resp.body_string(), response=resp) 207 | else: 208 | raise RequestFailed(resp.body_string(), 209 | http_code=resp.status_int, 210 | response=resp) 211 | else: 212 | break 213 | 214 | return resp 215 | 216 | def update_uri(self, path): 217 | """ 218 | to set a new uri absolute path 219 | """ 220 | self.uri = util.make_uri(self.uri, path, charset=self.charset, 221 | safe=self.safe, encode_keys=self.encode_keys) 222 | self.initial['uri'] = util.make_uri(self.initial['uri'], path, 223 | charset=self.charset, 224 | safe=self.safe, 225 | encode_keys=self.encode_keys) 226 | -------------------------------------------------------------------------------- /restkit/session.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from socketpool import ConnectionPool 7 | from restkit.conn import Connection 8 | 9 | 10 | _default_session = {} 11 | 12 | def get_session(backend_name, **options): 13 | global _default_session 14 | 15 | if not _default_session: 16 | _default_session = {} 17 | pool = ConnectionPool(factory=Connection, 18 | backend=backend_name, **options) 19 | _default_session[backend_name] = pool 20 | else: 21 | if backend_name not in _default_session: 22 | pool = ConnectionPool(factory=Connection, 23 | backend=backend_name, **options) 24 | 25 | _default_session[backend_name] = pool 26 | else: 27 | pool = _default_session.get(backend_name) 28 | return pool 29 | 30 | def set_session(backend_name, **options): 31 | 32 | global _default_session 33 | 34 | if not _default_session: 35 | _default_session = {} 36 | 37 | if backend_name in _default_session: 38 | pool = _default_session.get(backend_name) 39 | else: 40 | pool = ConnectionPool(factory=Connection, 41 | backend=backend_name, **options) 42 | _default_session[backend_name] = pool 43 | return pool 44 | -------------------------------------------------------------------------------- /restkit/tee.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | """ 8 | TeeInput replace old FileInput. It use a file 9 | if size > MAX_BODY or memory. It's now possible to rewind 10 | read or restart etc ... It's based on TeeInput from Gunicorn. 11 | 12 | """ 13 | import copy 14 | import os 15 | try: 16 | from cStringIO import StringIO 17 | except ImportError: 18 | from StringIO import StringIO 19 | import tempfile 20 | 21 | from restkit import conn 22 | 23 | class TeeInput(object): 24 | 25 | CHUNK_SIZE = conn.CHUNK_SIZE 26 | 27 | def __init__(self, stream): 28 | self.buf = StringIO() 29 | self.eof = False 30 | 31 | if isinstance(stream, basestring): 32 | stream = StringIO(stream) 33 | self.tmp = StringIO() 34 | else: 35 | self.tmp = tempfile.TemporaryFile() 36 | 37 | self.stream = stream 38 | 39 | def __enter__(self): 40 | return self 41 | 42 | def __exit__(self, exc_type, exc_val, traceback): 43 | return 44 | 45 | def seek(self, offset, whence=0): 46 | """ naive implementation of seek """ 47 | current_size = self._tmp_size() 48 | diff = 0 49 | if whence == 0: 50 | diff = offset - current_size 51 | elif whence == 2: 52 | diff = (self.tmp.tell() + offset) - current_size 53 | elif whence == 3 and not self.eof: 54 | # we read until the end 55 | while True: 56 | self.tmp.seek(0, 2) 57 | if not self._tee(self.CHUNK_SIZE): 58 | break 59 | 60 | if not self.eof and diff > 0: 61 | self._ensure_length(StringIO(), diff) 62 | self.tmp.seek(offset, whence) 63 | 64 | def flush(self): 65 | self.tmp.flush() 66 | 67 | def read(self, length=-1): 68 | """ read """ 69 | if self.eof: 70 | return self.tmp.read(length) 71 | 72 | if length < 0: 73 | buf = StringIO() 74 | buf.write(self.tmp.read()) 75 | while True: 76 | chunk = self._tee(self.CHUNK_SIZE) 77 | if not chunk: 78 | break 79 | buf.write(chunk) 80 | return buf.getvalue() 81 | else: 82 | dest = StringIO() 83 | diff = self._tmp_size() - self.tmp.tell() 84 | if not diff: 85 | dest.write(self._tee(length)) 86 | return self._ensure_length(dest, length) 87 | else: 88 | l = min(diff, length) 89 | dest.write(self.tmp.read(l)) 90 | return self._ensure_length(dest, length) 91 | 92 | def readline(self, size=-1): 93 | if self.eof: 94 | return self.tmp.readline() 95 | 96 | orig_size = self._tmp_size() 97 | if self.tmp.tell() == orig_size: 98 | if not self._tee(self.CHUNK_SIZE): 99 | return '' 100 | self.tmp.seek(orig_size) 101 | 102 | # now we can get line 103 | line = self.tmp.readline() 104 | if line.find("\n") >=0: 105 | return line 106 | 107 | buf = StringIO() 108 | buf.write(line) 109 | while True: 110 | orig_size = self.tmp.tell() 111 | data = self._tee(self.CHUNK_SIZE) 112 | if not data: 113 | break 114 | self.tmp.seek(orig_size) 115 | buf.write(self.tmp.readline()) 116 | if data.find("\n") >= 0: 117 | break 118 | return buf.getvalue() 119 | 120 | def readlines(self, sizehint=0): 121 | total = 0 122 | lines = [] 123 | line = self.readline() 124 | while line: 125 | lines.append(line) 126 | total += len(line) 127 | if 0 < sizehint <= total: 128 | break 129 | line = self.readline() 130 | return lines 131 | 132 | def close(self): 133 | if not self.eof: 134 | # we didn't read until the end 135 | self._close_unreader() 136 | return self.tmp.close() 137 | 138 | def next(self): 139 | r = self.readline() 140 | if not r: 141 | raise StopIteration 142 | return r 143 | __next__ = next 144 | 145 | def __iter__(self): 146 | return self 147 | 148 | def _tee(self, length): 149 | """ fetch partial body""" 150 | buf2 = self.buf 151 | buf2.seek(0, 2) 152 | chunk = self.stream.read(length) 153 | if chunk: 154 | self.tmp.write(chunk) 155 | self.tmp.flush() 156 | self.tmp.seek(0, 2) 157 | return chunk 158 | 159 | self._finalize() 160 | return "" 161 | 162 | def _finalize(self): 163 | """ here we wil fetch final trailers 164 | if any.""" 165 | self.eof = True 166 | 167 | def _tmp_size(self): 168 | if hasattr(self.tmp, 'fileno'): 169 | return int(os.fstat(self.tmp.fileno())[6]) 170 | else: 171 | return len(self.tmp.getvalue()) 172 | 173 | def _ensure_length(self, dest, length): 174 | if len(dest.getvalue()) < length: 175 | data = self._tee(length - len(dest.getvalue())) 176 | dest.write(data) 177 | return dest.getvalue() 178 | 179 | class ResponseTeeInput(TeeInput): 180 | 181 | CHUNK_SIZE = conn.CHUNK_SIZE 182 | 183 | def __init__(self, resp, connection, should_close=False): 184 | self.buf = StringIO() 185 | self.resp = resp 186 | self.stream =resp.body_stream() 187 | self.connection = connection 188 | self.should_close = should_close 189 | self.eof = False 190 | 191 | # set temporary body 192 | clen = int(resp.headers.get('content-length') or -1) 193 | if clen >= 0: 194 | if (clen <= conn.MAX_BODY): 195 | self.tmp = StringIO() 196 | else: 197 | self.tmp = tempfile.TemporaryFile() 198 | else: 199 | self.tmp = tempfile.TemporaryFile() 200 | 201 | def close(self): 202 | if not self.eof: 203 | # we didn't read until the end 204 | self._close_unreader() 205 | return self.tmp.close() 206 | 207 | def _close_unreader(self): 208 | if not self.eof: 209 | self.stream.close() 210 | self.connection.release(self.should_close) 211 | 212 | def _finalize(self): 213 | """ here we wil fetch final trailers 214 | if any.""" 215 | self.eof = True 216 | self._close_unreader() 217 | -------------------------------------------------------------------------------- /restkit/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import os 7 | import re 8 | import time 9 | import urllib 10 | import urlparse 11 | import warnings 12 | import Cookie 13 | 14 | from restkit.errors import InvalidUrl 15 | 16 | absolute_http_url_re = re.compile(r"^https?://", re.I) 17 | 18 | try:#python 2.6, use subprocess 19 | import subprocess 20 | subprocess.Popen # trigger ImportError early 21 | closefds = os.name == 'posix' 22 | 23 | def popen3(cmd, mode='t', bufsize=0): 24 | p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, 25 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 26 | close_fds=closefds) 27 | p.wait() 28 | return (p.stdin, p.stdout, p.stderr) 29 | except ImportError: 30 | subprocess = None 31 | popen3 = os.popen3 32 | 33 | def locate_program(program): 34 | if os.path.isabs(program): 35 | return program 36 | if os.path.dirname(program): 37 | program = os.path.normpath(os.path.realpath(program)) 38 | return program 39 | paths = os.getenv('PATH') 40 | if not paths: 41 | return False 42 | for path in paths.split(os.pathsep): 43 | filename = os.path.join(path, program) 44 | if os.access(filename, os.X_OK): 45 | return filename 46 | return False 47 | 48 | weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 49 | monthname = [None, 50 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 51 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 52 | 53 | def http_date(timestamp=None): 54 | """Return the current date and time formatted for a message header.""" 55 | if timestamp is None: 56 | timestamp = time.time() 57 | year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) 58 | s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( 59 | weekdayname[wd], 60 | day, monthname[month], year, 61 | hh, mm, ss) 62 | return s 63 | 64 | def parse_netloc(uri): 65 | host = uri.netloc 66 | port = None 67 | i = host.rfind(':') 68 | j = host.rfind(']') # ipv6 addresses have [...] 69 | if i > j: 70 | try: 71 | port = int(host[i+1:]) 72 | except ValueError: 73 | raise InvalidUrl("nonnumeric port: '%s'" % host[i+1:]) 74 | host = host[:i] 75 | else: 76 | # default port 77 | if uri.scheme == "https": 78 | port = 443 79 | else: 80 | port = 80 81 | 82 | if host and host[0] == '[' and host[-1] == ']': 83 | host = host[1:-1] 84 | return (host, port) 85 | 86 | def to_bytestring(s): 87 | if not isinstance(s, basestring): 88 | raise TypeError("value should be a str or unicode") 89 | 90 | if isinstance(s, unicode): 91 | return s.encode('utf-8') 92 | return s 93 | 94 | def url_quote(s, charset='utf-8', safe='/:'): 95 | """URL encode a single string with a given encoding.""" 96 | if isinstance(s, unicode): 97 | s = s.encode(charset) 98 | elif not isinstance(s, str): 99 | s = str(s) 100 | return urllib.quote(s, safe=safe) 101 | 102 | 103 | def url_encode(obj, charset="utf8", encode_keys=False): 104 | items = [] 105 | if isinstance(obj, dict): 106 | for k, v in list(obj.items()): 107 | items.append((k, v)) 108 | else: 109 | items = list(items) 110 | 111 | tmp = [] 112 | for k, v in items: 113 | if encode_keys: 114 | k = encode(k, charset) 115 | 116 | if not isinstance(v, (tuple, list)): 117 | v = [v] 118 | 119 | for v1 in v: 120 | if v1 is None: 121 | v1 = '' 122 | elif callable(v1): 123 | v1 = encode(v1(), charset) 124 | else: 125 | v1 = encode(v1, charset) 126 | tmp.append('%s=%s' % (urllib.quote(k), urllib.quote_plus(v1))) 127 | return '&'.join(tmp) 128 | 129 | def encode(v, charset="utf8"): 130 | if isinstance(v, unicode): 131 | v = v.encode(charset) 132 | else: 133 | v = str(v) 134 | return v 135 | 136 | 137 | def make_uri(base, *args, **kwargs): 138 | """Assemble a uri based on a base, any number of path segments, 139 | and query string parameters. 140 | 141 | """ 142 | 143 | # get encoding parameters 144 | charset = kwargs.pop("charset", "utf-8") 145 | safe = kwargs.pop("safe", "/:") 146 | encode_keys = kwargs.pop("encode_keys", True) 147 | 148 | base_trailing_slash = False 149 | if base and base.endswith("/"): 150 | base_trailing_slash = True 151 | base = base[:-1] 152 | retval = [base] 153 | 154 | # build the path 155 | _path = [] 156 | trailing_slash = False 157 | for s in args: 158 | if s is not None and isinstance(s, basestring): 159 | if len(s) > 1 and s.endswith('/'): 160 | trailing_slash = True 161 | else: 162 | trailing_slash = False 163 | _path.append(url_quote(s.strip('/'), charset, safe)) 164 | 165 | path_str ="" 166 | if _path: 167 | path_str = "/".join([''] + _path) 168 | if trailing_slash: 169 | path_str = path_str + "/" 170 | elif base_trailing_slash: 171 | path_str = path_str + "/" 172 | 173 | if path_str: 174 | retval.append(path_str) 175 | 176 | params_str = url_encode(kwargs, charset, encode_keys) 177 | if params_str: 178 | retval.extend(['?', params_str]) 179 | 180 | return ''.join(retval) 181 | 182 | 183 | def rewrite_location(host_uri, location, prefix_path=None): 184 | prefix_path = prefix_path or '' 185 | url = urlparse.urlparse(location) 186 | host_url = urlparse.urlparse(host_uri) 187 | 188 | if not absolute_http_url_re.match(location): 189 | # remote server doesn't follow rfc2616 190 | proxy_uri = '%s%s' % (host_uri, prefix_path) 191 | return urlparse.urljoin(proxy_uri, location) 192 | elif url.scheme == host_url.scheme and url.netloc == host_url.netloc: 193 | return urlparse.urlunparse((host_url.scheme, host_url.netloc, 194 | prefix_path + url.path, url.params, url.query, url.fragment)) 195 | 196 | return location 197 | 198 | def replace_header(name, value, headers): 199 | idx = -1 200 | for i, (k, v) in enumerate(headers): 201 | if k.upper() == name.upper(): 202 | idx = i 203 | break 204 | if idx >= 0: 205 | headers[i] = (name.title(), value) 206 | else: 207 | headers.append((name.title(), value)) 208 | return headers 209 | 210 | def replace_headers(new_headers, headers): 211 | hdrs = {} 212 | for (k, v) in new_headers: 213 | hdrs[k.upper()] = v 214 | 215 | found = [] 216 | for i, (k, v) in enumerate(headers): 217 | ku = k.upper() 218 | if ku in hdrs: 219 | headers[i] = (k.title(), hdrs[ku]) 220 | found.append(ku) 221 | if len(found) == len(new_headers): 222 | return 223 | 224 | for k, v in new_headers.items(): 225 | if k not in found: 226 | headers.append((k.title(), v)) 227 | return headers 228 | 229 | 230 | def parse_cookie(cookie, final_url): 231 | if cookie == '': 232 | return {} 233 | 234 | if not isinstance(cookie, Cookie.BaseCookie): 235 | try: 236 | c = Cookie.SimpleCookie() 237 | c.load(cookie) 238 | except Cookie.CookieError: 239 | # Invalid cookie 240 | return {} 241 | else: 242 | c = cookie 243 | 244 | cookiedict = {} 245 | 246 | for key in c.keys(): 247 | cook = c.get(key) 248 | cookiedict[key] = cook.value 249 | return cookiedict 250 | 251 | 252 | class deprecated_property(object): 253 | """ 254 | Wraps a decorator, with a deprecation warning or error 255 | """ 256 | def __init__(self, decorator, attr, message, warning=True): 257 | self.decorator = decorator 258 | self.attr = attr 259 | self.message = message 260 | self.warning = warning 261 | 262 | def __get__(self, obj, type=None): 263 | if obj is None: 264 | return self 265 | self.warn() 266 | return self.decorator.__get__(obj, type) 267 | 268 | def __set__(self, obj, value): 269 | self.warn() 270 | self.decorator.__set__(obj, value) 271 | 272 | def __delete__(self, obj): 273 | self.warn() 274 | self.decorator.__delete__(obj) 275 | 276 | def __repr__(self): 277 | return '' % ( 278 | self.attr, 279 | self.decorator) 280 | 281 | def warn(self): 282 | if not self.warning: 283 | raise DeprecationWarning( 284 | 'The attribute %s is deprecated: %s' % (self.attr, self.message)) 285 | else: 286 | warnings.warn( 287 | 'The attribute %s is deprecated: %s' % (self.attr, self.message), 288 | DeprecationWarning, 289 | stacklevel=3) 290 | 291 | -------------------------------------------------------------------------------- /restkit/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | version_info = (4, 2, 2) 7 | __version__ = ".".join(map(str, version_info)) 8 | -------------------------------------------------------------------------------- /scripts/restcli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of restkit released under the MIT license. 5 | # See the NOTICE for more information. 6 | 7 | from restkit.contrib.console import main 8 | 9 | if __name__ == '__main__': 10 | main() 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | exclude=(eventlet,gevent,ipython_shell|eventlet_pool|gevent_pool) 3 | with-coverage=1 4 | cover-package=restkit -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 - 3 | # 4 | # This file is part of restkit released under the MIT license. 5 | # See the NOTICE for more information. 6 | 7 | from __future__ import with_statement 8 | from setuptools import setup, find_packages 9 | 10 | import glob 11 | from imp import load_source 12 | import os 13 | import sys 14 | 15 | if not hasattr(sys, 'version_info') or sys.version_info < (2, 6, 0, 'final'): 16 | raise SystemExit("Restkit requires Python 2.6 or later.") 17 | 18 | extras = {} 19 | 20 | CLASSIFIERS = [ 21 | 'Development Status :: 4 - Beta', 22 | 'Environment :: Web Environment', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Topic :: Internet :: WWW/HTTP', 28 | 'Topic :: Software Development :: Libraries'] 29 | 30 | 31 | SCRIPTS = ['scripts/restcli'] 32 | 33 | def main(): 34 | version = load_source("version", os.path.join("restkit", 35 | "version.py")) 36 | 37 | # read long description 38 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: 39 | long_description = f.read() 40 | 41 | DATA_FILES = [ 42 | ('restkit', ["LICENSE", "MANIFEST.in", "NOTICE", "README.rst", 43 | "THANKS", "TODO.txt"]) 44 | ] 45 | 46 | options=dict( 47 | name = 'restkit', 48 | version = version.__version__, 49 | description = 'Python REST kit', 50 | long_description = long_description, 51 | author = 'Benoit Chesneau', 52 | author_email = 'benoitc@e-engura.org', 53 | license = 'MIT', 54 | url = 'http://benoitc.github.com/restkit', 55 | classifiers = CLASSIFIERS, 56 | packages = find_packages(exclude=['tests']), 57 | data_files = DATA_FILES, 58 | scripts = SCRIPTS, 59 | zip_safe = False, 60 | entry_points = { 61 | 'paste.app_factory': [ 62 | 'proxy = restkit.contrib.wsgi_proxy:make_proxy', 63 | 'host_proxy = restkit.contrib.wsgi_proxy:make_host_proxy', 64 | 'couchdb_proxy = restkit.contrib.wsgi_proxy:make_couchdb_proxy', 65 | ]}, 66 | install_requires = [ 67 | 'http-parser>=0.8.3', 68 | 'socketpool>=0.5.3'], 69 | test_suite = 'nose.collector' 70 | ) 71 | 72 | 73 | setup(**options) 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /tests/004-test-client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from __future__ import with_statement 7 | 8 | import cgi 9 | import imghdr 10 | import os 11 | import socket 12 | import threading 13 | import Queue 14 | import urlparse 15 | import sys 16 | import tempfile 17 | import time 18 | 19 | import t 20 | from restkit.filters import BasicAuth 21 | 22 | 23 | LONG_BODY_PART = """This is a relatively long body, that we send to the client... 24 | This is a relatively long body, that we send to the client... 25 | This is a relatively long body, that we send to the client... 26 | This is a relatively long body, that we send to the client... 27 | This is a relatively long body, that we send to the client... 28 | This is a relatively long body, that we send to the client... 29 | This is a relatively long body, that we send to the client... 30 | This is a relatively long body, that we send to the client... 31 | This is a relatively long body, that we send to the client... 32 | This is a relatively long body, that we send to the client... 33 | This is a relatively long body, that we send to the client... 34 | This is a relatively long body, that we send to the client... 35 | This is a relatively long body, that we send to the client... 36 | This is a relatively long body, that we send to the client... 37 | This is a relatively long body, that we send to the client... 38 | This is a relatively long body, that we send to the client... 39 | This is a relatively long body, that we send to the client... 40 | This is a relatively long body, that we send to the client... 41 | This is a relatively long body, that we send to the client... 42 | This is a relatively long body, that we send to the client... 43 | This is a relatively long body, that we send to the client... 44 | This is a relatively long body, that we send to the client... 45 | This is a relatively long body, that we send to the client... 46 | This is a relatively long body, that we send to the client... 47 | This is a relatively long body, that we send to the client... 48 | This is a relatively long body, that we send to the client... 49 | This is a relatively long body, that we send to the client... 50 | This is a relatively long body, that we send to the client... 51 | This is a relatively long body, that we send to the client... 52 | This is a relatively long body, that we send to the client... 53 | This is a relatively long body, that we send to the client... 54 | This is a relatively long body, that we send to the client... 55 | This is a relatively long body, that we send to the client... 56 | This is a relatively long body, that we send to the client... 57 | This is a relatively long body, that we send to the client... 58 | This is a relatively long body, that we send to the client... 59 | This is a relatively long body, that we send to the client... 60 | This is a relatively long body, that we send to the client... 61 | This is a relatively long body, that we send to the client... 62 | This is a relatively long body, that we send to the client... 63 | This is a relatively long body, that we send to the client... 64 | This is a relatively long body, that we send to the client... 65 | This is a relatively long body, that we send to the client... 66 | This is a relatively long body, that we send to the client... 67 | This is a relatively long body, that we send to the client... 68 | This is a relatively long body, that we send to the client... 69 | This is a relatively long body, that we send to the client... 70 | This is a relatively long body, that we send to the client... 71 | This is a relatively long body, that we send to the client... 72 | This is a relatively long body, that we send to the client... 73 | This is a relatively long body, that we send to the client... 74 | This is a relatively long body, that we send to the client... 75 | This is a relatively long body, that we send to the client... 76 | This is a relatively long body, that we send to the client... 77 | This is a relatively long body, that we send to the client...""" 78 | 79 | @t.client_request("/") 80 | def test_001(u, c): 81 | r = c.request(u) 82 | t.eq(r.body_string(), "welcome") 83 | 84 | @t.client_request("/unicode") 85 | def test_002(u, c): 86 | r = c.request(u) 87 | t.eq(r.body_string(charset="utf-8"), u"éàù@") 88 | 89 | @t.client_request("/éàù") 90 | def test_003(u, c): 91 | r = c.request(u) 92 | t.eq(r.body_string(), "ok") 93 | t.eq(r.status_int, 200) 94 | 95 | @t.client_request("/json") 96 | def test_004(u, c): 97 | r = c.request(u, headers={'Content-Type': 'application/json'}) 98 | t.eq(r.status_int, 200) 99 | r = c.request(u, headers={'Content-Type': 'text/plain'}) 100 | t.eq(r.status_int, 400) 101 | 102 | 103 | @t.client_request('/unkown') 104 | def test_005(u, c): 105 | r = c.request(u, headers={'Content-Type': 'application/json'}) 106 | t.eq(r.status_int, 404) 107 | 108 | @t.client_request('/query?test=testing') 109 | def test_006(u, c): 110 | r = c.request(u) 111 | t.eq(r.status_int, 200) 112 | t.eq(r.body_string(), "ok") 113 | 114 | 115 | @t.client_request('http://e-engura.com/images/logo.gif') 116 | def test_007(u, c): 117 | r = c.request(u) 118 | print r.status 119 | t.eq(r.status_int, 200) 120 | fd, fname = tempfile.mkstemp(suffix='.gif') 121 | f = os.fdopen(fd, "wb") 122 | f.write(r.body_string()) 123 | f.close() 124 | t.eq(imghdr.what(fname), 'gif') 125 | 126 | 127 | @t.client_request('http://e-engura.com/images/logo.gif') 128 | def test_008(u, c): 129 | r = c.request(u) 130 | t.eq(r.status_int, 200) 131 | fd, fname = tempfile.mkstemp(suffix='.gif') 132 | f = os.fdopen(fd, "wb") 133 | with r.body_stream() as body: 134 | for block in body: 135 | f.write(block) 136 | f.close() 137 | t.eq(imghdr.what(fname), 'gif') 138 | 139 | 140 | @t.client_request('/redirect') 141 | def test_009(u, c): 142 | c.follow_redirect = True 143 | r = c.request(u) 144 | 145 | complete_url = "%s/complete_redirect" % u.rsplit("/", 1)[0] 146 | t.eq(r.status_int, 200) 147 | t.eq(r.body_string(), "ok") 148 | t.eq(r.final_url, complete_url) 149 | 150 | 151 | @t.client_request('/') 152 | def test_010(u, c): 153 | r = c.request(u, 'POST', body="test") 154 | t.eq(r.body_string(), "test") 155 | 156 | 157 | @t.client_request('/bytestring') 158 | def test_011(u, c): 159 | r = c.request(u, 'POST', body="éàù@") 160 | t.eq(r.body_string(), "éàù@") 161 | 162 | 163 | @t.client_request('/unicode') 164 | def test_012(u, c): 165 | r = c.request(u, 'POST', body=u"éàù@") 166 | t.eq(r.body_string(), "éàù@") 167 | 168 | 169 | @t.client_request('/json') 170 | def test_013(u, c): 171 | r = c.request(u, 'POST', body="test", 172 | headers={'Content-Type': 'application/json'}) 173 | t.eq(r.status_int, 200) 174 | 175 | r = c.request(u, 'POST', body="test", 176 | headers={'Content-Type': 'text/plain'}) 177 | t.eq(r.status_int, 400) 178 | 179 | 180 | @t.client_request('/empty') 181 | def test_014(u, c): 182 | r = c.request(u, 'POST', body="", 183 | headers={'Content-Type': 'application/json'}) 184 | t.eq(r.status_int, 200) 185 | 186 | r = c.request(u, 'POST', body="", 187 | headers={'Content-Type': 'application/json'}) 188 | t.eq(r.status_int, 200) 189 | 190 | 191 | @t.client_request('/query?test=testing') 192 | def test_015(u, c): 193 | r = c.request(u, 'POST', body="", 194 | headers={'Content-Type': 'application/json'}) 195 | t.eq(r.status_int, 200) 196 | 197 | 198 | @t.client_request('/1M') 199 | def test_016(u, c): 200 | fn = os.path.join(os.path.dirname(__file__), "1M") 201 | with open(fn, "rb") as f: 202 | l = int(os.fstat(f.fileno())[6]) 203 | r = c.request(u, 'POST', body=f) 204 | t.eq(r.status_int, 200) 205 | t.eq(int(r.body_string()), l) 206 | 207 | 208 | @t.client_request('/large') 209 | def test_017(u, c): 210 | r = c.request(u, 'POST', body=LONG_BODY_PART) 211 | t.eq(r.status_int, 200) 212 | t.eq(int(r['content-length']), len(LONG_BODY_PART)) 213 | t.eq(r.body_string(), LONG_BODY_PART) 214 | 215 | 216 | 217 | def test_0018(): 218 | for i in range(10): 219 | t.client_request('/large')(test_017) 220 | 221 | @t.client_request('/') 222 | def test_019(u, c): 223 | r = c.request(u, 'PUT', body="test") 224 | t.eq(r.body_string(), "test") 225 | 226 | 227 | @t.client_request('/auth') 228 | def test_020(u, c): 229 | c.filters = [BasicAuth("test", "test")] 230 | c.load_filters() 231 | r = c.request(u) 232 | t.eq(r.status_int, 200) 233 | 234 | c.filters = [BasicAuth("test", "test2")] 235 | c.load_filters() 236 | r = c.request(u) 237 | t.eq(r.status_int, 403) 238 | 239 | 240 | @t.client_request('/list') 241 | def test_021(u, c): 242 | lines = ["line 1\n", " line2\n"] 243 | r = c.request(u, 'POST', body=lines, 244 | headers=[("Content-Length", "14")]) 245 | t.eq(r.status_int, 200) 246 | t.eq(r.body_string(), 'line 1\n line2\n') 247 | 248 | @t.client_request('/chunked') 249 | def test_022(u, c): 250 | lines = ["line 1\n", " line2\n"] 251 | r = c.request(u, 'POST', body=lines, 252 | headers=[("Transfer-Encoding", "chunked")]) 253 | t.eq(r.status_int, 200) 254 | t.eq(r.body_string(), '7\r\nline 1\n\r\n7\r\n line2\n\r\n0\r\n\r\n') 255 | 256 | @t.client_request("/cookie") 257 | def test_023(u, c): 258 | r = c.request(u) 259 | t.eq(r.cookies.get('fig'), 'newton') 260 | t.eq(r.status_int, 200) 261 | 262 | 263 | @t.client_request("/cookies") 264 | def test_024(u, c): 265 | r = c.request(u) 266 | t.eq(r.cookies.get('fig'), 'newton') 267 | t.eq(r.cookies.get('sugar'), 'wafer') 268 | t.eq(r.status_int, 200) 269 | 270 | 271 | -------------------------------------------------------------------------------- /tests/005-test-resource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | import t 8 | 9 | from restkit.errors import RequestFailed, ResourceNotFound, \ 10 | Unauthorized 11 | from restkit.resource import Resource 12 | from _server_test import HOST, PORT 13 | 14 | @t.resource_request() 15 | def test_001(res): 16 | r = res.get() 17 | t.eq(r.status_int, 200) 18 | t.eq(r.body_string(), "welcome") 19 | 20 | @t.resource_request() 21 | def test_002(res): 22 | r = res.get('/unicode') 23 | t.eq(r.body_string(), "éàù@") 24 | 25 | @t.resource_request() 26 | def test_003(res): 27 | r = res.get('/éàù') 28 | t.eq(r.status_int, 200) 29 | t.eq(r.body_string(), "ok") 30 | 31 | @t.resource_request() 32 | def test_004(res): 33 | r = res.get(u'/test') 34 | t.eq(r.status_int, 200) 35 | r = res.get(u'/éàù') 36 | t.eq(r.status_int, 200) 37 | 38 | @t.resource_request() 39 | def test_005(res): 40 | r = res.get('/json', headers={'Content-Type': 'application/json'}) 41 | t.eq(r.status_int, 200) 42 | t.raises(RequestFailed, res.get, '/json', 43 | headers={'Content-Type': 'text/plain'}) 44 | 45 | @t.resource_request() 46 | def test_006(res): 47 | t.raises(ResourceNotFound, res.get, '/unknown') 48 | 49 | @t.resource_request() 50 | def test_007(res): 51 | r = res.get('/query', test='testing') 52 | t.eq(r.status_int, 200) 53 | r = res.get('/qint', test=1) 54 | t.eq(r.status_int, 200) 55 | 56 | @t.resource_request() 57 | def test_008(res): 58 | r = res.post(payload="test") 59 | t.eq(r.body_string(), "test") 60 | 61 | @t.resource_request() 62 | def test_009(res): 63 | r = res.post('/bytestring', payload="éàù@") 64 | t.eq(r.body_string(), "éàù@") 65 | 66 | @t.resource_request() 67 | def test_010(res): 68 | r = res.post('/unicode', payload=u"éàù@") 69 | t.eq(r.body_string(), "éàù@") 70 | print "ok" 71 | r = res.post('/unicode', payload=u"éàù@") 72 | t.eq(r.body_string(charset="utf-8"), u"éàù@") 73 | 74 | @t.resource_request() 75 | def test_011(res): 76 | r = res.post('/json', payload="test", 77 | headers={'Content-Type': 'application/json'}) 78 | t.eq(r.status_int, 200) 79 | t.raises(RequestFailed, res.post, '/json', payload='test', 80 | headers={'Content-Type': 'text/plain'}) 81 | 82 | @t.resource_request() 83 | def test_012(res): 84 | r = res.post('/empty', payload="", 85 | headers={'Content-Type': 'application/json'}) 86 | t.eq(r.status_int, 200) 87 | r = res.post('/empty', headers={'Content-Type': 'application/json'}) 88 | t.eq(r.status_int, 200) 89 | 90 | @t.resource_request() 91 | def test_013(res): 92 | r = res.post('/query', test="testing") 93 | t.eq(r.status_int, 200) 94 | 95 | @t.resource_request() 96 | def test_014(res): 97 | r = res.post('/form', payload={ "a": "a", "b": "b" }) 98 | t.eq(r.status_int, 200) 99 | 100 | @t.resource_request() 101 | def test_015(res): 102 | r = res.put(payload="test") 103 | t.eq(r.body_string(), 'test') 104 | 105 | @t.resource_request() 106 | def test_016(res): 107 | r = res.head('/ok') 108 | t.eq(r.status_int, 200) 109 | 110 | @t.resource_request() 111 | def test_017(res): 112 | r = res.delete('/delete') 113 | t.eq(r.status_int, 200) 114 | 115 | @t.resource_request() 116 | def test_018(res): 117 | content_length = len("test") 118 | import StringIO 119 | content = StringIO.StringIO("test") 120 | r = res.post('/json', payload=content, 121 | headers={ 122 | 'Content-Type': 'application/json', 123 | 'Content-Length': str(content_length) 124 | }) 125 | t.eq(r.status_int, 200) 126 | 127 | @t.resource_request() 128 | def test_019(res): 129 | import StringIO 130 | content = StringIO.StringIO("test") 131 | t.raises(RequestFailed, res.post, '/json', payload=content, 132 | headers={'Content-Type': 'text/plain'}) 133 | 134 | def test_020(): 135 | u = "http://test:test@%s:%s/auth" % (HOST, PORT) 136 | res = Resource(u) 137 | r = res.get() 138 | t.eq(r.status_int, 200) 139 | u = "http://test:test2@%s:%s/auth" % (HOST, PORT) 140 | res = Resource(u) 141 | t.raises(Unauthorized, res.get) 142 | 143 | @t.resource_request() 144 | def test_021(res): 145 | r = res.post('/multivalueform', payload={ "a": ["a", "c"], "b": "b" }) 146 | t.eq(r.status_int, 200) 147 | 148 | @t.resource_request() 149 | def test_022(res): 150 | import os 151 | fn = os.path.join(os.path.dirname(__file__), "1M") 152 | f = open(fn, 'rb') 153 | l = int(os.fstat(f.fileno())[6]) 154 | b = {'a':'aa','b':['bb','éàù@'], 'f':f} 155 | h = {'content-type':"multipart/form-data"} 156 | r = res.post('/multipart2', payload=b, headers=h) 157 | t.eq(r.status_int, 200) 158 | t.eq(int(r.body_string()), l) 159 | 160 | @t.resource_request() 161 | def test_023(res): 162 | import os 163 | fn = os.path.join(os.path.dirname(__file__), "1M") 164 | f = open(fn, 'rb') 165 | l = int(os.fstat(f.fileno())[6]) 166 | b = {'a':'aa','b':'éàù@', 'f':f} 167 | h = {'content-type':"multipart/form-data"} 168 | r = res.post('/multipart3', payload=b, headers=h) 169 | t.eq(r.status_int, 200) 170 | t.eq(int(r.body_string()), l) 171 | 172 | @t.resource_request() 173 | def test_024(res): 174 | import os 175 | fn = os.path.join(os.path.dirname(__file__), "1M") 176 | f = open(fn, 'rb') 177 | content = f.read() 178 | f.seek(0) 179 | b = {'a':'aa','b':'éàù@', 'f':f} 180 | h = {'content-type':"multipart/form-data"} 181 | r = res.post('/multipart4', payload=b, headers=h) 182 | t.eq(r.status_int, 200) 183 | t.eq(r.body_string(), content) 184 | 185 | @t.resource_request() 186 | def test_025(res): 187 | import StringIO 188 | content = 'éàù@' 189 | f = StringIO.StringIO('éàù@') 190 | f.name = 'test.txt' 191 | b = {'a':'aa','b':'éàù@', 'f':f} 192 | h = {'content-type':"multipart/form-data"} 193 | r = res.post('/multipart4', payload=b, headers=h) 194 | t.eq(r.status_int, 200) 195 | t.eq(r.body_string(), content) -------------------------------------------------------------------------------- /tests/006-test-webob.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import unittest 7 | 8 | import webob.exc 9 | from restkit.contrib.webob_helper import wrap_exceptions 10 | 11 | 12 | wrap_exceptions() 13 | 14 | class ResourceTestCase(unittest.TestCase): 15 | 16 | def testWebobException(self): 17 | 18 | from restkit.errors import ResourceError 19 | self.assert_(issubclass(ResourceError, 20 | webob.exc.WSGIHTTPException) == True) 21 | 22 | if __name__ == '__main__': 23 | unittest.main() -------------------------------------------------------------------------------- /tests/007-test-util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | import t 8 | from restkit import util 9 | 10 | def test_001(): 11 | qs = {'a': "a"} 12 | t.eq(util.url_encode(qs), "a=a") 13 | qs = {'a': 'a', 'b': 'b'} 14 | t.eq(util.url_encode(qs), "a=a&b=b") 15 | qs = {'a': 1} 16 | t.eq(util.url_encode(qs), "a=1") 17 | qs = {'a': [1, 2]} 18 | t.eq(util.url_encode(qs), "a=1&a=2") 19 | qs = {'a': [1, 2], 'b': [3, 4]} 20 | t.eq(util.url_encode(qs), "a=1&a=2&b=3&b=4") 21 | qs = {'a': lambda : 1} 22 | t.eq(util.url_encode(qs), "a=1") 23 | 24 | def test_002(): 25 | t.eq(util.make_uri("http://localhost", "/"), "http://localhost/") 26 | t.eq(util.make_uri("http://localhost/"), "http://localhost/") 27 | t.eq(util.make_uri("http://localhost/", "/test/echo"), 28 | "http://localhost/test/echo") 29 | t.eq(util.make_uri("http://localhost/", "/test/echo/"), 30 | "http://localhost/test/echo/") 31 | t.eq(util.make_uri("http://localhost", "/test/echo/"), 32 | "http://localhost/test/echo/") 33 | t.eq(util.make_uri("http://localhost", "test/echo"), 34 | "http://localhost/test/echo") 35 | t.eq(util.make_uri("http://localhost", "test/echo/"), 36 | "http://localhost/test/echo/") 37 | 38 | -------------------------------------------------------------------------------- /tests/008-test-request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import os 7 | import uuid 8 | import t 9 | from restkit import request 10 | from restkit.forms import multipart_form_encode 11 | 12 | from _server_test import HOST, PORT 13 | 14 | LONG_BODY_PART = """This is a relatively long body, that we send to the client... 15 | This is a relatively long body, that we send to the client... 16 | This is a relatively long body, that we send to the client... 17 | This is a relatively long body, that we send to the client... 18 | This is a relatively long body, that we send to the client... 19 | This is a relatively long body, that we send to the client... 20 | This is a relatively long body, that we send to the client... 21 | This is a relatively long body, that we send to the client... 22 | This is a relatively long body, that we send to the client... 23 | This is a relatively long body, that we send to the client... 24 | This is a relatively long body, that we send to the client... 25 | This is a relatively long body, that we send to the client... 26 | This is a relatively long body, that we send to the client... 27 | This is a relatively long body, that we send to the client... 28 | This is a relatively long body, that we send to the client... 29 | This is a relatively long body, that we send to the client... 30 | This is a relatively long body, that we send to the client... 31 | This is a relatively long body, that we send to the client... 32 | This is a relatively long body, that we send to the client... 33 | This is a relatively long body, that we send to the client... 34 | This is a relatively long body, that we send to the client... 35 | This is a relatively long body, that we send to the client... 36 | This is a relatively long body, that we send to the client... 37 | This is a relatively long body, that we send to the client... 38 | This is a relatively long body, that we send to the client... 39 | This is a relatively long body, that we send to the client... 40 | This is a relatively long body, that we send to the client... 41 | This is a relatively long body, that we send to the client... 42 | This is a relatively long body, that we send to the client... 43 | This is a relatively long body, that we send to the client... 44 | This is a relatively long body, that we send to the client... 45 | This is a relatively long body, that we send to the client... 46 | This is a relatively long body, that we send to the client... 47 | This is a relatively long body, that we send to the client... 48 | This is a relatively long body, that we send to the client... 49 | This is a relatively long body, that we send to the client... 50 | This is a relatively long body, that we send to the client... 51 | This is a relatively long body, that we send to the client... 52 | This is a relatively long body, that we send to the client... 53 | This is a relatively long body, that we send to the client... 54 | This is a relatively long body, that we send to the client... 55 | This is a relatively long body, that we send to the client... 56 | This is a relatively long body, that we send to the client... 57 | This is a relatively long body, that we send to the client... 58 | This is a relatively long body, that we send to the client... 59 | This is a relatively long body, that we send to the client... 60 | This is a relatively long body, that we send to the client... 61 | This is a relatively long body, that we send to the client... 62 | This is a relatively long body, that we send to the client... 63 | This is a relatively long body, that we send to the client... 64 | This is a relatively long body, that we send to the client... 65 | This is a relatively long body, that we send to the client... 66 | This is a relatively long body, that we send to the client... 67 | This is a relatively long body, that we send to the client... 68 | This is a relatively long body, that we send to the client...""" 69 | 70 | def test_001(): 71 | u = "http://%s:%s" % (HOST, PORT) 72 | r = request(u) 73 | t.eq(r.status_int, 200) 74 | t.eq(r.body_string(), "welcome") 75 | 76 | def test_002(): 77 | u = "http://%s:%s" % (HOST, PORT) 78 | r = request(u, 'POST', body=LONG_BODY_PART) 79 | t.eq(r.status_int, 200) 80 | body = r.body_string() 81 | t.eq(len(body), len(LONG_BODY_PART)) 82 | t.eq(body, LONG_BODY_PART) 83 | 84 | def test_003(): 85 | u = "http://test:test@%s:%s/auth" % (HOST, PORT) 86 | r = request(u) 87 | t.eq(r.status_int, 200) 88 | u = "http://test:test2@%s:%s/auth" % (HOST, PORT) 89 | r = request(u) 90 | t.eq(r.status_int, 403) 91 | 92 | def test_004(): 93 | u = "http://%s:%s/multipart2" % (HOST, PORT) 94 | fn = os.path.join(os.path.dirname(__file__), "1M") 95 | f = open(fn, 'rb') 96 | l = int(os.fstat(f.fileno())[6]) 97 | b = {'a':'aa','b':['bb','éàù@'], 'f':f} 98 | h = {'content-type':"multipart/form-data"} 99 | body, headers = multipart_form_encode(b, h, uuid.uuid4().hex) 100 | r = request(u, method='POST', body=body, headers=headers) 101 | t.eq(r.status_int, 200) 102 | t.eq(int(r.body_string()), l) 103 | 104 | def test_005(): 105 | u = "http://%s:%s/multipart3" % (HOST, PORT) 106 | fn = os.path.join(os.path.dirname(__file__), "1M") 107 | f = open(fn, 'rb') 108 | l = int(os.fstat(f.fileno())[6]) 109 | b = {'a':'aa','b':'éàù@', 'f':f} 110 | h = {'content-type':"multipart/form-data"} 111 | body, headers = multipart_form_encode(b, h, uuid.uuid4().hex) 112 | r = request(u, method='POST', body=body, headers=headers) 113 | t.eq(r.status_int, 200) 114 | t.eq(int(r.body_string()), l) 115 | 116 | def test_006(): 117 | u = "http://%s:%s/multipart4" % (HOST, PORT) 118 | fn = os.path.join(os.path.dirname(__file__), "1M") 119 | f = open(fn, 'rb') 120 | content = f.read() 121 | f.seek(0) 122 | b = {'a':'aa','b':'éàù@', 'f':f} 123 | h = {'content-type':"multipart/form-data"} 124 | body, headers = multipart_form_encode(b, h, uuid.uuid4().hex) 125 | r = request(u, method='POST', body=body, headers=headers) 126 | t.eq(r.status_int, 200) 127 | t.eq(r.body_string(), content) 128 | 129 | def test_007(): 130 | import StringIO 131 | u = "http://%s:%s/multipart4" % (HOST, PORT) 132 | content = 'éàù@' 133 | f = StringIO.StringIO('éàù@') 134 | f.name = 'test.txt' 135 | b = {'a':'aa','b':'éàù@', 'f':f} 136 | h = {'content-type':"multipart/form-data"} 137 | body, headers = multipart_form_encode(b, h, uuid.uuid4().hex) 138 | r = request(u, method='POST', body=body, headers=headers) 139 | t.eq(r.status_int, 200) 140 | t.eq(r.body_string(), content) -------------------------------------------------------------------------------- /tests/009-test-oauth_filter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | # Request Token: http://oauth-sandbox.sevengoslings.net/request_token 8 | # Auth: http://oauth-sandbox.sevengoslings.net/authorize 9 | # Access Token: http://oauth-sandbox.sevengoslings.net/access_token 10 | # Two-legged: http://oauth-sandbox.sevengoslings.net/two_legged 11 | # Three-legged: http://oauth-sandbox.sevengoslings.net/three_legged 12 | # Key: bd37aed57e15df53 13 | # Secret: 0e9e6413a9ef49510a4f68ed02cd 14 | 15 | try: 16 | from urlparse import parse_qs, parse_qsl 17 | except ImportError: 18 | from cgi import parse_qs, parse_qsl 19 | import urllib 20 | 21 | from restkit import request, OAuthFilter 22 | from restkit.oauth2 import Consumer 23 | import t 24 | 25 | 26 | class oauth_request(object): 27 | oauth_uris = { 28 | 'request_token': '/request_token', 29 | 'authorize': '/authorize', 30 | 'access_token': '/access_token', 31 | 'two_legged': '/two_legged', 32 | 'three_legged': '/three_legged' 33 | } 34 | 35 | consumer_key = 'bd37aed57e15df53' 36 | consumer_secret = '0e9e6413a9ef49510a4f68ed02cd' 37 | host = 'http://oauth-sandbox.sevengoslings.net' 38 | 39 | def __init__(self, utype): 40 | self.consumer = Consumer(key=self.consumer_key, 41 | secret=self.consumer_secret) 42 | self.body = { 43 | 'foo': 'bar', 44 | 'bar': 'foo', 45 | 'multi': ['FOO','BAR'], 46 | 'blah': 599999 47 | } 48 | self.url = "%s%s" % (self.host, self.oauth_uris[utype]) 49 | 50 | def __call__(self, func): 51 | def run(): 52 | o = OAuthFilter('*', self.consumer) 53 | func(o, self.url, urllib.urlencode(self.body)) 54 | run.func_name = func.func_name 55 | return run 56 | 57 | @oauth_request('request_token') 58 | def test_001(o, u, b): 59 | r = request(u, filters=[o]) 60 | t.eq(r.status_int, 200) 61 | 62 | @oauth_request('request_token') 63 | def test_002(o, u, b): 64 | r = request(u, "POST", filters=[o]) 65 | t.eq(r.status_int, 200) 66 | f = dict(parse_qsl(r.body_string())) 67 | t.isin('oauth_token', f) 68 | t.isin('oauth_token_secret', f) 69 | 70 | 71 | @oauth_request('two_legged') 72 | def test_003(o, u, b): 73 | r = request(u, "POST", body=b, filters=[o], 74 | headers={"Content-type": "application/x-www-form-urlencoded"}) 75 | import sys 76 | print >>sys.stderr, r.body_string() 77 | t.eq(r.status_int, 200) 78 | # Because this is a POST and an application/x-www-form-urlencoded, the OAuth 79 | # can include the OAuth parameters directly into the body of the form, however 80 | # it MUST NOT include the 'oauth_body_hash' parameter in these circumstances. 81 | t.isnotin("oauth_body_hash", r.request.body) 82 | 83 | @oauth_request('two_legged') 84 | def test_004(o, u, b): 85 | r = request(u, "GET", filters=[o]) 86 | t.eq(r.status_int, 200) 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/010-test-proxies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This file is part of restkit released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import t 7 | from _server_test import HOST, PORT 8 | from restkit.contrib import wsgi_proxy 9 | 10 | root_uri = "http://%s:%s" % (HOST, PORT) 11 | 12 | def with_webob(func): 13 | def wrapper(*args, **kwargs): 14 | from webob import Request 15 | req = Request.blank('/') 16 | req.environ['SERVER_NAME'] = '%s:%s' % (HOST, PORT) 17 | return func(req) 18 | wrapper.func_name = func.func_name 19 | return wrapper 20 | 21 | @with_webob 22 | def test_001(req): 23 | req.path_info = '/query' 24 | proxy = wsgi_proxy.Proxy() 25 | resp = req.get_response(proxy) 26 | body = resp.body 27 | assert 'path: /query' in body, str(resp) 28 | 29 | @with_webob 30 | def test_002(req): 31 | req.path_info = '/json' 32 | req.environ['CONTENT_TYPE'] = 'application/json' 33 | req.method = 'POST' 34 | req.body = 'test post' 35 | proxy = wsgi_proxy.Proxy(allowed_methods=['POST']) 36 | resp = req.get_response(proxy) 37 | body = resp.body 38 | assert resp.content_length == 9, str(resp) 39 | 40 | proxy = wsgi_proxy.Proxy(allowed_methods=['GET']) 41 | resp = req.get_response(proxy) 42 | assert resp.status.startswith('403'), resp.status 43 | 44 | @with_webob 45 | def test_003(req): 46 | req.path_info = '/json' 47 | req.environ['CONTENT_TYPE'] = 'application/json' 48 | req.method = 'PUT' 49 | req.body = 'test post' 50 | proxy = wsgi_proxy.Proxy(allowed_methods=['PUT']) 51 | resp = req.get_response(proxy) 52 | body = resp.body 53 | assert resp.content_length == 9, str(resp) 54 | 55 | proxy = wsgi_proxy.Proxy(allowed_methods=['GET']) 56 | resp = req.get_response(proxy) 57 | assert resp.status.startswith('403'), resp.status 58 | 59 | @with_webob 60 | def test_004(req): 61 | req.path_info = '/ok' 62 | req.method = 'HEAD' 63 | proxy = wsgi_proxy.Proxy(allowed_methods=['HEAD']) 64 | resp = req.get_response(proxy) 65 | body = resp.body 66 | assert resp.content_type == 'text/plain', str(resp) 67 | 68 | @with_webob 69 | def test_005(req): 70 | req.path_info = '/delete' 71 | req.method = 'DELETE' 72 | proxy = wsgi_proxy.Proxy(allowed_methods=['DELETE']) 73 | resp = req.get_response(proxy) 74 | body = resp.body 75 | assert resp.content_type == 'text/plain', str(resp) 76 | 77 | proxy = wsgi_proxy.Proxy(allowed_methods=['GET']) 78 | resp = req.get_response(proxy) 79 | assert resp.status.startswith('403'), resp.status 80 | 81 | @with_webob 82 | def test_006(req): 83 | req.path_info = '/redirect' 84 | req.method = 'GET' 85 | proxy = wsgi_proxy.Proxy(allowed_methods=['GET']) 86 | resp = req.get_response(proxy) 87 | body = resp.body 88 | assert resp.location == '%s/complete_redirect' % root_uri, str(resp) 89 | 90 | @with_webob 91 | def test_007(req): 92 | req.path_info = '/redirect_to_url' 93 | req.method = 'GET' 94 | proxy = wsgi_proxy.Proxy(allowed_methods=['GET']) 95 | resp = req.get_response(proxy) 96 | body = resp.body 97 | 98 | print resp.location 99 | assert resp.location == '%s/complete_redirect' % root_uri, str(resp) 100 | 101 | @with_webob 102 | def test_008(req): 103 | req.path_info = '/redirect_to_url' 104 | req.script_name = '/name' 105 | req.method = 'GET' 106 | proxy = wsgi_proxy.Proxy(allowed_methods=['GET'], strip_script_name=True) 107 | resp = req.get_response(proxy) 108 | body = resp.body 109 | assert resp.location == '%s/name/complete_redirect' % root_uri, str(resp) 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /tests/1M: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/restkit/1af7d69838428acfb97271d5444992872b71e70c/tests/1M -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/t.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # Copyright 2009 Paul J. Davis 3 | # 4 | # This file is part of gunicorn released under the MIT license. 5 | # See the NOTICE for more information. 6 | 7 | from __future__ import with_statement 8 | 9 | import os 10 | from StringIO import StringIO 11 | import tempfile 12 | 13 | dirname = os.path.dirname(__file__) 14 | 15 | from restkit.client import Client 16 | from restkit.resource import Resource 17 | 18 | from _server_test import HOST, PORT, run_server_test 19 | run_server_test() 20 | 21 | def data_source(fname): 22 | buf = StringIO() 23 | with open(fname) as handle: 24 | for line in handle: 25 | line = line.rstrip("\n").replace("\\r\\n", "\r\n") 26 | buf.write(line) 27 | return buf 28 | 29 | 30 | class FakeSocket(object): 31 | 32 | def __init__(self, data): 33 | self.tmp = tempfile.TemporaryFile() 34 | if data: 35 | self.tmp.write(data.getvalue()) 36 | self.tmp.flush() 37 | self.tmp.seek(0) 38 | 39 | def fileno(self): 40 | return self.tmp.fileno() 41 | 42 | def len(self): 43 | return self.tmp.len 44 | 45 | def recv(self, length=None): 46 | return self.tmp.read() 47 | 48 | def recv_into(self, buf, length): 49 | tmp_buffer = self.tmp.read(length) 50 | v = len(tmp_buffer) 51 | for i, c in enumerate(tmp_buffer): 52 | buf[i] = c 53 | return v 54 | 55 | def send(self, data): 56 | self.tmp.write(data) 57 | self.tmp.flush() 58 | 59 | def seek(self, offset, whence=0): 60 | self.tmp.seek(offset, whence) 61 | 62 | class client_request(object): 63 | 64 | def __init__(self, path): 65 | if path.startswith("http://") or path.startswith("https://"): 66 | self.url = path 67 | else: 68 | self.url = 'http://%s:%s%s' % (HOST, PORT, path) 69 | 70 | def __call__(self, func): 71 | def run(): 72 | cli = Client(timeout=300) 73 | func(self.url, cli) 74 | run.func_name = func.func_name 75 | return run 76 | 77 | class resource_request(object): 78 | 79 | def __init__(self, url=None): 80 | if url is not None: 81 | self.url = url 82 | else: 83 | self.url = 'http://%s:%s' % (HOST, PORT) 84 | 85 | def __call__(self, func): 86 | def run(): 87 | res = Resource(self.url) 88 | func(res) 89 | run.func_name = func.func_name 90 | return run 91 | 92 | 93 | def eq(a, b): 94 | assert a == b, "%r != %r" % (a, b) 95 | 96 | def ne(a, b): 97 | assert a != b, "%r == %r" % (a, b) 98 | 99 | def lt(a, b): 100 | assert a < b, "%r >= %r" % (a, b) 101 | 102 | def gt(a, b): 103 | assert a > b, "%r <= %r" % (a, b) 104 | 105 | def isin(a, b): 106 | assert a in b, "%r is not in %r" % (a, b) 107 | 108 | def isnotin(a, b): 109 | assert a not in b, "%r is in %r" % (a, b) 110 | 111 | def has(a, b): 112 | assert hasattr(a, b), "%r has no attribute %r" % (a, b) 113 | 114 | def hasnot(a, b): 115 | assert not hasattr(a, b), "%r has an attribute %r" % (a, b) 116 | 117 | def raises(exctype, func, *args, **kwargs): 118 | try: 119 | func(*args, **kwargs) 120 | except exctype: 121 | pass 122 | else: 123 | func_name = getattr(func, "func_name", "") 124 | raise AssertionError("Function %s did not raise %s" % ( 125 | func_name, exctype.__name__)) 126 | 127 | -------------------------------------------------------------------------------- /tests/treq.py: -------------------------------------------------------------------------------- 1 | # Copyright 2009 Paul J. Davis 2 | # 3 | # This file is part of the pywebmachine package released 4 | # under the MIT license. 5 | 6 | from __future__ import with_statement 7 | 8 | import t 9 | 10 | import inspect 11 | import os 12 | import random 13 | from StringIO import StringIO 14 | import urlparse 15 | 16 | from restkit.datastructures import MultiDict 17 | from restkit.errors import ParseException 18 | from restkit.http import Request, Unreader 19 | 20 | class IterUnreader(Unreader): 21 | 22 | def __init__(self, iterable, **kwargs): 23 | self.buf = StringIO() 24 | self.iter = iter(iterable) 25 | 26 | 27 | def _data(self): 28 | if not self.iter: 29 | return "" 30 | try: 31 | return self.iter.next() 32 | except StopIteration: 33 | self.iter = None 34 | return "" 35 | 36 | 37 | dirname = os.path.dirname(__file__) 38 | random.seed() 39 | 40 | def uri(data): 41 | ret = {"raw": data} 42 | parts = urlparse.urlparse(data) 43 | ret["scheme"] = parts.scheme or None 44 | ret["host"] = parts.netloc.rsplit(":", 1)[0] or None 45 | ret["port"] = parts.port or 80 46 | if parts.path and parts.params: 47 | ret["path"] = ";".join([parts.path, parts.params]) 48 | elif parts.path: 49 | ret["path"] = parts.path 50 | elif parts.params: 51 | # Don't think this can happen 52 | ret["path"] = ";" + parts.path 53 | else: 54 | ret["path"] = None 55 | ret["query"] = parts.query or None 56 | ret["fragment"] = parts.fragment or None 57 | return ret 58 | 59 | 60 | def load_response_py(fname): 61 | config = globals().copy() 62 | config["uri"] = uri 63 | execfile(fname, config) 64 | return config["response"] 65 | 66 | class response(object): 67 | def __init__(self, fname, expect): 68 | self.fname = fname 69 | self.name = os.path.basename(fname) 70 | 71 | self.expect = expect 72 | if not isinstance(self.expect, list): 73 | self.expect = [self.expect] 74 | 75 | with open(self.fname) as handle: 76 | self.data = handle.read() 77 | self.data = self.data.replace("\n", "").replace("\\r\\n", "\r\n") 78 | self.data = self.data.replace("\\0", "\000") 79 | 80 | # Functions for sending data to the parser. 81 | # These functions mock out reading from a 82 | # socket or other data source that might 83 | # be used in real life. 84 | 85 | def send_all(self): 86 | yield self.data 87 | 88 | def send_lines(self): 89 | lines = self.data 90 | pos = lines.find("\r\n") 91 | while pos > 0: 92 | yield lines[:pos+2] 93 | lines = lines[pos+2:] 94 | pos = lines.find("\r\n") 95 | if len(lines): 96 | yield lines 97 | 98 | def send_bytes(self): 99 | for d in self.data: 100 | yield d 101 | 102 | def send_random(self): 103 | maxs = len(self.data) / 10 104 | read = 0 105 | while read < len(self.data): 106 | chunk = random.randint(1, maxs) 107 | yield self.data[read:read+chunk] 108 | read += chunk 109 | 110 | # These functions define the sizes that the 111 | # read functions will read with. 112 | 113 | def size_all(self): 114 | return -1 115 | 116 | def size_bytes(self): 117 | return 1 118 | 119 | def size_small_random(self): 120 | return random.randint(0, 4) 121 | 122 | def size_random(self): 123 | return random.randint(1, 4096) 124 | 125 | # Match a body against various ways of reading 126 | # a message. Pass in the request, expected body 127 | # and one of the size functions. 128 | 129 | def szread(self, func, sizes): 130 | sz = sizes() 131 | data = func(sz) 132 | if sz >= 0 and len(data) > sz: 133 | raise AssertionError("Read more than %d bytes: %s" % (sz, data)) 134 | return data 135 | 136 | def match_read(self, req, body, sizes): 137 | data = self.szread(req.body.read, sizes) 138 | count = 1000 139 | while len(body): 140 | if body[:len(data)] != data: 141 | raise AssertionError("Invalid body data read: %r != %r" % ( 142 | data, body[:len(data)])) 143 | body = body[len(data):] 144 | data = self.szread(req.body.read, sizes) 145 | if not data: 146 | count -= 1 147 | if count <= 0: 148 | raise AssertionError("Unexpected apparent EOF") 149 | 150 | if len(body): 151 | raise AssertionError("Failed to read entire body: %r" % body) 152 | elif len(data): 153 | raise AssertionError("Read beyond expected body: %r" % data) 154 | data = req.body.read(sizes()) 155 | if data: 156 | raise AssertionError("Read after body finished: %r" % data) 157 | 158 | def match_readline(self, req, body, sizes): 159 | data = self.szread(req.body.readline, sizes) 160 | count = 1000 161 | while len(body): 162 | if body[:len(data)] != data: 163 | raise AssertionError("Invalid data read: %r" % data) 164 | if '\n' in data[:-1]: 165 | raise AssertionError("Embedded new line: %r" % data) 166 | body = body[len(data):] 167 | data = self.szread(req.body.readline, sizes) 168 | if not data: 169 | count -= 1 170 | if count <= 0: 171 | raise AssertionError("Apparent unexpected EOF") 172 | if len(body): 173 | raise AssertionError("Failed to read entire body: %r" % body) 174 | elif len(data): 175 | raise AssertionError("Read beyond expected body: %r" % data) 176 | data = req.body.readline(sizes()) 177 | if data: 178 | raise AssertionError("Read data after body finished: %r" % data) 179 | 180 | def match_readlines(self, req, body, sizes): 181 | """\ 182 | This skips the sizes checks as we don't implement it. 183 | """ 184 | data = req.body.readlines() 185 | for line in data: 186 | if '\n' in line[:-1]: 187 | raise AssertionError("Embedded new line: %r" % line) 188 | if line != body[:len(line)]: 189 | raise AssertionError("Invalid body data read: %r != %r" % ( 190 | line, body[:len(line)])) 191 | body = body[len(line):] 192 | if len(body): 193 | raise AssertionError("Failed to read entire body: %r" % body) 194 | data = req.body.readlines(sizes()) 195 | if data: 196 | raise AssertionError("Read data after body finished: %r" % data) 197 | 198 | def match_iter(self, req, body, sizes): 199 | """\ 200 | This skips sizes because there's its not part of the iter api. 201 | """ 202 | for line in req.body: 203 | if '\n' in line[:-1]: 204 | raise AssertionError("Embedded new line: %r" % line) 205 | if line != body[:len(line)]: 206 | raise AssertionError("Invalid body data read: %r != %r" % ( 207 | line, body[:len(line)])) 208 | body = body[len(line):] 209 | if len(body): 210 | raise AssertionError("Failed to read entire body: %r" % body) 211 | try: 212 | data = iter(req.body).next() 213 | raise AssertionError("Read data after body finished: %r" % data) 214 | except StopIteration: 215 | pass 216 | 217 | # Construct a series of test cases from the permutations of 218 | # send, size, and match functions. 219 | 220 | def gen_cases(self): 221 | def get_funs(p): 222 | return [v for k, v in inspect.getmembers(self) if k.startswith(p)] 223 | senders = get_funs("send_") 224 | sizers = get_funs("size_") 225 | matchers = get_funs("match_") 226 | cfgs = [ 227 | (mt, sz, sn) 228 | for mt in matchers 229 | for sz in sizers 230 | for sn in senders 231 | ] 232 | 233 | ret = [] 234 | for (mt, sz, sn) in cfgs: 235 | mtn = mt.func_name[6:] 236 | szn = sz.func_name[5:] 237 | snn = sn.func_name[5:] 238 | def test_req(sn, sz, mt): 239 | self.check(sn, sz, mt) 240 | desc = "%s: MT: %s SZ: %s SN: %s" % (self.name, mtn, szn, snn) 241 | test_req.description = desc 242 | ret.append((test_req, sn, sz, mt)) 243 | return ret 244 | 245 | def check(self, sender, sizer, matcher): 246 | cases = self.expect[:] 247 | 248 | unreader = IterUnreader(sender()) 249 | resp = Request(unreader) 250 | self.same(resp, sizer, matcher, cases.pop(0)) 251 | t.eq(len(cases), 0) 252 | 253 | def same(self, resp, sizer, matcher, exp): 254 | t.eq(resp.status, exp["status"]) 255 | t.eq(resp.version, exp["version"]) 256 | t.eq(resp.headers, MultiDict(exp["headers"])) 257 | matcher(resp, exp["body"], sizer) 258 | t.eq(resp.trailers, exp.get("trailers", [])) 259 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py26, py27, pypy 8 | 9 | [testenv] 10 | commands = {envpython} setup.py test 11 | --------------------------------------------------------------------------------