Pyramid Duh - Tools you Want¶
This is just a collection of utilities that I found myself putting into every single pyramid project I made. So now they’re all in one place.
Code lives here: https://github.com/stevearc/pyramid_duh
User Guide¶
Request Parameters¶
There are two provided utilities for accessing request parameters. The first is the request.param() method. You can use this method by including pyramid_duh in your app (which comes with some other things), or if you only want the param() method you can include pyramid_duh.params:
config.include('pyramid_duh')
Or in the config file:
pyramid.includes =
pyramid_duh
Here is an example use case:
def register_user(request):
username = request.param('username')
password = request.param('password')
birthdate = request.param('birthdate', type=date)
metadata = request.param('metadata', {}, type=dict)
# insert into database
Note that you can pass in default values and perform type conversion. This will handle both form-encoded data and application/json. If a required argument is missing, it will raise a 400. For greater detail, see the function docs at param().
Argify¶
Let’s make the above example sexier:
from pyramid_duh import argify
@argify(birthdate=date, metadata=dict)
def register_user(request, username, password, birthdate, metadata=None):
# insert into database
Again, pretty intuitive. If any types are non-unicode, specify them in the @argify() decorator. Positional arguments are required; keyword arguments are optional. It even supports the value validation of request.param():
from pyramid_duh import argify
def is_natural_number(num):
return isinstance(num, int) and num > 0
@argify(age=(int, is_natural_number))
def set_age(request, username, age):
# Set user age
It also makes unit tests nicer:
def test_my_view(self):
request = DummyRequest()
ret = my_view(request, 'dsa', 'conspiracytheory', date(1989, 4, 1))
Note
If you’re only using @argify, you don’t need to include pyramid_duh.
Custom Parameter Types¶
You’re now using argument sugar and you’re loving it. But you’re hungry for more. You want to auto-convert to your own super-special Unicorn data type. Well who doesn’t?
Here are the POST parameters:
{
username: "stevearc",
pet: {
"name": "Sparklelord",
"sparkly": true,
"cuddly": true
}
}
And here is the code to parse that mess:
class Unicorn(object):
def __init__(self, name, sparkly, cuddly):
self.name = name
self.sparkly = sparkly
self.cuddly = cuddly
@classmethod
def __from_json__(cls, data):
return cls(**data)
@argify(pet=Unicorn)
def set_user_pet(request, username, pet):
# Set user pet
The __from_json__ method can be a classmethod or a staticmethod, and the signature must be either (arg) or (request, arg).
Note
I’m using @argify, but this also works with request.param().
You can also pass in a factory function as the type:
class Unicorn(object):
def __init__(self, name, sparkly, cuddly):
self.name = name
self.sparkly = sparkly
self.cuddly = cuddly
def make_unicorn(data):
return Unicorn(**data)
@argify(pet=make_unicorn)
def set_user_pet(request, username, pet):
# Set user pet
If you’re running into import dependency hell, you can use a dotted path for the type:
@argify(pet='mypkg.models.make_unicorn')
def set_user_pet(request, username, pet):
# Set user pet
Multi-Parameter Types¶
You can define custom types that will consume multiple request parameters. Let’s look at a new set of POST parameters;
{
name: "Sparklelord",
secret: "Radical",
}
Let’s say you want to pass up these parameters as login credentials. You would like to fetch the named Unicorn from the database and use that in your view. What would you call that argument? unicorn would make sense, but there aren’t any parameters named unicorn, so how would you inject a parameter that is generated from multiple request parameters? All you need to do is take your type factory function and decorate it with @argify as well.
@argify
def fetch_unicorn(request, name, secret):
return request.db.query_for_unicorn(name, secret)
@argify(unicorn=fetch_unicorn)
def make_rainbows(request, unicorn):
# Make some fukkin' rainbows
You’ll notice here that we’re injecting a field named unicorn, which doesn’t exist in the POST parameters. You can decorate factory methods or the __from_json__ magic method on type classes.
This particular functionality is kind of magic, and as such I would not recommend using it frequently because it obfuscates your code. This was really made with one thing in mind: user authentication. This is a great way to both authenticate a user and inject the User model into your view with minimal code duplication.
Add Slash¶
You have a view. It lies at http://example.com/path/to/resource/. But for some reason people keep going to http://example.com/path/to/resource. And it messes up relative asset paths. Well, pyramid’s solution is a little janky. They define a 404 handler that always attempts to add a slash to any view that wasn’t found. It works, but it’s global and you wouldn’t know about the behavior just from looking at the view callable. So do this:
from pyramid_duh import addslash
@addslash
def my_view(request):
# serve my resource
Easy peasy lemon squeezy.
Traversal¶
These are a couple templates for traversal tree nodes that I found myself reusing everywhere.
ISmartLookupResource¶
This is useful if you have nested resources in your tree, like /user/1234/post/9876. You can have a UserResource context in your path that has a user attribute, and a PostResource context that has a post attribute. As long as your final context inherits from ISmartLookupResource, it can access both the user and the post directly.
@view_config(context=PostResource)
def get_user_post(context, request):
if context.user.is_cool():
return context.post
This is also useful because it means you don’t have to pass the request object down your tree heirarchy. You can just attach it to the root and your nodes will be able to access it.
IStaticResource¶
Resource for static paths:
class MyResource(IStaticResource):
subobjects = {
'foo': foo_factory,
'bar': bar_factory,
}
This does what you think it does. But it prevents you from forgetting to set the __parent__ and __name__ attributes on the child. Because that produces terrible and subtle bugs.
IModelResource¶
Template for retrieving assets from a SQLAlchemy connection. Here’s an example:
class UserResource(IModelResource):
__model__ = User
__modelname__ = 'user'
@view_config(context=UserResource)
def get_user(context, request):
return context.user
This can be customized quite a bit, so look at the docstrings on IModelResource for more info.
Where is They?¶
Just import them
from pyramid_duh import ISmartLookupResource, IStaticResource, IModelResource
Subpath Predicate¶
One problem with pyramid’s traversal mechanism is that it doesn’t allow you to set view predicates on the subpath. If you aren’t already intimately familiar with the details of resource lookup via traversal, here are the docs.
So we’ve got the context, which is the last found resource. The name, which is the first url segment that had no new context, and then the subpath, which is all path components after the name.
To enforce a subpath matching, pass in a list or tuple as the vew predicate:
@view_config(context=MyCtxt, name='foobar', subpath=())
def my_view(request):
# do things
Assuming that MyCtxt maps to /mything, this view will match /mything/foobar and /mything/foobar/ only. No subpath allowed. Here is the format for matching a single subpath:
@view_config(context=MyCtxt, name='foobar', subpath=('post', '*'))
def my_view(request):
id = request.subpath[0]
# do things
You can name the subpaths and access them by name:
@view_config(context=MyCtxt, name='foobar', subpath=('post', 'id/*'))
def my_view(request):
id = request.named_subpaths['id']
# do things
And there are flags you can pass in that allow, among other things, PCRE matching:
@view_config(context=MyCtxt, name='foobar', subpath=('type/(post|tweet)/r', 'id/*'))
def my_view(request):
item_type = request.named_subpaths['type']
id = request.named_subpaths['id']
# do things
Check the docs on SubpathPredicate for all of the formats, and match() for details on match flags.
Including¶
You can use this predicate by including pyramid_duh in your app (which comes with some other things), or if you only want the predicate you can include pyramid_duh.view:
config.include('pyramid_duh')
Or in the config file:
pyramid.includes =
pyramid_duh
Settings¶
pyramid.settings has all the useful method for converting to non-string data structures. It has asbool, aslist, ...actually that’s it. We’re missing one.
users =
dsa = conspiracytheory
president_skroob = 12345
def includeme(config):
settings = config.get_settings()
users = asdict(settings['users'])
Short and sweet.
Changelog¶
0.1.2¶
- Bug fix: Fix potential timezone issue when converting unix time to datetime
0.1.1¶
- Bug fix: IStaticResource fails to look up self.request if nested 2-deep
- Bug fix: Name collisions with version_helper.py
- Bug fix: Subpath glob matching always respects case
- Feature: @argify works on view classes
- Feature: @argify can inject types that consume multiple parameters
- Feature: Parameter types can be a dotted path
0.1.0¶
- Package released into the wild
API Reference¶
pyramid_duh package¶
Submodules¶
pyramid_duh.auth module¶
Utilities for auth
- class pyramid_duh.auth.MixedAuthenticationPolicy(*policies)[source]¶
Bases: object
Auth policy that is backed by multiple other auth policies
Checks authentication against each contained policy in order. The first one to return a non-None userid is used. Principals are merged.
- authenticated_userid(request)[source]¶
Return the authenticated userid or None if no authenticated userid can be found. This method of the policy should ensure that a record exists in whatever persistent store is used related to the user (the user should not have been deleted); if a record associated with the current id does not exist in a persistent store, it should return None.
- effective_principals(request)[source]¶
Return a sequence representing the effective principals including the userid and any groups belonged to by the current user, including ‘system’ groups such as pyramid.security.Everyone and pyramid.security.Authenticated.
- forget(request)[source]¶
Return a set of headers suitable for ‘forgetting’ the current user on subsequent requests.
- remember(request, principal, **kw)[source]¶
Return a set of headers suitable for ‘remembering’ the principal named principal when set in a response. An individual authentication policy and its consumers can decide on the composition and meaning of **kw.
- unauthenticated_userid(request)[source]¶
Return the unauthenticated userid. This method performs the same duty as authenticated_userid but is permitted to return the userid based only on data present in the request; it needn’t (and shouldn’t) check any persistent store to ensure that the user record related to the request userid exists.
pyramid_duh.compat module¶
pyramid_duh.params module¶
Utilities for request parameters
- pyramid_duh.params.argify(*args, **type_kwargs)[source]¶
Request decorator for automagically passing in request parameters.
Notes
Here is a sample use case:
@argify(foo=dict, ts=datetime) def handle_request(request, foo, ts, bar='baz'): # do request handling
No special type is required for strings:
@argify def handle_request(request, foo, bar='baz'): # do request handling (both 'foo' and 'bar' are strings)
If any positional arguments are missing, it will raise a HTTPBadRequest exception. If any keyword arguments are missing, it will simply use whatever the default value is.
Note that unit tests should be unaffected by this decorator. This is valid:
@argify def myview(request, var1, var2='foo'): return 'bar' class TestReq(unittest.TestCase): def test_my_request(self): request = pyramid.testing.DummyRequest() retval = myview(request, 5, var2='foobar') self.assertEqual(retval, 'bar')
- pyramid_duh.params.param(request, name, default=<object object at 0x240e490>, type=None, validate=None)[source]¶
Access a parameter and perform type conversion.
Parameters: request : Request
name : str
The name of the parameter to retrieve
default : object, optional
The default value to use if none is found
type : object, optional
The type to convert the argument to. All python primitives are supported, as well as date and datetime. You may also pass in a factory function or an object that has a static __from_json__ method.
validate : callable, optional
Callable test to validate parameter value
Returns: arg : object
Raises: exc : HTTPBadRequest
If a parameter is requested that does not exist and no default was provided
pyramid_duh.route module¶
Utilities for traversal
- class pyramid_duh.route.IModelResource(model=None)[source]¶
Bases: pyramid_duh.route.ISmartLookupResource
Resource base class for wrapping models in a sqlalchemy database
Notes
Requires any parent node to set the ‘request’ attribute
- class pyramid_duh.route.ISmartLookupResource[source]¶
Bases: object
Resource base class that allows hierarchical lookup of attributes
Potential use case: /user/1234/post/5678
At the /user/1234 point in traversal you can set a ‘user’ attribute on the resource. At the ‘post/5678’ point in traversal you can set a ‘post’ attribute on that resource. Then the request can access both of them from the context directly:
def get_user_post(context, request): user = context.user if user.is_cool(): return context.post
- class pyramid_duh.route.IStaticResource[source]¶
Bases: pyramid_duh.route.ISmartLookupResource
Simple resource base class for static-mapping of paths
pyramid_duh.settings module¶
Utilities for parsing settings
pyramid_duh.view module¶
Utilities for view configuration
- class pyramid_duh.view.SubpathPredicate(paths, config)[source]¶
Bases: object
Generate a custom predicate that matches subpaths
Parameters: *paths : list
List of match specs.
Notes
A match spec may take one of three forms:
'glob' 'name/glob' 'name/glob/flags'
The name is optional, but if you wish to specify flags then you have to include the leading slash:
# A match spec with flags and no name '/foo.*/r'
The names will be accessible from the request.named_subpaths attribute.
@view_config(context=Root, name='simple', subpath=('package/*', 'version/*/?')) def simple(request) pkg = request.named_subpaths['package'] version = request.named_subpaths.get('version') request.response.body = '<h1>%s</h1>' % package if version is not None: request.response.body += '<h4>version: %s</h4>' % version return request.response
See match() for more information on match flags`
- pyramid_duh.view.addslash(fxn)[source]¶
View decorator that adds a trailing slash
Notes
Usage:
@view_config(context=MyCtxt, renderer='json') @addslash def do_view(request): return 'cool data'
- pyramid_duh.view.match(pattern, path, flags)[source]¶
Check if a pattern matches a path
Parameters: pattern : str
Glob or PCRE
path : str or None
The path to check, or None if no path
flags : {‘r’, ‘i’, ‘a’, ‘?’}
Special match flags. These may be combined (e.g. ‘ri?’). See the notes for an explanation of the different values.
Returns: match : bool or SRE_Match
A boolean indicating the match status, or the regex match object if there was a successful PCRE match.
Notes
Flag Description r Match using PCRE (default glob) i Case-insensitive match (must be used with ‘r’) a ASCII-only match (must be used with ‘r’, python 3 only) ? Path is optional (return True if path is None)