Merge lp:~openerp-dev/openobject-server/trunk-multirouters-niv into lp:openobject-server

Proposed by Nicolas Vanhoren (OpenERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-server/trunk-multirouters-niv
Merge into: lp:openobject-server
Diff against target: 446 lines (+221/-139) (has conflicts)
1 file modified
openerp/http.py (+221/-139)
Text conflict in openerp/http.py
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-multirouters-niv
Reviewer Review Type Date Requested Status
Xavier (Open ERP) Pending
Review via email: mp+193285@code.launchpad.net

Description of the change

Allows to define and register multiple "routers" in the http.py API.

Routers are objects that are used *after* the http.py api handled the session management and the database detection. Routers are dedicated to handle with a request (guess what method to call and call it). A router is always linked to a specific database and a new set of routers will be created for each database. This includes the "None" database, which means the cases when the server has to perform requests without knowing the current database.

Multiple routers creators functions can be defined, which means that multiple routers will exist per database. When a request arrives, each router will be tried one after the other and each time they return None we switch to the next one.

As is the behavior is almost completely unchanged. The logic that was in the Root class has moved in the WerkzeugRouter class. That last class is designed to be extended to create new routers with the same basic features and added functionality.

To post a comment you must log in.
4989. By Nicolas Vanhoren (OpenERP)

Fix problem with controllers

Unmerged revisions

4989. By Nicolas Vanhoren (OpenERP)

Fix problem with controllers

4988. By Nicolas Vanhoren (OpenERP)

Added magic to be able to create new Controller classes

4987. By Nicolas Vanhoren (OpenERP)

Architecture change to be able to detect if an url is routable without actually executing the action.

4986. By Nicolas Vanhoren (OpenERP)

Now trying multiple routers seems to work correctly

4985. By Nicolas Vanhoren (OpenERP)

Added router registerer

4984. By Nicolas Vanhoren (OpenERP)

Extracted the routing in the WerkzeugRouter class

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openerp/http.py'
2--- openerp/http.py 2013-10-30 18:24:00 +0000
3+++ openerp/http.py 2013-10-31 11:27:41 +0000
4@@ -486,35 +486,45 @@
5 #----------------------------------------------------------
6 addons_module = {}
7 addons_manifest = {}
8-controllers_per_module = {}
9-
10-class ControllerType(type):
11- def __init__(cls, name, bases, attrs):
12- super(ControllerType, cls).__init__(name, bases, attrs)
13-
14- # flag old-style methods with req as first argument
15- for k, v in attrs.items():
16- if inspect.isfunction(v):
17- spec = inspect.getargspec(v)
18- first_arg = spec.args[1] if len(spec.args) >= 2 else None
19- if first_arg in ["req", "request"]:
20- v._first_arg_is_req = True
21-
22- # store the controller in the controllers list
23- name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
24- class_path = name_class[0].split(".")
25- if not class_path[:2] == ["openerp", "addons"]:
26- module = ""
27- else:
28- # we want to know all modules that have controllers
29- module = class_path[2]
30- # but we only store controllers directly inheriting from Controller
31- if not "Controller" in globals() or not Controller in bases:
32- return
33- controllers_per_module.setdefault(module, []).append(name_class)
34-
35-class Controller(object):
36- __metaclass__ = ControllerType
37+
38+def create_controller_class():
39+ controllers = {}
40+
41+ TmpController = None
42+
43+ class ControllerType(type):
44+ def __init__(cls, name, bases, attrs):
45+ super(ControllerType, cls).__init__(name, bases, attrs)
46+
47+ # flag old-style methods with req as first argument
48+ for k, v in attrs.items():
49+ if inspect.isfunction(v):
50+ spec = inspect.getargspec(v)
51+ first_arg = spec.args[1] if len(spec.args) >= 2 else None
52+ if first_arg in ["req", "request"]:
53+ v._first_arg_is_req = True
54+
55+ # store the controller in the controllers list
56+ name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
57+ class_path = name_class[0].split(".")
58+ if not class_path[:2] == ["openerp", "addons"]:
59+ module = ""
60+ else:
61+ # we want to know all modules that have controllers
62+ module = class_path[2]
63+ # but we only store controllers directly inheriting from Controller
64+ if TmpController is None or not TmpController in bases:
65+ return
66+ controllers.setdefault(module, []).append(name_class)
67+
68+ class Controller(object):
69+ __metaclass__ = ControllerType
70+
71+ controllers_per_module = controllers
72+
73+ TmpController = Controller
74+
75+ return Controller
76
77 #----------------------------------------------------------
78 # HTTP Sessions
79@@ -848,10 +858,9 @@
80 """Root WSGI application for the OpenERP Web Client.
81 """
82 def __init__(self):
83- self.addons = {}
84 self.statics = {}
85
86- self.no_db_router = None
87+ self.no_db_routers = None
88
89 self.load_addons()
90
91@@ -860,7 +869,6 @@
92 self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
93 _logger.debug('HTTP sessions stored in: %s', path)
94
95-
96 def __call__(self, environ, start_response):
97 """ Handle a WSGI request
98 """
99@@ -870,44 +878,45 @@
100 """
101 Performs the actual WSGI dispatching for the application.
102 """
103+ httprequest = werkzeug.wrappers.Request(environ)
104+ httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
105+ httprequest.app = self
106+
107+ session_gc(self.session_store)
108+
109+ sid = httprequest.args.get('session_id')
110+ explicit_session = True
111+ if not sid:
112+ sid = httprequest.headers.get("X-Openerp-Session-Id")
113+ if not sid:
114+ sid = httprequest.cookies.get('session_id')
115+ explicit_session = False
116+ if sid is None:
117+ httprequest.session = self.session_store.new()
118+ else:
119+ httprequest.session = self.session_store.get(sid)
120+
121+ db = self._find_db(httprequest)
122+
123+ if not "lang" in httprequest.session.context:
124+ lang = httprequest.accept_languages.best or "en_US"
125+ lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
126+ httprequest.session.context["lang"] = lang
127+
128+ if db:
129+ openerp.modules.registry.RegistryManager.check_registry_signaling(db)
130+
131 try:
132- httprequest = werkzeug.wrappers.Request(environ)
133- httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
134- httprequest.app = self
135-
136- session_gc(self.session_store)
137-
138- sid = httprequest.args.get('session_id')
139- explicit_session = True
140- if not sid:
141- sid = httprequest.headers.get("X-Openerp-Session-Id")
142- if not sid:
143- sid = httprequest.cookies.get('session_id')
144- explicit_session = False
145- if sid is None:
146- httprequest.session = self.session_store.new()
147+ routers = self.get_db_routers(db)
148+ func = None
149+ for el in routers:
150+ func = el.route(httprequest)
151+ if func is not None:
152+ break
153+ if func is None:
154+ return werkzeug.exceptions.NotFound()(environ, start_response)
155 else:
156- httprequest.session = self.session_store.get(sid)
157-
158- self._find_db(httprequest)
159-
160- if not "lang" in httprequest.session.context:
161- lang = httprequest.accept_languages.best or "en_US"
162- lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
163- httprequest.session.context["lang"] = lang
164-
165- request = self._build_request(httprequest)
166- db = request.db
167-
168- if db:
169- openerp.modules.registry.RegistryManager.check_registry_signaling(db)
170-
171- with set_request(request):
172- self.find_handler()
173- result = request.dispatch()
174-
175- if db:
176- openerp.modules.registry.RegistryManager.signal_caches_change(db)
177+ result = func()
178
179 if isinstance(result, basestring):
180 headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
181@@ -927,23 +936,16 @@
182 response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
183
184 return response(environ, start_response)
185- except werkzeug.exceptions.HTTPException, e:
186- return e(environ, start_response)
187+ finally:
188+ if db:
189+ openerp.modules.registry.RegistryManager.signal_caches_change(db)
190
191 def _find_db(self, httprequest):
192 db = db_monodb(httprequest)
193 if db != httprequest.session.db:
194 httprequest.session.logout()
195 httprequest.session.db = db
196-
197- def _build_request(self, httprequest):
198- if httprequest.args.get('jsonp'):
199- return JsonRequest(httprequest)
200-
201- if httprequest.mimetype == "application/json":
202- return JsonRequest(httprequest)
203- else:
204- return HttpRequest(httprequest)
205+ return db
206
207 def load_addons(self):
208 """ Load all addons from addons patch containg static files and
209@@ -969,13 +971,99 @@
210 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
211 self.dispatch = DisableCacheMiddleware(app)
212
213- def _build_router(self, db):
214+ def get_db_routers(self, db):
215+ if db is None:
216+ routers = self.no_db_routers
217+ else:
218+ routers = getattr(openerp.modules.registry.RegistryManager.get(db), "http_routers", None)
219+ if not routers:
220+ routers = [x(db) for x in _router_registerers]
221+ routers = [x for x in routers if x is not None]
222+ routers.sort()
223+ routers = [x[1] for x in routers]
224+ if db is None:
225+ self.no_db_routers = routers
226+ else:
227+ openerp.modules.registry.RegistryManager.get(db).http_routers = routers
228+ return routers
229+
230+def db_list(force=False, httprequest=None):
231+ httprequest = httprequest or request.httprequest
232+ dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
233+ h = httprequest.environ['HTTP_HOST'].split(':')[0]
234+ d = h.split('.')[0]
235+ r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
236+ dbs = [i for i in dbs if re.match(r, i)]
237+ return dbs
238+
239+def db_monodb(httprequest=None):
240+ """
241+ Magic function to find the current database.
242+
243+ Implementation details:
244+
245+ * Magic
246+ * More magic
247+
248+ Returns ``None`` if the magic is not magic enough.
249+ """
250+ httprequest = httprequest or request.httprequest
251+ db = None
252+ redirect = None
253+
254+ dbs = db_list(True, httprequest)
255+
256+ # try the db already in the session
257+ db_session = httprequest.session.db
258+ if db_session in dbs:
259+ return db_session
260+
261+ # if dbfilters was specified when launching the server and there is
262+ # only one possible db, we take that one
263+ if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
264+ return dbs[0]
265+ return None
266+
267+class Router(object):
268+ """
269+ Base class for router objects.
270+ """
271+ def __init__(self, db_name):
272+ """
273+ :param basestring db_name: The name of the database or ``None``. ``None`` means that the
274+ router will be used when no database could be detected.
275+ """
276+ self.db_name = db_name
277+
278+ def route(self, httprequest):
279+ """
280+ This method is called to find a function to call for the request.
281+
282+ :param werkzeug.wrappers.Request httprequest: The HTTP request.
283+ :returns: A function to be called representing the function to call to fulfill the request.
284+ ``None`` can also be returned. In that cases it represents that the router was not able to
285+ correctly route the request and indicates that other routers can be tried to see if the
286+ request match one of them.
287+ """
288+ raise Exception("Not Implemented")
289+
290+Controller = create_controller_class()
291+
292+class WerkzeugRouter(Router):
293+
294+ def __init__(self, db_name):
295+ self.controller_class = Controller
296+ super(WerkzeugRouter, self).__init__(db_name)
297+ self._build_router()
298+
299+ def _build_router(self):
300+ db = self.db_name
301 _logger.info("Generating routing configuration for database %s" % db)
302 routing_map = routing.Map(strict_slashes=False)
303
304 def gen(modules, nodb_only):
305 for module in modules:
306- for v in controllers_per_module[module]:
307+ for v in self.controller_class.controllers_per_module[module]:
308 cls = v[1]
309
310 subclasses = cls.__subclasses__()
311@@ -997,11 +1085,12 @@
312 url = url[: -1]
313 routing_map.add(routing.Rule(url, endpoint=mv))
314
315- modules_set = set(controllers_per_module.keys()) - set(['', 'web'])
316+ modules_set = set(self.controller_class.controllers_per_module.keys()) - set(['', 'web'])
317 # building all none methods
318 gen(['', "web"] + sorted(modules_set), True)
319 if not db:
320- return routing_map
321+ self.routing_map = routing_map
322+ return
323
324 registry = openerp.modules.registry.RegistryManager.get(db)
325 with registry.cursor() as cr:
326@@ -1013,28 +1102,21 @@
327 # building all other methods
328 gen(['', "web"] + sorted(modules_set), False)
329
330- return routing_map
331-
332- def get_db_router(self, db):
333- if db is None:
334- router = self.no_db_router
335- else:
336- router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None)
337- if not router:
338- router = self._build_router(db)
339- if db is None:
340- self.no_db_router = router
341- else:
342- openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router
343- return router
344-
345- def find_handler(self):
346- """
347- Tries to discover the controller handling the request for the path specified in the request.
348- """
349+ self.routing_map = routing_map
350+
351+ def route(self, httprequest):
352+ request = self._build_request(httprequest)
353 path = request.httprequest.path
354+<<<<<<< TREE
355 urls = self.get_db_router(request.db).bind_to_environ(request.httprequest.environ)
356 func, arguments = urls.match(path)
357+=======
358+ urls = self.routing_map.bind("")
359+ try:
360+ func, arguments = urls.match(path)
361+ except werkzeug.exceptions.NotFound:
362+ return None
363+>>>>>>> MERGE-SOURCE
364 arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
365
366 @service_model.check
367@@ -1053,43 +1135,43 @@
368 request.func = nfunc
369 request.auth_method = getattr(func, "auth", "user")
370 request.func_request_type = func.exposed
371-
372-def db_list(force=False, httprequest=None):
373- httprequest = httprequest or request.httprequest
374- dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
375- h = httprequest.environ['HTTP_HOST'].split(':')[0]
376- d = h.split('.')[0]
377- r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
378- dbs = [i for i in dbs if re.match(r, i)]
379- return dbs
380-
381-def db_monodb(httprequest=None):
382- """
383- Magic function to find the current database.
384-
385- Implementation details:
386-
387- * Magic
388- * More magic
389-
390- Returns ``None`` if the magic is not magic enough.
391- """
392- httprequest = httprequest or request.httprequest
393- db = None
394- redirect = None
395-
396- dbs = db_list(True, httprequest)
397-
398- # try the db already in the session
399- db_session = httprequest.session.db
400- if db_session in dbs:
401- return db_session
402-
403- # if dbfilters was specified when launching the server and there is
404- # only one possible db, we take that one
405- if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1:
406- return dbs[0]
407- return None
408+ def perform_request():
409+ try:
410+ with set_request(request):
411+ result = request.dispatch()
412+ return result
413+ except werkzeug.exceptions.HTTPException, e:
414+ return e(environ, start_response)
415+ return perform_request
416+
417+ def _build_request(self, httprequest):
418+ if httprequest.args.get('jsonp'):
419+ return JsonRequest(httprequest)
420+
421+ if httprequest.mimetype == "application/json":
422+ return JsonRequest(httprequest)
423+ else:
424+ return HttpRequest(httprequest)
425+
426+_router_registerers = []
427+
428+def router_registerer(func):
429+ """
430+ A decorator to put on a function to make it able to produce routers that will be registered in the
431+ routing framework.
432+
433+ :param function func: The function that will produce routers. It must take one argument. That argument
434+ will be the name of the database on which the router is created. The database name can be ``None``, which
435+ means the database name could not be guessed for the request. The function must return a tuple with
436+ 2 parameters. The first one is the priority (lower means the router will be tryied before other routers).
437+ The second one is the router which must extend the ``Router`` class.
438+ """
439+ _router_registerers.append(func)
440+ return func
441+
442+@router_registerer
443+def _create_werkzeug_router(db):
444+ return (-1000, WerkzeugRouter(db))
445
446 class CommonController(Controller):
447