Methinks the most common use would be maintaining state for user
authentication. I don't know what other kinds of things people are
using it for.
Here's the sort of API I had in mind...
First, we make a "factory factory" function, called, say,
web.session_mw, which creates and returns a funciton like the
'session_mw' function in <http://webpy.org/track/wiki/
SimpleAuthenticationMethod>. This factory-factory takes a SessionStore
type followed by any positional and keyword arguments, to be passed to
the SessionStore type's initializer later. It returns a middleware
factory via flup's SessionMiddleware. In other words, the session
middleware bit from that session example would be:
session_mw = web.session_mw(DiskSessionStore, storeDir="%s/sessions/"
% os.getcwd(), timeout=5)
Also, when the resulting function creates the SessionStore instance,
in addition to passing it the user-provided arguments, it'll also pass
it a sessionClass keyword argument. We'll subclass Session to add
web.Storage as a mixin, and pass that new class.
So then each request has
web.ctx.environ['com.saddi.service.session'].session as a Storage.
Next we just need to have it add web.session (or at least
web.ctx.session) as a reference to that, for convenience.
On Mar 28, 8:42 pm, "Adam Atlas" <a...@atlas.st> wrote:
Neat! How do I actually use this? Would I use this instead of the
example at: http://webpy.org/track/wiki/SimpleAuthenticationMethod ?
-Greg
Here's an example of how you'd rewrite that example with this
addition.
http://adamatlas.org/2007/02/SimpleAuthenticationMethod.py
As you can see, it's not a huge change, but you should be able to
notice the differences. (For what it's worth, the original was 104
lines and this is 82.) But would you use this INSTEAD of that example?
I'm not sure what you mean by that, but if you're asking if this
actually handles any authentication stuff, the answer is no. It's just
a simple wrapper for flup's session API, as I explained before.
I actually was considering writing up a proposal for a common user
authentication API/toolset for web.py, but I figured we should get a
better session API first.
On Mar 28, 10:42 pm, "krypton" <kryptonsupe...@yahoo.com> wrote:
> A way to store flup sessions in Memcached, so then there is really a
> shared nothing architecture and we can scaled webpy apps indefinitely
> by just adding nodes :)
Memcached is a cache, not a store. You are not guaranteed to be able
to fetch a key - even if it is stored in the cache. rtfm and don't
even think about using memcached as a sessionstore. Memcached is fast
- not reliable - in terms of fetching/writing data.
krypton is right, memcached provides no warranty about storage, exactly
because it's *just* a cache.
Maybe they also use something persistent to store sessions, and
memcached to speed things up. Or maybe they just don't care and assume
that the memcached servers won't drop the session often because they
have huges caches, and that in that weird case the user will re-login,
and expiring sessions manually on a persistent backend might be
expensive.
I don't like promoting stuff I wrote, but if you need something like
memcached with a persistent backend, you can take a look at nmdb
(http://auriga.wearlab.de/~alb/nmdb/) because it does exactly that (give
or take some features).
Thanks,
Alberto
On Mar 30, 2:31 pm, "krypton" <kryptonsupe...@yahoo.com> wrote:
> It is the same as the example in webpy wiki what is different?
What did you expect to be different? The changes are as I described:
you can access the session as web.session, you can use it as a Storage
(instead of just a dictionary), and setting up the middleware is more
simple and concise.
On Mar 30, 8:37 pm, "Brent Pedersen" <bpede...@gmail.com> wrote:
> hi, this looks like a much nicer syntax.
> what does the @checkaccess on login.GET() do?
> if i understand correctly (unlikely) that's not needed. is that the case?
> thanks.
> -brent
Not sure... it was like that in the original. I also noticed it looked
superfluous, but I didn't change it because it wasn't relevant to the
session add-on I'm demonstrating.
>
> Sorry, typo. The file is:
> http://adamatlas.org/2007/03/SimpleAuthenticationMethod.py
I think 3 level nested functions is complicated.
I have achieved the same using decorators + metaclasses.
def has_perm(perm):
"""check if the current user has the requested permission."""
pass
def decorator(f):
def g(self, *a, **kw):
perm = getattr(self, 'perm', 'none')
if has_perm(perm):
return f(self, *a, **kw)
else:
web.seeother("/login")
class metapage(type):
def __init__(klass, name, bases, attrs):
type.__init__(klass, name, bases, attrs)
if decorator:
if 'GET' in attrs:
setattr(klass, 'GET', decorator(attrs['GET']))
if 'POST' in attrs:
setattr(klass, 'POST', decorator(attrs['POST']))
class page:
__metaclass__ = metapage
class hello:
perm = "view"
def GET(self):
print 'hello world'
class admin:
perm = "admin"
def GET(self):
print 'this page is has admin permission'
On Mar 30, 6:10 pm, Marco Alfonso Ocampo Puente <m...@gulegro.net>
wrote:
I know what decorators are. We were just commenting on the use of the
checkaccess decorator for methods like index.GET. If you look at its
implementation, you'll see that it doesn't change anything about the
decorated method at all if the decorator factory isn't passed either
of its optional arguments (i.e. @checkaccess()).
On Mar 30, 8:58 pm, Anand <anandol...@gmail.com> wrote:
> I think 3 level nested functions is complicated.
> I have achieved the same using decorators + metaclasses.
Fair enough. Something feels wrong about criticizing something for
being complicated and then suggesting something involving
metaclasses. :)
But anyway, as I said, that's not my code. I took it from <http://
webpy.org/track/wiki/SimpleAuthenticationMethod> (it was posted by
someone called bla...@laflamme.org), and I only modified it to
demonstrate what my proposed session API would change/improve.
Small problem: because Storage is mixed-in with Session when flup
tries to set attributes on the session, a la the constructor:
assert self.isIdentifierValid(identifier)
self._identifier = identifier
self._creationTime = self._lastAccessTime = time.time()
self._isValid = True
These are actually stored in the dict. Now when a Session is
invalidate()ed, flup says:
self.clear()
This clears the _* attributes as well, which flup needs in order to
check the session back in (as invalid). Hence invalidate() doesn't
work, and Session intenals are being exposed in an unfashionable
manner (try a repr(web.session)).
A simple fix follows, although there is probably a more elegant
solution:
class SessionStorage(flup.Session, web.Storage):
def __getattr__(self, k):
if not k.startswith('_'):
return web.Storage.__getattr__(k)
return flup.Session.__getattr__(k)
def __setattr__(self, k, v):
if not k.startswith('_'):
return web.Storage.__setattr__(k, v)
return flup.Session.__setattr__(k, v)
def __delattr__(self, k):
if not k.startswith('_'):
return web.Storage.__delattr__(k)
return flup.Session.__delattr__(k)
Of course you would have to include "self" in the method definitions :-
$
Here is a corrected version which has now been tested:
class SessionStorage(flup.Session, web.utils.Storage):
def __getattr__(self, k):
if not k.startswith('_'):
return web.utils.Storage.__getattr__(self, k)
return flup.Session.__getattr__(self, k)
def __setattr__(self, k, v):
if not k.startswith('_'):
return web.utils.Storage.__setattr__(self, k, v)
return flup.Session.__setattr__(self, k, v)
def __delattr__(self, k):
if not k.startswith('_'):
return web.utils.Storage.__delattr__(self, k)
return flup.Session.__delattr__(self, k)
(I use web.utils.Storage because I import session in the web.py
__init__.py)
The @checkaccess() is used on every GET/POST functions to be sure a
session exists and to standardize my code. It's easy to miss one just
because sometimes it's there or not when you have hundreds of classes
=)
However, now that I think about it, this does raise the question of
what you do if you *want* to store a session variable that starts with
an underscore. You can always use the [] syntax instead of the
attribute syntax, but consistency would be preferable. (As would
predictability -- when you're using a Storage, it is reasonable to
expect that, after '''x['<something>'] = y''', '''x.<something> ==
y''' will always be true.) Any thoughts? Maybe we could pass it along
to flup.Session.__*attr__ only if it's in a specific set of reserved
attribute names, instead of all underscore-prefixed names?
Yeah, that was the exact debate happening in my mind as I wrote it. I
was going to implement a list of reserved attributes but then I
realised that would not be persistent across flup version and we're
suddenly tied to implementation-specific details. Or at least, more
specific implementation-specific details.
It's either that or we abandon the whole "Storage" thing, but it makes
it so nice. ;-)
def sessions(storetype, *args, **kwargs):
serviceKW = dict(serviceClass=Service, defaults={},
cookieName='_SID_', cookieAttributes={})
for (k, v) in serviceKW.items():
serviceKW[k] = kwargs.pop(k, v)
def session_middleware(app):
session_store = storetype(sessionClass=SessionStorage, *args,
**kwargs)
return flup.SessionMiddleware(session_store, app, **serviceKW)
return session_middleware
This should let us pass a subset of kwargs to the SessionService class
(through SessionMiddleware).
I have tested this code and it seems to work. I can now pass
cookieAttributes = {'domain': 'secure.ucc.asn.au', 'path': '/openid/',
'secure': 'secure'} into web.sessions and it works great.
On Apr 4, 2:13 am, "Sam" <samuel.coch...@gmail.com> wrote:
> Something else useful... If you want to change the cookie attributes
> (like binding it to the current domain/path and setting an expiry for
> security) at the moment it's impossible to pass anything to the
> flup.SessionMiddleware instantiation and thus to the SessionService.
> To relive this, I've modified the sessions function slightly:
Hmm... it's a valid point, but I don't know if using the same inbound
keyword arguments for everything is a good idea. It could be confusing
to read. Maybe it would be best to put one as a dictionary in an
argument like `service_args`/`store_args`. Not as immediately
'pretty', but ultimately it would be easier to discern what's going on
in a given call to sessions().
(P.S. "Aaron and/or Anand" is my new favourite tongue-twister.)
http://adamatlas.org/2007/03/sessions.py
It looks nice and simple, although this makes me worry:
web.session = self.session
Shouldn't that be web.ctx.session? Is it threadsafe otherwise?
I've never actually used the flup session middleware, so I don't know
how it works, but it seems like the ideal thing would be if the
official web.py API didn't require including it as middleware. Perhaps
some code in _load can look to see if there's a session cookie and a
web.session() function can initialize a session object and add a
cookie or something?
You'd need to include my first bug-fix to SessionStorage, otherwise
sessions can't be invalidated.
> It looks nice and simple, although this makes me worry:
>
> web.session = self.session
>
> Shouldn't that be web.ctx.session? Is it threadsafe otherwise?
No, it's not thread-safe. I hadn't thought about it too hard. :-S
The flup library only persists the middleware itself, the session
service is created every request which makes a session object
available. This session service is assigning the session object to
web.session each request. *Not* thread-safe.
So, with all the changes I've made, here's my version of the file
(which is infinitely messier than Adam's beautifully crafted
original). It could use a bit of polish. I put it in web/ and made
__init__ import it.. it works quite nicely.
import web
import threading
import flup.middleware.session as flup
from flup.middleware.session import SessionStore, MemorySessionStore,
\
ShelveSessionStore, DiskSessionStore
__all__ = [
'sessions',
'SessionStore',
'MemorySessionStore',
'ShelveSessionStore',
'DiskSessionStore',
]
def sessions(storetype, *args, **kwargs):
serviceKW = dict(serviceClass=SessionService, defaults={},
cookieName='_SID_', cookieAttributes={})
for (k, v) in serviceKW.items():
serviceKW[k] = kwargs.pop(k, v)
def session_middleware(app):
session_store = storetype(sessionClass=SessionStorage, *args,
**kwargs)
return flup.SessionMiddleware(session_store, app, **serviceKW)
return session_middleware
class SessionStorage(flup.Session, web.utils.Storage):
def __getattr__(self, k):
if not k.startswith('_'):
return web.utils.Storage.__getattr__(self, k)
return flup.Session.__getattr__(self, k)
def __setattr__(self, k, v):
if not k.startswith('_'):
return web.utils.Storage.__setattr__(self, k, v)
return flup.Session.__setattr__(self, k, v)
def __delattr__(self, k):
if not k.startswith('_'):
return web.utils.Storage.__delattr__(self, k)
return flup.Session.__delattr__(self, k)
sessiondict = {}
web.session = web.utils.threadeddict(sessiondict)
class SessionService(flup.SessionService):
def __init__(self, *args, **kwargs):
defaults = kwargs.pop('defaults')
super(SessionService, self).__init__(*args, **kwargs)
sessiondict[threading.currentThread()] = self.session
for k, v in defaults.iteritems():
self.session.setdefault(k, v)
Well, that was just a sort of prototype. If we are indeed going to add
it to web.py, it still needs to be polished up a bit.
> It looks nice and simple, although this makes me worry:
>
> web.session = self.session
>
> Shouldn't that be web.ctx.session? Is it threadsafe otherwise?
Ah, yes. I'll change that.
> I've never actually used the flup session middleware, so I don't know
> how it works, but it seems like the ideal thing would be if the
> official web.py API didn't require including it as middleware. Perhaps
> some code in _load can look to see if there's a session cookie and a
> web.session() function can initialize a session object and add a
> cookie or something?
Maybe what is now named `sessions.sessions` could be changed to
`sessions.setup` (or something like setup_sessions so it could be
included in web.*). Or we could use web.config instead of having a
separate setup function. In any case, if a configuration is present,
perhaps web.wsgifunc could automatically add the middleware to the
beginning of the middleware chain. My concern with having _load() do
it would be that it wouldn't give other middleware the chance to
access the session object if need be.
I'm afraid I'm having a bit of trouble understanding how checkaccess
works:
def checkaccess(auth=False, access=''):
def decorator(func):
def proxyfunc(self, *args, **kw):
if (auth == True and not checkauth(session)) or \
(access != '' and access not in web.session.groups):
return web.redirect('/login')
return func(self, *args, **kw)
return proxyfunc
return decorator
I know it's used as a decorator as in:
class index:
@checkaccess(auth=True, access='admin')
def GET(self):
web.render('index.html')
But why are there so many levels of functions? What exactly happens
when index.GET is called? I've read all the tutorials on decorators,
but for some reason I've never really aqquired a firm grasp on them.
-Greg
when I wrote the initial code it was based on webpy.utils.timelimit()
function. If you try to remove a level function you'll get this error
related to autoassign and Storage:
AttributeError: (,)
Traceback:
function module body in run.py at line 32
function proxyfunc ⎋ in access.py at line 53 <- checkaccess function
function __getattr__ ⎋ in utils.py at line 679
function __getattr__ ⎋ in utils.py at line 54
So if someone can clarify how decorator works and how to do it the
right way in webpy I'll be pleased =)
On Apr 7, 4:10 pm, "gregpin...@gmail.com" <gregpin...@gmail.com>
wrote:
On Apr 5, 12:18 am, "Aaron Swartz" <m...@aaronsw.com> wrote:
> I've never actually used the flup session middleware, so I don't know
> how it works, but it seems like the ideal thing would be if the
> official web.py API didn't require including it as middleware. Perhaps
> some code in _load can look to see if there's a session cookie and a
> web.session() function can initialize a session object and add a
> cookie or something?
Another thought on this...
Flup's session API is definitely meant to be used as middleware; I'm
not sure if it can be used any other way. But maybe we could still
make it implicit by having web.wsgifunc automatically add it to the
beginning of the middleware chain if web.config.session is defined.
(I'm rewriting it to use web.config instead of that setup function.)
Here's a patch against webapi.py that adds hooks for adding implicit
middleware. This could be useful in general... a lot of possible
components would make the most sense as middleware, and this way
middleware-based services will be able to work seamlessly as part of
web.py without needing webapi.py to be modified.
Index: webapi.py
===================================================================
--- webapi.py (revision 153)
+++ webapi.py (working copy)
@@ -11,7 +11,7 @@
"setcookie", "cookies",
"ctx",
"loadhooks", "load", "unloadhooks", "unload", "_loadhooks",
- "wsgifunc"
+ "wsgifunc", "_pre_middleware", "_post_middleware"
]
import sys, os, cgi, threading, Cookie, pprint, traceback
@@ -289,6 +289,9 @@
def _unload():
unload()
+_pre_middleware = []
+_post_middleware = []
+
def wsgifunc(func, *middleware):
"""Returns a WSGI-compatible function from a webpy-function."""
middleware = list(middleware)
@@ -358,7 +361,7 @@
_unload()
raise Exception, "Invalid ctx.output"
- for mw_func in middleware:
+ for mw_func in (_pre_middleware + middleware + _post_middleware):
wsgifunc = mw_func(wsgifunc)
return wsgifunc
What do you think?
I like it. Considering web.py is the foundation for the application in
general it would make sense to have it initiating a series of
middleware. Perhaps we should be looking at adding a web.middleware
list instead of a parameter to run*() and _pre_ and _post_middleware
variables. Something like web.config.middleware which is the list of
middleware. This would allow for things like
web.config.middleware[0:0] = [web.sessions()] if we wanted the session
middleware to be first, or just web.config.middleware =
[my_middleware(), web.sessions(), my_other_middleware()]
Flup's session API could be shoehorned into a loadhook by rewriting
the SessionMiddleware class as a loadhook which instantiates a
SessionService for each load. I agree, however, that it should be
middleware and there's no point re-inventing the wheel.
That's an interesting idea, but it's still convenient to be able to
pass middleware directly to web.run.
Maybe we could just get rid of _post_middleware, and have
_pre_middleware become web.middleware. Looking at it now, I'm not
sure if there's much you could do with _post_ that you couldn't do
with _pre_ or loadhooks.
> Flup's session API could be shoehorned into a loadhook by rewriting
> the SessionMiddleware class as a loadhook which instantiates a
> SessionService for each load. I agree, however, that it should be
> middleware and there's no point re-inventing the wheel.
Indeed. I've already gotten it working as implicit middleware with
these hooks. I'm in a bit of a hurry right now, but I'll post the
updated sessions.py here later.
Okay, attached is the new version (which requires that modification
to webapi.py).
I think this is mostly complete, but I'd still like to see if there's
a better way to do the SessionStorage attribute stuff.
On Apr 1, 11:14 pm, "Sam" <samuel.coch...@gmail.com> wrote:
> Small problem: because Storage is mixed-in with Session when flup
> tries to set attributes on the session, a la the constructor:
> These are actually stored in the dict.
Was this post an April Fools' joke? ;)
Because I was trying out different possible ways of solving this, and
weirdly enough, I ended up finding that I'm not able to reproduce the
problem in the first place. (Should have tried that *before* I started
looking for solutions!)
Here's what happens for me when SessionStorage is just `class
SessionStorage(flup.Session, web.utils.Storage): pass`.
>>> import sessions
>>> x = sessions.SessionStorage(sessions.SessionStorage.generateIdentifier())
>>> x
<Storage {}>
>>> x.foo = 'bluh'
>>> x._identifier
'FIQ1-SuN0u6G5mr4m-Cx4UMoWc7iNjz7'
>>> x.__dict__
{'_identifier': 'FIQ1-SuN0u6G5mr4m-Cx4UMoWc7iNjz7', '_creationTime':
1176167420.466917, '_isValid': True, '_lastAccessTime':
1176167420.466917}
>>> x.keys()
['foo']
>>> x._identifier = 'silly'
>>> x
<Storage {'foo': 'bluh'}>
>>> x.__dict__
{'_identifier': 'silly', '_creationTime': 1176167420.466917,
'_isValid': True, '_lastAccessTime': 1176167420.466917}
I imagine this has to do with some newstyle class method-binding/
resolution stuff... I'm still not completely clear on how all that
works. In any case, any idea why I'm not having the problem you
observed?