1 # This file is part of the Enkel web programming library.
3 # Copyright (C) 2007 Espen Angell Kristiansen (espeak@users.sourceforge.net)
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 from enkel
.error
.http
import Http404
24 class RESTroute(object):
25 """ A RESTful routing middleware.
29 RESTroute is responsible for forwarding requests to
30 apps based on the REQUEST_METHOD and the "path" part of
31 the url. With "path" we mean SCRIPT_NAME + PATH_INFO which
32 are both WSGI environ variables (and defined in PEP 333).
34 So lets say we have two wsgi applications named:
36 - myapps.validate_input
38 We could create a route like this::
41 route.add("/input", GET=myapps.get_input)
42 route.add("/validate", POST=myapps.validate_input)
44 Now lets say our server is "https://example.com" and
45 the "route" app is running there. A user visiting
46 "https://example.com/input" would be routed/directed to
49 Now let us assume this in an internationalized website and we
50 use the URL to identify the preferred language. We could make
51 the routing app place the preferred language in the WSGI
52 environ variable "wsgiorg.routing_args" like this::
55 route.add("/{w:lang}/input", GET=myapps.get_input)
56 route.add("/{w:lang}/validate", POST=myapps.validate_input)
58 The {w:lang} is the pattern language at work. "w" is called the
59 "type-identifyer" and is defined L{here <types>}. "lang" is the
60 name of the "wsgiorg.routing_args" keyword created when the
61 url is matched. When a used visits "https:/example.com/en/input,
62 they would get the english version of our app.. Or at least
63 the application would be notified that english is the preferred
64 language. What to do with this information is of course up
70 The pattern sent as first argument to L{add} is already
71 described above. But there are a litte bit more to it.
73 A pattern can start with a "*". This means that it the
74 pattern does not start matching at the first character
77 It can also end with ">". This means that the pattern does
78 not match until the end of the "path". So both with
79 the pattern "/home/test>", both "http://example.com/home/test"
80 and "http://example.com/home/testing/my/car" would be matched.
82 The two mechanisms described are mainly intended to enable
83 nested routes (one route containing other routes). This
84 is used in the example below.
86 You can also specify optional parts of the pattern by
87 enclosing them in []. Like this: "/{age}[/{gender}]"
92 We use L{echo} to see what's going on behind the scenes.
93 Anyone with a basic understanding of regular-expressions
94 should be able to understand the regular expressions
95 generated from patterns.
97 >>> def app(env, start_response):
98 ... start_response("200 OK", [("content-type", "text/plain")])
99 ... return [str(env["wsgiorg.routing_args"])]
100 >>> route = RESTroute()
101 >>> route.echo = True
102 >>> subroute = RESTroute()
103 >>> subroute.echo = True
105 >>> route.add("/{d:year}/{a:user}", GET=app)
106 GET /{d:year}/{a:user} ^/(?P<year>\d+)/(?P<user>[a-zA-Z]+)$
108 >>> subroute.add("*/{w:message}", GET=app)
109 GET */{w:message} .*/(?P<message>\w+)$
110 >>> route.add("/sub>", GET=subroute)
113 >>> route.add("/test[/{d:year}]", GET=app)
114 GET /test[/{d:year}] ^/test(?:/(?P<year>\d+))?$
120 >>> from enkel.wansgli.apptester import AppTester
121 >>> t = AppTester(route, [], "http://example.com/2050/jack")
123 "([], {'user': 'jack', 'year': '2050'})"
125 >>> t = AppTester(route, [], "http://example.com/sub/hello")
127 "([], {'message': 'hello'})"
129 >>> t = AppTester(route, [], "http://example.com/test/100000")
131 "([], {'year': '100000'})"
133 >>> t = AppTester(route, [], "http://example.com/test")
135 "([], {'year': None})"
138 Adding your own types
139 =====================
140 An example of adding a type which matches our own narrow
141 view of what is an animal.. We only look cow, tiger and dog:)
143 >>> route = RESTroute()
144 >>> route.types["animal"] = "(cow|tiger|dog)"
145 >>> route.echo = True
146 >>> route.add("/{animal:pet}", app)
147 GET /{animal:pet} ^/(?P<pet>(cow|tiger|dog))$
151 @ivar echo: If True, print some info about the patterns added
153 @cvar types: A mapping of keywords to regular-expressions. Every
154 node is a pair of (type-identifyer, regular-expression).
157 - w: Word. 0-9, a-z, A-Z and '_'.
158 - f: Filename. 0-9, a-z, A-Z, '_', '.' and '-'.
160 - date: iso date. yyyy-mm-dd.
161 - year: matches 4 digits.
167 f
= r
"[a-zA-Z0-9_.-]+",
168 date
= r
"\d{4}-\d{2}-\d{2}",
171 type_expr
= re
.compile("\{([a-z]+):(\w+)\}")
172 opt_expr
= re
.compile("\[([^\]]+)\]")
181 def _opt_to_re(self
, matchobj
):
182 return "(?:%s)?" % matchobj
.group(1)
184 def _type_to_re(self
, matchobj
):
185 t
, name
= matchobj
.groups()
189 raise ValueError("unsupported type-identifyer: %s" % t
)
190 return "(?P<%s>%s)" % (name
, e
)
192 def _patt_to_re(self
, patt
):
193 """ Converts "patt" to regular expression. Used in L{add}.
199 >>> r._patt_to_re("/archives/{d:year}/{w:name}/{a:id}")
200 '^/archives/(?P<year>\\\d+)/(?P<name>\\\w+)/(?P<id>[a-zA-Z]+)$'
202 >>> r._patt_to_re("/archives/>")
205 >>> r._patt_to_re("*/{d:id}")
208 >>> r._patt_to_re("/home[/a_{w:user}]")
209 '^/home(?:/a_(?P<user>\\\w+))?$'
211 e
= self
.opt_expr
.sub(self
._opt
_to
_re
, patt
)
212 e
= self
.type_expr
.sub(self
._type
_to
_re
, e
)
213 if patt
.startswith("*"):
217 if patt
.endswith(">"):
223 def add(self
, patt
, GET
=None, POST
=None, PUT
=None, DELETE
=None, env
={}):
224 r
= self
._patt
_to
_re
(patt
)
226 for method
in "GET", "POST", "PUT", "DELETE":
229 getattr(self
, method
).append((patt
, app
, re
.compile(r
), env
))
231 print "%s %s %s" % (method
, patt
, r
)
233 def __call__(self
, env
, start_response
):
234 path
= env
["SCRIPT_NAME"] + env
["PATH_INFO"]
236 for p
, app
, r
, extra_env
in getattr(self
, env
["REQUEST_METHOD"]):
237 match
= r
.match(path
)
239 kw
= match
.groupdict()
240 env
["wsgiorg.routing_args"] = ([], kw
)
241 env
.update(extra_env
)
242 return app(env
, start_response
)
243 raise Http404(long_message
=path
)
249 return doctest
.DocTestSuite()
251 if __name__
== "__main__":
252 from enkel
.wansgli
.testhelpers
import run_suite