Welcome to Confeitaria's documentation! Confeitaria is a Web framework for Python whose main purpose is to test some hypothesis and possibilities about Web development. Those hypothesis are the principles behind Confeitaria. Let's take a look at them... after some initial tests.
The very first use to Confeitaria is to see its own documentation. You should be able to install Confeitaria with pip:
$ pip install confeitaria
Now, just run
$ python -mconfeitaria
and access http://localhost:8000. VoilĂ ! You will see this same documentation.
You would rather show your own page, for sure. In Confeitaria, a page is an object with a bounded method named index() (henceforward named the index method) or a bounded method named action() (the action method). The instances of the class below would be valid pages:
>>> class TestPage(object): ... def index(self): ... return "This is a test"
The simplest way so far of running a Confeitaria object is to use confeitaria.run(). It starts up a server to serve the return of the index() method:
import confeitaria page = TestPage() confeitaria.run(page)
If you access http://localhost:8000 after this, you will see This is a test in the browser.
If the page passed to confeitaria.run() / Server has an attribute, and this attribute is also a page, then we only need to add the attribute name as part of the path in the URL to get its output. The attribute page is a subpage and can has its own subpages. For example, if we have the classes below:
>>> class RootPage(object): ... def index(self): ... return 'root' >>> class SubPage(object): ... def index(self): ... return 'a subpage' >>> class SubSubPage(object): ... def index(self): ... return 'another subpage'
...and then we build a structure as such:
>>> root = RootPage() >>> root.sub = SubPage() >>> root.sub.another = SubSubPage()
... then we should expect the following responses:
>>> with Server(root): ... requests.get('http://localhost:8000/').text ... requests.get('http://localhost:8000/sub').text ... requests.get('http://localhost:8000/sub/another').text u'root' u'a subpage' u'another subpage'
Naturally, most pages should get information from the browser. This information can be passed to the index method by arguments. The values for the arguments are retrieved from the HTTP request parameters. It can be done in two ways:
If the index function has mandatory arguments, their values will come from the query path, as below:
>>> class SumPage(object): ... def index(self, p1, p2): ... v1, v2 = int(p1), int(p2) ... return "{0} + {1} = {2}".format(v1, v2, v1 + v2) >>> with Server(SumPage()): ... requests.get('http://localhost:8000/3/2').text ... requests.get('http://localhost:8000/-2/3').text u'3 + 2 = 5' u'-2 + 3 = 1'
If the URL path does not a value for the given parameter, a 404 Not Found response will be served:
>>> class NonePage(object): ... def index(self, arg): ... return "arg: {0}, arg type: {1}".format(arg, type(arg)) >>> with Server(NonePage()): ... requests.get('http://localhost:8000/').status_code 404
If the URL path has more values than the number of index method's mandatory parameters, a 404 Not Found error should be the result:
>>> class NonePage(object): ... def index(self, arg): ... return "arg: {0} arg type: {1}".format(arg, type(arg)) >>> with Server(NonePage()): ... requests.get('http://localhost:8000/a/b').status_code 404
If the index function has optional arguments, their values will come from the query string parameters, as below:
>>> class HelloWorldPage(object): ... def index(self, greeting='Hello', greeted='World'): ... return greeting + " " + greeted + "!" >>> with Server(HelloWorldPage()): ... requests.get('http://localhost:8000/').text ... requests.get('http://localhost:8000/?greeting=Hi').text ... requests.get( ... 'http://localhost:8000/?greeting=Hi&greeted=Earth').text u'Hello World!' u'Hi World!' u'Hi Earth!'
Which one to use is up to the developer. We believe mandatory arguments are good to pass mandatory identifiers, such as database primary keys and usernames, as in http://example.com/report/1081 or http://example.com/user/juju. Optional parameters are nice in most other cases, such as when executing operations (as in http://example.com/user/update?id=324&username=Ju) or giving extra options (as in http://example.com/report/1081?pages=all).
Advanced warning: what if one wants to give the values for mandatory arguments with query string parameters (e.g. using the URL http://localhost:8000/?p2=3&p1=2 to hit SumPage) or optional arguments with path components (generating a URL such as http://localhost:8000/hello/world to access HelloWorldPage)? This behavior is undefined on purpose. Confeitaria should play well with many other frameworks and the best behavior can vary between them. In our reference implementation, it fails, and we don't think it is a good practice anyway.
Index methods only handle GET requests. If a request uses the POST HTTP method, it should be handled by an action method.
Action methods are not expected to return HTML documents, they are only called for their side effects. Any relevant content should be returned by an index method.
Consider, for example, the following mock of an authetication page:
>>> class AuthenticationPage(object): ... username = None ... def action(self, username=None): ... AuthenticationPage.username = username
It could be a subpage of a root page as the one below:
>>> class MainPage(object): ... def index(self): ... if AuthenticationPage.username: ... return 'You are logged in as {0}.'.format( ... AuthenticationPage.username ... ) ... else: ... return 'You are not logged in.'
So we would have this tree:
>>> page = MainPage() >>> page.auth = AuthenticationPage()
By default, nobody would be authenticated:
>>> with Server(page): ... requests.get('http://localhost:8000/').text u'You are not logged in.'
We can, however, send a POST request for log in:
>>> with Server(page): ... requests.get('http://localhost:8000/').text ... _ = requests.post( ... 'http://localhost:8000/auth', data={'username': 'alice'}, ... allow_redirects=False # Why to do it? We'll see... soon. ... ) ... requests.get('http://localhost:8000/').text u'You are not logged in.' u'You are logged in as alice.'
If a page has a bound method named set_url() which receives one argument, this method will be called and the parameter value will be the URL of the page. This means that each page can know what is its own URL on the server:
>>> class URLAwarePage(object): ... def set_url(self, url): ... self.url = url ... def index(self): ... return 'My URL is ' + self.url >>> root = URLAwarePage() >>> root.sub = URLAwarePage() >>> with Server(root): ... requests.get('http://localhost:8000/').text ... requests.get('http://localhost:8000/sub').text u'My URL is /' u'My URL is /sub'
This URL is immutable, it is set in the server start up. This means that a page can even know the URL of its subpages:
>>> class RootPage(object): ... def __init__(self): ... self.sub = URLAwarePage() ... def index(self): ... return ( ... 'Subpage is at {0}. ' ... '<a href="{0}">Go there!</a>'.format(self.sub.url) ... ) >>> with Server(RootPage()): ... requests.get('http://localhost:8000/').text u'Subpage is at /sub. <a href="/sub">Go there!</a>'
Note: one could argue that the "URLs" in these examples are actually just paths, not full URLs. We hope, however, to make it possible to a page to have a totally different URL, even in another domain. We do not have this feature now; yet, assuming that the URLs defined with set_url() can be more complex than paths is the way to go - even if the current examples are quite simple.
To save you from typing the same method over and over, we also provide the class confeitaria.interface.URLedPage. It implements this protocol and has a get_url() method:
>>> import confeitaria.interfaces >>> class URLAwarePage(confeitaria.interfaces.URLedPage): ... def index(self): ... return 'My URL is ' + self.get_url() >>> root = URLAwarePage() >>> root.sub = URLAwarePage() >>> with Server(root): ... requests.get('http://localhost:8000/').text ... requests.get('http://localhost:8000/sub').text u'My URL is /' u'My URL is /sub'
If a page has a bound method named set_requests() with one argument, this method will be called and the argument value will be an object representing the HTTP request being processed. This request object can given information, for example, about query parameters:
>>> class ActionPage(object): ... def set_request(self, request): ... self.request = request ... def index(self): ... return ( ... 'The action is ' + self.request.query_args['action'] ... ) >>> page = ActionPage() >>> with Server(page): ... requests.get('http://localhost:8000/?action=update').text u'The action is update'
The class confeitaria.interfaces.RequestedPage implements the set_request() method, as well as a get_request() to retrieve the set request:
>>> class ActionPage(confeitaria.interfaces.RequestedPage): ... def index(self): ... request = self.get_request() ... return ( ... 'The action is ' + request.query_args['action'] ... ) >>> page = ActionPage() >>> with Server(page): ... requests.get('http://localhost:8000/?action=update').text u'The action is update'
Cookies are the most standard way of recalling information between different requests from the same browser. Once a server sends instructos for setting cookies to a browser, the browser is expected to send this information back with each request.
If a page has a bound method named set_cookies() with one argument, this method will be called and the argument value will be an object representing a set of cookies. This cookies object should behave as the Cookie.SimpleCookie. Consider, for example, the cart page below - it stores the number of items in a cookie:
>>> class CartPage(object): ... def set_cookies(self, cookies): ... self.cookies = cookies ... def action(self, items=None): ... if items is not None: ... self.cookies['items'] = items ... def index(self): ... if 'items' in self.cookies: ... return 'You have {0} items'.format( ... self.cookies['items'].value ... ) ... else: ... return 'You have no items'
The results from using it is:
>>> page = CartPage() >>> with Server(page): ... requests.get('http://localhost:8000/').text ... r = requests.post( ... 'http://localhost:8000/', data={'items': '2'}, ... allow_redirects=False ... ) ... r.cookies['items'] ... requests.get('http://localhost:8000/', cookies=r.cookies).text u'You have no items' '2' u'You have 2 items'
If you extend confeitaria.interfaces.CookiedPage you will have a pair of methods to set a retrieve the cookies:
>>> class CartPage(confeitaria.interfaces.CookiedPage): ... def action(self, items=None): ... if items is not None: ... self.get_cookies()['items'] = items ... def index(self): ... if 'items' in self.get_cookies(): ... cookies = self.get_cookies() ... return 'You have {0} items'.format( ... self.get_cookies()['items'].value ... ) ... else: ... return 'You have no items' >>> page = CartPage() >>> with Server(page): ... requests.get('http://localhost:8000/').text ... r = requests.post( ... 'http://localhost:8000/', data={'items': '2'}, ... allow_redirects=False ... ) ... r.cookies['items'] ... requests.get('http://localhost:8000/', cookies=r.cookies).text u'You have no items' '2' u'You have 2 items'
If your page needs to preserve state between requests, you can use sessions. To get a session, just add a set_session() method to your page - as usual, it should receive an argument, which will be a dict-like session object:
>>> class SessionPage(object): ... def set_session(self, session): ... self.session = session ... def action(self, username=None): ... self.session['username'] = username ... def index(self): ... if 'username' in self.session: ... return 'User: {0}'.format(self.session['username']) ... else: ... return 'Not authenticated'
The received session is preserved between requests. The default implementation saves the session variables in memory only and preserve the session through cookies:
>>> page = SessionPage() >>> with Server(page): ... r = requests.get('http://localhost:8000/') ... r.text ... r = requests.post( ... 'http://localhost:8000/', data={'username': 'juju'}, ... cookies=r.cookies, allow_redirects=False ... ) ... requests.get('http://localhost:8000/', cookies=r.cookies).text u'Not authenticated' u'User: juju'
Note that there is no need to handle the cookies directly: in the server, Confeitaria takes care of this; in the client, the browser should handle it by itself.
You can also get the request's session by extending confeitaria.interfaces.SessionedPage. The method confeitaria.interfaces.SessionedPage.get_session() will return the session object:
>>> class SessionPage(confeitaria.interfaces.SessionedPage): ... def action(self, username=None): ... self.get_session()['username'] = username ... def index(self): ... if 'username' in self.get_session(): ... return 'User: {0}'.format(self.get_session()['username']) ... else: ... return 'Not authenticated' >>> page = SessionPage() >>> with Server(page): ... r = requests.get('http://localhost:8000/') ... r.text ... r = requests.post( ... 'http://localhost:8000/', data={'username': 'juju'}, ... cookies=r.cookies, allow_redirects=False ... ) ... requests.get('http://localhost:8000/', cookies=r.cookies).text u'Not authenticated' u'User: juju'
In practice, we almost always want to have access to the URL page, cookies, session and the request object. Instead of implementing all the required methods by hand, or extending all awareness interface classes, we can just extend the confeitaria.interfaces.Page class, and our page will be aware of all these informations. Consider, for example, an e-commerce page which stores the user in session and the cart in the cookies:
>>> class ShopPage(confeitaria.interfaces.Page): ... def index(self): ... result = "Welcome to {0}. ".format(self.get_url()) ... user = self.get_session().get('username', 'nobody') ... result += "You are {0}. ".format(user) ... if 'items' in self.get_cookies(): ... items = self.get_cookies()['items'].value ... else: ... items = 0 ... result += "You have {0} items.".format(items) ... return result ... def action(self, username=None, items=None): ... if username is not None: ... self.get_session()['username'] = username ... if items is not None: ... self.get_cookies()['items'] = items
It would yield the following results:
>>> page = ShopPage() >>> with Server(page): ... r = requests.get('http://localhost:8000/') ... r.text ... r = requests.post( ... 'http://localhost:8000/', data={'username': 'juju'}, ... cookies=r.cookies ... ) ... r.text ... r = requests.post( ... 'http://localhost:8000/', data={'items': '2'}, ... cookies=r.cookies ... ) ... r.text ... r.cookies['items'] u'Welcome to /. You are nobody. You have 0 items.' u'Welcome to /. You are juju. You have 0 items.' u'Welcome to /. You are juju. You have 2 items.' '2'
HTTP redirect responses are a common need. For example, you may want to redirect the browser to another URL to where the looked upon content was moved. You just need to raise the confeitaria.responses.MovedPermanently exception:
>>> import confeitaria.responses >>> class OldPage(object): ... def index(self): ... raise confeitaria.responses.MovedPermanently('/new') >>> class NewPage(object): ... def index(self): ... return 'page: new' >>> page = OldPage() >>> page.new = NewPage() >>> with Server(page): ... r = requests.get('http://localhost:8000/', allow_redirects=False) ... r.status_code ... r.headers['location'] 301 '/new' >>> with Server(page): ... r = requests.get('http://localhost:8000/') ... r.status_code ... r.text 200 u'page: new'
If, however, one wants to implement the POST-REDIRECT-GET pattern, it is better to use the SeeOther response:
>>> class LoginPage(object): ... username = None ... def index(self): ... if LoginPage.username is None: ... return 'Nobody is logged in.' ... else: ... return '{0} is logged in.'.format(LoginPage.username) ... def action(self, username=None): ... LoginPage.username = username ... raise confeitaria.responses.SeeOther('/') >>> with Server(LoginPage()): ... requests.get('http://localhost:8000/').text ... r = requests.post( ... 'http://localhost:8000/', data={'username': 'bob'} ... ) ... r.status_code ... r.text u'Nobody is logged in.' 200 u'bob is logged in.'
If no parameter is given to the SeeOther or MovedPermanently constructor, the browser will be redirected to the originally requested URL:
>>> class RedirectPage(object): ... def action(self, username=None): ... raise confeitaria.responses.SeeOther() >>> with Server(RedirectPage()): ... r = requests.post( ... 'http://localhost:8000/?a=b', allow_redirects=False ... ) ... r.status_code ... r.headers['location'] 303 '/?a=b'
However, one does not even need to raise the response: if an action method returns without raising any response, it will redirect to the original URL by default:
>>> class MagicRedirectPage(object): ... def action(self, username=None): ... pass >>> with Server(MagicRedirectPage()): ... r = requests.post( ... 'http://localhost:8000/?magic=true', allow_redirects=False ... ) ... r.status_code ... r.headers['location'] 303 '/?magic=true'
In Confeitaria, we try to follow some principles as much as possible. We do not know how much they are feasible or advantageus, they are not necessarily original and we are not saying you have to follow them. We will try, however.
We may add more principles, or give up some of them - that is acceptable. The main objective here, after all, is to discover what is possible to do.