├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── restfulie ├── __init__.py ├── restfulie.py ├── parser.py ├── resources │ ├── __init__.py │ ├── json.py │ └── xml.py ├── opensearch.py ├── links.py ├── request.py ├── response.py ├── dsl.py ├── processor.py └── converters.py ├── test ├── restfulie_test.py ├── parser_test.py ├── response_test.py ├── httpserver.py ├── dsl_test.py ├── lazy_response_test.py ├── json_resource_test.py ├── xml_resource_test.py ├── request_test.py ├── integration_test.py ├── converters_test.py └── processor_test.py ├── Makefile.example ├── LICENSE ├── maze └── solver.py ├── setup.py ├── spec └── client_spec.py └── README.textile /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include restfulie *.py 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | include=should 3 | verbosity=2 4 | nocapture=1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .project 3 | .pydevproject 4 | env/ 5 | restfulie.egg-info/ 6 | dist/ 7 | Makefile 8 | -------------------------------------------------------------------------------- /restfulie/__init__.py: -------------------------------------------------------------------------------- 1 | from restfulie import Restfulie 2 | from pkg_resources import declare_namespace 3 | 4 | declare_namespace(__name__) 5 | -------------------------------------------------------------------------------- /test/restfulie_test.py: -------------------------------------------------------------------------------- 1 | from restfulie import Restfulie 2 | from restfulie.dsl import Dsl 3 | 4 | 5 | class restfulie_test: 6 | 7 | def should_return_a_dsl_object(self): 8 | assert type(Restfulie.at("www.caelum.com.br")) == Dsl 9 | -------------------------------------------------------------------------------- /restfulie/restfulie.py: -------------------------------------------------------------------------------- 1 | from dsl import Dsl 2 | 3 | 4 | class Restfulie(object): 5 | """ 6 | Restfulie DSL entry point. 7 | """ 8 | 9 | @staticmethod 10 | def at(uri): 11 | """ 12 | Create a new entry point for executing requests to the given uri. 13 | """ 14 | return Dsl(uri) 15 | -------------------------------------------------------------------------------- /restfulie/parser.py: -------------------------------------------------------------------------------- 1 | class Parser(object): 2 | """ 3 | Executes processors ordered by the list 4 | """ 5 | 6 | def __init__(self, processors): 7 | self.processors = processors 8 | 9 | def follow(self, request, env={}): 10 | processor = self.processors.pop(0) 11 | result = processor.execute(self, request, env) 12 | return result 13 | -------------------------------------------------------------------------------- /restfulie/resources/__init__.py: -------------------------------------------------------------------------------- 1 | class Resource: 2 | 3 | def links(self): 4 | """ 5 | Returns a list of all links. 6 | """ 7 | raise NotImplementedError('Subclasses must implement this method') 8 | 9 | def link(self, rel): 10 | """ 11 | Return a Link with rel. 12 | """ 13 | raise NotImplementedError('Subclasses must implement this method') 14 | -------------------------------------------------------------------------------- /test/parser_test.py: -------------------------------------------------------------------------------- 1 | from restfulie.parser import Parser 2 | from mockito import mock, when 3 | 4 | 5 | class parser_test: 6 | 7 | def should_execute_processor(self): 8 | 9 | request = mock() 10 | processor = mock() 11 | resource = mock() 12 | 13 | parser = Parser([processor]) 14 | when(processor).execute(parser, request, {}).thenReturn(resource) 15 | 16 | assert parser.follow(request, {}) == resource 17 | -------------------------------------------------------------------------------- /Makefile.example: -------------------------------------------------------------------------------- 1 | # RESTFULIE-PY 2 | 3 | # SETUP 4 | 5 | PYTHON="python" 6 | EASYINSTALL="easy_install" 7 | NOSETESTS="nosetests" 8 | 9 | # TARGETS 10 | 11 | all: test install 12 | 13 | dev: devinstall test 14 | 15 | console: 16 | ${PYTHON} 17 | 18 | deps: 19 | ${EASYINSTALL} httplib2 20 | ${EASYINSTALL} nose 21 | ${EASYINSTALL} mockito 22 | 23 | devinstall: deps 24 | ${PYTHON} setup.py develop 25 | 26 | install: 27 | ${PYTHON} setup.py install 28 | 29 | test: 30 | ${PYTHON} test/httpserver.py > /dev/null 2> /dev/null & 31 | ${NOSETESTS} test 32 | curl http://localhost:20144/stop > /dev/null 2> /dev/null 33 | 34 | .PHONY: deps devinstall install test 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2009 Caelum - www.caelum.com.br/opensource 3 | * All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | -------------------------------------------------------------------------------- /maze/solver.py: -------------------------------------------------------------------------------- 1 | from restfulie import Restfulie 2 | 3 | found = False 4 | visited = {} 5 | steps = 0 6 | 7 | def solve(current): 8 | global found 9 | global visited 10 | if not found and not current.link('exit'): 11 | directions = ["start", "east", "west", "south", "north"] 12 | for direction in directions: 13 | link = current.link(direction) 14 | if not found and link and not visited.get(link.href): 15 | visited[link.href] = True 16 | solve(link.follow().get()) 17 | 18 | else: 19 | print "FOUND!" 20 | found = True 21 | 22 | current = Restfulie.at('http://amundsen.com/examples/mazes/2d/five-by-five/').accepts("application/xml").get() 23 | 24 | solve(current) 25 | 26 | -------------------------------------------------------------------------------- /test/response_test.py: -------------------------------------------------------------------------------- 1 | from restfulie.response import Response 2 | 3 | 4 | class response_test: 5 | 6 | def trivial_test(self): 7 | 8 | response = ({'status': 200}, 'Hello') 9 | 10 | r = Response(response) 11 | assert r.body == 'Hello' 12 | assert r.code == 200 13 | 14 | 15 | def resource_test(self): 16 | 17 | response = ({'status': 200, 'content-type': \ 18 | 'text/plain; charset=utf-8'}, 'Hello') 19 | 20 | r = Response(response) 21 | assert r.resource() == 'Hello' 22 | 23 | 24 | def link_test(self): 25 | 26 | response = ({'status': 200, 'link': '; rel="alternate"'}, 'Hello') 27 | r = Response(response) 28 | link = r.link('alternate') 29 | assert link.href == '/feed' 30 | assert link.rel == 'alternate' 31 | -------------------------------------------------------------------------------- /restfulie/opensearch.py: -------------------------------------------------------------------------------- 1 | import dsl 2 | 3 | 4 | class OpenSearchDescription(object): 5 | """ 6 | OpenSearchDescription object wraps the OpenSearch logic 7 | """ 8 | 9 | def __init__(self, element_tree): 10 | self.url_type = 'application/rss+xml' 11 | self.element_tree = element_tree 12 | 13 | def use(self, url_type): 14 | """ 15 | Set the OpenSearch type 16 | """ 17 | self.url_type = url_type 18 | return self 19 | 20 | def search(self, searchTerms, startPage): 21 | """ 22 | Make a search with 'searchTerms' 23 | It will find the url_type URL that you have chosen 24 | """ 25 | tag = '{http://a9.com/-/spec/opensearch/1.1/}Url' 26 | urls = self.element_tree.findall(tag) 27 | for url in urls: 28 | if url.get('type') == self.url_type: 29 | template = url.get('template') 30 | template = template.replace('{searchTerms}', searchTerms) 31 | template = template.replace('{startPage?}', str(startPage)) 32 | return dsl.Dsl(template).accepts(self.url_type).get() 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup(name='restfulie', 6 | version='0.9.2', 7 | description='Writing hypermedia aware resource based clients and servers', 8 | author=' ', 9 | author_email=' ', 10 | url='http://restfulie.caelumobjects.com/', 11 | long_description='CRUD through HTTP is a good step forward to using resources and becoming RESTful, another step further is to make use of hypermedia aware resources and Restfulie allows you to do it in Python.', 12 | download_url='https://github.com/caelum/restfulie-py', 13 | keywords='restfulie rest http hypermedia', 14 | classifiers=[ 15 | "Development Status :: 4 - Beta", 16 | "Environment :: Web Environment", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: Apache Software License", 19 | "Operating System :: MacOS :: MacOS X", 20 | "Operating System :: Microsoft :: Windows", 21 | "Operating System :: POSIX", 22 | "Programming Language :: Python", 23 | ], 24 | test_suite = "nose.collector", 25 | install_requires= ['httplib2>=0.6.0'], 26 | packages=find_packages(), 27 | include_package_data=True, 28 | ) 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/httpserver.py: -------------------------------------------------------------------------------- 1 | from base64 import encodestring 2 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler 3 | 4 | 5 | class RequestHandler(BaseHTTPRequestHandler): 6 | 7 | def do_GET(self, *args, **kwargs): 8 | if (self.path == "/stop"): 9 | self.send_response(200) 10 | httpd.socket.close() 11 | else: 12 | if not self.path == "/auth": 13 | self.wfile.write("Response for %s %s" % \ 14 | (self.path, str(self.headers))) 15 | else: 16 | auth = self.headers.getheader('authorization') 17 | if auth == "Basic %s" % encodestring('test:test')[:-1]: 18 | self.wfile.write('worked') 19 | 20 | def do_POST(self, *args, **kwargs): 21 | content_size = int(self.headers.getheader('content-length')) 22 | body = self.rfile.read(content_size) 23 | self.wfile.write('This is a test.') 24 | if '' in body: 25 | if body.split('') == 'test': 26 | self.wfile.write('This is a test.') 27 | else: 28 | self.wfile.write('Fail.') 29 | 30 | server_address = ('', 20144) 31 | httpd = HTTPServer(server_address, RequestHandler) 32 | try: 33 | httpd.serve_forever() 34 | except: 35 | pass 36 | -------------------------------------------------------------------------------- /restfulie/links.py: -------------------------------------------------------------------------------- 1 | import dsl 2 | 3 | 4 | class Link(object): 5 | """ 6 | Link represents generic link. You can follow it. 7 | """ 8 | 9 | def __init__(self, href, rel, content_type='application/xml'): 10 | self.href = href 11 | self.rel = rel 12 | self.content_type = content_type 13 | 14 | def follow(self): 15 | """ 16 | Return a DSL object with the Content-Type 17 | set. 18 | """ 19 | return dsl.Dsl(self.href).as_(self.content_type) 20 | 21 | 22 | class Links: 23 | """ 24 | Links a simple Link collection. There are some 25 | methods to put syntax sugar 26 | """ 27 | 28 | def __init__(self, links): 29 | """ 30 | Enable Links to access attributes with 'dot' 31 | """ 32 | self.links = {} 33 | for link in links: 34 | self.links[link.rel] = link 35 | setattr(self, link.rel, link) 36 | 37 | def get(self, rel): 38 | """ 39 | Checks if a Link exists. If exists returns the 40 | object, else returns None 41 | """ 42 | return self.links.get(rel) 43 | 44 | def __len__(self): 45 | """ 46 | The length of Links is the length of the links 47 | dictionary inside it 48 | """ 49 | return len(self.links) 50 | -------------------------------------------------------------------------------- /test/dsl_test.py: -------------------------------------------------------------------------------- 1 | from restfulie.dsl import Dsl 2 | from restfulie.processor import ExecuteRequestProcessor 3 | 4 | 5 | class dsl_test: 6 | 7 | def setup(self): 8 | self.dsl = Dsl("www.caelum.com.br") 9 | 10 | def should_add_a_processor(self): 11 | processor = ExecuteRequestProcessor() 12 | assert self.dsl.use(processor) == self.dsl 13 | assert self.dsl.processors[0] == processor 14 | 15 | def should_configure_the_content_type(self): 16 | self.dsl.as_("content") 17 | assert self.dsl.headers["Content-Type"] == "content" 18 | 19 | def should_configure_valid_http_methods(self): 20 | for verb in Dsl.HTTP_VERBS: 21 | method = self.dsl.__getattr__(verb) 22 | assert method.config == self.dsl 23 | assert self.dsl.verb == verb.upper() 24 | 25 | def should_configure_simple_auth_credentials(self): 26 | dsl = Dsl('http://caelum.com.br').auth('user', 'pass', 'simple') 27 | assert dsl.credentials == ('user', 'pass', 'simple') 28 | assert dsl.uri == "http://caelum.com.br" 29 | 30 | def should_configure_simple_auth_if_no_auth_method_is_specified(self): 31 | dsl = Dsl('http://caelum.com.br').auth('user', 'pass') 32 | assert dsl.credentials == ('user', 'pass', 'simple') 33 | assert dsl.uri == "http://caelum.com.br" 34 | 35 | def should_fail_when_asked_to_use_an_invalid_http_method(self): 36 | try: 37 | self.dsl.poop 38 | raise AssertionError("should have failed") 39 | except AttributeError: 40 | pass 41 | 42 | def should_configure_the_callback_method(self): 43 | def callback(*args): 44 | pass 45 | 46 | self.dsl.async(callback) 47 | assert self.dsl.callback == callback 48 | -------------------------------------------------------------------------------- /restfulie/request.py: -------------------------------------------------------------------------------- 1 | from response import LazyResponse 2 | from parser import Parser 3 | from threading import Thread 4 | from multiprocessing import Pipe 5 | 6 | 7 | class Request(object): 8 | """ 9 | HTTP request. 10 | """ 11 | 12 | def __init__(self, config): 13 | """ 14 | Initialize an HTTP request instance for a given configuration. 15 | """ 16 | self.config = config 17 | 18 | def __call__(self, **kwargs): 19 | """ 20 | Perform the request 21 | 22 | The optional payload argument is sent to the server. 23 | """ 24 | if (not self.config.is_async): 25 | return self._process_flow(kwargs) 26 | else: 27 | return self._process_async_flow(kwargs) 28 | 29 | def _process_flow(self, payload): 30 | """ 31 | Put payload environment and start the chain. 32 | """ 33 | env = {} 34 | if payload: 35 | env = {'payload': payload} 36 | 37 | procs = list(self.config.processors) 38 | return Parser(procs).follow(self.config, env) 39 | 40 | def _process_async_flow(self, payload): 41 | """ 42 | Starts an async chain. 43 | """ 44 | 45 | self.config.pipe, child_pipe = Pipe() 46 | 47 | def handle_async(): 48 | if self.config.is_async and self.config.callback is None: 49 | self._process_flow(payload=payload) 50 | else: 51 | self.config.callback(self._process_flow(payload=payload), \ 52 | *self.config.callback_args) 53 | 54 | self._start_new_thread(handle_async) 55 | 56 | return LazyResponse(child_pipe) 57 | 58 | def _start_new_thread(self, target): 59 | thread = Thread(target=target) 60 | thread.start() 61 | -------------------------------------------------------------------------------- /restfulie/resources/json.py: -------------------------------------------------------------------------------- 1 | from restfulie.resources import Resource 2 | from restfulie.links import Links, Link 3 | 4 | class JsonResource(Resource): 5 | """ 6 | This resource is returned when a JSON is unmarshalled. 7 | """ 8 | 9 | def __init__(self, dict_): 10 | """ 11 | JsonResource attributes can be accessed with 'dot'. 12 | """ 13 | links = self._parse_links(dict_) 14 | self._dict = dict_ 15 | self._links = Links(links) 16 | 17 | for key, value in dict_.items(): 18 | if isinstance(value, (list, tuple)): 19 | d = [JsonResource(x) if isinstance(x, dict) else x for x in value] 20 | setattr(self, key, d) 21 | else: 22 | d = JsonResource(value) if isinstance(value, dict) else value 23 | setattr(self, key, d) 24 | 25 | def _find_dicts_in_dict(self, structure): 26 | """ 27 | Get all dictionaries on a structure and returns a list of it. 28 | """ 29 | dicts = [] 30 | if isinstance(structure, dict): 31 | dicts.append(structure) 32 | for k, v in structure.items(): 33 | dicts.extend(self._find_dicts_in_dict(v)) 34 | return dicts 35 | 36 | def _parse_links(self, dict_): 37 | """ 38 | Find links on JSON dictionary. 39 | """ 40 | for d in self._find_dicts_in_dict(dict_): 41 | if 'link' in d: 42 | return [Link(href=link.get('href'), rel=link.get('rel'), content_type=link.get('type')) for link in d['link']] 43 | 44 | return [] 45 | 46 | def links(self): 47 | return self._links 48 | 49 | def link(self, rel): 50 | return self.links().get(rel) 51 | 52 | def __len__(self): 53 | return len(self._dict) 54 | -------------------------------------------------------------------------------- /test/lazy_response_test.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pipe 2 | from restfulie.response import LazyResponse, Response 3 | from mockito import mock, verify 4 | 5 | 6 | class lazy_response_test: 7 | 8 | def forwarding_attrs_from_real_response_test(self): 9 | 10 | response = mock() 11 | response.code = 200 12 | response.body = 'Hello' 13 | 14 | child_pipe = None 15 | lazy_response = LazyResponse(child_pipe) 16 | lazy_response._response = response 17 | 18 | assert lazy_response.code == 200 19 | assert lazy_response.body == 'Hello' 20 | verify(response).code 21 | verify(response).body 22 | 23 | 24 | def simple_test(self): 25 | 26 | response = ({'status': 200}, 'Hello') 27 | r = Response(response) 28 | 29 | pipe, child_pipe = Pipe() 30 | lazy_response = LazyResponse(child_pipe) 31 | pipe.send(r) 32 | 33 | assert lazy_response.code == 200 34 | assert lazy_response.body == 'Hello' 35 | 36 | 37 | def resource_test(self): 38 | 39 | response = ({'status': 200, 'content-type': 'text/plain; charset=utf-8'}, \ 40 | 'Hello') 41 | r = Response(response) 42 | 43 | pipe, child_pipe = Pipe() 44 | lazy_response = LazyResponse(child_pipe) 45 | pipe.send(r) 46 | 47 | assert lazy_response.resource() == 'Hello' 48 | 49 | 50 | def link_test(self): 51 | 52 | response = ({'status': 200, 'link': '; rel="alternate"'}, 'Hello') 53 | r = Response(response) 54 | 55 | pipe, child_pipe = Pipe() 56 | lazy_response = LazyResponse(child_pipe) 57 | pipe.send(r) 58 | 59 | link = lazy_response.link('alternate') 60 | assert link.href == '/feed' 61 | assert link.rel == 'alternate' 62 | -------------------------------------------------------------------------------- /test/json_resource_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from restfulie.resources.json import JsonResource 4 | 5 | def test_json_parsing(): 6 | 7 | test_json = """ 8 | { "order" : { 9 | "product" : "rails training", 10 | "description" : "rest training", 11 | "price" : "512.45", 12 | "link" : [ 13 | { "rel" : "self", "href" : "http://www.caelum.com.br/orders/1"}, 14 | { "rel" : "payment", "href" : "http://www.caelum.com.br/orders/1/payment"} 15 | ] 16 | } 17 | } 18 | """ 19 | 20 | resource = JsonResource(json.loads(test_json)) 21 | links = resource.links() 22 | 23 | assert len(resource) == 1 24 | assert len(resource.order) == 4 25 | assert len(links) == 2 26 | assert links.self.rel == "self" 27 | assert links.self.href == "http://www.caelum.com.br/orders/1" 28 | assert links.payment.rel == "payment" 29 | assert links.payment.href == "http://www.caelum.com.br/orders/1/payment" 30 | 31 | def test_json_without_links(): 32 | 33 | test_json = """ 34 | { "order" : { 35 | "product" : "rails training", 36 | "description" : "rest training", 37 | "price" : "512.45" 38 | } 39 | } 40 | """ 41 | 42 | resource = JsonResource(json.loads(test_json)) 43 | links = resource.links() 44 | 45 | assert len(links) == 0 46 | 47 | def test_find_dicts_in_dict(): 48 | 49 | link = [{'rel': 'self', 'href': 'http://www.caelum.com.br/orders/1'}] 50 | d1 = { 51 | "product" : "rails training", 52 | "description" : "rest training", 53 | "price" : "512.45", 54 | "link" : link 55 | } 56 | d2 = {'order': d1} 57 | 58 | resource = JsonResource({}) 59 | dicts = resource._find_dicts_in_dict(d2) 60 | 61 | assert len(dicts) == 2 62 | assert d1 in dicts 63 | assert d2 in dicts 64 | assert link not in dicts 65 | -------------------------------------------------------------------------------- /test/xml_resource_test.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | from restfulie.converters import XmlConverter 4 | from restfulie.resources.xml import XMLResource 5 | 6 | def test_xml_parsing(): 7 | 8 | test_xml = """ 9 | 10 | 11 | rails training 12 | rest training 13 | 512.45 14 | 15 | 16 | 17 | 18 | 19 | 20 | """ 21 | 22 | e = ElementTree.fromstring(test_xml) 23 | order = XMLResource(e) 24 | links = order.links() 25 | 26 | assert len(order.order) == 4 27 | assert order.order.product == 'rails training' 28 | assert links.self.rel == "self" 29 | assert links.self.href == "http://www.caelum.com.br/orders/1" 30 | assert links.payment.rel == "payment" 31 | assert links.payment.href == "http://www.caelum.com.br/orders/1/payment" 32 | 33 | def test_xml_without_links(): 34 | 35 | test_xml = """ 36 | 37 | 38 | rails training 39 | rest training 40 | 512.45 41 | 42 | 43 | """ 44 | 45 | e = ElementTree.fromstring(test_xml) 46 | order = XMLResource(e) 47 | links = order.links() 48 | 49 | assert len(links) == 0 50 | 51 | def test_find_xml_resource_in_xml_resource(): 52 | 53 | test_xml = """ 54 | 55 | 56 | 57 | rails training 58 | rest training 59 | 512.45 60 | 61 | 62 | 63 | """ 64 | 65 | e = ElementTree.fromstring(test_xml) 66 | resource = XMLResource(e) 67 | 68 | assert resource.order.product.name == "rails training" 69 | assert resource.order.product.description == "rest training" 70 | assert resource.order.product.price == "512.45" 71 | -------------------------------------------------------------------------------- /restfulie/resources/xml.py: -------------------------------------------------------------------------------- 1 | from restfulie.resources import Resource 2 | from restfulie.links import Links, Link 3 | 4 | class XMLResource(Resource): 5 | """ 6 | This resource is returned when a XML is unmarshalled. 7 | """ 8 | 9 | def __init__ (self, element_tree): 10 | self.element_tree = element_tree 11 | self._links = Links(self._parse_links()) 12 | self._enhance_element_tree() 13 | 14 | def _enhance_element_tree(self): 15 | """ 16 | Enables access to XMLResources attributes with 'dot'. 17 | """ 18 | setattr(self, "tag", self.element_tree.tag) 19 | 20 | for root_child in list(self.element_tree): 21 | if root_child.tag != 'link': 22 | if len(self.element_tree.findall(root_child.tag)) > 1: 23 | setattr(self, root_child.tag, self.element_tree.findall(root_child.tag)) 24 | elif len(list(root_child)) == 0: 25 | setattr(self, root_child.tag, root_child.text) 26 | else: 27 | setattr(self, root_child.tag, self.element_tree.find(root_child.tag)) 28 | 29 | for element in self.element_tree.getiterator(): 30 | for child in list(element): 31 | if len(element.findall(child.tag)) > 1: 32 | setattr(element, child.tag, element.findall(child.tag)) 33 | elif len(list(child)) == 0: 34 | setattr(element, child.tag, child.text) 35 | else: 36 | setattr(element, child.tag, element.find(child.tag)) 37 | 38 | def _parse_links(self): 39 | """ 40 | Find links in a ElementTree 41 | """ 42 | links = [] 43 | for element in self.element_tree.getiterator('link'): 44 | link = Link(href=element.attrib.get('href'), 45 | rel=element.attrib.get('rel'), 46 | content_type=element.attrib.get('type') or 'application/xml') 47 | 48 | links.append(link) 49 | 50 | return links 51 | 52 | def links(self): 53 | return self._links 54 | 55 | def link(self, rel): 56 | return self.links().get(rel) 57 | -------------------------------------------------------------------------------- /test/request_test.py: -------------------------------------------------------------------------------- 1 | from restfulie.dsl import Dsl 2 | from restfulie.request import Request 3 | from mockito import mock 4 | from threading import Semaphore 5 | 6 | 7 | class callable_mock(): 8 | 9 | def __init__(self): 10 | self.called = 0 11 | 12 | def __call__(self, *args, **kwargs): 13 | self.called = self.called + 1 14 | 15 | 16 | class http_method_test: 17 | 18 | def setup(self): 19 | self.dsl = mock(Dsl) 20 | self.request = Request(self.dsl) 21 | 22 | def should_make_synchronous_invocations_with_simple_auth(self): 23 | self.dsl.credentials = 'test:test' 24 | self.dsl.callback = None 25 | self.dsl.is_async = False 26 | self.request._process_flow = callable_mock() 27 | self.request() 28 | assert self.request._process_flow.called == 1 29 | 30 | def should_make_synchronous_invocations_if_callback_isnt_configured(self): 31 | self.dsl.callback = None 32 | self.dsl.is_async = False 33 | self.request._process_flow = callable_mock() 34 | self.request() 35 | assert self.request._process_flow.called == 1 36 | 37 | def should_make_asynchronous_invocations_if_callback_is_configured(self): 38 | self.dsl.callback = lambda: None 39 | self.dsl.is_async = True 40 | self.request._process_async_flow = callable_mock() 41 | self.request() 42 | assert self.request._process_async_flow.called == 1 43 | 44 | def should_call_callback_function_on_asynchronous_request(self): 45 | barrier = Semaphore(0) 46 | 47 | def callback(request): 48 | barrier.release() 49 | 50 | self.dsl.is_async = True 51 | self.dsl.callback = callback 52 | self.dsl.callback_args = () 53 | self.request._process_flow = lambda payload: None 54 | self.request() 55 | 56 | barrier.acquire() 57 | 58 | def should_call_callback_on_async_request_and_pass_arguments(self): 59 | barrier = Semaphore(0) 60 | 61 | def callback(request, arg1, arg2, arg3): 62 | assert (arg1, arg2, arg3) == (1, 2, 3) 63 | barrier.release() 64 | 65 | self.dsl.is_async = True 66 | self.dsl.callback = callback 67 | self.dsl.callback_args = (1, 2, 3) 68 | self.request._process_flow = lambda payload: None 69 | self.request() 70 | 71 | barrier.acquire() 72 | -------------------------------------------------------------------------------- /test/integration_test.py: -------------------------------------------------------------------------------- 1 | from restfulie.restfulie import Restfulie 2 | from threading import Semaphore 3 | 4 | 5 | class integration_test: 6 | 7 | def should_perform_ordinary_requests(self): 8 | body = Restfulie.at("http://localhost:20144/hello").get().body 9 | assert "Response for" in body 10 | assert "/hello" in body 11 | 12 | def should_perform_requests_with_parameters_as_kwargs(self): 13 | response = Restfulie.at("http://localhost:20144").post(action="test") 14 | print response.body 15 | body = response.body 16 | assert "This is a test" in body 17 | 18 | def should_perform_requests_with_parameters_as_dict(self): 19 | d = {"action": "test"} 20 | response = Restfulie.at("http://localhost:20144").post(**d) 21 | print response.body 22 | body = response.body 23 | assert "This is a test" in body 24 | 25 | def should_perform_ordinary_requests_with_simple_auth(self): 26 | r = Restfulie.at("http://localhost:20144/auth").auth('test', 'test') 27 | response = r.get() 28 | body = response.body 29 | assert "worked" in body 30 | 31 | def should_perform_async_requests(self): 32 | barrier = Semaphore(0) 33 | 34 | def callback(response): 35 | body = response.body 36 | assert "Response for" in body 37 | assert "/hello" in body 38 | barrier.release() 39 | 40 | r = Restfulie.at("http://localhost:20144/hello").async(callback).get() 41 | barrier.acquire() 42 | assert "Response for" in r.body 43 | assert "/hello" in r.body 44 | 45 | def should_perform_async_requests_with_arguments_to_the_callback(self): 46 | barrier = Semaphore(0) 47 | 48 | def callback(response, extra1, extra2): 49 | body = response.body 50 | assert "Response for" in body 51 | assert "/hello" in body 52 | assert extra1 == "first" 53 | assert extra2 == "second" 54 | barrier.release() 55 | 56 | r = Restfulie.at("http://localhost:20144/hello") 57 | r = r.async(callback, args=("first", "second")).get() 58 | barrier.acquire() 59 | assert "Response for" in r.body 60 | assert "/hello" in r.body 61 | 62 | def should_perform_async_requests_without_callback(self): 63 | 64 | r = Restfulie.at("http://localhost:20144/hello").async().get() 65 | assert "Response for" in r.body 66 | assert "/hello" in r.body 67 | -------------------------------------------------------------------------------- /restfulie/response.py: -------------------------------------------------------------------------------- 1 | from converters import Converters 2 | import re 3 | 4 | from links import Links, Link 5 | 6 | 7 | class Response(object): 8 | """ 9 | Handle and parse a HTTP response 10 | """ 11 | 12 | def __init__(self, response): 13 | self.response = response 14 | self.headers = self.response[0] 15 | self.code = self.response[0]['status'] 16 | self.body = self.response[1] 17 | 18 | def resource(self): 19 | """ 20 | Returns the unmarshalled object of the response body 21 | """ 22 | if 'content-type' in self.response[0]: 23 | contenttype = self.response[0]['content-type'].split(';')[0] 24 | else: 25 | contenttype = None 26 | 27 | converter = Converters.marshaller_for(contenttype) 28 | return converter.unmarshal(self.body) 29 | 30 | def links(self): 31 | """ 32 | Returns the Links of the header 33 | """ 34 | r = self._link_header_to_array() 35 | return Links(r) 36 | 37 | def link(self, rel): 38 | """ 39 | Get a link with 'rel' from header 40 | """ 41 | return self.links().get(rel) 42 | 43 | def _link_header_to_array(self): 44 | """ 45 | Split links in headers and return a list of dicts 46 | """ 47 | values = self.headers['link'].split(',') 48 | links = [] 49 | for link_string in values: 50 | links.append(self._string_to_link(link_string)) 51 | 52 | return links 53 | 54 | def _string_to_link(self, l): 55 | """ 56 | Parses a link header string to a dictionary 57 | """ 58 | uri = re.search('<([^>]*)', l) and re.search('<([^>]*)', l).group(1) 59 | rest = re.search('.*>(.*)', l) and re.search('.*>(.*)', l).group(1) 60 | rel = (re.search('rel=(.*)', rest) and 61 | re.search('rel="(.*)"', rest).group(1)) 62 | tpe = (re.search('type=(.*)', rest) and 63 | re.search('type="(.*)"', rest).group(1)) 64 | 65 | return Link(href=uri, rel=rel, content_type=tpe) 66 | 67 | 68 | class LazyResponse(object): 69 | """ 70 | Lazy response for async calls 71 | """ 72 | 73 | def __init__(self, response_pipe): 74 | self._response_pipe = response_pipe 75 | 76 | def __getattr__(self, attr): 77 | if self._response_pipe is not None: 78 | self._response = self._response_pipe.recv() 79 | self._response_pipe = None 80 | return getattr(self._response, attr) 81 | -------------------------------------------------------------------------------- /test/converters_test.py: -------------------------------------------------------------------------------- 1 | from restfulie.converters import Converters, PlainConverter, \ 2 | XmlConverter, JsonConverter 3 | 4 | 5 | class converters_test: 6 | 7 | def setup(self): 8 | Converters.types = {} 9 | 10 | def should_register_converters(self): 11 | converter = PlainConverter() 12 | Converters.register("text/plain", converter) 13 | 14 | assert Converters.types["text/plain"] == converter 15 | 16 | def test_should_discover_a_marshaller_for_a_type(self): 17 | assert Converters.marshaller_for("application/atom").__class__ == \ 18 | XmlConverter().__class__ 19 | converter = PlainConverter() 20 | Converters.register("text/plain", converter) 21 | assert Converters.marshaller_for("text/plain") == converter 22 | 23 | 24 | class generic_marshaller_test: 25 | 26 | def should_marshal(self): 27 | converter = PlainConverter() 28 | result = converter.marshal("Hello World") 29 | assert result == "Hello World" 30 | 31 | def should_unmarshal(self): 32 | converter = PlainConverter() 33 | result = converter.unmarshal("Hello World") 34 | assert result == "Hello World" 35 | 36 | 37 | class json_marshaller_test: 38 | 39 | def should_marshal(self): 40 | converter = JsonConverter() 41 | d = {'a': {'c': [1, 2, 3]}, 'b': 2} 42 | result = converter.marshal(d) 43 | assert result == '{"a": {"c": [1, 2, 3]}, "b": 2}' 44 | 45 | def should_unmarshal(self): 46 | converter = JsonConverter() 47 | json = '{"a": {"c": [1, 2, 3]}, "b": 2}' 48 | result = converter.unmarshal(json) 49 | assert result.a.c == [1, 2, 3] 50 | assert result.b == 2 51 | 52 | 53 | class xml_marshaller_test: 54 | 55 | def should_marshal(self): 56 | converter = XmlConverter() 57 | d = {'html': {'img': ''}} 58 | result = converter.marshal(d) 59 | assert result == '' 60 | 61 | def should_unmarshal(self): 62 | xml = XmlConverter() 63 | result = xml.unmarshal('
' + 65 | 'A Link
' + 67 | '
Hello World
' + 68 | '') 69 | 70 | assert result.tag == 'html' 71 | assert len(result.div) == 2 72 | assert result.div[0].tag == 'div' 73 | assert result.div[0].link[0].text == 'A Link' 74 | assert len(result.div[0].link) == 2 75 | assert len(result.links()) == 2 76 | -------------------------------------------------------------------------------- /spec/client_spec.py: -------------------------------------------------------------------------------- 1 | from restfulie import Restfulie 2 | import time 3 | 4 | def search(what): 5 | description = Restfulie.at("http://localhost:3000/products/opensearch.xml").accepts('application/opensearchdescription+xml').get().resource() 6 | items = description.use("application/xml").search(searchTerms = what, startPage = 1) 7 | return items 8 | 9 | 10 | MY_ORDER = {"order": {"address": "R. Vergueiro 3185, Sao Paulo, Brazil"}} 11 | 12 | def should_be_able_to_search_items(): 13 | items = search("20") 14 | assert len(items.resource().product) == 2 15 | 16 | 17 | def should_be_able_to_create_an_empty_order(): 18 | response = search("20") 19 | response = response.resource().links().order.follow().post(**MY_ORDER) 20 | assert response.resource().address == MY_ORDER['order']['address'] 21 | 22 | 23 | def should_be_able_to_add_an_item_to_an_order(): 24 | results = search("20") 25 | 26 | product = results.resource().product[0] 27 | selected = {'order': {'product': product.id, 'quantity': 1}} 28 | 29 | result = results.resource().links().order.follow().post(**MY_ORDER).resource() 30 | result = result.link('self').follow().put(**selected).resource() 31 | 32 | assert result.price == product.price 33 | 34 | 35 | def should_be_able_to_pay(): 36 | results = search("20") 37 | 38 | product = results.resource().product[0] 39 | selected = {'order': {'product': product.id, 'quantity': 1}} 40 | 41 | result = results.resource().links().order.follow().post(**MY_ORDER).resource() 42 | result = result.link('self').follow().put(**selected).resource() 43 | 44 | result = pay(result) 45 | print result 46 | assert result.state == "processing_payment" 47 | 48 | 49 | def pay(result): 50 | card = {'payment': {'card_holder': "guilherme silveira", 'card_number': 4444, 'value': result.price}} 51 | result = result.links().payment.follow().post(**card).resource() 52 | return result 53 | 54 | 55 | def wait_payment_success(attempts, result): 56 | results = search("20") 57 | 58 | for product in results.resource().product: 59 | order = {'order': {'state': 'paid'}} 60 | response = results.resource().links().order.follow().post(**order) 61 | return response.resource() 62 | 63 | if result.state == "unpaid" and attempts > 0: 64 | print("Ugh! Payment rejected! Get some credits my boy... I am trying it again.") 65 | result = pay(result) 66 | wait_payment_success(attempts-1, result) 67 | else: 68 | return result 69 | 70 | 71 | def should_try_and_pay_for_it(): 72 | results = search("20") 73 | 74 | product = results.resource().product[0] 75 | selected = {'order': {'product': product.id, 'quantity': 1}} 76 | 77 | result = results.resource().links().order.follow().post(**MY_ORDER).resource() 78 | result = result.link('self').follow().put(**selected).resource() 79 | 80 | result = pay(result) 81 | 82 | result = wait_payment_success(1, result) 83 | assert result.state == "preparing" 84 | -------------------------------------------------------------------------------- /restfulie/dsl.py: -------------------------------------------------------------------------------- 1 | from processor import RedirectProcessor, PayloadMarshallingProcessor, \ 2 | ExecuteRequestProcessor, AuthenticationProcessor 3 | from request import Request 4 | 5 | 6 | class Dsl(object): 7 | """ 8 | Configuration object for requests at a given URI. 9 | """ 10 | 11 | HTTP_VERBS = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 12 | 'trace'] 13 | 14 | def __init__(self, uri): 15 | """ 16 | Initialize the configuration for requests at the given URI. 17 | """ 18 | self.credentials = None 19 | self.uri = uri 20 | self.processors = [AuthenticationProcessor(), 21 | RedirectProcessor(), 22 | PayloadMarshallingProcessor(), 23 | ExecuteRequestProcessor(), ] 24 | self.headers = {'Content-Type': 'application/xml', 25 | 'Accept': 'application/xml'} 26 | self.is_async = False 27 | self.callback = None 28 | self.callback_args = () 29 | 30 | def __getattr__(self, name): 31 | """ 32 | Perform an HTTP request. This method supports calls to the following 33 | methods: delete, get, head, options, patch, post, put, trace 34 | 35 | Once the HTTP call is performed, a response is returned (unless the 36 | async method is used). 37 | """ 38 | if (self._is_verb(name)): 39 | self.verb = name.upper() 40 | return Request(self) 41 | else: 42 | raise AttributeError(name) 43 | 44 | def _is_verb(self, name): 45 | """ 46 | Checks if a string is a HTTP verb 47 | """ 48 | return name in self.HTTP_VERBS 49 | 50 | def use(self, feature): 51 | """ 52 | Register a feature at this configuration. 53 | """ 54 | self.processors.insert(0, feature) 55 | return self 56 | 57 | def async(self, callback=None, args=()): 58 | """ 59 | Use asynchronous calls. A HTTP call performed through this object will 60 | return immediately, giving None as response. Once the request is 61 | completed, the callback function is called and the response and the 62 | optional extra args defined in args are passed as parameters. 63 | """ 64 | self.is_async = True 65 | self.callback = callback 66 | self.callback_args = args 67 | return self 68 | 69 | def as_(self, content_type): 70 | """ 71 | Set up the Content-Type 72 | """ 73 | if content_type: 74 | self.headers["Content-Type"] = content_type 75 | return self 76 | 77 | def accepts(self, content_type): 78 | """ 79 | Configure the accepted response format. 80 | """ 81 | self.headers['Accept'] = content_type 82 | return self 83 | 84 | def auth(self, username, password, method='simple'): 85 | """ 86 | Authentication feature. It does simple HTTP auth 87 | """ 88 | self.credentials = (username, password, method) 89 | return self 90 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. restfulie-py 2 | 3 | "One minute tutorial":https://github.com/caelum/restfulie-py/wiki/Python-One-Minute-Tutorial 4 | "Hypermedia Implementation":https://github.com/caelum/restfulie-py/wiki/Hypermedia-Implementation 5 | "Client":https://github.com/caelum/restfulie-py/wiki/Python-Client 6 | "Examples":https://github.com/caelum/restfulie-py/wiki/python-examples 7 | "Hypermedia Examples":https://github.com/caelum/restfulie-py/wiki/Python-Hypermedia-Clients 8 | "OpenSearch Examples":https://github.com/caelum/restfulie-py/wiki/Python-OpenSearch 9 | 10 | h2. Introduction 11 | 12 | This is a one minute guide to get you going with Restfulie Python 13 | We are ready to go, hypermedia supported: 14 | 15 |
 16 | from restfulie import Restfulie
 17 | 
 18 | # using restfulie as an http api:
 19 | >>> response = Restfulie.at('http://localhost:8080/items').accepts('application/xml').get()
 20 | >>> print response.body
 21 | 
 22 |     
 23 |         Car
 24 |         32000.00
 25 |     
 26 |     
 27 |         House
 28 |         231000.00
 29 |     
 30 | 
 31 | 
 32 | >>> print response.code
 33 | 200
 34 | 
 35 | # unmarshalling the items response
 36 | >>> r = response.resource()
 37 | >>> print len(r.item)
 38 | 2
 39 | >>> print len(r.item[0].name)
 40 | Car
 41 | 
 42 | # navigating through hypermedia
 43 | # using kwargs as request parameters
 44 | >>> result = items.link("self").follow().post(name='New product', price=30)
 45 | 
 46 | # or, using a dict as request parameters
 47 | >>> parameters = {"name":"New product", "price":30}
 48 | >>> result = items.link("self").follow().post(**parameters)
 49 | 
 50 | >>> print result.code
 51 | 200
 52 | 
 53 | 
