Change the view prefix in pyramid traversal from "##" to "+" - python

I am looking at moving a web app from pylons to pyramid (formally repoze.bfg) because traversal will fit my app much better than url dispatch.
Currently, when I have a obj with a number of views, I have the view names prefixed with a '+' in the url. e.g.:
/path/to/obj/ (default view)
/path/to/obj/+custom_view1
/path/to/obj/+custom_view2
/path/to/obj/+edit
/path/to/obj/+delete
/path/to/obj/sub_obj/
Pyramid has a feature to handle this is a nice way, but it uses the prefix "##". Is there a way to change this to "+" so that I can keep my urls the same (you know what they say about cool urls,) and use this feature.

Yes and no, you can change view prefix from "##" but the new prefix should also be 2-symbold length, see sources for traverser.
Subclassing ResourceTreeTraverser and then registering it instead of the default one should do the trick:
from pyramid.traversal import ResourceTreeTraverser as BaseResourceTreeTraverser
class ResourceTreeTraverser(BaseResourceTreeTraverser):
VIEW_SELECTOR = "++"
from pyramid.config import Configurator
from pyramid.interfaces import ITraverser
from zope.interface import Interface
config = Configurator()
config.registry.registerAdapter(ResourceTreeTraverser, (Interface,), ITraverser)
Personally, I think VIEW_SELECTOR should be refactored to allow any length view prefixes.

Related

How to map separate sets of URL prefixes to Flask blueprints

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>

Routes with trailing slashes in Pyramid

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.

How to change the template engine in Pyramid?

In particular I want to use pystache but any guide for another template engine should be good enough to set it up.
If I understood correctly, I have to register the renderer factory in the __init__.py of my pyramid application.
config = Configurator(settings=settings)
config.add_renderer(None, 'pystache_renderer_factory')
Now I need to create the renderer factory and don't know how.
Even though I found the documentation about how to add a template engine, I didn't manage to set it up.
Finally I was able to add the pystache template engine following this guide:
https://groups.google.com/forum/#!searchin/pylons-discuss/add_renderer/pylons-discuss/Y4MoKwWKiUA/cyqldA-vHjkJ
What I did:
created the file mustacherenderer.py:
from pyramid.asset import abspath_from_asset_spec
import pystache
import os
def pystache_renderer_factory(info):
template = os.path.join(abspath_from_asset_spec('myproj:templates', False),
info.name)
f = open(template)
s = f.read()
f.close()
def _render(value, system):
return pystache.render(s, value)
return _render
added this to the __init__.py:
config.add_renderer('.pmt', 'myproj.mustacherenderer.pystache_renderer_factory')
working :)
add_renderer's second argument is supposed to be a class that implements the interface shown in "Adding a New Renderer". Pyramid will take pystache_renderer_factory and attempt to import it, so in your code the line import pystache_renderer_factory would have to work. This example won't ever resolve to a class, only a module or package, so you'll have to fix that first. It should be something like mypackage.pystache_renderer_factory.
The best way to learn how to write a renderer is probably to look at some that have been written already. Specifically the pyramid_jinja2 package, or in Pyramid's source there are very simple implementations of json and jsonp renderers. Notice how they all provide fairly unique ways to implement the required interface. Each factory accepts an info object, and returns a callable that accepts value and system objects.
https://github.com/Pylons/pyramid_jinja2/blob/master/pyramid_jinja2/init.py#L260
https://github.com/Pylons/pyramid/blob/master/pyramid/renderers.py#L135
Note that this answer works well until you create your Pyramid project with a scaffold. Once you do so, this related answer will prove more useful when constructing your Pystache/Mustache_Renderer_Factory: How to integrate pystache with pyramid?.

How to get Registry().settings during Pyramid app startup time?

