I have a Flask app that handles a range of URLs. I'm splitting it up into multiple handler modules, where the handler depends on the first element of the path. The mapping between path/URL prefixes and handlers is a bit like this:
/one/... => Handler A
/two/... => Handler A
/three/... => Handler B
/four/... => Handler B
Calling a given URL under /one/... gets you something very similar (though not identical) to the same URL under /two/ - hence the desire to use the same handler for both those sets of URLs. At the same time Handler A does something very different to Handler B, therefore the desire to implement a clear separation, with separate modules for each.
Blueprints seem to be a great way to do this - and for the most part appear to work well. Where I'm struggling is in setting up differentiated behavior for /one/ vs /two/ (and /three/ vs /four/). In other words, exposing the actual URL prefix to the handler.
As an example handler A looks like
# handler_A.py
from flask import Blueprint
handler_A = Blueprint('handler_A', __name__)
#handler_A('/somepage', methods=['GET'])
def get_page():
return "You've reached somepage provided to you by handler A"
And handler B has a similar structure (but does something very different).
Then the app looks like
# app.py
from flask import Flask
from handler_A import handler_A
from handler_B import handler_B
app = Flask(__name__)
app.register_blueprint(handler_A, url_prefix='/one')
app.register_blueprint(handler_A, url_prefix='/two')
app.register_blueprint(handler_B, url_prefix='/three')
app.register_blueprint(handler_B, url_prefix='/four')
The part that I can't seem to do "nicely" is figuring out whether an endpoint within Handler A (for example) was called from a URL prefixed with /one/ or /two/. This is an important distinction for me, though as soon as the handler is called that information is obscured. I've looked through the docs but can't find a clean way to do this.
The following are the options I've thought of/attempted so far:
Grabbing request.path from within the handler and pulling out the prefix from the string. This is simple and it works, but seems awkward
Setting up a separate blueprint for each top-level path, and "merging" the execution flow from four blueprints into the two handlers I have
Making the top-level of the URL be a parameter that's recorded in the context (like in https://flask.palletsprojects.com/en/1.1.x/patterns/urlprocessors/#internationalized-blueprint-urls). However I think this also requires me to write a custom URL processor, if I want "one" and "two" to match one url_prefix, while "two" and "three" match another.
Is there smart way to do this that I'm missing?
To me the request.path is a good solution. It's awkward in that it's an implicit parameter of the function; the only difference between this and the solution you're looking for is this explicit vs. implicit parameters. I think this method albeit imperfect is more readable than the complexities required to make this explicit.
If you want, you could extract the core function and pass it the results of the request.path s.t. it's more explicit.
#handler_A('/somepage', methods=['GET'])
def getPage():
return pageForPath(request.path)
def pageForPath(path):
return '<html> .... path ... </html>
Related
I have a web2py website that has two applications: the standard admin application and a custom 'myapp' application. For the sake of discussion we will say that the website lives at 'sub.projdomain.com'. I have been trying for hours to wrap my head around routes and python regular expressions, but I cannot seem to nail the functionality I desire. I am attempting to replicate the following behavior:
URL in address bar Effective URL
------------------ -------------
sub.projdomain.com sub.projdomain.com/myapp/default/index
sub.projdomain.com/ sub.projdomain.com/myapp/default/index
sub.projdomain.com/foo sub.projdomain.com/myapp/foo/index
sub.projdomain.com/foo?p=1 sub.projdomain.com/myapp/foo/index?p=1
sub.projdomain.com/foo/?p=2 sub.projdomain.com/myapp/foo/index?p=2
sub.projdomain.com/foo/bar?p=3 sub.projdomain.com/myapp/foo/bar?p=3
sub.projdomain.com/foo/bar/?p=4 sub.projdomain.com/myapp/foo/bar?p=4
sub.projdomain.com/a sub.projdomain.com/admin/default/site
I always seem to have the problem where I call URL('login') or URL('/login') from the page "sub.projdomain.com/foo/bar/" and get a bad URL of the form "sub.projdomain.com/foo/login/" or something similar. Currently I am setting the 'routes_in' and 'routes_out' tuples because I couldn't get it working with 'routers'. I feel like I am overlooking what should be a very simple set of routes. Am I overthinking the problem?
---- UPDATE ----
Alright, I think I managed to figure out a set of routes to get the behavior I want, but I would really like to know if this is the correct way to accomplish it.
routes_in = (
#Map the default index to '/'
('/', '/myapp/default/index'),
#Allow the admin application to be accessed
('/a', '/admin/default/index'),
('/a/$anything', '/admin/$anything'),
#Re-wire actions as controllers unless the controller is also specified
('/$controller', '/myapp/$controller/index'),
('/$controller/', '/myapp/$controller/index'),
('/$controller/$action', '/myapp/$controller/$action'),
#Map static access to our app
('/static/$anything', '/myapp/static/$anything'),
)
routes_out = [(x, y) for (y, x) in routes_in]
This setup requires that I carefully construct the parameters that I pass to URL(..). I would like it to be easier to predict what URL(..) produces, but I doubt it gets any simpler than this... any help is appreciated!
You should be able to achieve roughly the same outcome using the parameter-based rewrite system:
routers = dict(
BASE=dict(
default_application='myapp',
default_controller='default',
default_function='index',
functions=dict(
default=['list', 'of', 'functions', 'in', 'default.py'],
foo=['list', 'of', 'functions', 'in', 'foo.py']
)
),
admin=dict(
default_function='site',
functions=['list', 'of', 'functions', 'in', 'default.py']
)
)
The only difference is the "admin" app will still require "admin" in the URL (though I suppose you could try changing the name of the app itself by renaming its folder to "a").
Note, you only need to list the functions in the controllers if you want to remove "index" (or "site" in the case of admin) when the URL has additional args appended to it (the router needs to be able to distinguish between URL args and controller functions).
Alright, I think I figured this out. I have yet to come across a case whereby URL() fails to generate the correct string. I believe that this could be simplified further, but here it goes:
routes_in = (
#Map the default index to '/'
('/', '/myapp/default/index'),
#Allow the admin application to be accessed
('/a', '/a/default/index'),
('/a/$anything', '/a/$anything'),
#Re-wire actions as controllers unless the controller is also specified
('/$controller', '/myapp/$controller/index'),
('/$controller/', '/myapp/$controller/index'),
('/$controller/$action', '/myapp/$controller/$action'),
#Map static access to our application
('/static/$anything', '/myapp/static/$anything'),
)
#Reverse only some of the mappings so that URL() works correctly
routes_out = (
('/myapp/default/index', '/'),
('/a/$anything', '/a/$anything'),
('/myapp/$controller/index', '/$controller'),
('/myapp/$controller/$action', '/$controller/$action'),
('/myapp/static/$anything', '/static/$anything'),
)
I renamed the 'admin' application to 'a'. Anytime I call URL() I must pass explicitly 2 parameters. The first parameter is always the name of the controller, and the second parameter is always the name of the action following by any GET request variables. E.g. URL('default', 'index') for the main index page or URL('foo', 'bar?param=3') for action bar in foo with param specified. However, URL('foo', '?indexParam=test'), is not allowed since index is not specified before the variable in the second parameter. As long as I follow these rules everything seems to work consistently.
I am open to any other suggestions, but this is my accepted answer. I hope this helps someone else going through the same dilemma.
This is a relatively straightforward question.
Is if possible for a method in views.py to dynamically throw back a URL that it caught in err and let a later handler process it. For example:
urls.py
urlpatterns = patterns('myapp.views',
url(r'^foo/(?P<fiz>\d+)/?$', too_broad_method,name="foo"),
url(r'^foo/bar/?$', just_right_method,name="foo"),
)
views.py
def too_broad_method(request,fiz=None):
if fiz == some_dynamic_value:
# under some runtime conditions fiz can equal bar
# Throw some exception to give the URL back??
else:
return process_it()
Lets say for example, /foo/bar should be caught and processed by too_broad_method if an item has the name bar but otherwise it should be processed by just_right_method.
For extra context, I am trying to catch urls of the form app_label/model_name, which doesn't follow any pattern. I'd like these to be caught first, before anything else, which means using a very broad regex.
(Edited since the entire premise of the question changed)
If you need to catch app_name/model_name URLs, my suggestion is that you generate your URL patterns dynamically. There's no reason you couldn't iterate through INSTALLED_APPS, get all available classes that inherit from models.Model, and create URL patterns in a list accordingly. Then you can feed that into the patterns function at the end.
Trying to inform the URL dispatcher that it was somehow "wrong" is misguided, as I've already explained, and you're solving the wrong problem. Instead, you should focus on configuring the URL patterns how you actually need them.
So I have a view set up as such:
class toi(FlaskView):
def index(self):
...
return render_template('home.html')
#route('/api/')
#route('/api/<int:urlgid>/')
#route('/api/<int:urlgid>/<int:urlper>/')
def APIInstruction(self, urlgid=None, urlper=None):
return render_template('toi-instructions.html')
And then in my main app.py I have
from views.toi import toi
toi.register(app)
And then in the HTML that toi:index is ouputting I have
... how to use the API ...
This gives me a BuildError (no further details) and I've been pulling my hair out trying to figure this out. If I remove the #routes the error goes away. If I get rid of the 2nd and 3rd #routes it does not give me a builderror. If I put the urlgid and urlper in the url_for() function it does not change anything. I've tried changing the endpoints, I've tried changing url_for to toi:api.
I am not sure what is wrong to cause this BuildError.
When you use multiple routes for a single view, what happens is multiple endpoints get created (one for each route). In order to help you distinguish between each endpoint, Flask-Classy will append an index to the end of the expected route name. The order is from 0 to n starting from the last route defined. So given your example:
#route('/api/') # use: "toi:APIInstruction_2"
#route('/api/<int:urlgid>/') # use: "toi:APIInstruction_1"
#route('/api/<int:urlgid>/<int:urlper>/') # use: "toi:APIInstruction_0"
def APIInstruction(self, urlgid=None, urlper=None):
return render_template('toi-instructions.html')
You can read more about this behavior here:
http://pythonhosted.org/Flask-Classy/#using-multiple-routes-for-a-single-view
Alternatively (this is the method I prefer) you can specify the endpoint you want to use explicitly in any #route decorator. For example:
#route('/api/', endpoint='apibase')
Will be accessible using:
url_for('apibase')
Let's say I have a route '/foo/bar/baz'.
I would also like to have another view corresponding to '/foo' or '/foo/'.
But I don't want to systematically append trailing slashes for other routes, only for /foo and a few others (/buz but not /biz)
From what I saw I cannot simply define two routes with the same route_name.
I currently do this:
config.add_route('foo', '/foo')
config.add_route('foo_slash', '/foo/')
config.add_view(lambda _,__: HTTPFound('/foo'), route_name='foo_slash')
Is there something more elegant in Pyramid to do this ?
Pyramid has a way for HTTPNotFound views to automatically append a slash and test the routes again for a match (the way Django's APPEND_SLASH=True works). Take a look at:
http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html#redirecting-to-slash-appended-routes
As per this example, you can use config.add_notfound_view(notfound, append_slash=True), where notfound is a function that defines your HTTPNotFound view. If a view is not found (because it didn't match due to a missing slash), the HTTPNotFound view will append a slash and try again. The example shown in the link above is pretty informative, but let me know if you have any additional questions.
Also, heed the warning that this should not be used with POST requests.
There are also many ways to skin a cat in Pyramid, so you can play around and achieve this in different ways too, but you have the concept now.
Found this solution when I was looking for the same thing for my project
def add_auto_route(config,name, pattern, **kw):
config.add_route(name, pattern, **kw)
if not pattern.endswith('/'):
config.add_route(name + '_auto', pattern + '/')
def redirector(request):
return HTTPMovedPermanently(request.route_url(name))
config.add_view(redirector, route_name=name + '_auto')
And then during route configuration,
add_auto_route(config,'events','/events')
Rather than doing config.add_route('events','/events')
Basically it is a hybrid of your methods. A new route with name ending in _auto is defined and its view redirects to the original route.
EDIT
The solution does not take into account dynamic URL components and GET parameters. For a URL like /abc/{def}?m=aasa, using add_auto_route() will throw a key error because the redirector function does not take into account request.matchdict. The below code does that. To access GET parameters it also uses _query=request.GET
def add_auto_route(config,name, pattern, **kw):
config.add_route(name, pattern, **kw)
if not pattern.endswith('/'):
config.add_route(name + '_auto', pattern + '/')
def redirector(request):
return HTTPMovedPermanently(request.route_url(name,_query=request.GET,**request.matchdict))
config.add_view(redirector, route_name=name + '_auto')
I found another solution. It looks like we can chain two #view_config. So this solution is possible:
#view_config(route_name='foo_slash', renderer='myproject:templates/foo.mako')
#view_config(route_name='foo', renderer='myproject:templates/foo.mako')
def foo(request):
#do something
Its behavior is also different from the question. The solution from the question performs a redirect, so the url changes in the browser. In the second form both /foo and /foo/ can appear in the browser, depending on what the user entered. I don't really mind, but repeating the renderer path is also awkward.
In my Pylons app, some content is located at URLs that look like http://mysite/data/31415. Users can go to that URL directly, or search for "31415" via the search page. My constraints, however, mean that http://mysite/data/000031415 should go to the same page as the above, as should searches for "0000000000031415." Can I strip leading zeroes from that string in Routes itself, or do I need to do that substitution in the controller file? If it's possible to do it in routing.py, I'd rather do it there - but I can't quite figure it out from the documentation that I'm reading.
You can actually do that via conditional functions, since they let you modify the variables from the URL in place.
I know I am cheating by introducing a different routing library, since I haven't used Routes, but here's how this is done with Werkzeug's routing package. It lets you specify that a given fragment of the path is an integer. You can also implement a more specialized "converter" by inheriting werkzeug.routing.BaseConverter, if you wanted to parse something more interesting (e.g. a UUID).
Perhaps, Routes has a similar mechanism in place for specialized path-fragment-parsing needs.
import unittest
from werkzeug.routing import Map, Rule
class RoutingWithInts(unittest.TestCase):
m = Map([Rule('/data/<int:record_locator>', endpoint='data_getter')])
def test_without_leading_zeros(self):
urls = self.m.bind('localhost')
endpoint, urlvars = urls.match('/data/31415')
self.assertEquals({'record_locator': 31415}, urlvars)
def test_with_leading_zeros(self):
urls = self.m.bind('localhost')
endpoint, urlvars = urls.match('/data/000031415')
self.assertEquals({'record_locator': 31415}, urlvars)
unittest.main()