54 | 55 | This is it. Adding hypermedia capabilities and following links. Now its time to use it in the right way. 56 | 57 | 58 | h2. Installing Restfulie 59 | 60 | On project root, run: 61 | 62 | @$ python setup.py install@ 63 | 64 | If you like to install from pip, run: 65 | 66 | @pip install restfulie@ 67 | 68 | Or with easy_install: 69 | 70 | @easy_install restfulie@ 71 | 72 | 73 | h2. Installing Restfulie for development 74 | 75 | First, create your @Makefile@ based on @Makefile.example@. 76 | 77 | Then, make the installation: 78 | 79 | @$ make dev@ 80 | 81 | The required dependencies should be installed automatically. 82 | 83 | 84 | h2. Running tests 85 | 86 | On project root, run: 87 | 88 | @$ make test@ 89 | 90 | To run restfulie-restbuy integration test, first start "restfulie-restbuy":https://github.com/caelum/restfulie-restbuy server and run: 91 | 92 | @$ python setup.py nosetests -i "spec|should"@ 93 | 94 | 95 | h2. Team 96 | 97 | "Alexandre Atoji":https://github.com/atoji 98 | "Andrew Toshiaki Nakayama Kurauchi":https://github.com/toshikurauchi 99 | "BecaMotta":https://github.com/BecaMotta 100 | "Douglas Camata":https://github.com/douglascamata 101 | "Guilherme Silveira":https://github.com/guilhermesilveira 102 | "Hugo Lopes Tavares":https://github.com/hugobr 103 | "Marianna Reis":https://github.com/mariannareis 104 | "Pedro Matiello":http://pmatiello.appspot.com/ 105 | "Rodrigo Manhães":https://github.com/rodrigomanhaes 106 | "Tarsis Azevedo":https://github.com/tarsis 107 | 108 | -------------------------------------------------------------------------------- /restfulie/processor.py: -------------------------------------------------------------------------------- 1 | from base64 import encodestring 2 | import httplib2 3 | from response import Response 4 | from converters import Converters 5 | import restfulie 6 | 7 | 8 | class RequestProcessor(object): 9 | def execute(self, chain, request, env={}): 10 | raise NotImplementedError('Subclasses must implement this method') 11 | 12 | 13 | class AuthenticationProcessor(RequestProcessor): 14 | """ 15 | Processor responsible for making HTTP simple auth 16 | """ 17 | def execute(self, chain, request, env={}): 18 | if request.credentials is not None: 19 | encoded_credentials = self._encode_credentials(request.credentials) 20 | request.headers['authorization'] = "Basic %s" % encoded_credentials 21 | return chain.follow(request, env) 22 | 23 | def _encode_credentials(self, credentials): 24 | username = credentials[0] 25 | password = credentials[1] 26 | method = credentials[2] 27 | if (method == 'simple'): 28 | return encodestring("%s:%s" % (username, password))[:-1] 29 | 30 | 31 | class ExecuteRequestProcessor(RequestProcessor): 32 | """ 33 | Processor responsible for getting the body from environment and 34 | making a request with it. 35 | """ 36 | def __init__(self): 37 | self.http = httplib2.Http() 38 | 39 | def execute(self, chain, request, env={}): 40 | if "body" in env: 41 | response = self.http.request(request.uri, request.verb, 42 | env.get("body"), request.headers) 43 | else: 44 | response = self.http.request(request.uri, request.verb, 45 | headers=request.headers) 46 | 47 | resource = Response(response) 48 | if request.is_async: 49 | request.pipe.send(resource) 50 | 51 | return resource 52 | 53 | 54 | class PayloadMarshallingProcessor(RequestProcessor): 55 | """ 56 | Processor responsible for marshalling the payload in environment. 57 | """ 58 | def execute(self, chain, request, env={}): 59 | if "payload" in env: 60 | content_type = request.headers.get("Content-Type") 61 | marshaller = Converters.marshaller_for(content_type) 62 | env["body"] = marshaller.marshal(env["payload"]) 63 | del(env["payload"]) 64 | 65 | return chain.follow(request, env) 66 | 67 | 68 | class RedirectProcessor(RequestProcessor): 69 | """ 70 | A processor responsible for redirecting a client to another URI when the 71 | server returns the location header and a response code related to 72 | redirecting. 73 | """ 74 | REDIRECT_CODES = ['201', '301', '302'] 75 | 76 | def redirect_location_for(self, result): 77 | if (result.code in self.REDIRECT_CODES): 78 | return (result.headers.get("Location") or 79 | result.headers.get("location")) 80 | return None 81 | 82 | def execute(self, chain, request, env={}): 83 | result = chain.follow(request, env) 84 | location = self.redirect_location_for(result) 85 | if location: 86 | return self.redirect(location, 87 | request.headers.get("Content-Type")) 88 | 89 | return result 90 | 91 | def redirect(self, location, request_type): 92 | return restfulie.Restfulie.at(location).as_(request_type).get() 93 | -------------------------------------------------------------------------------- /restfulie/converters.py: -------------------------------------------------------------------------------- 1 | import json 2 | from xml.etree import ElementTree 3 | from opensearch import OpenSearchDescription 4 | from resources.xml import XMLResource 5 | from resources.json import JsonResource 6 | 7 | class Converters(object): 8 | """ 9 | Utility methods for converters. 10 | """ 11 | 12 | types = {} 13 | 14 | @staticmethod 15 | def register(a_type, converter): 16 | """ 17 | Register a converter for the given type. 18 | """ 19 | Converters.types[a_type] = converter 20 | 21 | @staticmethod 22 | def marshaller_for(a_type): 23 | """ 24 | Return a converter for the given type. 25 | """ 26 | return Converters.types.get(a_type) or XmlConverter() 27 | 28 | 29 | class JsonConverter(object): 30 | """ 31 | Converts objects from and to JSON. 32 | """ 33 | 34 | def marshal(self, content): 35 | """ 36 | Produces a JSON representation of the given content. 37 | """ 38 | return json.dumps(content) 39 | 40 | def unmarshal(self, json_content): 41 | """ 42 | Produces an object for a given JSON content. 43 | """ 44 | return JsonResource(json.loads(json_content)) 45 | 46 | class XmlConverter(object): 47 | """ 48 | Converts objects from and to XML. 49 | """ 50 | 51 | def marshal(self, content): 52 | """ 53 | Produces a XML representation of the given content. 54 | """ 55 | return ElementTree.tostring(self._dict_to_etree(content)) 56 | 57 | def _dict_to_etree(self, content): 58 | """ 59 | Receives a dictionary and converts to an ElementTree 60 | """ 61 | tree = ElementTree.Element(content.keys()[0]) 62 | self._dict_to_etree_rec(content[content.keys()[0]], tree) 63 | return tree 64 | 65 | def _dict_to_etree_rec(self, content, tree): 66 | """ 67 | Auxiliar function of _dict_to_etree_rec 68 | """ 69 | if type(content) == dict: 70 | for key, value in content.items(): 71 | e = ElementTree.Element(key) 72 | self._dict_to_etree_rec(value, e) 73 | tree.append(e) 74 | else: 75 | tree.text = str(content) 76 | 77 | def unmarshal(self, content): 78 | """ 79 | Produces an ElementTree object for a given XML content. 80 | """ 81 | e = ElementTree.fromstring(content) 82 | return XMLResource(e) 83 | 84 | 85 | class OpenSearchConverter(object): 86 | def marshal(self, content): 87 | return XmlConverter().marshal(content) 88 | 89 | def unmarshal(self, content): 90 | """ 91 | Produces an OpenSearchDescription object from an 92 | OpenSearch XML 93 | """ 94 | e_tree = ElementTree.fromstring(content) 95 | return OpenSearchDescription(e_tree) 96 | 97 | 98 | class PlainConverter(object): 99 | def marshal(self, content): 100 | """ 101 | Does nothing 102 | """ 103 | return content 104 | 105 | def unmarshal(self, content): 106 | """ 107 | Returns content without modification 108 | """ 109 | return content 110 | 111 | Converters.register('application/xml', XmlConverter()) 112 | Converters.register('text/xml', XmlConverter()) 113 | Converters.register('xml', XmlConverter()) 114 | Converters.register('text/plain', PlainConverter()) 115 | Converters.register('text/json', JsonConverter()) 116 | Converters.register('application/json', JsonConverter()) 117 | Converters.register('json', JsonConverter()) 118 | Converters.register('application/opensearchdescription+xml', 119 | OpenSearchConverter()) 120 | -------------------------------------------------------------------------------- /test/processor_test.py: -------------------------------------------------------------------------------- 1 | from mockito import mock, when, verify 2 | from restfulie.processor import ExecuteRequestProcessor, PayloadMarshallingProcessor, \ 3 | RedirectProcessor, AuthenticationProcessor 4 | 5 | 6 | class request_processor_test: 7 | 8 | def test_execute_should_return_a_resource_without_body(self): 9 | 10 | http = mock() 11 | response = ({'status': 200}, "body") 12 | request = mock() 13 | request.headers = {"Content-Type": "application/xml"} 14 | request.verb = "GET" 15 | request.uri = "http://www.caelum.com.br" 16 | request.callback = None 17 | request.is_async = False 18 | request.credentials = None 19 | 20 | when(http).request(request.uri, request.verb, \ 21 | headers=request.headers).thenReturn(response) 22 | 23 | processor = ExecuteRequestProcessor() 24 | processor.http = http 25 | resource = processor.execute([], request) 26 | 27 | assert resource.code == 200 28 | assert resource.body == "body" 29 | 30 | def test_execute_should_return_a_resource_with_body(self): 31 | 32 | http = mock() 33 | response = ({'status': 404}, "anybody") 34 | env = {"body": "body"} 35 | request = mock() 36 | request.headers = {"Content-Type": "application/xml"} 37 | request.verb = "GET" 38 | request.uri = "http://www.caelum.com.br" 39 | request.callback = None 40 | request.is_async = False 41 | request.credentials = None 42 | 43 | when(http).request(request.uri, request.verb, \ 44 | env['body'], request.headers).thenReturn(response) 45 | 46 | processor = ExecuteRequestProcessor() 47 | processor.http = http 48 | 49 | resource = processor.execute([], request, env) 50 | 51 | assert resource.code == 404 52 | assert resource.body == "anybody" 53 | 54 | 55 | class payload_processor_test: 56 | 57 | def test_payload_is_marshalled(self): 58 | 59 | request = mock() 60 | request.headers = {'Content-type': 'text/plain'} 61 | chain = mock() 62 | env = {'payload': {'product': 'car'}} 63 | 64 | processor = PayloadMarshallingProcessor() 65 | processor.execute(chain, request, env) 66 | 67 | verify(chain).follow(request, {'body': 'car'}) 68 | 69 | 70 | class redirect_processor_test: 71 | 72 | def check_redirect_on(self, code): 73 | http = mock() 74 | response = {'status': 200}, "anybody" 75 | request = mock() 76 | request.headers = {"Content-Type": "application/xml"} 77 | request.verb = "GET" 78 | request.uri = "http://www.caelum.com.br" 79 | result = mock() 80 | result.code = code 81 | result.headers = {'Location': 'http://www.caelum.com.br'} 82 | result.code = '200' 83 | result.body = "anybody" 84 | chain = mock() 85 | when(chain).follow(request, {}).thenReturn(result) 86 | processor = RedirectProcessor() 87 | processor.http = http 88 | processor.redirect = lambda self: response 89 | resource = processor.execute(chain, request) 90 | assert resource.code == '200' 91 | assert resource.body == "anybody" 92 | 93 | def test_redirect(self): 94 | for codex in RedirectProcessor.REDIRECT_CODES: 95 | self.check_redirect_on(codex) 96 | 97 | class authentication_processor_test: 98 | 99 | def setUp(self): 100 | self.chain = mock() 101 | self.request = mock() 102 | self.request.headers = {} 103 | 104 | def should_add_simple_auth_credentials_to_request_headers(self): 105 | self.request.credentials = ('user', 'pass', 'simple') 106 | authprocessor = AuthenticationProcessor() 107 | authprocessor.execute(self.chain, self.request, {}) 108 | assert self.request.headers.has_key('authorization') 109 | assert "Basic" in self.request.headers['authorization'] 110 | 111 | def should_not_add_auth_credentials_if_none_is_set(self): 112 | self.request.credentials = None 113 | authprocessor = AuthenticationProcessor() 114 | authprocessor.execute(self.chain, self.request, {}) 115 | assert not self.request.headers.has_key('authorization') 116 | --------------------------------------------------------------------------------