I am used to develop web applications on Django and gunicorn.
In case of Django, any application modules in a Django application can get deployment settings through django.conf.settings. The "settings.py" is written in Python, so that any arbitrary settings and pre-processing can be defined dynamically.
In case of gunicorn, it has three configuration places in order of precedence, and one settings registry class instance combines those.(But usually these settings are used only for gunicorn not application.)
Command line parameters.
Configuration file. (like Django, written in
Python which can have any arbitrary
settings dynamically.)
Paster application settings.
In case of Pyramid, according to Pyramid documentation, deployment settings may be usually put into pyramid.registry.Registry().settings. But it seems to be accessed only when a pyramid.router.Router() instances exists.
That is pyramid.threadlocal.get_current_registry().settings returns None, during the startup process in an application "main.py".
For example, I usually define some business logic in SQLAlchemy model modules, which requires deployment settings as follows.
myapp/models.py
from sqlalchemy import Table, Column, Types
from sqlalchemy.orm import mapper
from pyramid.threadlocal import get_current_registry
from myapp.db import session, metadata
settings = get_current_registry().settings
mytable = Table('mytable', metadata,
Column('id', Types.INTEGER, primary_key=True,)
(other columns)...
)
class MyModel(object):
query = session.query_property()
external_api_endpoint = settings['external_api_uri']
timezone = settings['timezone']
def get_api_result(self):
(interact with external api ...)
mapper(MyModel, mytable)
But, "settings['external_api_endpoint']" raises a TypeError exception because the "settings" is None.
I thought two solutions.
Define a callable which accepts "config" argument in "models.py" and "main.py" calls it with a
Configurator() instance.
myapp/models.py
from sqlalchemy import Table, Column, Types
from sqlalchemy.orm import mapper
from myapp.db import session, metadata
_g = globals()
def initialize(config):
settings = config.get_settings()
mytable = Table('mytable', metadata,
Column('id', Types.INTEGER, rimary_key = True,)
(other columns ...)
)
class MyModel(object):
query = session.query_property()
external_api_endpoint = settings['external_api_endpoint']
def get_api_result(self):
(interact with external api)...
mapper(MyModel, mytable)
_g['MyModel'] = MyModel
_g['mytable'] = mytable
Or, put an empty module "app/settings.py", and put setting into it later.
myapp/__init__.py
from pyramid.config import Configurator
from .resources import RootResource
def main(global_config, **settings):
config = Configurator(
settings = settings,
root_factory = RootResource,
)
import myapp.settings
myapp.setting.settings = config.get_settings()
(other configurations ...)
return config.make_wsgi_app()
Both and other solutions meet the requirements, but I feel troublesome. What I want is the followings.
development.ini
defines rough settings because development.ini can have only string type constants.
[app:myapp]
use = egg:myapp
env = dev0
api_signature = xxxxxx
myapp/settings.py
defines detail settings based on development.ini, beacause any arbitrary variables(types) can be set.
import datetime, urllib
from pytz import timezone
from pyramid.threadlocal import get_current_registry
pyramid_settings = get_current_registry().settings
if pyramid_settings['env'] == 'production':
api_endpoint_uri = 'http://api.external.com/?{0}'
timezone = timezone('US/Eastern')
elif pyramid_settings['env'] == 'dev0':
api_endpoint_uri = 'http://sandbox0.external.com/?{0}'
timezone = timezone('Australia/Sydney')
elif pyramid_settings['env'] == 'dev1':
api_endpoint_uri = 'http://sandbox1.external.com/?{0}'
timezone = timezone('JP/Tokyo')
api_endpoint_uri = api_endpoint_uri.format(urllib.urlencode({'signature':pyramid_settings['api_signature']}))
Then, other modules can get arbitrary deployment settings through "import myapp.settings".
Or, if Registry().settings is preferable than "settings.py", **settings kwargs and "settings.py" may be combined and registered into Registry().settings during "main.py" startup process.
Anyway, how to get the settings dictionay during startup time ? Or, Pyramid gently forces us to put every code which requires deployment settings in "views" callables which can get settings dictionary anytime through request.registry.settings ?
EDIT
Thanks, Michael and Chris.
I at last understand why Pyramid uses threadlocal variables(registry and request), in particular registry object for more than one Pyramid applications.
In my opinion, however, deployment settings usually affect business logics that may define application-specific somethings. Those logics are usually put in one or more Python modules that may be other than "app/init.py" or "app/views.py" that can easily get access to Config() or Registry(). Those Python modules are normally "global" at Python process level.
That is, even when more than one Pyramid applications coexist, despite their own threadlocal variables, they have to share those "global" Python modules that may contain applicatin-specific somethings at Python process level.
Of cause, every those modules can have "initialize()" callalbe which is called with a Configurator() by the application "main" callable, or passing Registory() or Request() object through so long series of function calls can meet usual requirements. But, I guess Pyramid beginers (like me) or developers who has "large application or so many settings" may feel troublesome, although that is Pyramid design.
So, I think, Registry().settings should have only real "thread-local" variables, and should not have normal business-logic settings. Responsibility for segregation of multiple application-specific module, classes, callables variables etc. should be taken by developer.
As of now, from my viewpoint, I will take Chris's answer. Or in "main" callable, do "execfile('settings.py', settings, settings)" and put it in some "global" space.
Another option, if you enjoy global configuration via Python, create a settings.py file. If it needs values from the ini file, parse the ini file and grab them out (at module scope, so it runs at import time):
from paste.deploy.loadwsgi import appconfig
config = appconfig('config:development.ini', 'myapp', relative_to='.')
if config['env'] == 'production':
api_endpoint_uri = 'http://api.external.com/?{0}'
timezone = timezone('US/Eastern')
# .. and so on ...
'config:development.ini' is the name of the ini file (prefixed with 'config:'). 'myapp' is the section name in the config file representing your app (e.g. [app:myapp]). "relative_to" is the directory name in which the config file can be found.
The pattern that I use is to pass the Configurator to modules that need to be initialized. Pyramid doesn't use any global variables because a design goal is to be able to run multiple instances of Pyramid in the same process. The threadlocals are global, but they are local to the current request, so different Pyramid apps can push to them at the same time from different threads.
With this in mind, if you do want a global settings dictionary you'll have to take care of that yourself. You could even push the registry onto the threadlocal manager yourself by calling config.begin().
I think the major thing to take away here is that you shouldn't be calling get_current_registry() at the module level, because at the time of import you aren't really guaranteed that the threadlocals are initialized, however in your init_model() function if you call get_current_registry(), you'd be fine if you previously called config.begin().
Sorry this is a little convoluted, but it's a common question and the best answer is: pass the configurator to your submodules that need it and allow them to add stuff to the registry/settings objects for use later.
Pyramid uses static configration by PasteDeploy, unlike Django.
Your [EDIT] part is a nice solution, I think Pyramid community should consider such usage.

Rewriting An URL With Regular Expression Substitution in Routes

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()

Categories