project under MIT license
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or sell
20 | copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 |
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 |
36 | oauth2:
37 | ------
38 |
39 | oauth2 is under MIT license.
40 |
41 | Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
42 |
43 | Permission is hereby granted, free of charge, to any person obtaining a copy
44 | of this software and associated documentation files (the "Software"), to deal
45 | in the Software without restriction, including without limitation the rights
46 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
47 | copies of the Software, and to permit persons to whom the Software is
48 | furnished to do so, subject to the following conditions:
49 |
50 | The above copyright notice and this permission notice shall be included in
51 | all copies or substantial portions of the Software.
52 |
53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
54 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
55 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
56 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
57 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
59 | THE SOFTWARE.
60 |
--------------------------------------------------------------------------------
/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 | import sys
75 | print >>sys.stderr, r.body_string()
76 | t.eq(r.status_int, 200)
77 |
78 | @oauth_request('two_legged')
79 | def test_004(o, u, b):
80 | r = request(u, "GET", filters=[o])
81 | t.eq(r.status_int, 200)
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/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 %}
--------------------------------------------------------------------------------
/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/streaming.rst:
--------------------------------------------------------------------------------
1 | Stream you content
2 | ==================
3 |
4 | With Restkit you can easily stream your content to an 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 `payload`. 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', payload=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', payload=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', payload=myiterator,
28 | headers={'content-Length': 12})
29 |
30 | Stream from
31 | -----------
32 |
33 | Each requests return a :api:`restkit.client.HttpResponse` 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/_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 | }
--------------------------------------------------------------------------------
/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 select
8 | import socket
9 | import ssl
10 | import time
11 |
12 | from socketpool import Connector
13 |
14 | CHUNK_SIZE = 16 * 1024
15 | MAX_BODY = 1024 * 112
16 | DNS_TIMEOUT = 60
17 |
18 |
19 | class Connection(Connector):
20 |
21 | def __init__(self, host, port, pool=None, is_ssl=False,
22 | extra_headers=[], backend_mod=None, **ssl_args):
23 | self._s = backend_mod.Socket(socket.AF_INET, socket.SOCK_STREAM)
24 |
25 | self._s.connect((host, port))
26 |
27 | if is_ssl:
28 | self._s = ssl.wrap_socket(self._s, **ssl_args)
29 |
30 | self.pool = pool
31 | self.extra_headers = extra_headers
32 | self.is_ssl = is_ssl
33 | self.backend_mod = backend_mod
34 | self.host = host
35 | self.port = port
36 | self._connected = True
37 | self._life = time.time()
38 | self._released = False
39 |
40 | def matches(self, **match_options):
41 | target_host = match_options.get('host')
42 | target_port = match_options.get('port')
43 | return target_host == self.host and target_port == self.port
44 |
45 | def is_connected(self):
46 | if self._connected:
47 | try:
48 | r, _, _ = self.backend_mod.Select([self._s], [], [], 0.0)
49 | if not r:
50 | return True
51 | except (ValueError, select.error,):
52 | return False
53 | self.close()
54 | return False
55 |
56 | def handle_exception(self, exception):
57 | raise
58 |
59 | def get_lifetime(self):
60 | return self._life
61 |
62 | def invalidate(self):
63 | self.close()
64 | self._connected = False
65 | self._life = -1
66 |
67 | def release(self, should_close=False):
68 | if self._released:
69 | return
70 |
71 | self._released = True
72 | if should_close:
73 | self.close()
74 | else:
75 | self.pool.release_connection(self)
76 |
77 | def close(self):
78 | if not self._s or not hasattr(self._s, "close"):
79 | return
80 | try:
81 | self._s.close()
82 | except:
83 | pass
84 |
85 | def socket(self):
86 | return self._s
87 |
88 | def send_chunk(self, data):
89 | chunk = "".join(("%X\r\n" % len(data), data, "\r\n"))
90 | self._s.sendall(chunk)
91 |
92 | def send(self, data, chunked=False):
93 | if chunked:
94 | return self.send_chunk(data)
95 |
96 | return self._s.sendall(data)
97 |
98 | def sendlines(self, lines, chunked=False):
99 | for line in list(lines):
100 | self.send(line, chunked=chunked)
101 |
102 |
103 | # TODO: add support for sendfile api
104 | def sendfile(self, data, chunked=False):
105 | """ send a data from a FileObject """
106 |
107 | if hasattr(data, 'seek'):
108 | data.seek(0)
109 |
110 | while True:
111 | binarydata = data.read(CHUNK_SIZE)
112 | if binarydata == '':
113 | break
114 | self.send(binarydata, chunked=chunked)
115 |
116 |
117 | def recv(self, size=1024):
118 | return self._s.recv(size)
119 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 _build/html/api
40 | $(EPYDOC) -o _build/html/api ../restkit
41 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
42 | ${SITEMAPGEN} --config=sitemap_config.xml
43 |
44 | @echo
45 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
46 |
47 | github:
48 | @echo "Send to github"
49 | $(GHPIMPORT) -p $(BUILDDIR)/html
50 |
51 | dirhtml:
52 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
53 | @echo
54 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
55 |
56 | pickle:
57 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
58 | @echo
59 | @echo "Build finished; now you can process the pickle files."
60 |
61 | json:
62 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
63 | @echo
64 | @echo "Build finished; now you can process the JSON files."
65 |
66 | htmlhelp:
67 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
68 | @echo
69 | @echo "Build finished; now you can run HTML Help Workshop with the" \
70 | ".hhp project file in $(BUILDDIR)/htmlhelp."
71 |
72 | 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.qhc"
80 |
81 | latex:
82 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
83 | @echo
84 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
85 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
86 | "run these through (pdf)latex."
87 |
88 | changes:
89 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
90 | @echo
91 | @echo "The overview file is in $(BUILDDIR)/changes."
92 |
93 | linkcheck:
94 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
95 | @echo
96 | @echo "Link check complete; look for any errors in the above output " \
97 | "or in $(BUILDDIR)/linkcheck/output.txt."
98 |
99 | doctest:
100 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
101 | @echo "Testing of doctests in the sources finished, look at the " \
102 | "results in $(BUILDDIR)/doctest/output.txt."
103 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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,
51 | method='GET',
52 | body=None,
53 | headers=None,
54 | **kwargs):
55 | """ Quick shortcut method to pass a request
56 |
57 | :param url: str, url string
58 | :param method: str, by default GET. http verbs
59 | :param body: the body, could be a string, an iterator or a file-like object
60 | :param headers: dict or list of tupple, http headers
61 |
62 | Client parameters
63 | ~~~~~~~~~~~~~~~~~
64 |
65 | :param follow_redirect: follow redirection, by default False
66 | :param max_ollow_redirect: number of redirections available
67 | :filters: http filters to pass
68 | :param decompress: allows the client to decompress the response
69 | body
70 | :param max_status_line_garbage: defines the maximum number of ignorable
71 | lines before we expect a HTTP response's status line. With
72 | HTTP/1.1 persistent connections, the problem arises that broken
73 | scripts could return a wrong Content-Length (there are more
74 | bytes sent than specified). Unfortunately, in some cases, this
75 | cannot be detected after the bad response, but only before the
76 | next one. So the client is abble to skip bad lines using this
77 | limit. 0 disable garbage collection, None means unlimited number
78 | of tries.
79 | :param max_header_count: determines the maximum HTTP header count
80 | allowed. by default no limit.
81 | :param manager: the manager to use. By default we use the global
82 | one.
83 | :parama response_class: the response class to use
84 | :param timeout: the default timeout of the connection
85 | (SO_TIMEOUT)
86 |
87 | :param max_tries: the number of tries before we give up a
88 | connection
89 | :param wait_tries: number of time we wait between each tries.
90 | :param ssl_args: ssl named arguments,
91 | See http://docs.python.org/library/ssl.html informations
92 | """
93 |
94 | # detect credentials from url
95 | u = urlparse.urlparse(url)
96 | if u.username is not None:
97 | password = u.password or ""
98 | filters = kwargs.get('filters') or []
99 | url = urlparse.urlunparse((u.scheme, u.netloc.split("@")[-1],
100 | u.path, u.params, u.query, u.fragment))
101 | filters.append(BasicAuth(u.username, password))
102 |
103 | kwargs['filters'] = filters
104 |
105 | http_client = Client(**kwargs)
106 | return http_client.request(url, method=method, body=body,
107 | headers=headers)
108 |
--------------------------------------------------------------------------------
/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 |
97 | oauth_req.sign_request(self.method, self.consumer, self.token)
98 |
99 | if form:
100 | request.body = oauth_req.to_postdata()
101 |
102 | request.headers['Content-Length'] = len(request.body)
103 | elif request.method in ('GET', 'HEAD'):
104 | request.original_url = request.url
105 | request.url = oauth_req.to_url()
106 | else:
107 | oauth_headers = oauth_req.to_header(realm=self.realm)
108 | request.headers.update(oauth_headers)
109 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.http_code, 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/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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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/
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | About
2 | -----
3 |
4 | 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.
5 |
6 | Restkit is a full HTTP client using pure socket calls and its own HTTP parser. It's not based on httplib or urllib2.
7 |
8 | Installation
9 | ------------
10 |
11 | Restkit requires Python 2.x superior to 2.5 and http-parser 0.5.3 or
12 | sup.
13 |
14 | To install restkit using pip you must make sure you have a
15 | recent version of distribute installed::
16 |
17 | $ curl -O http://python-distribute.org/distribute_setup.py
18 | $ sudo python distribute_setup.py
19 | $ easy_install pip
20 |
21 | To install or upgrade to the latest released version of restkit::
22 |
23 | $ pip install http-parser
24 | $ pip install restkit
25 |
26 |
27 | Note: if you get an error on MacOSX try to install with the following
28 | arguments::
29 |
30 | $ env ARCHFLAGS="-arch i386 -arch x86_64" pip install http-parser
31 |
32 | Usage
33 | =====
34 |
35 | Perform HTTP call support with `restkit.request`.
36 | ++++++++++++++++++++++++++++++++++++++++++++++++++
37 |
38 | Usage example, get friendpaste page::
39 |
40 | from restkit import request
41 | resp = request('http://friendpaste.com')
42 | print resp.body_string()
43 | print resp.status_int
44 |
45 |
46 | Create a simple Twitter Search resource
47 | +++++++++++++++++++++++++++++++++++++++
48 |
49 | Building a resource object is easy using `restkit.Resource` class.
50 | We use `simplejson `_ to
51 | handle deserialisation of data.
52 |
53 | Here is the snippet::
54 |
55 | from restkit import Resource
56 |
57 | try:
58 | import simplejson as json
59 | except ImportError:
60 | import json # py2.6 only
61 |
62 | class TwitterSearch(Resource):
63 |
64 | def __init__(self, pool_instance=None, **kwargs):
65 | search_url = "http://search.twitter.com"
66 | super(TwitterSearch, self).__init__(search_url, follow_redirect=True,
67 | max_follow_redirect=10,
68 | pool_instance=pool_instance,
69 | **kwargs)
70 |
71 | def search(self, query):
72 | return self.get('search.json', q=query)
73 |
74 | def request(self, *args, **kwargs):
75 | resp = super(TwitterSearch, self).request(*args, **kwargs)
76 | return json.loads(resp.body_string())
77 |
78 | if __name__ == "__main__":
79 | s = TwitterSearch()
80 | print s.search("gunicorn")
81 |
82 | Reuses connections
83 | ------------------
84 |
85 | Reusing connections is good. Restkit can maintain for you the http connections and
86 | reuse them if the server allows it. To do that you can pass to any object a pool
87 | instance inheriting `reskit.pool.PoolInterface`. By default a threadsafe pool is
88 | used in any application:
89 |
90 | ::
91 |
92 | from restkit import *
93 | from socketpool import ConnectionPool
94 |
95 | # set a pool
96 | pool = ConnectionPool(factory=Connection, max_size=10)
97 |
98 | # do the connection
99 | res = Resource('http://friendpaste.com', pool=pool)
100 |
101 |
102 | or if you use `Gevent `_:
103 |
104 | ::
105 |
106 | from restkit import *
107 | from socketpool import ConnectionPool
108 |
109 | # set a pool
110 | pool = ConnectionPool(factory=Connection, backend="gevent",
111 | max_size=10)
112 |
113 | # do the connection
114 | res = Resource('http://friendpaste.com', pool=pool)
115 |
116 |
117 | Authentication
118 | ==============
119 |
120 | Restkit support for now `basic authentication`_ and `OAuth`_. But any
121 | other authentication schema can easily be added using http filters.
122 |
123 | Basic authentication
124 | ++++++++++++++++++++
125 |
126 |
127 | To use `basic authentication` in a `Resource object` you can do::
128 |
129 | from restkit import Resource, BasicAuth
130 |
131 | auth = BasicAuth("username", "password")
132 | r = Resource("http://friendpaste.com", filters=[auth])
133 |
134 | Or simply use an authentication url::
135 |
136 | r = Resource("http://username:password@friendpaste.com")
137 |
138 | .. _basic authentification: http://www.ietf.org/rfc/rfc2617.txt
139 | .. _OAuth: http://oauth.net/
140 |
141 | OAuth
142 | +++++
143 |
144 | Restkit OAuth is based on `simplegeo python-oauth2 module `_ So you don't need other installation to use OAuth (you can also simply use `restkit.oauth2` module in your applications).
145 |
146 | The OAuth filter `restkit.oauth2.filter.OAuthFilter` allow you to associate a consumer per resource (path). Initalize Oauth filter with::
147 |
148 | path, consumer, token, signaturemethod
149 |
150 | `token` and `method signature` are optionnals. Consumer should be an instance of `restkit.oauth2.Consumer`, token an instance of `restkit.oauth2.Token` signature method an instance of `oauth2.SignatureMethod` (`restkit.oauth2.Token` is only needed for three-legged requests.
151 |
152 | 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.
153 |
154 | 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`.
155 |
156 | Simple client example:
157 | ~~~~~~~~~~~~~~~~~~~~~~
158 |
159 | ::
160 |
161 | from restkit import OAuthFilter, request
162 | import restkit.oauth2 as oauth
163 |
164 | # Create your consumer with the proper key/secret.
165 | consumer = oauth.Consumer(key="your-twitter-consumer-key",
166 | secret="your-twitter-consumer-secret")
167 |
168 | # Request token URL for Twitter.
169 | request_token_url = "http://twitter.com/oauth/request_token"
170 |
171 | # Create our filter.
172 | auth = oauth.OAuthFilter('*', consumer)
173 |
174 | # The request.
175 | resp = request(request_token_url, filters=[auth])
176 | print resp.body_string()
177 |
178 |
179 | If you want to add OAuth to your `TwitterSearch` resource::
180 |
181 | # Create your consumer with the proper key/secret.
182 | consumer = oauth.Consumer(key="your-twitter-consumer-key",
183 | secret="your-twitter-consumer-secret")
184 |
185 | # Create our filter.
186 | client = oauth.OAuthfilter('*', consumer)
187 |
188 | s = TwitterSearch(filters=[client])
189 |
190 |
--------------------------------------------------------------------------------
/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/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/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/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 | :param uri: str, full uri to the server.
40 | :param 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 | :param path: string additionnal path to the uri
109 | :param headers: dict, optionnal headers that will
110 | be added to HTTP request.
111 | :param 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 | :param payload: string passed to the body of the request
137 | :param path: string additionnal path to the uri
138 | :param headers: dict, optionnal headers that will
139 | be added to HTTP request.
140 | :param 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 | :param payload: string or File object passed to the body of the request
172 | :param path: string additionnal path to the uri
173 | :param 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 | :param 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 |
--------------------------------------------------------------------------------
/doc/news.rst:
--------------------------------------------------------------------------------
1 | .. _news:
2 |
3 | News
4 | ====
5 |
6 | 4.1.0 / 2012-01-31
7 | ------------------
8 |
9 | - fix connection reusing. When connection is closed or an EPIPE/EAGAIN
10 | error happen, we now retry it.
11 | - fix wgsi_proxy contrib
12 | - fix examples
13 |
14 | 4.0.0 / 2012-01-25
15 | ------------------
16 |
17 | - Replace the socket pool by `socketpool
18 | `_ improve connection handling
19 | and better management of gevent & eventlet.
20 | - Fix NoMoreData issue
21 | - Fix SSL connections
22 | - multipart forms: quote is now configurable & flush size cache
23 |
24 |
25 | 3.3.1 / 2011-09-18
26 | ------------------
27 |
28 | - Add `hook `_ for BoundaryItem subclasses to handle unreadable values
29 | - Add realm support to restkit
30 | - Fix restcli --shell, upgrade it for IPython 0.11
31 | - Stop catching KeyboardInterrupt and SystemExit exceptions
32 | - Make sure we don't release the socket twice
33 |
34 | 3.3.0 / 2011-06-20
35 | ------------------
36 |
37 | - New HTTP parser, using python `http-parser `_
38 | in C based on http-parser from Ryan Dahl.
39 | - Fix UnboundLocalError
40 | - Sync oauth with last python-oauth2 (fix POST & encoding issues)
41 | - Improve sending
42 |
43 | Breaking changes:
44 | +++++++++++++++++
45 |
46 | - Headers is an IOrderdDict object now, wich means by default you can
47 | get any headers case insensitively using get or headers[key], as a
48 | result the method **iget** has been removed.
49 |
50 | 3.2.1 / 2011-03-22
51 | ------------------
52 |
53 | - Fix sending on linux.
54 |
55 | 3.2 / 2011-02-18
56 | ----------------
57 |
58 | - Some deep rewrite of the client. Requests and Connections are now
59 | maintened in their own instances, so we don't rely on client instance
60 | to close or release the connection Also we don't pass local variable
61 | to handle a request. At the end each requests are more isolated and we are
62 | fully threadsafe.
63 | - Improve error report.
64 | - Handle case where the connection is closed but the OS still accept
65 | sending. From the man: "When the message does not fit into the send
66 | buffer of the socket, send() normally blocks, unless th socket has
67 | been placed in nonblocking I/O mode.""" . Spotted by loftus on irc.
68 | Thanks.
69 |
70 | Breaking changes:
71 | +++++++++++++++++
72 |
73 | - Rewrite filters handling. We now pass a request instance to the
74 | on_request filters. If a request filter return a response, we stop to
75 | perform here. response filters accept now the response and request
76 | instances as arguments. There is no more on_connect filters (it was a
77 | bad idea)
78 | - Proxy support. Proxies are now supported by passing the argument
79 | "use_proxy=True" to client, request and resources objects.
80 |
81 | 3.0 / 2011-02-02
82 | ----------------
83 |
84 | - New Connection management: Better concurrency handling and iddle
85 | connections are now closed after a time.
86 | - Improved Client.
87 | - Fix redirect
88 | - Better error handling
89 | - Timeout can now be set on each request.
90 | - Major refactoring. consolidation of some module, ease the HTTP parser
91 | code.
92 | - Fix timeout errors.
93 |
94 | 2.3.0 / 2010-11-25
95 | ------------------
96 | - Refactored Http Connections management (reuse connections).
97 | restkit.pool is now replaced by restkit.conn module. SimplePool has
98 | been replaced by TConnectionManager (threadsafe). Now by default all
99 | connections are reusing connections using TConnectionManager (10
100 | connections per route).
101 | - Improved Gevent & Eventlet support
102 | - Added an ``decompress`` option to ``request`` function and ``Resource``
103 | instance to decompress the body or not. By default it's true.
104 | - Added ``params_dict`` to keywords arguments of ``Resource`` instances
105 | methods. Allows you to pass any argument to the query.
106 | - Fix response 100-continue
107 | - Fix compressed atatchments
108 | - Fix body readline
109 | - Fix basic authentication
110 | - Stop when system exit or keyboard interrupt
111 | - Fix oauth2
112 |
113 | More details `here `_ .
114 |
115 | 2.2.1 / 2010-09-18
116 | ------------------
117 | - Fix readline `b7365155 `_ .
118 |
119 | 2.2.0 / 2010-09-14
120 | ------------------
121 | - Refactor client code. Improve header parsing
122 | - Fix Deflate/Gzip decompression and make it fully
123 | streamed.
124 | - Fix oauth2 in POST requests
125 | - Fix import with Python 2.5/2.4
126 | - Fix Exceptions
127 | - body, unicod_body and body_file methods have been removed from the
128 | HTTP response.
129 |
130 | 2.1.6 / 2010-09-
131 | -----------------
132 | - Fix debian packaging
133 | - Fix oauth
134 |
135 | 2.1.4 / 2008-08-11
136 | ------------------
137 |
138 | - Improve HTTP parsing (backport from Gunicorn)
139 | - Handle KeyboardInterrupt and SystemExit exceptions in client.
140 |
141 | 2.1.3 / 2008-08-11
142 | ------------------
143 |
144 | - Repackaged due to a spurious print.
145 |
146 | 2.1.2 / 2008-08-11
147 | ------------------
148 |
149 | - `Fix` a nasty bug in BasicAuth
150 |
151 | 2.1.1/ 2010-08-05
152 | -----------------
153 |
154 | - Fix clone and __call__, make sure we use original client_opts rather
155 | than an instance
156 |
157 | 2.1.0 / 2010-07-24
158 | ------------------
159 |
160 | - Added make_params, make_headers method to the Resource allowing you to modify headers and params
161 | - Added unauthorized method to Resource allowing you to react on 401/403, return True
162 | by default
163 | - make sure default pool is only set one time in the main thread in
164 | Resource object
165 | - Added Resouce.close() method: close the pool connections
166 | - Added Pool.close() method: clear the pool and stop monitoring
167 | - Updated Oauth2 module
168 | - Handle ECONNRESET error in HTTP client
169 | - Fix keep-alive handling
170 | - Fix Content-Type headerfor GET
171 | - Fix "Accept-Encoding" header
172 | - Fix HttpResponse.close() method
173 | - Make sure we use ssl when https scheme is used
174 | - Fix "__call__" and clone() methods from restkit.Resource object.
175 |
176 | 2.0 / 2010-06-28
177 | ----------------
178 |
179 | - Complete refactoring of pool. Now handle more concurrent connections (priority to read)
180 |
181 | - Added full ssl support in restkit. It needs `ssl `_ module on Python 2.5x
182 | - New HTTP parser.
183 | - Added close method to response object to make sure the socket is correctly released.
184 | - Improved default http client, so form objects can be directly handled.
185 | - Improved request function
186 |
187 |
188 | Breaking changes:
189 | +++++++++++++++++
190 |
191 | - **Default HttpResponse isn't any more persistent**. You have to save it to reuse it. A persistent response will be provided in restkit 2.1 .
192 | - Deprecate HttpResponse body, unicode_body and body_file properties. They are replaced by body_string and body_stream methods.
193 | - Resource arguments
194 | - Complete refactoring of filters. Now they have to be declared when you create a resource or http client. An on_connect method can be used in filter now. This method is used before the connection happen, it's useful for proxy support for example.
195 | - Oauth2 filter has been simplfied, see `example `_
196 |
197 | 1.3.1 / 2010-04-09
198 | ------------------
199 |
200 | - Fixed Python 2.5 compatibility for ssl connections
201 |
202 | 1.3 / 2010-04-02
203 | ----------------
204 |
205 | - Added IPython shell extension (`restkit --shell`)
206 | - fix Python 2.5 compatibility
207 | - fix Eventlet and Gevent spools extensions
208 | - By default accept all methods in proxy
209 |
210 | 1.2.1 / 2010-03-08
211 | ------------------
212 |
213 | - Improve console client
214 |
215 | 1.2 / 2010-03-06
216 | ------------------------
217 |
218 | - Added `GEvent `_ Support
219 | - Added `wsgi_proxy `_ using webob and restkit
220 | - Improved pool management
221 | - Make HTTP parsing faster.
222 | - Fix TeeInput
223 |
224 |
225 | 1.1.3 / 2010-03-04
226 | ------------------
227 |
228 | - Fix ssl connections
229 |
230 | 1.1.2 / 2010-03-02
231 | ------------------
232 |
233 | - More logging information
234 | - Fix retry loop so an error is raised instead of returning None.
235 |
236 | 1.1 / 2010-03-01
237 | ----------------
238 |
239 | - Improved HTTP Parser - Now buffered.
240 | - Logging facility
241 |
242 | 1.0 / 2010-02-28
243 | ----------------
244 |
245 | - New HTTP Parser and major refactoring
246 | - Added OAuth support
247 | - Added HTTP Filter
248 | - Added support of chunked encoding
249 | - Removed `rest.RestClient`
250 | - Add Connection pool working with Eventlet 0.9.6
251 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------