Removed some leftovers from media.xsl.
[enkel.git] / enkel / batteri / rest_route.py
blob955878d375573cc2bfe548e32676f5d72c5cdb7e
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.
19 import re
21 from enkel.error.http import Http404
24 class RESTroute(object):
25 """ A RESTful routing middleware.
27 How it works
28 ============
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:
35 - myapps.get_input
36 - myapps.validate_input
38 We could create a route like this::
40 route = RESTroute()
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
47 myapp.get_input.
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::
54 route = RESTroute()
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
65 to the application.
68 The pattern
69 ===========
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
75 in the "path".
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}]"
90 An example
91 ==========
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)
111 GET /sub> ^/sub
113 >>> route.add("/test[/{d:year}]", GET=app)
114 GET /test[/{d:year}] ^/test(?:/(?P<year>\d+))?$
118 Testing the result
119 ------------------
120 >>> from enkel.wansgli.apptester import AppTester
121 >>> t = AppTester(route, [], "http://example.com/2050/jack")
122 >>> t.run_get().body
123 "([], {'user': 'jack', 'year': '2050'})"
125 >>> t = AppTester(route, [], "http://example.com/sub/hello")
126 >>> t.run_get().body
127 "([], {'message': 'hello'})"
129 >>> t = AppTester(route, [], "http://example.com/test/100000")
130 >>> t.run_get().body
131 "([], {'year': '100000'})"
133 >>> t = AppTester(route, [], "http://example.com/test")
134 >>> t.run_get().body
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
152 with L{add}.
153 @cvar types: A mapping of keywords to regular-expressions. Every
154 node is a pair of (type-identifyer, regular-expression).
155 Type-identifyers:
156 - d: A whole number.
157 - w: Word. 0-9, a-z, A-Z and '_'.
158 - f: Filename. 0-9, a-z, A-Z, '_', '.' and '-'.
159 - a: a-z and A-Z.
160 - date: iso date. yyyy-mm-dd.
161 - year: matches 4 digits.
163 types = dict(
164 d = r"\d+",
165 w = r"\w+",
166 a = r"[a-zA-Z]+",
167 f = r"[a-zA-Z0-9_.-]+",
168 date = r"\d{4}-\d{2}-\d{2}",
169 year = r"\d{4}"
171 type_expr = re.compile("\{([a-z]+):(\w+)\}")
172 opt_expr = re.compile("\[([^\]]+)\]")
173 def __init__(self):
174 self.GET = []
175 self.POST = []
176 self.DELETE = []
177 self.PUT = []
178 self.echo = False
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()
186 try:
187 e = self.types[t]
188 except KeyError:
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}.
195 Some doctest
196 ============
197 >>> r = RESTroute()
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/>")
203 '^/archives/'
205 >>> r._patt_to_re("*/{d:id}")
206 '.*/(?P<id>\\\d+)$'
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("*"):
214 e = "." + e
215 else:
216 e = "^" + e
217 if patt.endswith(">"):
218 e = e[:-1]
219 else:
220 e += "$"
221 return e
223 def add(self, patt, GET=None, POST=None, PUT=None, DELETE=None, env={}):
224 r = self._patt_to_re(patt)
225 l = locals()
226 for method in "GET", "POST", "PUT", "DELETE":
227 app = l[method]
228 if app:
229 getattr(self, method).append((patt, app, re.compile(r), env))
230 if self.echo:
231 print "%s %s %s" % (method, patt, r)
233 def __call__(self, env, start_response):
234 path = env["SCRIPT_NAME"] + env["PATH_INFO"]
235 match = None
236 for p, app, r, extra_env in getattr(self, env["REQUEST_METHOD"]):
237 match = r.match(path)
238 if match:
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)
247 def suite():
248 import doctest
249 return doctest.DocTestSuite()
251 if __name__ == "__main__":
252 from enkel.wansgli.testhelpers import run_suite
253 run_suite(suite())