├── tests ├── __init__.py ├── test_namespacing_files │ ├── __init__.py │ ├── namespace_get.py │ └── namespace_go.py ├── test_namespacing.py ├── test_synchronizer.py ├── test_pbkdf2.py ├── test_cookie_expires.py ├── test_converters.py ├── test_cookie_domain_only.py ├── test_syncdict.py ├── test_domain_setting.py ├── test_database.py ├── test_sqla.py ├── test_unicode_cache_keys.py ├── test_container.py ├── test_cachemanager.py ├── test_increment.py ├── test_cache_decorator.py ├── test_cookie_only.py ├── test_cache.py ├── test_session.py └── test_memcached.py ├── beaker ├── ext │ ├── __init__.py │ ├── google.py │ ├── sqla.py │ ├── database.py │ └── memcached.py ├── __init__.py ├── docs │ ├── changes.rst │ ├── modules │ │ ├── middleware.rst │ │ ├── pbkdf2.rst │ │ ├── google.rst │ │ ├── database.rst │ │ ├── sqla.rst │ │ ├── util.rst │ │ ├── synchronization.rst │ │ ├── cache.rst │ │ ├── session.rst │ │ ├── memcached.rst │ │ └── container.rst │ ├── glossary.rst │ ├── index.rst │ ├── Makefile │ ├── conf.py │ ├── sessions.rst │ ├── caching.rst │ └── configuration.rst ├── crypto │ ├── util.py │ ├── pycrypto.py │ ├── jcecrypto.py │ ├── __init__.py │ ├── nsscrypto.py │ └── pbkdf2.py ├── exceptions.py ├── converters.py ├── cookie.py ├── _compat.py ├── middleware.py └── synchronization.py ├── .travis.yml ├── setup.cfg ├── .gitignore ├── LICENSE ├── README.rst └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /beaker/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_namespacing_files/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /beaker/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.7.0' 2 | -------------------------------------------------------------------------------- /beaker/docs/changes.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _changes: 4 | 5 | Changes in Beaker 6 | ***************** 7 | 8 | .. include:: ../../CHANGELOG 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.2" 6 | - "3.3" 7 | - "3.4" 8 | install: 9 | - pip install -e .[testsuite] 10 | script: 11 | - nosetests -v -d --with-coverage --cover-package=beaker 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | #[egg_info] 2 | #tag_build = dev 3 | #tag_svn_revision = false 4 | 5 | [nosetests] 6 | where=tests 7 | verbose=True 8 | detailed-errors=True 9 | with-doctest=True 10 | #with-coverage=True 11 | cover-package=beaker 12 | cover-inclusive=True 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info 3 | *.pyc 4 | *$py.class 5 | *.pt.py 6 | *.txt.py 7 | *~ 8 | .coverage 9 | .tox/ 10 | nosetests.xml 11 | build/ 12 | dist/ 13 | bin/ 14 | lib/ 15 | include/ 16 | .idea/ 17 | distribute-*.tar.gz 18 | bookenv/ 19 | jyenv/ 20 | pypyenv/ 21 | env*/ 22 | tests/test.db 23 | -------------------------------------------------------------------------------- /beaker/docs/modules/middleware.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.middleware` -- Middleware classes 2 | ============================================== 3 | 4 | .. automodule:: beaker.middleware 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: CacheMiddleware 10 | .. autoclass:: SessionMiddleware 11 | -------------------------------------------------------------------------------- /beaker/docs/modules/pbkdf2.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.crypto.pbkdf2` -- PKCS#5 v2.0 Password-Based Key Derivation classes 2 | ================================================================================ 3 | 4 | .. automodule:: beaker.crypto.pbkdf2 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autofunction:: pbkdf2 10 | -------------------------------------------------------------------------------- /tests/test_namespacing.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def teardown(): 6 | import shutil 7 | shutil.rmtree('./cache', True) 8 | 9 | 10 | def test_consistent_namespacing(): 11 | sys.path.append(os.path.dirname(__file__)) 12 | from tests.test_namespacing_files.namespace_go import go 13 | go() 14 | -------------------------------------------------------------------------------- /beaker/docs/modules/google.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.ext.google` -- Google Container and NameSpace Manager classes 2 | ========================================================================== 3 | 4 | .. automodule:: beaker.ext.google 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: GoogleContainer 10 | .. autoclass:: GoogleNamespaceManager 11 | -------------------------------------------------------------------------------- /beaker/docs/modules/database.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.ext.database` -- Database Container and NameSpace Manager classes 2 | ============================================================================== 3 | 4 | .. automodule:: beaker.ext.database 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: DatabaseContainer 10 | .. autoclass:: DatabaseNamespaceManager 11 | -------------------------------------------------------------------------------- /beaker/docs/modules/sqla.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.ext.sqla` -- SqlAlchemy Container and NameSpace Manager classes 2 | ============================================================================ 3 | 4 | .. automodule:: beaker.ext.sqla 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autofunction:: make_cache_table 10 | .. autoclass:: SqlaContainer 11 | .. autoclass:: SqlaNamespaceManager 12 | -------------------------------------------------------------------------------- /beaker/docs/modules/util.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.util` -- Beaker Utilities 2 | ======================================================== 3 | 4 | .. automodule:: beaker.util 5 | 6 | Module Contents 7 | --------------- 8 | .. autofunction:: encoded_path 9 | .. autofunction:: func_namespace 10 | .. autoclass:: SyncDict 11 | .. autoclass:: ThreadLocal 12 | .. autofunction:: verify_directory 13 | .. autofunction:: parse_cache_config_options -------------------------------------------------------------------------------- /beaker/docs/modules/synchronization.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.synchronization` -- Synchronization classes 2 | ======================================================== 3 | 4 | .. automodule:: beaker.synchronization 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: ConditionSynchronizer 10 | .. autoclass:: FileSynchronizer 11 | .. autoclass:: NameLock 12 | .. autoclass:: null_synchronizer 13 | .. autoclass:: SynchronizerImpl 14 | :members: 15 | -------------------------------------------------------------------------------- /beaker/docs/modules/cache.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.cache` -- Cache module 2 | ================================================ 3 | 4 | .. automodule:: beaker.cache 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autodata:: beaker.cache.cache_regions 10 | .. autofunction:: cache_region 11 | .. autofunction:: region_invalidate 12 | .. autoclass:: Cache 13 | :members: get, clear 14 | .. autoclass:: CacheManager 15 | :members: region, region_invalidate, cache, invalidate 16 | -------------------------------------------------------------------------------- /beaker/docs/modules/session.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.session` -- Session classes 2 | ======================================== 3 | 4 | .. automodule:: beaker.session 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: CookieSession 10 | :members: save, expire, delete, invalidate 11 | .. autoclass:: Session 12 | :members: save, revert, lock, unlock, delete, invalidate 13 | .. autoclass:: SessionObject 14 | :members: persist, get_by_id, accessed 15 | .. autoclass:: SignedCookie 16 | -------------------------------------------------------------------------------- /beaker/docs/modules/memcached.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.ext.memcached` -- Memcached Container and NameSpace Manager classes 2 | ================================================================================ 3 | 4 | .. automodule:: beaker.ext.memcached 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: MemcachedContainer 10 | :show-inheritance: 11 | .. autoclass:: MemcachedNamespaceManager 12 | :show-inheritance: 13 | .. autoclass:: PyLibMCNamespaceManager 14 | :show-inheritance: 15 | -------------------------------------------------------------------------------- /beaker/crypto/util.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | 3 | try: 4 | # Use PyCrypto (if available) 5 | from Crypto.Hash import HMAC as hmac, SHA as hmac_sha1 6 | sha1 = hmac_sha1.new 7 | 8 | except ImportError: 9 | 10 | # PyCrypto not available. Use the Python standard library. 11 | import hmac 12 | 13 | # NOTE: We have to use the callable with hashlib (hashlib.sha1), 14 | # otherwise hmac only accepts the sha module object itself 15 | from hashlib import sha1 16 | hmac_sha1 = sha1 -------------------------------------------------------------------------------- /tests/test_namespacing_files/namespace_get.py: -------------------------------------------------------------------------------- 1 | from beaker.cache import CacheManager 2 | from beaker.util import parse_cache_config_options 3 | from datetime import datetime 4 | 5 | defaults = {'cache.data_dir':'./cache', 'cache.type':'dbm', 'cache.expire': 60, 'cache.regions': 'short_term'} 6 | 7 | cache = CacheManager(**parse_cache_config_options(defaults)) 8 | 9 | def get_cached_value(): 10 | @cache.region('short_term', 'test_namespacing') 11 | def get_value(): 12 | return datetime.now() 13 | 14 | return get_value() 15 | 16 | -------------------------------------------------------------------------------- /beaker/exceptions.py: -------------------------------------------------------------------------------- 1 | """Beaker exception classes""" 2 | 3 | 4 | class BeakerException(Exception): 5 | pass 6 | 7 | 8 | class BeakerWarning(RuntimeWarning): 9 | """Issued at runtime.""" 10 | 11 | 12 | class CreationAbortedError(Exception): 13 | """Deprecated.""" 14 | 15 | 16 | class InvalidCacheBackendError(BeakerException, ImportError): 17 | pass 18 | 19 | 20 | class MissingCacheParameter(BeakerException): 21 | pass 22 | 23 | 24 | class LockError(BeakerException): 25 | pass 26 | 27 | 28 | class InvalidCryptoBackendError(BeakerException): 29 | pass 30 | -------------------------------------------------------------------------------- /tests/test_synchronizer.py: -------------------------------------------------------------------------------- 1 | from beaker.synchronization import * 2 | 3 | # TODO: spawn threads, test locking. 4 | 5 | 6 | def teardown(): 7 | import shutil 8 | shutil.rmtree('./cache', True) 9 | 10 | def test_reentrant_file(): 11 | sync1 = file_synchronizer('test', lock_dir='./cache') 12 | sync2 = file_synchronizer('test', lock_dir='./cache') 13 | sync1.acquire_write_lock() 14 | sync2.acquire_write_lock() 15 | sync2.release_write_lock() 16 | sync1.release_write_lock() 17 | 18 | def test_null(): 19 | sync = null_synchronizer() 20 | assert sync.acquire_write_lock() 21 | sync.release_write_lock() 22 | 23 | def test_mutex(): 24 | sync = mutex_synchronizer('someident') 25 | sync.acquire_write_lock() 26 | sync.release_write_lock() 27 | 28 | -------------------------------------------------------------------------------- /tests/test_namespacing_files/namespace_go.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import time 3 | 4 | 5 | def go(): 6 | from . import namespace_get 7 | a = namespace_get.get_cached_value() 8 | time.sleep(0.3) 9 | b = namespace_get.get_cached_value() 10 | 11 | time.sleep(0.3) 12 | 13 | from ..test_namespacing_files import namespace_get as upper_ns_get 14 | c = upper_ns_get.get_cached_value() 15 | time.sleep(0.3) 16 | d = upper_ns_get.get_cached_value() 17 | 18 | print(a) 19 | print(b) 20 | print(c) 21 | print(d) 22 | 23 | assert a == b, 'Basic caching problem - should never happen' 24 | assert c == d, 'Basic caching problem - should never happen' 25 | assert a == c, 'Namespaces not consistent when using different import paths' 26 | -------------------------------------------------------------------------------- /beaker/docs/modules/container.rst: -------------------------------------------------------------------------------- 1 | :mod:`beaker.container` -- Container and Namespace classes 2 | ========================================================== 3 | 4 | .. automodule:: beaker.container 5 | 6 | Module Contents 7 | --------------- 8 | 9 | .. autoclass:: DBMNamespaceManager 10 | :show-inheritance: 11 | .. autoclass:: FileNamespaceManager 12 | :show-inheritance: 13 | .. autoclass:: MemoryNamespaceManager 14 | :show-inheritance: 15 | .. autoclass:: NamespaceManager 16 | :members: 17 | .. autoclass:: OpenResourceNamespaceManager 18 | :show-inheritance: 19 | .. autoclass:: Value 20 | :members: 21 | :undoc-members: 22 | 23 | Deprecated Classes 24 | ------------------ 25 | .. autoclass:: Container 26 | .. autoclass:: ContainerMeta 27 | :show-inheritance: 28 | .. autoclass:: DBMContainer 29 | :show-inheritance: 30 | .. autoclass:: FileContainer 31 | :show-inheritance: 32 | .. autoclass:: MemoryContainer 33 | :show-inheritance: 34 | -------------------------------------------------------------------------------- /beaker/converters.py: -------------------------------------------------------------------------------- 1 | from beaker._compat import string_type 2 | 3 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 4 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 5 | def asbool(obj): 6 | if isinstance(obj, string_type): 7 | obj = obj.strip().lower() 8 | if obj in ['true', 'yes', 'on', 'y', 't', '1']: 9 | return True 10 | elif obj in ['false', 'no', 'off', 'n', 'f', '0']: 11 | return False 12 | else: 13 | raise ValueError( 14 | "String is not true/false: %r" % obj) 15 | return bool(obj) 16 | 17 | 18 | def aslist(obj, sep=None, strip=True): 19 | if isinstance(obj, string_type): 20 | lst = obj.split(sep) 21 | if strip: 22 | lst = [v.strip() for v in lst] 23 | return lst 24 | elif isinstance(obj, (list, tuple)): 25 | return obj 26 | elif obj is None: 27 | return [] 28 | else: 29 | return [obj] 30 | -------------------------------------------------------------------------------- /beaker/crypto/pycrypto.py: -------------------------------------------------------------------------------- 1 | """Encryption module that uses pycryptopp or pycrypto""" 2 | try: 3 | # Pycryptopp is preferred over Crypto because Crypto has had 4 | # various periods of not being maintained, and pycryptopp uses 5 | # the Crypto++ library which is generally considered the 'gold standard' 6 | # of crypto implementations 7 | from pycryptopp.cipher import aes 8 | 9 | def aesEncrypt(data, key): 10 | cipher = aes.AES(key) 11 | return cipher.process(data) 12 | 13 | # magic. 14 | aesDecrypt = aesEncrypt 15 | 16 | except ImportError: 17 | from Crypto.Cipher import AES 18 | from Crypto.Util import Counter 19 | 20 | def aesEncrypt(data, key): 21 | cipher = AES.new(key, AES.MODE_CTR, 22 | counter=Counter.new(128, initial_value=0)) 23 | 24 | return cipher.encrypt(data) 25 | 26 | def aesDecrypt(data, key): 27 | cipher = AES.new(key, AES.MODE_CTR, 28 | counter=Counter.new(128, initial_value=0)) 29 | return cipher.decrypt(data) 30 | 31 | 32 | 33 | def getKeyLength(): 34 | return 32 35 | -------------------------------------------------------------------------------- /beaker/crypto/jcecrypto.py: -------------------------------------------------------------------------------- 1 | """ 2 | Encryption module that uses the Java Cryptography Extensions (JCE). 3 | 4 | Note that in default installations of the Java Runtime Environment, the 5 | maximum key length is limited to 128 bits due to US export 6 | restrictions. This makes the generated keys incompatible with the ones 7 | generated by pycryptopp, which has no such restrictions. To fix this, 8 | download the "Unlimited Strength Jurisdiction Policy Files" from Sun, 9 | which will allow encryption using 256 bit AES keys. 10 | """ 11 | from javax.crypto import Cipher 12 | from javax.crypto.spec import SecretKeySpec, IvParameterSpec 13 | 14 | import jarray 15 | 16 | # Initialization vector filled with zeros 17 | _iv = IvParameterSpec(jarray.zeros(16, 'b')) 18 | 19 | 20 | def aesEncrypt(data, key): 21 | cipher = Cipher.getInstance('AES/CTR/NoPadding') 22 | skeySpec = SecretKeySpec(key, 'AES') 23 | cipher.init(Cipher.ENCRYPT_MODE, skeySpec, _iv) 24 | return cipher.doFinal(data).tostring() 25 | 26 | # magic. 27 | aesDecrypt = aesEncrypt 28 | 29 | 30 | def getKeyLength(): 31 | maxlen = Cipher.getMaxAllowedKeyLength('AES/CTR/NoPadding') 32 | return min(maxlen, 256) / 8 33 | -------------------------------------------------------------------------------- /tests/test_pbkdf2.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from binascii import b2a_hex, a2b_hex 4 | from beaker.crypto.pbkdf2 import pbkdf2 5 | 6 | 7 | def test_pbkdf2_test1(): 8 | result = pbkdf2("password", "ATHENA.MIT.EDUraeburn", 1, dklen=16) 9 | expected = a2b_hex(b"cdedb5281bb2f801565a1122b2563515") 10 | assert result == expected, (result, expected) 11 | 12 | 13 | def test_pbkdf2_test2(): 14 | result = b2a_hex(pbkdf2("password", "ATHENA.MIT.EDUraeburn", 1200, dklen=32)) 15 | expected = b"5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13" 16 | assert result == expected, (result, expected) 17 | 18 | 19 | def test_pbkdf2_test3(): 20 | result = b2a_hex(pbkdf2("X"*64, "pass phrase equals block size", 1200, dklen=32)) 21 | expected = b"139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1" 22 | assert result == expected, (result, expected) 23 | 24 | 25 | def test_pbkdf2_test4(): 26 | result = b2a_hex(pbkdf2("X"*65, "pass phrase exceeds block size", 1200, dklen=32)) 27 | expected = b"9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a" 28 | assert result == expected, (result, expected) 29 | 30 | 31 | def test_pbkd2_issue81(): 32 | """Test for Regression on Incorrect behavior of bytes_() under Python3.4 33 | 34 | https://github.com/bbangert/beaker/issues/81 35 | """ 36 | result = pbkdf2("MASTER_KEY", b"SALT", 1) 37 | expected = pbkdf2("MASTER_KEY", "SALT", 1) 38 | assert result == expected, (result, expected) 39 | -------------------------------------------------------------------------------- /beaker/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | from .._compat import JYTHON 2 | 3 | from warnings import warn 4 | 5 | from beaker.crypto.pbkdf2 import pbkdf2 6 | from beaker.crypto.util import hmac, sha1, hmac_sha1, md5 7 | from beaker import util 8 | 9 | keyLength = None 10 | 11 | if JYTHON: 12 | try: 13 | from beaker.crypto.jcecrypto import getKeyLength, aesEncrypt 14 | keyLength = getKeyLength() 15 | except ImportError: 16 | pass 17 | else: 18 | try: 19 | from beaker.crypto.nsscrypto import getKeyLength, aesEncrypt, aesDecrypt 20 | keyLength = getKeyLength() 21 | except ImportError: 22 | try: 23 | from beaker.crypto.pycrypto import getKeyLength, aesEncrypt, aesDecrypt 24 | keyLength = getKeyLength() 25 | except ImportError: 26 | pass 27 | 28 | if not keyLength: 29 | has_aes = False 30 | else: 31 | has_aes = True 32 | 33 | if has_aes and keyLength < 32: 34 | warn('Crypto implementation only supports key lengths up to %d bits. ' 35 | 'Generated session cookies may be incompatible with other ' 36 | 'environments' % (keyLength * 8)) 37 | 38 | 39 | def generateCryptoKeys(master_key, salt, iterations): 40 | # NB: We XOR parts of the keystream into the randomly-generated parts, just 41 | # in case os.urandom() isn't as random as it should be. Note that if 42 | # os.urandom() returns truly random data, this will have no effect on the 43 | # overall security. 44 | return pbkdf2(master_key, salt, iterations=iterations, dklen=keyLength) 45 | -------------------------------------------------------------------------------- /beaker/docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | 8 | Cache Regions 9 | Bundles of configuration options keyed to a user-defined variable 10 | for use with the :meth:`beaker.cache.CacheManager.region` 11 | decorator. 12 | 13 | Container 14 | A Beaker container is a storage object for a specific cache value 15 | and the key under the namespace it has been assigned. 16 | 17 | Dog-Pile Effect 18 | What occurs when a cached object expires, and multiple requests to 19 | fetch it are made at the same time. In systems that don't lock or 20 | use a scheme to prevent multiple instances from simultaneously 21 | creating the same thing, every request will cause the system to 22 | create a new value to be cached. 23 | 24 | Beaker alleviates this with file locking to ensure that only a single 25 | copy is re-created while other requests for the same object are 26 | instead given the old value until the new one is ready. 27 | 28 | NamespaceManager 29 | A Beaker namespace manager, is best thought of as a collection of 30 | containers with various keys. For example, a single template to be 31 | cached might vary slightly depending on search term, or user login, so 32 | the template would be keyed based on the variable that changes its 33 | output. 34 | 35 | The namespace would be the template name, while each container would 36 | correspond to one of the values and the key it responds to. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006, 2007 Ben Bangert, Mike Bayer, Philip Jenvey 2 | and contributors. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 3. The name of the author or contributors may not be used to endorse or 14 | promote products derived from this software without specific prior 15 | written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tests/test_cookie_expires.py: -------------------------------------------------------------------------------- 1 | from beaker.middleware import SessionMiddleware 2 | from beaker.session import Session 3 | from nose.tools import * 4 | import datetime 5 | import re 6 | 7 | def test_cookie_expires(): 8 | """Explore valid arguments for cookie_expires.""" 9 | def app(*args, **kw): 10 | pass 11 | 12 | key = 'beaker.session.cookie_expires' 13 | now = datetime.datetime.now() 14 | 15 | values = ['300', 300, 16 | True, 'True', 'true', 't', 17 | False, 'False', 'false', 'f', 18 | datetime.timedelta(minutes=5), now] 19 | 20 | expected = [datetime.timedelta(seconds=300), 21 | datetime.timedelta(seconds=300), 22 | True, True, True, True, 23 | False, False, False, False, 24 | datetime.timedelta(minutes=5), now] 25 | 26 | actual = [] 27 | 28 | for pos, v in enumerate(values): 29 | try: 30 | s = SessionMiddleware(app, config={key:v}) 31 | val = s.options['cookie_expires'] 32 | except: 33 | val = None 34 | assert_equal(val, expected[pos]) 35 | 36 | 37 | def test_cookie_exprires_2(): 38 | """Exhibit Set-Cookie: values.""" 39 | expires = Session( 40 | {}, cookie_expires=True 41 | ).cookie.output() 42 | 43 | assert re.match('Set-Cookie: beaker.session.id=[0-9a-f]{32}; Path=/', expires), expires 44 | no_expires = Session( 45 | {}, cookie_expires=False 46 | ).cookie.output() 47 | 48 | assert re.match('Set-Cookie: beaker.session.id=[0-9a-f]{32}; expires=(Mon|Tue), 1[89]-Jan-2038 [0-9:]{8} GMT; Path=/', no_expires), no_expires 49 | 50 | -------------------------------------------------------------------------------- /beaker/crypto/nsscrypto.py: -------------------------------------------------------------------------------- 1 | """Encryption module that uses nsscrypto""" 2 | import nss.nss 3 | 4 | nss.nss.nss_init_nodb() 5 | 6 | # Apparently the rest of beaker doesn't care about the particluar cipher, 7 | # mode and padding used. 8 | # NOTE: A constant IV!!! This is only secure if the KEY is never reused!!! 9 | _mech = nss.nss.CKM_AES_CBC_PAD 10 | _iv = '\0' * nss.nss.get_iv_length(_mech) 11 | 12 | def aesEncrypt(data, key): 13 | slot = nss.nss.get_best_slot(_mech) 14 | 15 | key_obj = nss.nss.import_sym_key(slot, _mech, nss.nss.PK11_OriginGenerated, 16 | nss.nss.CKA_ENCRYPT, nss.nss.SecItem(key)) 17 | 18 | param = nss.nss.param_from_iv(_mech, nss.nss.SecItem(_iv)) 19 | ctx = nss.nss.create_context_by_sym_key(_mech, nss.nss.CKA_ENCRYPT, key_obj, 20 | param) 21 | l1 = ctx.cipher_op(data) 22 | # Yes, DIGEST. This needs fixing in NSS, but apparently nobody (including 23 | # me :( ) cares enough. 24 | l2 = ctx.digest_final() 25 | 26 | return l1 + l2 27 | 28 | def aesDecrypt(data, key): 29 | slot = nss.nss.get_best_slot(_mech) 30 | 31 | key_obj = nss.nss.import_sym_key(slot, _mech, nss.nss.PK11_OriginGenerated, 32 | nss.nss.CKA_DECRYPT, nss.nss.SecItem(key)) 33 | 34 | param = nss.nss.param_from_iv(_mech, nss.nss.SecItem(_iv)) 35 | ctx = nss.nss.create_context_by_sym_key(_mech, nss.nss.CKA_DECRYPT, key_obj, 36 | param) 37 | l1 = ctx.cipher_op(data) 38 | # Yes, DIGEST. This needs fixing in NSS, but apparently nobody (including 39 | # me :( ) cares enough. 40 | l2 = ctx.digest_final() 41 | 42 | return l1 + l2 43 | 44 | def getKeyLength(): 45 | return 32 46 | -------------------------------------------------------------------------------- /beaker/docs/index.rst: -------------------------------------------------------------------------------- 1 | Beaker Documentation 2 | ==================== 3 | 4 | Beaker is a library for caching and sessions for use with web applications and 5 | stand-alone Python scripts and applications. It comes with WSGI middleware for 6 | easy drop-in use with WSGI based web applications, and caching decorators for 7 | ease of use with any Python based application. 8 | 9 | * **Lazy-Loading Sessions**: No performance hit for having sessions active in a request unless they're actually used 10 | * **Performance**: Utilizes a multiple-reader / single-writer locking system to prevent the Dog Pile effect when caching. 11 | * **Multiple Back-ends**: File-based, DBM files, memcached, memory, and database (via SQLAlchemy) back-ends available for sessions and caching 12 | * **Cookie-based Sessions**: SHA-1 signatures with optional AES encryption for client-side cookie-based session storage 13 | * **Flexible Caching**: Data can be cached per function to different back-ends, with different expirations, and different keys 14 | * **Extensible Back-ends**: Add more back-ends using setuptools entrypoints to support new back-ends. 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | configuration 20 | sessions 21 | caching 22 | 23 | .. toctree:: 24 | :maxdepth: 1 25 | 26 | changes 27 | 28 | 29 | Indices and tables 30 | ================== 31 | 32 | * :ref:`genindex` 33 | * :ref:`modindex` 34 | * :ref:`search` 35 | * :ref:`glossary` 36 | 37 | Module Listing 38 | -------------- 39 | 40 | .. toctree:: 41 | :maxdepth: 2 42 | 43 | modules/cache 44 | modules/container 45 | modules/middleware 46 | modules/session 47 | modules/synchronization 48 | modules/util 49 | modules/database 50 | modules/google 51 | modules/memcached 52 | modules/sqla 53 | modules/pbkdf2 54 | 55 | -------------------------------------------------------------------------------- /tests/test_converters.py: -------------------------------------------------------------------------------- 1 | from beaker._compat import u_ 2 | import unittest 3 | 4 | from beaker.converters import asbool, aslist 5 | 6 | 7 | class AsBool(unittest.TestCase): 8 | def test_truth_str(self): 9 | for v in ('true', 'yes', 'on', 'y', 't', '1'): 10 | self.assertTrue(asbool(v), "%s should be considered True" % (v,)) 11 | v = v.upper() 12 | self.assertTrue(asbool(v), "%s should be considered True" % (v,)) 13 | 14 | def test_false_str(self): 15 | for v in ('false', 'no', 'off', 'n', 'f', '0'): 16 | self.assertFalse(asbool(v), v) 17 | v = v.upper() 18 | self.assertFalse(asbool(v), v) 19 | 20 | def test_coerce(self): 21 | """Things that can coerce right straight to booleans.""" 22 | self.assertTrue(asbool(True)) 23 | self.assertTrue(asbool(1)) 24 | self.assertTrue(asbool(42)) 25 | self.assertFalse(asbool(False)) 26 | self.assertFalse(asbool(0)) 27 | 28 | def test_bad_values(self): 29 | self.assertRaises(ValueError, asbool, ('mommy!')) 30 | self.assertRaises(ValueError, asbool, (u_('Blargl?'))) 31 | 32 | 33 | class AsList(unittest.TestCase): 34 | def test_string(self): 35 | self.assertEqual(aslist('abc'), ['abc']) 36 | self.assertEqual(aslist('1a2a3', 'a'), ['1', '2', '3']) 37 | 38 | def test_None(self): 39 | self.assertEqual(aslist(None), []) 40 | 41 | def test_listy_noops(self): 42 | """Lists and tuples should come back unchanged.""" 43 | x = [1, 2, 3] 44 | self.assertEqual(aslist(x), x) 45 | y = ('z', 'y', 'x') 46 | self.assertEqual(aslist(y), y) 47 | 48 | def test_listify(self): 49 | """Other objects should just result in a single item list.""" 50 | self.assertEqual(aslist(dict()), [{}]) 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | 56 | -------------------------------------------------------------------------------- /tests/test_cookie_domain_only.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | import beaker.session 5 | from beaker.middleware import SessionMiddleware 6 | from nose import SkipTest 7 | 8 | try: 9 | from webtest import TestApp 10 | except ImportError: 11 | raise SkipTest("webtest not installed") 12 | 13 | from beaker import crypto 14 | if not crypto.has_aes: 15 | raise SkipTest("No AES library is installed, can't test cookie-only " 16 | "Sessions") 17 | 18 | def simple_app(environ, start_response): 19 | session = environ['beaker.session'] 20 | if not session.has_key('value'): 21 | session['value'] = 0 22 | session['value'] += 1 23 | domain = environ.get('domain') 24 | if domain: 25 | session.domain = domain 26 | if not environ['PATH_INFO'].startswith('/nosave'): 27 | session.save() 28 | start_response('200 OK', [('Content-type', 'text/plain')]) 29 | msg = 'The current value is: %d and cookie is %s' % (session['value'], session) 30 | return [msg.encode('utf-8')] 31 | 32 | 33 | def test_increment(): 34 | options = {'session.validate_key':'hoobermas', 35 | 'session.type':'cookie'} 36 | app = TestApp(SessionMiddleware(simple_app, **options)) 37 | res = app.get('/') 38 | assert 'current value is: 1' in res 39 | 40 | res = app.get('/', extra_environ=dict(domain='.hoop.com', 41 | HTTP_HOST='www.hoop.com')) 42 | assert 'current value is: 1' in res 43 | assert 'Domain=.hoop.com' in res.headers['Set-Cookie'] 44 | 45 | res = app.get('/', extra_environ=dict(HTTP_HOST='www.hoop.com')) 46 | assert 'Domain=.hoop.com' in res.headers['Set-Cookie'] 47 | assert 'current value is: 2' in res 48 | 49 | 50 | if __name__ == '__main__': 51 | from paste import httpserver 52 | wsgi_app = SessionMiddleware(simple_app, {}) 53 | httpserver.serve(wsgi_app, host='127.0.0.1', port=8080) 54 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Cache and Session Library 3 | ========================= 4 | 5 | About 6 | ===== 7 | 8 | Beaker is a web session and general caching library that includes WSGI 9 | middleware for use in web applications. 10 | 11 | As a general caching library, Beaker can handle storing for various times 12 | any Python object that can be pickled with optional back-ends on a 13 | fine-grained basis. 14 | 15 | Beaker was built largely on the code from MyghtyUtils, then refactored and 16 | extended with database support. 17 | 18 | Beaker includes Cache and Session WSGI middleware to ease integration with 19 | WSGI capable frameworks, and is automatically used by `Pylons 20 | `_ and 21 | `TurboGears `_. 22 | 23 | 24 | Features 25 | ======== 26 | 27 | * Fast, robust performance 28 | * Multiple reader/single writer lock system to avoid duplicate simultaneous 29 | cache creation 30 | * Cache back-ends include dbm, file, memory, memcached, and database (Using 31 | SQLAlchemy for multiple-db vendor support) 32 | * Signed cookie's to prevent session hijacking/spoofing 33 | * Cookie-only sessions to remove the need for a db or file backend (ideal 34 | for clustered systems) 35 | * Extensible Container object to support new back-ends 36 | * Cache's can be divided into namespaces (to represent templates, objects, 37 | etc.) then keyed for different copies 38 | * Create functions for automatic call-backs to create new cache copies after 39 | expiration 40 | * Fine-grained toggling of back-ends, keys, and expiration per Cache object 41 | 42 | 43 | Documentation 44 | ============= 45 | 46 | Documentation can be found on the `Official Beaker Docs site 47 | `_. 48 | 49 | 50 | Source 51 | ====== 52 | 53 | The latest developer version is available in a `github repository 54 | `_. 55 | 56 | Contributing 57 | ============ 58 | 59 | Bugs can be filed on github, **should be accompanied by a test case** to 60 | retain current code coverage, and should be in a Pull request when ready to be 61 | accepted into the beaker code-base. 62 | -------------------------------------------------------------------------------- /tests/test_syncdict.py: -------------------------------------------------------------------------------- 1 | from beaker.util import SyncDict, WeakValuedRegistry 2 | import random, time, weakref 3 | import threading 4 | 5 | class Value(object): 6 | values = {} 7 | 8 | def do_something(self, id): 9 | Value.values[id] = self 10 | 11 | def stop_doing_something(self, id): 12 | del Value.values[id] 13 | 14 | mutex = threading.Lock() 15 | 16 | def create(id): 17 | assert not Value.values, "values still remain" 18 | global totalcreates 19 | totalcreates += 1 20 | return Value() 21 | 22 | def threadtest(s, id): 23 | print("create thread %d starting" % id) 24 | 25 | global running 26 | global totalgets 27 | while running: 28 | try: 29 | value = s.get('test', lambda: create(id)) 30 | value.do_something(id) 31 | except Exception as e: 32 | print("Error", e) 33 | running = False 34 | break 35 | else: 36 | totalgets += 1 37 | time.sleep(random.random() * .01) 38 | value.stop_doing_something(id) 39 | del value 40 | time.sleep(random.random() * .01) 41 | 42 | def runtest(s): 43 | 44 | global values 45 | values = {} 46 | 47 | global totalcreates 48 | totalcreates = 0 49 | 50 | global totalgets 51 | totalgets = 0 52 | 53 | global running 54 | running = True 55 | 56 | threads = [] 57 | for id_ in range(1, 20): 58 | t = threading.Thread(target=threadtest, args=(s, id_)) 59 | t.start() 60 | threads.append(t) 61 | 62 | for i in range(0, 10): 63 | if not running: 64 | break 65 | time.sleep(1) 66 | 67 | failed = not running 68 | 69 | running = False 70 | 71 | for t in threads: 72 | t.join() 73 | 74 | assert not failed, "test failed" 75 | 76 | print("total object creates %d" % totalcreates) 77 | print("total object gets %d" % totalgets) 78 | 79 | 80 | def test_dict(): 81 | # normal dictionary test, where we will remove the value 82 | # periodically. the number of creates should be equal to 83 | # the number of removes plus one. 84 | print("\ntesting with normal dict") 85 | runtest(SyncDict()) 86 | 87 | 88 | def test_weakdict(): 89 | print("\ntesting with weak dict") 90 | runtest(WeakValuedRegistry()) 91 | -------------------------------------------------------------------------------- /beaker/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " pickle to make pickle files" 20 | @echo " json to make JSON files" 21 | @echo " htmlhelp to make HTML files and a HTML help project" 22 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 23 | @echo " changes to make an overview over all changed/added/deprecated items" 24 | @echo " linkcheck to check all external links for integrity" 25 | 26 | clean: 27 | -rm -rf build/* 28 | 29 | html: 30 | mkdir -p build/html build/doctrees 31 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html 32 | @echo 33 | @echo "Build finished. The HTML pages are in build/html." 34 | 35 | pickle: 36 | mkdir -p build/pickle build/doctrees 37 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle 38 | @echo 39 | @echo "Build finished; now you can process the pickle files." 40 | 41 | web: pickle 42 | 43 | json: 44 | mkdir -p build/json build/doctrees 45 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json 46 | @echo 47 | @echo "Build finished; now you can process the JSON files." 48 | 49 | htmlhelp: 50 | mkdir -p build/htmlhelp build/doctrees 51 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp 52 | @echo 53 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 54 | ".hhp project file in build/htmlhelp." 55 | 56 | latex: 57 | mkdir -p build/latex build/doctrees 58 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex 59 | @echo 60 | @echo "Build finished; the LaTeX files are in build/latex." 61 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 62 | "run these through (pdf)latex." 63 | 64 | changes: 65 | mkdir -p build/changes build/doctrees 66 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes 67 | @echo 68 | @echo "The overview file is in build/changes." 69 | 70 | linkcheck: 71 | mkdir -p build/linkcheck build/doctrees 72 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck 73 | @echo 74 | @echo "Link check complete; look for any errors in the above output " \ 75 | "or in build/linkcheck/output.txt." 76 | -------------------------------------------------------------------------------- /tests/test_domain_setting.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | from beaker.middleware import SessionMiddleware 5 | from nose import SkipTest 6 | try: 7 | from webtest import TestApp 8 | except ImportError: 9 | raise SkipTest("webtest not installed") 10 | 11 | def teardown(): 12 | import shutil 13 | shutil.rmtree('./cache', True) 14 | 15 | def simple_app(environ, start_response): 16 | session = environ['beaker.session'] 17 | domain = environ.get('domain') 18 | if domain: 19 | session.domain = domain 20 | if not session.has_key('value'): 21 | session['value'] = 0 22 | session['value'] += 1 23 | if not environ['PATH_INFO'].startswith('/nosave'): 24 | session.save() 25 | start_response('200 OK', [('Content-type', 'text/plain')]) 26 | msg = 'The current value is: %s, session id is %s' % (session.get('value', 0), 27 | session.id) 28 | return [msg.encode('utf-8')] 29 | 30 | 31 | def test_same_domain(): 32 | options = {'session.data_dir':'./cache', 33 | 'session.secret':'blah', 34 | 'session.cookie_domain': '.hoop.com'} 35 | app = TestApp(SessionMiddleware(simple_app, **options)) 36 | res = app.get('/', extra_environ=dict(HTTP_HOST='subdomain.hoop.com')) 37 | assert 'current value is: 1' in res 38 | assert 'Domain=.hoop.com' in res.headers['Set-Cookie'] 39 | res = app.get('/', extra_environ=dict(HTTP_HOST='another.hoop.com')) 40 | assert 'current value is: 2' in res 41 | assert [] == res.headers.getall('Set-Cookie') 42 | res = app.get('/', extra_environ=dict(HTTP_HOST='more.subdomain.hoop.com')) 43 | assert 'current value is: 3' in res 44 | 45 | 46 | def test_different_domain(): 47 | options = {'session.data_dir':'./cache', 48 | 'session.secret':'blah'} 49 | app = TestApp(SessionMiddleware(simple_app, **options)) 50 | res = app.get('/', extra_environ=dict(domain='.hoop.com', 51 | HTTP_HOST='www.hoop.com')) 52 | res = app.get('/', extra_environ=dict(domain='.hoop.co.uk', 53 | HTTP_HOST='www.hoop.com')) 54 | assert 'Domain=.hoop.co.uk' in res.headers['Set-Cookie'] 55 | assert 'current value is: 2' in res 56 | 57 | res = app.get('/', extra_environ=dict(domain='.hoop.co.uk', 58 | HTTP_HOST='www.test.com')) 59 | assert 'current value is: 1' in res 60 | 61 | 62 | if __name__ == '__main__': 63 | from paste import httpserver 64 | wsgi_app = SessionMiddleware(simple_app, {}) 65 | httpserver.serve(wsgi_app, host='127.0.0.1', port=8080) 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import re 4 | import inspect 5 | 6 | from setuptools import setup, find_packages 7 | 8 | py_version = sys.version_info[:2] 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | v = open(os.path.join(here, 'beaker', '__init__.py')) 11 | VERSION = re.compile(r".*__version__ = '(.*?)'", re.S).match(v.read()).group(1) 12 | v.close() 13 | 14 | try: 15 | README = open(os.path.join(here, 'README.rst')).read() 16 | except IOError: 17 | README = '' 18 | 19 | 20 | INSTALL_REQUIRES = [] 21 | if not hasattr(inspect, 'signature'): 22 | # On Python 2.6, 2.7 and 3.2 we need funcsigs dependency 23 | INSTALL_REQUIRES.append('funcsigs') 24 | 25 | 26 | TESTS_REQUIRE = ['nose', 'webtest', 'Mock', 'pycrypto'] 27 | 28 | if py_version == (3, 2): 29 | TESTS_REQUIRE.append('coverage < 4.0') 30 | else: 31 | TESTS_REQUIRE.append('coverage') 32 | 33 | if not sys.platform.startswith('java') and not sys.platform == 'cli': 34 | TESTS_REQUIRE.extend(['SQLALchemy']) 35 | try: 36 | import sqlite3 37 | except ImportError: 38 | TESTS_REQUIRE.append('pysqlite') 39 | 40 | setup(name='Beaker', 41 | version=VERSION, 42 | description="A Session and Caching library with WSGI Middleware", 43 | long_description=README, 44 | classifiers=[ 45 | 'Development Status :: 5 - Production/Stable', 46 | 'Environment :: Web Environment', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: BSD License', 49 | 'Programming Language :: Python', 50 | 'Programming Language :: Python :: 2.6', 51 | 'Programming Language :: Python :: 2.7', 52 | 'Programming Language :: Python :: 3', 53 | 'Programming Language :: Python :: 3.2', 54 | 'Programming Language :: Python :: 3.3', 55 | 'Programming Language :: Python :: 3.4', 56 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 57 | 'Topic :: Internet :: WWW/HTTP :: WSGI', 58 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', 59 | ], 60 | keywords='wsgi myghty session web cache middleware', 61 | author='Ben Bangert, Mike Bayer, Philip Jenvey', 62 | author_email='ben@groovie.org, pjenvey@groovie.org', 63 | url='http://beaker.rtfd.org/', 64 | license='BSD', 65 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests', 'tests.*']), 66 | zip_safe=False, 67 | install_requires=INSTALL_REQUIRES, 68 | extras_require={ 69 | 'crypto': ['pycryptopp>=0.5.12'], 70 | 'pycrypto': ['pycrypto'], 71 | 'testsuite': [TESTS_REQUIRE] 72 | }, 73 | test_suite='nose.collector', 74 | tests_require=TESTS_REQUIRE, 75 | entry_points=""" 76 | [paste.filter_factory] 77 | beaker_session = beaker.middleware:session_filter_factory 78 | 79 | [paste.filter_app_factory] 80 | beaker_session = beaker.middleware:session_filter_app_factory 81 | 82 | [beaker.backends] 83 | database = beaker.ext.database:DatabaseNamespaceManager 84 | memcached = beaker.ext.memcached:MemcachedNamespaceManager 85 | google = beaker.ext.google:GoogleNamespaceManager 86 | sqla = beaker.ext.sqla:SqlaNamespaceManager 87 | """ 88 | ) 89 | -------------------------------------------------------------------------------- /beaker/cookie.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from ._compat import http_cookies 3 | 4 | # Some versions of Python 2.7 and later won't need this encoding bug fix: 5 | _cookie_encodes_correctly = http_cookies.SimpleCookie().value_encode(';') == (';', '"\\073"') 6 | 7 | # Cookie pickling bug is fixed in Python 2.7.9 and Python 3.4.3+ 8 | # http://bugs.python.org/issue22775 9 | cookie_pickles_properly = ( 10 | (sys.version_info[:2] == (2, 7) and sys.version_info >= (2, 7, 9)) or 11 | sys.version_info >= (3, 4, 3) 12 | ) 13 | 14 | 15 | # Adapted from Django.http.cookies and always enabled the bad_cookies 16 | # behaviour to cope with any invalid cookie key while keeping around 17 | # the session. 18 | class SimpleCookie(http_cookies.SimpleCookie): 19 | if not cookie_pickles_properly: 20 | def __setitem__(self, key, value): 21 | # Apply the fix from http://bugs.python.org/issue22775 where 22 | # it's not fixed in Python itself 23 | if isinstance(value, http_cookies.Morsel): 24 | # allow assignment of constructed Morsels (e.g. for pickling) 25 | dict.__setitem__(self, key, value) 26 | else: 27 | super(SimpleCookie, self).__setitem__(key, value) 28 | 29 | if not _cookie_encodes_correctly: 30 | def value_encode(self, val): 31 | # Some browsers do not support quoted-string from RFC 2109, 32 | # including some versions of Safari and Internet Explorer. 33 | # These browsers split on ';', and some versions of Safari 34 | # are known to split on ', '. Therefore, we encode ';' and ',' 35 | 36 | # SimpleCookie already does the hard work of encoding and decoding. 37 | # It uses octal sequences like '\\012' for newline etc. 38 | # and non-ASCII chars. We just make use of this mechanism, to 39 | # avoid introducing two encoding schemes which would be confusing 40 | # and especially awkward for javascript. 41 | 42 | # NB, contrary to Python docs, value_encode returns a tuple containing 43 | # (real val, encoded_val) 44 | val, encoded = super(SimpleCookie, self).value_encode(val) 45 | 46 | encoded = encoded.replace(";", "\\073").replace(",", "\\054") 47 | # If encoded now contains any quoted chars, we need double quotes 48 | # around the whole string. 49 | if "\\" in encoded and not encoded.startswith('"'): 50 | encoded = '"' + encoded + '"' 51 | 52 | return val, encoded 53 | 54 | def load(self, rawdata): 55 | self.bad_cookies = set() 56 | super(SimpleCookie, self).load(rawdata) 57 | for key in self.bad_cookies: 58 | del self[key] 59 | 60 | # override private __set() method: 61 | # (needed for using our Morsel, and for laxness with CookieError 62 | def _BaseCookie__set(self, key, real_value, coded_value): 63 | try: 64 | super(SimpleCookie, self)._BaseCookie__set(key, real_value, coded_value) 65 | except http_cookies.CookieError: 66 | if not hasattr(self, 'bad_cookies'): 67 | self.bad_cookies = set() 68 | self.bad_cookies.add(key) 69 | dict.__setitem__(self, key, http_cookies.Morsel()) 70 | -------------------------------------------------------------------------------- /beaker/crypto/pbkdf2.py: -------------------------------------------------------------------------------- 1 | from beaker._compat import bytes_, xrange_ 2 | 3 | import hmac 4 | import struct 5 | import hashlib 6 | import binascii 7 | 8 | 9 | def _bin_to_long(x): 10 | """Convert a binary string into a long integer""" 11 | return int(binascii.hexlify(x), 16) 12 | 13 | 14 | def _long_to_bin(x, hex_format_string): 15 | """ 16 | Convert a long integer into a binary string. 17 | hex_format_string is like "%020x" for padding 10 characters. 18 | """ 19 | return binascii.unhexlify((hex_format_string % x).encode('ascii')) 20 | 21 | 22 | if hasattr(hashlib, "pbkdf2_hmac"): 23 | def pbkdf2(password, salt, iterations, dklen=0, digest=None): 24 | """ 25 | Implements PBKDF2 using the stdlib. This is used in Python 2.7.8+ and 3.4+. 26 | 27 | HMAC+SHA256 is used as the default pseudo random function. 28 | 29 | As of 2014, 100,000 iterations was the recommended default which took 30 | 100ms on a 2.7Ghz Intel i7 with an optimized implementation. This is 31 | probably the bare minimum for security given 1000 iterations was 32 | recommended in 2001. 33 | """ 34 | if digest is None: 35 | digest = hashlib.sha1 36 | if not dklen: 37 | dklen = None 38 | password = bytes_(password) 39 | salt = bytes_(salt) 40 | return hashlib.pbkdf2_hmac( 41 | digest().name, password, salt, iterations, dklen) 42 | else: 43 | def pbkdf2(password, salt, iterations, dklen=0, digest=None): 44 | """ 45 | Implements PBKDF2 as defined in RFC 2898, section 5.2 46 | 47 | HMAC+SHA256 is used as the default pseudo random function. 48 | 49 | As of 2014, 100,000 iterations was the recommended default which took 50 | 100ms on a 2.7Ghz Intel i7 with an optimized implementation. This is 51 | probably the bare minimum for security given 1000 iterations was 52 | recommended in 2001. This code is very well optimized for CPython and 53 | is about five times slower than OpenSSL's implementation. 54 | """ 55 | assert iterations > 0 56 | if not digest: 57 | digest = hashlib.sha1 58 | password = bytes_(password) 59 | salt = bytes_(salt) 60 | hlen = digest().digest_size 61 | if not dklen: 62 | dklen = hlen 63 | if dklen > (2 ** 32 - 1) * hlen: 64 | raise OverflowError('dklen too big') 65 | l = -(-dklen // hlen) 66 | r = dklen - (l - 1) * hlen 67 | 68 | hex_format_string = "%%0%ix" % (hlen * 2) 69 | 70 | inner, outer = digest(), digest() 71 | if len(password) > inner.block_size: 72 | password = digest(password).digest() 73 | password += b'\x00' * (inner.block_size - len(password)) 74 | inner.update(password.translate(hmac.trans_36)) 75 | outer.update(password.translate(hmac.trans_5C)) 76 | 77 | def F(i): 78 | u = salt + struct.pack(b'>I', i) 79 | result = 0 80 | for j in xrange_(int(iterations)): 81 | dig1, dig2 = inner.copy(), outer.copy() 82 | dig1.update(u) 83 | dig2.update(dig1.digest()) 84 | u = dig2.digest() 85 | result ^= _bin_to_long(u) 86 | return _long_to_bin(result, hex_format_string) 87 | 88 | T = [F(x) for x in xrange_(1, l)] 89 | return b''.join(T) + F(l)[:r] 90 | -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from beaker._compat import u_ 3 | 4 | from beaker.cache import clsmap, Cache, util 5 | from beaker.exceptions import InvalidCacheBackendError 6 | from beaker.middleware import CacheMiddleware 7 | from nose import SkipTest 8 | 9 | try: 10 | from webtest import TestApp 11 | except ImportError: 12 | TestApp = None 13 | 14 | 15 | try: 16 | clsmap['ext:database']._init_dependencies() 17 | except InvalidCacheBackendError: 18 | raise SkipTest("an appropriate SQLAlchemy backend is not installed") 19 | 20 | db_url = 'sqlite:///test.db' 21 | 22 | def simple_app(environ, start_response): 23 | extra_args = {} 24 | clear = False 25 | if environ.get('beaker.clear'): 26 | clear = True 27 | extra_args['type'] = 'ext:database' 28 | extra_args['url'] = db_url 29 | extra_args['data_dir'] = './cache' 30 | cache = environ['beaker.cache'].get_cache('testcache', **extra_args) 31 | if clear: 32 | cache.clear() 33 | try: 34 | value = cache.get_value('value') 35 | except: 36 | value = 0 37 | cache.set_value('value', value+1) 38 | start_response('200 OK', [('Content-type', 'text/plain')]) 39 | return [('The current value is: %s' % cache.get_value('value')).encode('utf-8')] 40 | 41 | def cache_manager_app(environ, start_response): 42 | cm = environ['beaker.cache'] 43 | cm.get_cache('test')['test_key'] = 'test value' 44 | 45 | start_response('200 OK', [('Content-type', 'text/plain')]) 46 | yield ("test_key is: %s\n" % cm.get_cache('test')['test_key']).encode('utf-8') 47 | cm.get_cache('test').clear() 48 | 49 | try: 50 | test_value = cm.get_cache('test')['test_key'] 51 | except KeyError: 52 | yield ("test_key cleared").encode('utf-8') 53 | else: 54 | yield ("test_key wasn't cleared, is: %s\n" % test_value).encode('utf-8') 55 | 56 | def test_has_key(): 57 | cache = Cache('test', data_dir='./cache', url=db_url, type='ext:database') 58 | o = object() 59 | cache.set_value("test", o) 60 | assert cache.has_key("test") 61 | assert "test" in cache 62 | assert not cache.has_key("foo") 63 | assert "foo" not in cache 64 | cache.remove_value("test") 65 | assert not cache.has_key("test") 66 | 67 | def test_has_key_multicache(): 68 | cache = Cache('test', data_dir='./cache', url=db_url, type='ext:database') 69 | o = object() 70 | cache.set_value("test", o) 71 | assert cache.has_key("test") 72 | assert "test" in cache 73 | cache = Cache('test', data_dir='./cache', url=db_url, type='ext:database') 74 | assert cache.has_key("test") 75 | cache.remove_value('test') 76 | 77 | def test_clear(): 78 | cache = Cache('test', data_dir='./cache', url=db_url, type='ext:database') 79 | o = object() 80 | cache.set_value("test", o) 81 | assert cache.has_key("test") 82 | cache.clear() 83 | assert not cache.has_key("test") 84 | 85 | def test_unicode_keys(): 86 | cache = Cache('test', data_dir='./cache', url=db_url, type='ext:database') 87 | o = object() 88 | cache.set_value(u_('hiŏ'), o) 89 | assert u_('hiŏ') in cache 90 | assert u_('hŏa') not in cache 91 | cache.remove_value(u_('hiŏ')) 92 | assert u_('hiŏ') not in cache 93 | 94 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 95 | def test_increment(): 96 | app = TestApp(CacheMiddleware(simple_app)) 97 | res = app.get('/', extra_environ={'beaker.clear':True}) 98 | assert 'current value is: 1' in res 99 | res = app.get('/') 100 | assert 'current value is: 2' in res 101 | res = app.get('/') 102 | assert 'current value is: 3' in res 103 | 104 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 105 | def test_cache_manager(): 106 | app = TestApp(CacheMiddleware(cache_manager_app)) 107 | res = app.get('/') 108 | assert 'test_key is: test value' in res 109 | assert 'test_key cleared' in res 110 | -------------------------------------------------------------------------------- /tests/test_sqla.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from beaker._compat import u_ 3 | from beaker.cache import clsmap, Cache, util 4 | from beaker.exceptions import InvalidCacheBackendError 5 | from beaker.middleware import CacheMiddleware 6 | from nose import SkipTest 7 | 8 | try: 9 | from webtest import TestApp 10 | except ImportError: 11 | TestApp = None 12 | 13 | try: 14 | clsmap['ext:sqla']._init_dependencies() 15 | except InvalidCacheBackendError: 16 | raise SkipTest("an appropriate SQLAlchemy backend is not installed") 17 | 18 | import sqlalchemy as sa 19 | from beaker.ext.sqla import make_cache_table 20 | 21 | engine = sa.create_engine('sqlite://') 22 | metadata = sa.MetaData() 23 | cache_table = make_cache_table(metadata) 24 | metadata.create_all(engine) 25 | 26 | def simple_app(environ, start_response): 27 | extra_args = {} 28 | clear = False 29 | if environ.get('beaker.clear'): 30 | clear = True 31 | extra_args['type'] = 'ext:sqla' 32 | extra_args['bind'] = engine 33 | extra_args['table'] = cache_table 34 | extra_args['data_dir'] = './cache' 35 | cache = environ['beaker.cache'].get_cache('testcache', **extra_args) 36 | if clear: 37 | cache.clear() 38 | try: 39 | value = cache.get_value('value') 40 | except: 41 | value = 0 42 | cache.set_value('value', value+1) 43 | start_response('200 OK', [('Content-type', 'text/plain')]) 44 | return [('The current value is: %s' % cache.get_value('value')).encode('utf-8')] 45 | 46 | def cache_manager_app(environ, start_response): 47 | cm = environ['beaker.cache'] 48 | cm.get_cache('test')['test_key'] = 'test value' 49 | 50 | start_response('200 OK', [('Content-type', 'text/plain')]) 51 | yield ("test_key is: %s\n" % cm.get_cache('test')['test_key']).encode('utf-8') 52 | cm.get_cache('test').clear() 53 | 54 | try: 55 | test_value = cm.get_cache('test')['test_key'] 56 | except KeyError: 57 | yield ("test_key cleared").encode('utf-8') 58 | else: 59 | test_value = cm.get_cache('test')['test_key'] 60 | yield ("test_key wasn't cleared, is: %s\n" % test_value).encode('utf-8') 61 | 62 | def make_cache(): 63 | """Return a ``Cache`` for use by the unit tests.""" 64 | return Cache('test', data_dir='./cache', bind=engine, table=cache_table, 65 | type='ext:sqla') 66 | 67 | def test_has_key(): 68 | cache = make_cache() 69 | o = object() 70 | cache.set_value("test", o) 71 | assert cache.has_key("test") 72 | assert "test" in cache 73 | assert not cache.has_key("foo") 74 | assert "foo" not in cache 75 | cache.remove_value("test") 76 | assert not cache.has_key("test") 77 | 78 | def test_has_key_multicache(): 79 | cache = make_cache() 80 | o = object() 81 | cache.set_value("test", o) 82 | assert cache.has_key("test") 83 | assert "test" in cache 84 | cache = make_cache() 85 | assert cache.has_key("test") 86 | cache.remove_value('test') 87 | 88 | def test_clear(): 89 | cache = make_cache() 90 | o = object() 91 | cache.set_value("test", o) 92 | assert cache.has_key("test") 93 | cache.clear() 94 | assert not cache.has_key("test") 95 | 96 | def test_unicode_keys(): 97 | cache = make_cache() 98 | o = object() 99 | cache.set_value(u_('hiŏ'), o) 100 | assert u_('hiŏ') in cache 101 | assert u_('hŏa') not in cache 102 | cache.remove_value(u_('hiŏ')) 103 | assert u_('hiŏ') not in cache 104 | 105 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 106 | def test_increment(): 107 | app = TestApp(CacheMiddleware(simple_app)) 108 | res = app.get('/', extra_environ={'beaker.clear': True}) 109 | assert 'current value is: 1' in res 110 | res = app.get('/') 111 | assert 'current value is: 2' in res 112 | res = app.get('/') 113 | assert 'current value is: 3' in res 114 | 115 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 116 | def test_cache_manager(): 117 | app = TestApp(CacheMiddleware(cache_manager_app)) 118 | res = app.get('/') 119 | assert 'test_key is: test value' in res 120 | assert 'test_key cleared' in res 121 | -------------------------------------------------------------------------------- /beaker/ext/google.py: -------------------------------------------------------------------------------- 1 | from beaker._compat import pickle 2 | 3 | import logging 4 | from datetime import datetime 5 | 6 | from beaker.container import OpenResourceNamespaceManager, Container 7 | from beaker.exceptions import InvalidCacheBackendError 8 | from beaker.synchronization import null_synchronizer 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | db = None 13 | 14 | 15 | class GoogleNamespaceManager(OpenResourceNamespaceManager): 16 | tables = {} 17 | 18 | @classmethod 19 | def _init_dependencies(cls): 20 | global db 21 | if db is not None: 22 | return 23 | try: 24 | db = __import__('google.appengine.ext.db').appengine.ext.db 25 | except ImportError: 26 | raise InvalidCacheBackendError("Datastore cache backend requires the " 27 | "'google.appengine.ext' library") 28 | 29 | def __init__(self, namespace, table_name='beaker_cache', **params): 30 | """Creates a datastore namespace manager""" 31 | OpenResourceNamespaceManager.__init__(self, namespace) 32 | 33 | def make_cache(): 34 | table_dict = dict(created=db.DateTimeProperty(), 35 | accessed=db.DateTimeProperty(), 36 | data=db.BlobProperty()) 37 | table = type(table_name, (db.Model,), table_dict) 38 | return table 39 | self.table_name = table_name 40 | self.cache = GoogleNamespaceManager.tables.setdefault(table_name, make_cache()) 41 | self.hash = {} 42 | self._is_new = False 43 | self.loaded = False 44 | self.log_debug = logging.DEBUG >= log.getEffectiveLevel() 45 | 46 | # Google wants namespaces to start with letters, change the namespace 47 | # to start with a letter 48 | self.namespace = 'p%s' % self.namespace 49 | 50 | def get_access_lock(self): 51 | return null_synchronizer() 52 | 53 | def get_creation_lock(self, key): 54 | # this is weird, should probably be present 55 | return null_synchronizer() 56 | 57 | def do_open(self, flags, replace): 58 | # If we already loaded the data, don't bother loading it again 59 | if self.loaded: 60 | self.flags = flags 61 | return 62 | 63 | item = self.cache.get_by_key_name(self.namespace) 64 | 65 | if not item: 66 | self._is_new = True 67 | self.hash = {} 68 | else: 69 | self._is_new = False 70 | try: 71 | self.hash = pickle.loads(str(item.data)) 72 | except (IOError, OSError, EOFError, pickle.PickleError): 73 | if self.log_debug: 74 | log.debug("Couln't load pickle data, creating new storage") 75 | self.hash = {} 76 | self._is_new = True 77 | self.flags = flags 78 | self.loaded = True 79 | 80 | def do_close(self): 81 | if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): 82 | if self._is_new: 83 | item = self.cache(key_name=self.namespace) 84 | item.data = pickle.dumps(self.hash) 85 | item.created = datetime.now() 86 | item.accessed = datetime.now() 87 | item.put() 88 | self._is_new = False 89 | else: 90 | item = self.cache.get_by_key_name(self.namespace) 91 | item.data = pickle.dumps(self.hash) 92 | item.accessed = datetime.now() 93 | item.put() 94 | self.flags = None 95 | 96 | def do_remove(self): 97 | item = self.cache.get_by_key_name(self.namespace) 98 | item.delete() 99 | self.hash = {} 100 | 101 | # We can retain the fact that we did a load attempt, but since the 102 | # file is gone this will be a new namespace should it be saved. 103 | self._is_new = True 104 | 105 | def __getitem__(self, key): 106 | return self.hash[key] 107 | 108 | def __contains__(self, key): 109 | return key in self.hash 110 | 111 | def __setitem__(self, key, value): 112 | self.hash[key] = value 113 | 114 | def __delitem__(self, key): 115 | del self.hash[key] 116 | 117 | def keys(self): 118 | return self.hash.keys() 119 | 120 | 121 | class GoogleContainer(Container): 122 | namespace_class = GoogleNamespaceManager 123 | -------------------------------------------------------------------------------- /beaker/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # True if we are running on Python 2. 4 | PY2 = sys.version_info[0] == 2 5 | PYVER = sys.version_info[:2] 6 | JYTHON = sys.platform.startswith('java') 7 | 8 | if PY2 and not JYTHON: # pragma: no cover 9 | import cPickle as pickle 10 | else: # pragma: no cover 11 | import pickle 12 | 13 | 14 | if not PY2: # pragma: no cover 15 | xrange_ = range 16 | NoneType = type(None) 17 | 18 | string_type = str 19 | unicode_text = str 20 | byte_string = bytes 21 | 22 | from urllib.parse import urlencode as url_encode 23 | from urllib.parse import quote as url_quote 24 | from urllib.parse import unquote as url_unquote 25 | from urllib.parse import urlparse as url_parse 26 | from urllib.request import url2pathname 27 | import http.cookies as http_cookies 28 | from base64 import b64decode as _b64decode, b64encode as _b64encode 29 | 30 | try: 31 | import dbm as anydbm 32 | except: 33 | import dumbdbm as anydbm 34 | 35 | def b64decode(b): 36 | return _b64decode(b.encode('ascii')) 37 | 38 | def b64encode(s): 39 | return _b64encode(s).decode('ascii') 40 | 41 | def u_(s): 42 | return str(s) 43 | 44 | def bytes_(s): 45 | if isinstance(s, byte_string): 46 | return s 47 | return str(s).encode('ascii', 'strict') 48 | 49 | def dictkeyslist(d): 50 | return list(d.keys()) 51 | 52 | else: 53 | xrange_ = xrange 54 | from types import NoneType 55 | 56 | string_type = basestring 57 | unicode_text = unicode 58 | byte_string = str 59 | 60 | from urllib import urlencode as url_encode 61 | from urllib import quote as url_quote 62 | from urllib import unquote as url_unquote 63 | from urlparse import urlparse as url_parse 64 | from urllib import url2pathname 65 | import Cookie as http_cookies 66 | from base64 import b64decode, b64encode 67 | import anydbm 68 | 69 | def u_(s): 70 | if isinstance(s, unicode_text): 71 | return s 72 | 73 | if not isinstance(s, byte_string): 74 | s = str(s) 75 | return unicode(s, 'utf-8') 76 | 77 | def bytes_(s): 78 | if isinstance(s, byte_string): 79 | return s 80 | return str(s) 81 | 82 | def dictkeyslist(d): 83 | return d.keys() 84 | 85 | 86 | def im_func(f): 87 | if not PY2: # pragma: no cover 88 | return getattr(f, '__func__', None) 89 | else: 90 | return getattr(f, 'im_func', None) 91 | 92 | 93 | def default_im_func(f): 94 | if not PY2: # pragma: no cover 95 | return getattr(f, '__func__', f) 96 | else: 97 | return getattr(f, 'im_func', f) 98 | 99 | 100 | def im_self(f): 101 | if not PY2: # pragma: no cover 102 | return getattr(f, '__self__', None) 103 | else: 104 | return getattr(f, 'im_self', None) 105 | 106 | 107 | def im_class(f): 108 | if not PY2: # pragma: no cover 109 | self = im_self(f) 110 | if self is not None: 111 | return self.__class__ 112 | else: 113 | return None 114 | else: 115 | return getattr(f, 'im_class', None) 116 | 117 | 118 | def add_metaclass(metaclass): 119 | """Class decorator for creating a class with a metaclass.""" 120 | def wrapper(cls): 121 | orig_vars = cls.__dict__.copy() 122 | slots = orig_vars.get('__slots__') 123 | if slots is not None: 124 | if isinstance(slots, str): 125 | slots = [slots] 126 | for slots_var in slots: 127 | orig_vars.pop(slots_var) 128 | orig_vars.pop('__dict__', None) 129 | orig_vars.pop('__weakref__', None) 130 | return metaclass(cls.__name__, cls.__bases__, orig_vars) 131 | return wrapper 132 | 133 | 134 | if not PY2: # pragma: no cover 135 | import builtins 136 | exec_ = getattr(builtins, "exec") 137 | 138 | def reraise(tp, value, tb=None): 139 | if value.__traceback__ is not tb: 140 | raise value.with_traceback(tb) 141 | raise value 142 | else: # pragma: no cover 143 | def exec_(code, globs=None, locs=None): 144 | """Execute code in a namespace.""" 145 | if globs is None: 146 | frame = sys._getframe(1) 147 | globs = frame.f_globals 148 | if locs is None: 149 | locs = frame.f_locals 150 | del frame 151 | elif locs is None: 152 | locs = globs 153 | exec("""exec code in globs, locs""") 154 | 155 | exec_("""def reraise(tp, value, tb=None): 156 | raise tp, value, tb 157 | """) 158 | 159 | 160 | try: 161 | from inspect import signature as func_signature 162 | except ImportError: 163 | from funcsigs import signature as func_signature 164 | 165 | 166 | def bindfuncargs(arginfo, args, kwargs): 167 | boundargs = arginfo.bind(*args, **kwargs) 168 | return boundargs.args, boundargs.kwargs -------------------------------------------------------------------------------- /tests/test_unicode_cache_keys.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """If we try to use a character not in ascii range as a cache key, we get an 3 | unicodeencode error. See 4 | https://bitbucket.org/bbangert/beaker/issue/31/cached-function-decorators-break-when-some 5 | for more on this 6 | """ 7 | 8 | from nose.tools import * 9 | from beaker._compat import u_ 10 | from beaker.cache import CacheManager 11 | 12 | memory_cache = CacheManager(type='memory') 13 | 14 | @memory_cache.cache('foo') 15 | def foo(whatever): 16 | return whatever 17 | 18 | class bar(object): 19 | 20 | @memory_cache.cache('baz') 21 | def baz(self, qux): 22 | return qux 23 | 24 | @classmethod 25 | @memory_cache.cache('bar') 26 | def quux(cls, garply): 27 | return garply 28 | 29 | def test_A_unicode_encode_key_str(): 30 | eq_(foo('Espanol'), 'Espanol') 31 | eq_(foo(12334), 12334) 32 | eq_(foo(u_('Espanol')), u_('Espanol')) 33 | eq_(foo(u_('Español')), u_('Español')) 34 | b = bar() 35 | eq_(b.baz('Espanol'), 'Espanol') 36 | eq_(b.baz(12334), 12334) 37 | eq_(b.baz(u_('Espanol')), u_('Espanol')) 38 | eq_(b.baz(u_('Español')), u_('Español')) 39 | eq_(b.quux('Espanol'), 'Espanol') 40 | eq_(b.quux(12334), 12334) 41 | eq_(b.quux(u_('Espanol')), u_('Espanol')) 42 | eq_(b.quux(u_('Español')), u_('Español')) 43 | 44 | 45 | def test_B_replacing_non_ascii(): 46 | """we replace the offending character with other non ascii one. Since 47 | the function distinguishes between the two it should not return the 48 | past value 49 | """ 50 | assert_false(foo(u_('Espaáol'))==u_('Español')) 51 | eq_(foo(u_('Espaáol')), u_('Espaáol')) 52 | 53 | def test_C_more_unicode(): 54 | """We again test the same stuff but this time we use 55 | http://tools.ietf.org/html/draft-josefsson-idn-test-vectors-00#section-5 56 | as keys""" 57 | keys = [ 58 | # arabic (egyptian) 59 | u_("\u0644\u064a\u0647\u0645\u0627\u0628\u062a\u0643\u0644\u0645\u0648\u0634\u0639\u0631\u0628\u064a\u061f"), 60 | # Chinese (simplified) 61 | u_("\u4ed6\u4eec\u4e3a\u4ec0\u4e48\u4e0d\u8bf4\u4e2d\u6587"), 62 | # Chinese (traditional) 63 | u_("\u4ed6\u5011\u7232\u4ec0\u9ebd\u4e0d\u8aaa\u4e2d\u6587"), 64 | # czech 65 | u_("\u0050\u0072\u006f\u010d\u0070\u0072\u006f\u0073\u0074\u011b\u006e\u0065\u006d\u006c\u0075\u0076\u00ed\u010d\u0065\u0073\u006b\u0079"), 66 | # hebrew 67 | u_("\u05dc\u05de\u05d4\u05d4\u05dd\u05e4\u05e9\u05d5\u05d8\u05dc\u05d0\u05de\u05d3\u05d1\u05e8\u05d9\u05dd\u05e2\u05d1\u05e8\u05d9\u05ea"), 68 | # Hindi (Devanagari) 69 | u_("\u092f\u0939\u0932\u094b\u0917\u0939\u093f\u0928\u094d\u0926\u0940\u0915\u094d\u092f\u094b\u0902\u0928\u0939\u0940\u0902\u092c\u094b\u0932\u0938\u0915\u0924\u0947\u0939\u0948\u0902"), 70 | # Japanese (kanji and hiragana) 71 | u_("\u306a\u305c\u307f\u3093\u306a\u65e5\u672c\u8a9e\u3092\u8a71\u3057\u3066\u304f\u308c\u306a\u3044\u306e\u304b"), 72 | # Russian (Cyrillic) 73 | u_("\u043f\u043e\u0447\u0435\u043c\u0443\u0436\u0435\u043e\u043d\u0438\u043d\u0435\u0433\u043e\u0432\u043e\u0440\u044f\u0442\u043f\u043e\u0440\u0443\u0441\u0441\u043a\u0438"), 74 | # Spanish 75 | u_("\u0050\u006f\u0072\u0071\u0075\u00e9\u006e\u006f\u0070\u0075\u0065\u0064\u0065\u006e\u0073\u0069\u006d\u0070\u006c\u0065\u006d\u0065\u006e\u0074\u0065\u0068\u0061\u0062\u006c\u0061\u0072\u0065\u006e\u0045\u0073\u0070\u0061\u00f1\u006f\u006c"), 76 | # Vietnamese 77 | u_("\u0054\u1ea1\u0069\u0073\u0061\u006f\u0068\u1ecd\u006b\u0068\u00f4\u006e\u0067\u0074\u0068\u1ec3\u0063\u0068\u1ec9\u006e\u00f3\u0069\u0074\u0069\u1ebf\u006e\u0067\u0056\u0069\u1ec7\u0074"), 78 | # Japanese 79 | u_("\u0033\u5e74\u0042\u7d44\u91d1\u516b\u5148\u751f"), 80 | # Japanese 81 | u_("\u5b89\u5ba4\u5948\u7f8e\u6075\u002d\u0077\u0069\u0074\u0068\u002d\u0053\u0055\u0050\u0045\u0052\u002d\u004d\u004f\u004e\u004b\u0045\u0059\u0053"), 82 | # Japanese 83 | u_("\u0048\u0065\u006c\u006c\u006f\u002d\u0041\u006e\u006f\u0074\u0068\u0065\u0072\u002d\u0057\u0061\u0079\u002d\u305d\u308c\u305e\u308c\u306e\u5834\u6240"), 84 | # Japanese 85 | u_("\u3072\u3068\u3064\u5c4b\u6839\u306e\u4e0b\u0032"), 86 | # Japanese 87 | u_("\u004d\u0061\u006a\u0069\u3067\u004b\u006f\u0069\u3059\u308b\u0035\u79d2\u524d"), 88 | # Japanese 89 | u_("\u30d1\u30d5\u30a3\u30fc\u0064\u0065\u30eb\u30f3\u30d0"), 90 | # Japanese 91 | u_("\u305d\u306e\u30b9\u30d4\u30fc\u30c9\u3067"), 92 | # greek 93 | u_("\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"), 94 | # Maltese (Malti) 95 | u_("\u0062\u006f\u006e\u0121\u0075\u0073\u0061\u0127\u0127\u0061"), 96 | # Russian (Cyrillic) 97 | u_("\u043f\u043e\u0447\u0435\u043c\u0443\u0436\u0435\u043e\u043d\u0438\u043d\u0435\u0433\u043e\u0432\u043e\u0440\u044f\u0442\u043f\u043e\u0440\u0443\u0441\u0441\u043a\u0438") 98 | ] 99 | for i in keys: 100 | eq_(foo(i),i) 101 | 102 | def test_D_invalidate(): 103 | """Invalidate cache""" 104 | memory_cache.invalidate(foo) 105 | eq_(foo('Espanol'), 'Espanol') 106 | -------------------------------------------------------------------------------- /beaker/ext/sqla.py: -------------------------------------------------------------------------------- 1 | from beaker._compat import pickle 2 | 3 | import logging 4 | import pickle 5 | from datetime import datetime 6 | 7 | from beaker.container import OpenResourceNamespaceManager, Container 8 | from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter 9 | from beaker.synchronization import file_synchronizer, null_synchronizer 10 | from beaker.util import verify_directory, SyncDict 11 | 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | sa = None 16 | 17 | 18 | class SqlaNamespaceManager(OpenResourceNamespaceManager): 19 | binds = SyncDict() 20 | tables = SyncDict() 21 | 22 | @classmethod 23 | def _init_dependencies(cls): 24 | global sa 25 | if sa is not None: 26 | return 27 | try: 28 | import sqlalchemy as sa 29 | except ImportError: 30 | raise InvalidCacheBackendError("SQLAlchemy, which is required by " 31 | "this backend, is not installed") 32 | 33 | def __init__(self, namespace, bind, table, data_dir=None, lock_dir=None, 34 | **kwargs): 35 | """Create a namespace manager for use with a database table via 36 | SQLAlchemy. 37 | 38 | ``bind`` 39 | SQLAlchemy ``Engine`` or ``Connection`` object 40 | 41 | ``table`` 42 | SQLAlchemy ``Table`` object in which to store namespace data. 43 | This should usually be something created by ``make_cache_table``. 44 | """ 45 | OpenResourceNamespaceManager.__init__(self, namespace) 46 | 47 | if lock_dir: 48 | self.lock_dir = lock_dir 49 | elif data_dir: 50 | self.lock_dir = data_dir + "/container_db_lock" 51 | if self.lock_dir: 52 | verify_directory(self.lock_dir) 53 | 54 | self.bind = self.__class__.binds.get(str(bind.url), lambda: bind) 55 | self.table = self.__class__.tables.get('%s:%s' % (bind.url, table.name), 56 | lambda: table) 57 | self.hash = {} 58 | self._is_new = False 59 | self.loaded = False 60 | 61 | def get_access_lock(self): 62 | return null_synchronizer() 63 | 64 | def get_creation_lock(self, key): 65 | return file_synchronizer( 66 | identifier="databasecontainer/funclock/%s" % self.namespace, 67 | lock_dir=self.lock_dir) 68 | 69 | def do_open(self, flags, replace): 70 | if self.loaded: 71 | self.flags = flags 72 | return 73 | select = sa.select([self.table.c.data], 74 | (self.table.c.namespace == self.namespace)) 75 | result = self.bind.execute(select).fetchone() 76 | if not result: 77 | self._is_new = True 78 | self.hash = {} 79 | else: 80 | self._is_new = False 81 | try: 82 | self.hash = result['data'] 83 | except (IOError, OSError, EOFError, pickle.PickleError, 84 | pickle.PickleError): 85 | log.debug("Couln't load pickle data, creating new storage") 86 | self.hash = {} 87 | self._is_new = True 88 | self.flags = flags 89 | self.loaded = True 90 | 91 | def do_close(self): 92 | if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): 93 | if self._is_new: 94 | insert = self.table.insert() 95 | self.bind.execute(insert, namespace=self.namespace, data=self.hash, 96 | accessed=datetime.now(), created=datetime.now()) 97 | self._is_new = False 98 | else: 99 | update = self.table.update(self.table.c.namespace == self.namespace) 100 | self.bind.execute(update, data=self.hash, accessed=datetime.now()) 101 | self.flags = None 102 | 103 | def do_remove(self): 104 | delete = self.table.delete(self.table.c.namespace == self.namespace) 105 | self.bind.execute(delete) 106 | self.hash = {} 107 | self._is_new = True 108 | 109 | def __getitem__(self, key): 110 | return self.hash[key] 111 | 112 | def __contains__(self, key): 113 | return key in self.hash 114 | 115 | def __setitem__(self, key, value): 116 | self.hash[key] = value 117 | 118 | def __delitem__(self, key): 119 | del self.hash[key] 120 | 121 | def keys(self): 122 | return self.hash.keys() 123 | 124 | 125 | class SqlaContainer(Container): 126 | namespace_manager = SqlaNamespaceManager 127 | 128 | 129 | def make_cache_table(metadata, table_name='beaker_cache', schema_name=None): 130 | """Return a ``Table`` object suitable for storing cached values for the 131 | namespace manager. Do not create the table.""" 132 | return sa.Table(table_name, metadata, 133 | sa.Column('namespace', sa.String(255), primary_key=True), 134 | sa.Column('accessed', sa.DateTime, nullable=False), 135 | sa.Column('created', sa.DateTime, nullable=False), 136 | sa.Column('data', sa.PickleType, nullable=False), 137 | schema=schema_name if schema_name else metadata.schema) 138 | -------------------------------------------------------------------------------- /tests/test_container.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import time 4 | from beaker.container import * 5 | from beaker.synchronization import _synchronizers 6 | from beaker.cache import clsmap 7 | from threading import Thread 8 | 9 | class CachedWidget(object): 10 | totalcreates = 0 11 | delay = 0 12 | 13 | def __init__(self): 14 | CachedWidget.totalcreates += 1 15 | time.sleep(CachedWidget.delay) 16 | self.time = time.time() 17 | 18 | def _run_container_test(cls, totaltime, expiretime, delay, threadlocal): 19 | print("\ntesting %s for %d secs with expiretime %s delay %d" % ( 20 | cls, totaltime, expiretime, delay)) 21 | 22 | CachedWidget.totalcreates = 0 23 | CachedWidget.delay = delay 24 | 25 | # allow for python overhead when checking current time against expire times 26 | fudge = 10 27 | 28 | starttime = time.time() 29 | 30 | running = [True] 31 | class RunThread(Thread): 32 | def run(self): 33 | print("%s starting" % self) 34 | 35 | if threadlocal: 36 | localvalue = Value( 37 | 'test', 38 | cls('test', data_dir='./cache'), 39 | createfunc=CachedWidget, 40 | expiretime=expiretime, 41 | starttime=starttime) 42 | localvalue.clear_value() 43 | else: 44 | localvalue = value 45 | 46 | try: 47 | while running[0]: 48 | item = localvalue.get_value() 49 | if expiretime is not None: 50 | currenttime = time.time() 51 | itemtime = item.time 52 | assert itemtime + expiretime + delay + fudge >= currenttime, \ 53 | "created: %f expire: %f delay: %f currenttime: %f" % \ 54 | (itemtime, expiretime, delay, currenttime) 55 | time.sleep(random.random() * .00001) 56 | except: 57 | running[0] = False 58 | raise 59 | print("%s finishing" % self) 60 | 61 | if not threadlocal: 62 | value = Value( 63 | 'test', 64 | cls('test', data_dir='./cache'), 65 | createfunc=CachedWidget, 66 | expiretime=expiretime, 67 | starttime=starttime) 68 | value.clear_value() 69 | else: 70 | value = None 71 | 72 | threads = [RunThread() for i in range(1, 8)] 73 | 74 | for t in threads: 75 | t.start() 76 | 77 | time.sleep(totaltime) 78 | 79 | failed = not running[0] 80 | running[0] = False 81 | 82 | for t in threads: 83 | t.join() 84 | 85 | assert not failed, "One or more threads failed" 86 | if expiretime is None: 87 | expected = 1 88 | else: 89 | expected = totaltime / expiretime + 1 90 | assert CachedWidget.totalcreates <= expected, \ 91 | "Number of creates %d exceeds expected max %d" % (CachedWidget.totalcreates, expected) 92 | 93 | def test_memory_container(totaltime=10, expiretime=None, delay=0, threadlocal=False): 94 | _run_container_test(clsmap['memory'], 95 | totaltime, expiretime, delay, threadlocal) 96 | 97 | def test_dbm_container(totaltime=10, expiretime=None, delay=0): 98 | _run_container_test(clsmap['dbm'], totaltime, expiretime, delay, False) 99 | 100 | def test_file_container(totaltime=10, expiretime=None, delay=0, threadlocal=False): 101 | _run_container_test(clsmap['file'], totaltime, expiretime, delay, threadlocal) 102 | 103 | def test_memory_container_tlocal(): 104 | test_memory_container(expiretime=15, delay=2, threadlocal=True) 105 | 106 | def test_memory_container_2(): 107 | test_memory_container(expiretime=12) 108 | 109 | def test_memory_container_3(): 110 | test_memory_container(expiretime=15, delay=2) 111 | 112 | def test_dbm_container_2(): 113 | test_dbm_container(expiretime=12) 114 | 115 | def test_dbm_container_3(): 116 | test_dbm_container(expiretime=15, delay=2) 117 | 118 | def test_file_container_2(): 119 | test_file_container(expiretime=12) 120 | 121 | def test_file_container_3(): 122 | test_file_container(expiretime=15, delay=2) 123 | 124 | def test_file_container_tlocal(): 125 | test_file_container(expiretime=15, delay=2, threadlocal=True) 126 | 127 | def test_file_open_bug(): 128 | """ensure errors raised during reads or writes don't lock the namespace open.""" 129 | 130 | value = Value('test', clsmap['file']('reentrant_test', data_dir='./cache')) 131 | if os.path.exists(value.namespace.file): 132 | os.remove(value.namespace.file) 133 | 134 | value.set_value("x") 135 | 136 | f = open(value.namespace.file, 'w') 137 | f.write("BLAH BLAH BLAH") 138 | f.close() 139 | 140 | # TODO: do we have an assertRaises() in nose to use here ? 141 | try: 142 | value.set_value("y") 143 | assert False 144 | except: 145 | pass 146 | 147 | _synchronizers.clear() 148 | 149 | value = Value('test', clsmap['file']('reentrant_test', data_dir='./cache')) 150 | 151 | # TODO: do we have an assertRaises() in nose to use here ? 152 | try: 153 | value.set_value("z") 154 | assert False 155 | except: 156 | pass 157 | 158 | 159 | def test_removing_file_refreshes(): 160 | """test that the cache doesn't ignore file removals""" 161 | 162 | x = [0] 163 | def create(): 164 | x[0] += 1 165 | return x[0] 166 | 167 | value = Value('test', 168 | clsmap['file']('refresh_test', data_dir='./cache'), 169 | createfunc=create, starttime=time.time() 170 | ) 171 | if os.path.exists(value.namespace.file): 172 | os.remove(value.namespace.file) 173 | assert value.get_value() == 1 174 | assert value.get_value() == 1 175 | os.remove(value.namespace.file) 176 | assert value.get_value() == 2 177 | 178 | 179 | def teardown(): 180 | import shutil 181 | shutil.rmtree('./cache', True) 182 | -------------------------------------------------------------------------------- /tests/test_cachemanager.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | from beaker.cache import CacheManager, cache_regions 5 | from beaker.util import parse_cache_config_options 6 | 7 | defaults = {'cache.data_dir':'./cache', 'cache.type':'dbm', 'cache.expire': 2} 8 | 9 | def teardown(): 10 | import shutil 11 | shutil.rmtree('./cache', True) 12 | 13 | def make_cache_obj(**kwargs): 14 | opts = defaults.copy() 15 | opts.update(kwargs) 16 | cache = CacheManager(**parse_cache_config_options(opts)) 17 | return cache 18 | 19 | def make_region_cached_func(): 20 | global _cache_obj 21 | opts = {} 22 | opts['cache.regions'] = 'short_term, long_term' 23 | opts['cache.short_term.expire'] = '2' 24 | cache = make_cache_obj(**opts) 25 | 26 | @cache.region('short_term', 'region_loader') 27 | def load(person): 28 | now = datetime.now() 29 | return "Hi there %s, its currently %s" % (person, now) 30 | _cache_obj = cache 31 | return load 32 | 33 | def make_cached_func(): 34 | global _cache_obj 35 | cache = make_cache_obj() 36 | 37 | @cache.cache('loader') 38 | def load(person): 39 | now = datetime.now() 40 | return "Hi there %s, its currently %s" % (person, now) 41 | _cache_obj = cache 42 | return load 43 | 44 | def test_parse_doesnt_allow_none(): 45 | opts = {} 46 | opts['cache.regions'] = 'short_term, long_term' 47 | for region, params in parse_cache_config_options(opts)['cache_regions'].items(): 48 | for k, v in params.items(): 49 | assert v != 'None', k 50 | 51 | def test_parse_doesnt_allow_empty_region_name(): 52 | opts = {} 53 | opts['cache.regions'] = '' 54 | regions = parse_cache_config_options(opts)['cache_regions'] 55 | assert len(regions) == 0 56 | 57 | def test_decorators(): 58 | for func in (make_region_cached_func, make_cached_func): 59 | yield check_decorator, func() 60 | 61 | def check_decorator(func): 62 | result = func('Fred') 63 | assert 'Fred' in result 64 | 65 | result2 = func('Fred') 66 | assert result == result2 67 | 68 | result3 = func('George') 69 | assert 'George' in result3 70 | result4 = func('George') 71 | assert result3 == result4 72 | 73 | time.sleep(2) 74 | result2 = func('Fred') 75 | assert result != result2 76 | 77 | def test_check_invalidate_region(): 78 | func = make_region_cached_func() 79 | result = func('Fred') 80 | assert 'Fred' in result 81 | 82 | result2 = func('Fred') 83 | assert result == result2 84 | _cache_obj.region_invalidate(func, None, 'region_loader', 'Fred') 85 | 86 | result3 = func('Fred') 87 | assert result3 != result2 88 | 89 | result2 = func('Fred') 90 | assert result3 == result2 91 | 92 | # Invalidate a non-existent key 93 | _cache_obj.region_invalidate(func, None, 'region_loader', 'Fredd') 94 | assert result3 == result2 95 | 96 | def test_check_invalidate(): 97 | func = make_cached_func() 98 | result = func('Fred') 99 | assert 'Fred' in result 100 | 101 | result2 = func('Fred') 102 | assert result == result2 103 | _cache_obj.invalidate(func, 'loader', 'Fred') 104 | 105 | result3 = func('Fred') 106 | assert result3 != result2 107 | 108 | result2 = func('Fred') 109 | assert result3 == result2 110 | 111 | # Invalidate a non-existent key 112 | _cache_obj.invalidate(func, 'loader', 'Fredd') 113 | assert result3 == result2 114 | 115 | def test_long_name(): 116 | func = make_cached_func() 117 | name = 'Fred' * 250 118 | result = func(name) 119 | assert name in result 120 | 121 | result2 = func(name) 122 | assert result == result2 123 | # This won't actually invalidate it since the key won't be sha'd 124 | _cache_obj.invalidate(func, 'loader', name, key_length=8000) 125 | 126 | result3 = func(name) 127 | assert result3 == result2 128 | 129 | # And now this should invalidate it 130 | _cache_obj.invalidate(func, 'loader', name) 131 | result4 = func(name) 132 | assert result3 != result4 133 | 134 | 135 | def test_cache_region_has_default_key_length(): 136 | try: 137 | cache = CacheManager(cache_regions={ 138 | 'short_term_without_key_length':{ 139 | 'expire': 60, 140 | 'type': 'memory' 141 | } 142 | }) 143 | 144 | # Check CacheManager registered the region in global regions 145 | assert 'short_term_without_key_length' in cache_regions 146 | 147 | @cache.region('short_term_without_key_length') 148 | def load_without_key_length(person): 149 | now = datetime.now() 150 | return "Hi there %s, its currently %s" % (person, now) 151 | 152 | # Ensure that same person gets same time 153 | msg = load_without_key_length('fred') 154 | msg2 = load_without_key_length('fred') 155 | assert msg == msg2, (msg, msg2) 156 | 157 | # Ensure that different person gets different time 158 | msg3 = load_without_key_length('george') 159 | assert msg3.split(',')[-1] != msg2.split(',')[-1] 160 | 161 | finally: 162 | # throw away region for this test 163 | cache_regions.pop('short_term_without_key_length', None) 164 | 165 | 166 | def test_cache_region_expire_is_always_int(): 167 | try: 168 | cache = CacheManager(cache_regions={ 169 | 'short_term_with_string_expire': { 170 | 'expire': '60', 171 | 'type': 'memory' 172 | } 173 | }) 174 | 175 | # Check CacheManager registered the region in global regions 176 | assert 'short_term_with_string_expire' in cache_regions 177 | 178 | @cache.region('short_term_with_string_expire') 179 | def load_with_str_expire(person): 180 | now = datetime.now() 181 | return "Hi there %s, its currently %s" % (person, now) 182 | 183 | # Ensure that same person gets same time 184 | msg = load_with_str_expire('fred') 185 | msg2 = load_with_str_expire('fred') 186 | assert msg == msg2, (msg, msg2) 187 | 188 | finally: 189 | # throw away region for this test 190 | cache_regions.pop('short_term_with_string_expire', None) 191 | -------------------------------------------------------------------------------- /beaker/ext/database.py: -------------------------------------------------------------------------------- 1 | from beaker._compat import pickle 2 | 3 | import logging 4 | import pickle 5 | from datetime import datetime 6 | 7 | from beaker.container import OpenResourceNamespaceManager, Container 8 | from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter 9 | from beaker.synchronization import file_synchronizer, null_synchronizer 10 | from beaker.util import verify_directory, SyncDict 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | sa = None 15 | pool = None 16 | types = None 17 | 18 | 19 | class DatabaseNamespaceManager(OpenResourceNamespaceManager): 20 | metadatas = SyncDict() 21 | tables = SyncDict() 22 | 23 | @classmethod 24 | def _init_dependencies(cls): 25 | global sa, pool, types 26 | if sa is not None: 27 | return 28 | try: 29 | import sqlalchemy as sa 30 | import sqlalchemy.pool as pool 31 | from sqlalchemy import types 32 | except ImportError: 33 | raise InvalidCacheBackendError("Database cache backend requires " 34 | "the 'sqlalchemy' library") 35 | 36 | def __init__(self, namespace, url=None, sa_opts=None, optimistic=False, 37 | table_name='beaker_cache', data_dir=None, lock_dir=None, 38 | schema_name=None, **params): 39 | """Creates a database namespace manager 40 | 41 | ``url`` 42 | SQLAlchemy compliant db url 43 | ``sa_opts`` 44 | A dictionary of SQLAlchemy keyword options to initialize the engine 45 | with. 46 | ``optimistic`` 47 | Use optimistic session locking, note that this will result in an 48 | additional select when updating a cache value to compare version 49 | numbers. 50 | ``table_name`` 51 | The table name to use in the database for the cache. 52 | ``schema_name`` 53 | The schema name to use in the database for the cache. 54 | """ 55 | OpenResourceNamespaceManager.__init__(self, namespace) 56 | 57 | if sa_opts is None: 58 | sa_opts = params 59 | 60 | self.lock_dir = None 61 | 62 | if lock_dir: 63 | self.lock_dir = lock_dir 64 | elif data_dir: 65 | self.lock_dir = data_dir + "/container_db_lock" 66 | if self.lock_dir: 67 | verify_directory(self.lock_dir) 68 | 69 | # Check to see if the table's been created before 70 | url = url or sa_opts['sa.url'] 71 | table_key = url + table_name 72 | 73 | def make_cache(): 74 | # Check to see if we have a connection pool open already 75 | meta_key = url + table_name 76 | 77 | def make_meta(): 78 | # SQLAlchemy pops the url, this ensures it sticks around 79 | # later 80 | sa_opts['sa.url'] = url 81 | engine = sa.engine_from_config(sa_opts, 'sa.') 82 | meta = sa.MetaData() 83 | meta.bind = engine 84 | return meta 85 | meta = DatabaseNamespaceManager.metadatas.get(meta_key, make_meta) 86 | # Create the table object and cache it now 87 | cache = sa.Table(table_name, meta, 88 | sa.Column('id', types.Integer, primary_key=True), 89 | sa.Column('namespace', types.String(255), nullable=False), 90 | sa.Column('accessed', types.DateTime, nullable=False), 91 | sa.Column('created', types.DateTime, nullable=False), 92 | sa.Column('data', types.PickleType, nullable=False), 93 | sa.UniqueConstraint('namespace'), 94 | schema=schema_name if schema_name else meta.schema 95 | ) 96 | cache.create(checkfirst=True) 97 | return cache 98 | self.hash = {} 99 | self._is_new = False 100 | self.loaded = False 101 | self.cache = DatabaseNamespaceManager.tables.get(table_key, make_cache) 102 | 103 | def get_access_lock(self): 104 | return null_synchronizer() 105 | 106 | def get_creation_lock(self, key): 107 | return file_synchronizer( 108 | identifier="databasecontainer/funclock/%s/%s" % ( 109 | self.namespace, key 110 | ), 111 | lock_dir=self.lock_dir) 112 | 113 | def do_open(self, flags, replace): 114 | # If we already loaded the data, don't bother loading it again 115 | if self.loaded: 116 | self.flags = flags 117 | return 118 | 119 | cache = self.cache 120 | result = sa.select([cache.c.data], 121 | cache.c.namespace == self.namespace 122 | ).execute().fetchone() 123 | if not result: 124 | self._is_new = True 125 | self.hash = {} 126 | else: 127 | self._is_new = False 128 | try: 129 | self.hash = result['data'] 130 | except (IOError, OSError, EOFError, pickle.PickleError, 131 | pickle.PickleError): 132 | log.debug("Couln't load pickle data, creating new storage") 133 | self.hash = {} 134 | self._is_new = True 135 | self.flags = flags 136 | self.loaded = True 137 | 138 | def do_close(self): 139 | if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): 140 | cache = self.cache 141 | if self._is_new: 142 | cache.insert().execute(namespace=self.namespace, data=self.hash, 143 | accessed=datetime.now(), 144 | created=datetime.now()) 145 | self._is_new = False 146 | else: 147 | cache.update(cache.c.namespace == self.namespace).execute( 148 | data=self.hash, accessed=datetime.now()) 149 | self.flags = None 150 | 151 | def do_remove(self): 152 | cache = self.cache 153 | cache.delete(cache.c.namespace == self.namespace).execute() 154 | self.hash = {} 155 | 156 | # We can retain the fact that we did a load attempt, but since the 157 | # file is gone this will be a new namespace should it be saved. 158 | self._is_new = True 159 | 160 | def __getitem__(self, key): 161 | return self.hash[key] 162 | 163 | def __contains__(self, key): 164 | return key in self.hash 165 | 166 | def __setitem__(self, key, value): 167 | self.hash[key] = value 168 | 169 | def __delitem__(self, key): 170 | del self.hash[key] 171 | 172 | def keys(self): 173 | return self.hash.keys() 174 | 175 | 176 | class DatabaseContainer(Container): 177 | namespace_manager = DatabaseNamespaceManager 178 | -------------------------------------------------------------------------------- /beaker/middleware.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | try: 4 | from paste.registry import StackedObjectProxy 5 | beaker_session = StackedObjectProxy(name="Beaker Session") 6 | beaker_cache = StackedObjectProxy(name="Cache Manager") 7 | except: 8 | beaker_cache = None 9 | beaker_session = None 10 | 11 | from beaker.cache import CacheManager 12 | from beaker.session import Session, SessionObject 13 | from beaker.util import coerce_cache_params, coerce_session_params, \ 14 | parse_cache_config_options 15 | 16 | 17 | class CacheMiddleware(object): 18 | cache = beaker_cache 19 | 20 | def __init__(self, app, config=None, environ_key='beaker.cache', **kwargs): 21 | """Initialize the Cache Middleware 22 | 23 | The Cache middleware will make a CacheManager instance available 24 | every request under the ``environ['beaker.cache']`` key by 25 | default. The location in environ can be changed by setting 26 | ``environ_key``. 27 | 28 | ``config`` 29 | dict All settings should be prefixed by 'cache.'. This 30 | method of passing variables is intended for Paste and other 31 | setups that accumulate multiple component settings in a 32 | single dictionary. If config contains *no cache. prefixed 33 | args*, then *all* of the config options will be used to 34 | intialize the Cache objects. 35 | 36 | ``environ_key`` 37 | Location where the Cache instance will keyed in the WSGI 38 | environ 39 | 40 | ``**kwargs`` 41 | All keyword arguments are assumed to be cache settings and 42 | will override any settings found in ``config`` 43 | 44 | """ 45 | self.app = app 46 | config = config or {} 47 | 48 | self.options = {} 49 | 50 | # Update the options with the parsed config 51 | self.options.update(parse_cache_config_options(config)) 52 | 53 | # Add any options from kwargs, but leave out the defaults this 54 | # time 55 | self.options.update( 56 | parse_cache_config_options(kwargs, include_defaults=False)) 57 | 58 | # Assume all keys are intended for cache if none are prefixed with 59 | # 'cache.' 60 | if not self.options and config: 61 | self.options = config 62 | 63 | self.options.update(kwargs) 64 | self.cache_manager = CacheManager(**self.options) 65 | self.environ_key = environ_key 66 | 67 | def __call__(self, environ, start_response): 68 | if environ.get('paste.registry'): 69 | if environ['paste.registry'].reglist: 70 | environ['paste.registry'].register(self.cache, 71 | self.cache_manager) 72 | environ[self.environ_key] = self.cache_manager 73 | return self.app(environ, start_response) 74 | 75 | 76 | class SessionMiddleware(object): 77 | session = beaker_session 78 | 79 | def __init__(self, wrap_app, config=None, environ_key='beaker.session', 80 | **kwargs): 81 | """Initialize the Session Middleware 82 | 83 | The Session middleware will make a lazy session instance 84 | available every request under the ``environ['beaker.session']`` 85 | key by default. The location in environ can be changed by 86 | setting ``environ_key``. 87 | 88 | ``config`` 89 | dict All settings should be prefixed by 'session.'. This 90 | method of passing variables is intended for Paste and other 91 | setups that accumulate multiple component settings in a 92 | single dictionary. If config contains *no session. prefixed 93 | args*, then *all* of the config options will be used to 94 | intialize the Session objects. 95 | 96 | ``environ_key`` 97 | Location where the Session instance will keyed in the WSGI 98 | environ 99 | 100 | ``**kwargs`` 101 | All keyword arguments are assumed to be session settings and 102 | will override any settings found in ``config`` 103 | 104 | """ 105 | config = config or {} 106 | 107 | # Load up the default params 108 | self.options = dict(invalidate_corrupt=True, type=None, 109 | data_dir=None, key='beaker.session.id', 110 | timeout=None, secret=None, log_file=None) 111 | 112 | # Pull out any config args meant for beaker session. if there are any 113 | for dct in [config, kwargs]: 114 | for key, val in dct.items(): 115 | if key.startswith('beaker.session.'): 116 | self.options[key[15:]] = val 117 | if key.startswith('session.'): 118 | self.options[key[8:]] = val 119 | if key.startswith('session_'): 120 | warnings.warn('Session options should start with session. ' 121 | 'instead of session_.', DeprecationWarning, 2) 122 | self.options[key[8:]] = val 123 | 124 | # Coerce and validate session params 125 | coerce_session_params(self.options) 126 | 127 | # Assume all keys are intended for session if none are prefixed with 128 | # 'session.' 129 | if not self.options and config: 130 | self.options = config 131 | 132 | self.options.update(kwargs) 133 | self.wrap_app = self.app = wrap_app 134 | self.environ_key = environ_key 135 | 136 | def __call__(self, environ, start_response): 137 | session = SessionObject(environ, **self.options) 138 | if environ.get('paste.registry'): 139 | if environ['paste.registry'].reglist: 140 | environ['paste.registry'].register(self.session, session) 141 | environ[self.environ_key] = session 142 | environ['beaker.get_session'] = self._get_session 143 | 144 | if 'paste.testing_variables' in environ and 'webtest_varname' in self.options: 145 | environ['paste.testing_variables'][self.options['webtest_varname']] = session 146 | 147 | def session_start_response(status, headers, exc_info=None): 148 | if session.accessed(): 149 | session.persist() 150 | if session.__dict__['_headers']['set_cookie']: 151 | cookie = session.__dict__['_headers']['cookie_out'] 152 | if cookie: 153 | headers.append(('Set-cookie', cookie)) 154 | return start_response(status, headers, exc_info) 155 | return self.wrap_app(environ, session_start_response) 156 | 157 | def _get_session(self): 158 | return Session({}, use_cookies=False, **self.options) 159 | 160 | 161 | def session_filter_factory(global_conf, **kwargs): 162 | def filter(app): 163 | return SessionMiddleware(app, global_conf, **kwargs) 164 | return filter 165 | 166 | 167 | def session_filter_app_factory(app, global_conf, **kwargs): 168 | return SessionMiddleware(app, global_conf, **kwargs) 169 | -------------------------------------------------------------------------------- /tests/test_increment.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | from beaker.middleware import SessionMiddleware 5 | from nose import SkipTest 6 | try: 7 | from webtest import TestApp 8 | except ImportError: 9 | raise SkipTest("webtest not installed") 10 | 11 | 12 | def teardown(): 13 | import shutil 14 | shutil.rmtree('./cache', True) 15 | 16 | def no_save_app(environ, start_response): 17 | session = environ['beaker.session'] 18 | sess_id = environ.get('SESSION_ID') 19 | start_response('200 OK', [('Content-type', 'text/plain')]) 20 | msg = 'The current value is: %s, session id is %s' % (session.get('value'), 21 | session.id) 22 | return [msg.encode('utf-8')] 23 | 24 | def simple_app(environ, start_response): 25 | session = environ['beaker.session'] 26 | sess_id = environ.get('SESSION_ID') 27 | if sess_id: 28 | session = session.get_by_id(sess_id) 29 | if not session: 30 | start_response('200 OK', [('Content-type', 'text/plain')]) 31 | return [("No session id of %s found." % sess_id).encode('utf-8')] 32 | if not 'value' in session: 33 | session['value'] = 0 34 | session['value'] += 1 35 | if not environ['PATH_INFO'].startswith('/nosave'): 36 | session.save() 37 | start_response('200 OK', [('Content-type', 'text/plain')]) 38 | msg = 'The current value is: %s, session id is %s' % (session.get('value'), 39 | session.id) 40 | return [msg.encode('utf-8')] 41 | 42 | def simple_auto_app(environ, start_response): 43 | """Like the simple_app, but assume that sessions auto-save""" 44 | session = environ['beaker.session'] 45 | sess_id = environ.get('SESSION_ID') 46 | if sess_id: 47 | session = session.get_by_id(sess_id) 48 | if not session: 49 | start_response('200 OK', [('Content-type', 'text/plain')]) 50 | return [("No session id of %s found." % sess_id).encode('utf-8')] 51 | if not 'value' in session: 52 | session['value'] = 0 53 | session['value'] += 1 54 | if environ['PATH_INFO'].startswith('/nosave'): 55 | session.revert() 56 | start_response('200 OK', [('Content-type', 'text/plain')]) 57 | msg = 'The current value is: %s, session id is %s' % (session.get('value', 0), 58 | session.id) 59 | return [msg.encode('utf-8')] 60 | 61 | def test_no_save(): 62 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 63 | app = TestApp(SessionMiddleware(no_save_app, **options)) 64 | res = app.get('/') 65 | assert 'current value is: None' in res 66 | assert [] == res.headers.getall('Set-Cookie') 67 | 68 | 69 | def test_increment(): 70 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 71 | app = TestApp(SessionMiddleware(simple_app, **options)) 72 | res = app.get('/') 73 | assert 'current value is: 1' in res 74 | res = app.get('/') 75 | assert 'current value is: 2' in res 76 | res = app.get('/') 77 | assert 'current value is: 3' in res 78 | 79 | def test_increment_auto(): 80 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 81 | app = TestApp(SessionMiddleware(simple_auto_app, auto=True, **options)) 82 | res = app.get('/') 83 | assert 'current value is: 1' in res 84 | res = app.get('/') 85 | assert 'current value is: 2' in res 86 | res = app.get('/') 87 | assert 'current value is: 3' in res 88 | 89 | 90 | def test_different_sessions(): 91 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 92 | app = TestApp(SessionMiddleware(simple_app, **options)) 93 | app2 = TestApp(SessionMiddleware(simple_app, **options)) 94 | res = app.get('/') 95 | assert 'current value is: 1' in res 96 | res = app2.get('/') 97 | assert 'current value is: 1' in res 98 | res = app2.get('/') 99 | res = app2.get('/') 100 | res = app2.get('/') 101 | res2 = app.get('/') 102 | assert 'current value is: 2' in res2 103 | assert 'current value is: 4' in res 104 | 105 | def test_different_sessions_auto(): 106 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 107 | app = TestApp(SessionMiddleware(simple_auto_app, auto=True, **options)) 108 | app2 = TestApp(SessionMiddleware(simple_auto_app, auto=True, **options)) 109 | res = app.get('/') 110 | assert 'current value is: 1' in res 111 | res = app2.get('/') 112 | assert 'current value is: 1' in res 113 | res = app2.get('/') 114 | res = app2.get('/') 115 | res = app2.get('/') 116 | res2 = app.get('/') 117 | assert 'current value is: 2' in res2 118 | assert 'current value is: 4' in res 119 | 120 | def test_nosave(): 121 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 122 | app = TestApp(SessionMiddleware(simple_app, **options)) 123 | res = app.get('/nosave') 124 | assert 'current value is: 1' in res 125 | res = app.get('/nosave') 126 | assert 'current value is: 1' in res 127 | 128 | res = app.get('/') 129 | assert 'current value is: 1' in res 130 | res = app.get('/') 131 | assert 'current value is: 2' in res 132 | 133 | def test_revert(): 134 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 135 | app = TestApp(SessionMiddleware(simple_auto_app, auto=True, **options)) 136 | res = app.get('/nosave') 137 | assert 'current value is: 0' in res 138 | res = app.get('/nosave') 139 | assert 'current value is: 0' in res 140 | 141 | res = app.get('/') 142 | assert 'current value is: 1' in res 143 | assert [] == res.headers.getall('Set-Cookie') 144 | res = app.get('/') 145 | assert [] == res.headers.getall('Set-Cookie') 146 | assert 'current value is: 2' in res 147 | 148 | # Finally, ensure that reverting shows the proper one 149 | res = app.get('/nosave') 150 | assert [] == res.headers.getall('Set-Cookie') 151 | assert 'current value is: 2' in res 152 | 153 | def test_load_session_by_id(): 154 | options = {'session.data_dir':'./cache', 'session.secret':'blah'} 155 | app = TestApp(SessionMiddleware(simple_app, **options)) 156 | res = app.get('/') 157 | assert 'current value is: 1' in res 158 | res = app.get('/') 159 | res = app.get('/') 160 | assert 'current value is: 3' in res 161 | old_id = re.sub(r'^.*?session id is (\S+)$', r'\1', res.body.decode('utf-8'), re.M) 162 | 163 | # Clear the cookies and do a new request 164 | app = TestApp(SessionMiddleware(simple_app, **options)) 165 | res = app.get('/') 166 | assert 'current value is: 1' in res 167 | 168 | # Load a bogus session to see that its not there 169 | res = app.get('/', extra_environ={'SESSION_ID': 'jil2j34il2j34ilj23'}) 170 | assert 'No session id of' in res 171 | 172 | # Saved session was at 3, now it'll be 4 173 | res = app.get('/', extra_environ={'SESSION_ID': str(old_id)}) 174 | assert 'current value is: 4' in res 175 | 176 | # Prior request is now up to 2 177 | res = app.get('/') 178 | assert 'current value is: 2' in res 179 | 180 | 181 | if __name__ == '__main__': 182 | from paste import httpserver 183 | wsgi_app = SessionMiddleware(simple_app, {}) 184 | httpserver.serve(wsgi_app, host='127.0.0.1', port=8080) 185 | -------------------------------------------------------------------------------- /beaker/docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Beaker documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Sep 19 15:12:15 2008. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # The contents of this file are pickled, so don't put values in the namespace 9 | # that aren't pickleable (module imports are okay, they're removed automatically). 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys 15 | import os 16 | 17 | # If your extensions are in another directory, add it here. If the directory 18 | # is relative to the documentation root, use os.path.abspath to make it 19 | # absolute, like shown here. 20 | sys.path.insert(0, os.path.abspath('../..')) 21 | 22 | # General configuration 23 | # --------------------- 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be extensions 26 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 27 | extensions = ['sphinx.ext.autodoc'] 28 | 29 | # Add any paths that contain templates here, relative to this directory. 30 | # templates_path = ['_templates'] 31 | 32 | # The suffix of source filenames. 33 | source_suffix = '.rst' 34 | 35 | # The master toctree document. 36 | master_doc = 'index' 37 | 38 | # General information about the project. 39 | project = u'Beaker' 40 | copyright = u'2008-2012, Ben Bangert, Mike Bayer' 41 | 42 | # The version info for the project you're documenting, acts as replacement for 43 | # |version| and |release|, also used in various other places throughout the 44 | # built documents. 45 | # 46 | # The short X.Y version. 47 | version = '1.7' 48 | # The full version, including alpha/beta/rc tags. 49 | release = '1.7.0' 50 | 51 | # The language for content autogenerated by Sphinx. Refer to documentation 52 | # for a list of supported languages. 53 | #language = None 54 | 55 | # There are two options for replacing |today|: either, you set today to some 56 | # non-false value, then it is used: 57 | #today = '' 58 | # Else, today_fmt is used as the format for a strftime call. 59 | #today_fmt = '%B %d, %Y' 60 | 61 | # List of documents that shouldn't be included in the build. 62 | #unused_docs = [] 63 | 64 | # List of directories, relative to source directory, that shouldn't be searched 65 | # for source files. 66 | exclude_trees = [] 67 | 68 | # The reST default role (used for this markup: `text`) to use for all documents. 69 | #default_role = None 70 | 71 | # If true, '()' will be appended to :func: etc. cross-reference text. 72 | #add_function_parentheses = True 73 | 74 | # If true, the current module name will be prepended to all description 75 | # unit titles (such as .. function::). 76 | #add_module_names = True 77 | 78 | # If true, sectionauthor and moduleauthor directives will be shown in the 79 | # output. They are ignored by default. 80 | show_authors = True 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | pygments_style = 'pastie' 84 | 85 | 86 | # Options for HTML output 87 | # ----------------------- 88 | 89 | # The style sheet to use for HTML and HTML Help pages. A file of that name 90 | # must exist either in Sphinx' static/ path, or in one of the custom paths 91 | # given in html_static_path. 92 | # html_style = 'default.css' 93 | 94 | # The name for this set of Sphinx documents. If None, it defaults to 95 | # " v documentation". 96 | #html_title = None 97 | 98 | # A shorter title for the navigation bar. Default is the same as html_title. 99 | #html_short_title = None 100 | 101 | # The name of an image file (within the static path) to place at the top of 102 | # the sidebar. 103 | #html_logo = None 104 | 105 | # The name of an image file (within the static path) to use as favicon of the 106 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 107 | # pixels large. 108 | #html_favicon = None 109 | 110 | # Add any paths that contain custom static files (such as style sheets) here, 111 | # relative to this directory. They are copied after the builtin static files, 112 | # so a file named "default.css" will overwrite the builtin "default.css". 113 | html_static_path = ['_static'] 114 | 115 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 116 | # using the given strftime format. 117 | #html_last_updated_fmt = '%b %d, %Y' 118 | 119 | # If true, SmartyPants will be used to convert quotes and dashes to 120 | # typographically correct entities. 121 | #html_use_smartypants = True 122 | 123 | # html_index = 'contents.html' 124 | 125 | # Custom sidebar templates, maps document names to template names. 126 | # html_sidebars = {'index': 'indexsidebar.html'} 127 | 128 | # Additional templates that should be rendered to pages, maps page names to 129 | # template names. 130 | # html_additional_pages = {'index': 'index.html'} 131 | 132 | html_theme_options = { 133 | "bgcolor": "#fff", 134 | "footertextcolor": "#666", 135 | "relbarbgcolor": "#fff", 136 | "relbarlinkcolor": "#590915", 137 | "relbartextcolor": "#FFAA2D", 138 | "sidebarlinkcolor": "#590915", 139 | "sidebarbgcolor": "#fff", 140 | "sidebartextcolor": "#333", 141 | "footerbgcolor": "#fff", 142 | "linkcolor": "#590915", 143 | "bodyfont": "helvetica, 'bitstream vera sans', sans-serif", 144 | "headfont": "georgia, 'bitstream vera sans serif', 'lucida grande', helvetica, verdana, sans-serif", 145 | "headbgcolor": "#fff", 146 | "headtextcolor": "#12347A", 147 | "codebgcolor": "#fff", 148 | } 149 | 150 | # If false, no module index is generated. 151 | #html_use_modindex = True 152 | 153 | # If false, no index is generated. 154 | #html_use_index = True 155 | 156 | # If true, the index is split into individual pages for each letter. 157 | #html_split_index = False 158 | 159 | # If true, the reST sources are included in the HTML build as _sources/. 160 | #html_copy_source = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | html_use_opensearch = 'http://beaker.rtfd.org/' 166 | 167 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = '' 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'Beakerdoc' 172 | 173 | 174 | # Options for LaTeX output 175 | # ------------------------ 176 | 177 | # The paper size ('letter' or 'a4'). 178 | #latex_paper_size = 'letter' 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #latex_font_size = '10pt' 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, document class [howto/manual]). 185 | latex_documents = [ 186 | ('contents', 'Beaker.tex', u'Beaker Documentation', 187 | u'Ben Bangert, Mike Bayer', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # Additional stuff for the LaTeX preamble. 199 | latex_preamble = ''' 200 | \usepackage{palatino} 201 | \definecolor{TitleColor}{rgb}{0.7,0,0} 202 | \definecolor{InnerLinkColor}{rgb}{0.7,0,0} 203 | \definecolor{OuterLinkColor}{rgb}{0.8,0,0} 204 | \definecolor{VerbatimColor}{rgb}{0.985,0.985,0.985} 205 | \definecolor{VerbatimBorderColor}{rgb}{0.8,0.8,0.8} 206 | ''' 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | latex_use_modindex = False 213 | 214 | # Added to handle docs in middleware.py 215 | autoclass_content = "both" 216 | -------------------------------------------------------------------------------- /beaker/ext/memcached.py: -------------------------------------------------------------------------------- 1 | from .._compat import PY2 2 | 3 | from beaker.container import NamespaceManager, Container 4 | from beaker.crypto.util import sha1 5 | from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter 6 | from beaker.synchronization import file_synchronizer 7 | from beaker.util import verify_directory, SyncDict, parse_memcached_behaviors 8 | import warnings 9 | 10 | MAX_KEY_LENGTH = 250 11 | 12 | _client_libs = {} 13 | 14 | 15 | def _load_client(name='auto'): 16 | if name in _client_libs: 17 | return _client_libs[name] 18 | 19 | def _pylibmc(): 20 | global pylibmc 21 | import pylibmc 22 | return pylibmc 23 | 24 | def _cmemcache(): 25 | global cmemcache 26 | import cmemcache 27 | warnings.warn("cmemcache is known to have serious " 28 | "concurrency issues; consider using 'memcache' " 29 | "or 'pylibmc'") 30 | return cmemcache 31 | 32 | def _memcache(): 33 | global memcache 34 | import memcache 35 | return memcache 36 | 37 | def _bmemcached(): 38 | global bmemcached 39 | import bmemcached 40 | return bmemcached 41 | 42 | def _auto(): 43 | for _client in (_pylibmc, _cmemcache, _memcache, _bmemcached): 44 | try: 45 | return _client() 46 | except ImportError: 47 | pass 48 | else: 49 | raise InvalidCacheBackendError( 50 | "Memcached cache backend requires one " 51 | "of: 'pylibmc' or 'memcache' to be installed.") 52 | 53 | clients = { 54 | 'pylibmc': _pylibmc, 55 | 'cmemcache': _cmemcache, 56 | 'memcache': _memcache, 57 | 'bmemcached': _bmemcached, 58 | 'auto': _auto 59 | } 60 | _client_libs[name] = clib = clients[name]() 61 | return clib 62 | 63 | 64 | def _is_configured_for_pylibmc(memcache_module_config, memcache_client): 65 | return memcache_module_config == 'pylibmc' or \ 66 | memcache_client.__name__.startswith('pylibmc') 67 | 68 | 69 | class MemcachedNamespaceManager(NamespaceManager): 70 | """Provides the :class:`.NamespaceManager` API over a memcache client library.""" 71 | 72 | clients = SyncDict() 73 | 74 | def __new__(cls, *args, **kw): 75 | memcache_module = kw.pop('memcache_module', 'auto') 76 | 77 | memcache_client = _load_client(memcache_module) 78 | 79 | if _is_configured_for_pylibmc(memcache_module, memcache_client): 80 | return object.__new__(PyLibMCNamespaceManager) 81 | else: 82 | return object.__new__(MemcachedNamespaceManager) 83 | 84 | def __init__(self, namespace, url, 85 | memcache_module='auto', 86 | data_dir=None, lock_dir=None, 87 | **kw): 88 | NamespaceManager.__init__(self, namespace) 89 | 90 | _memcache_module = _client_libs[memcache_module] 91 | 92 | if not url: 93 | raise MissingCacheParameter("url is required") 94 | 95 | self.lock_dir = None 96 | 97 | if lock_dir: 98 | self.lock_dir = lock_dir 99 | elif data_dir: 100 | self.lock_dir = data_dir + "/container_mcd_lock" 101 | if self.lock_dir: 102 | verify_directory(self.lock_dir) 103 | 104 | # Check for pylibmc namespace manager, in which case client will be 105 | # instantiated by subclass __init__, to handle behavior passing to the 106 | # pylibmc client 107 | if not _is_configured_for_pylibmc(memcache_module, _memcache_module): 108 | self.mc = MemcachedNamespaceManager.clients.get( 109 | (memcache_module, url), 110 | _memcache_module.Client, 111 | url.split(';')) 112 | 113 | def get_creation_lock(self, key): 114 | return file_synchronizer( 115 | identifier="memcachedcontainer/funclock/%s/%s" % 116 | (self.namespace, key), lock_dir=self.lock_dir) 117 | 118 | def _format_key(self, key): 119 | if not isinstance(key, str): 120 | key = key.decode('ascii') 121 | formated_key = (self.namespace + '_' + key).replace(' ', '\302\267') 122 | if len(formated_key) > MAX_KEY_LENGTH: 123 | if not PY2: 124 | formated_key = formated_key.encode('utf-8') 125 | formated_key = sha1(formated_key).hexdigest() 126 | return formated_key 127 | 128 | def __getitem__(self, key): 129 | return self.mc.get(self._format_key(key)) 130 | 131 | def __contains__(self, key): 132 | value = self.mc.get(self._format_key(key)) 133 | return value is not None 134 | 135 | def has_key(self, key): 136 | return key in self 137 | 138 | def set_value(self, key, value, expiretime=None): 139 | if expiretime: 140 | self.mc.set(self._format_key(key), value, time=expiretime) 141 | else: 142 | self.mc.set(self._format_key(key), value) 143 | 144 | def __setitem__(self, key, value): 145 | self.set_value(key, value) 146 | 147 | def __delitem__(self, key): 148 | self.mc.delete(self._format_key(key)) 149 | 150 | def do_remove(self): 151 | self.mc.flush_all() 152 | 153 | def keys(self): 154 | raise NotImplementedError( 155 | "Memcache caching does not " 156 | "support iteration of all cache keys") 157 | 158 | 159 | class PyLibMCNamespaceManager(MemcachedNamespaceManager): 160 | """Provide thread-local support for pylibmc.""" 161 | 162 | pools = SyncDict() 163 | 164 | def __init__(self, *arg, **kw): 165 | super(PyLibMCNamespaceManager, self).__init__(*arg, **kw) 166 | 167 | memcache_module = kw.get('memcache_module', 'auto') 168 | _memcache_module = _client_libs[memcache_module] 169 | protocol = kw.get('protocol', 'text') 170 | username = kw.get('username', None) 171 | password = kw.get('password', None) 172 | url = kw.get('url') 173 | behaviors = parse_memcached_behaviors(kw) 174 | 175 | self.mc = MemcachedNamespaceManager.clients.get( 176 | (memcache_module, url), 177 | _memcache_module.Client, 178 | servers=url.split(';'), behaviors=behaviors, 179 | binary=(protocol == 'binary'), username=username, 180 | password=password) 181 | self.pool = PyLibMCNamespaceManager.pools.get( 182 | (memcache_module, url), 183 | pylibmc.ThreadMappedPool, self.mc) 184 | 185 | def __getitem__(self, key): 186 | with self.pool.reserve() as mc: 187 | return mc.get(self._format_key(key)) 188 | 189 | def __contains__(self, key): 190 | with self.pool.reserve() as mc: 191 | value = mc.get(self._format_key(key)) 192 | return value is not None 193 | 194 | def has_key(self, key): 195 | return key in self 196 | 197 | def set_value(self, key, value, expiretime=None): 198 | with self.pool.reserve() as mc: 199 | if expiretime: 200 | mc.set(self._format_key(key), value, time=expiretime) 201 | else: 202 | mc.set(self._format_key(key), value) 203 | 204 | def __setitem__(self, key, value): 205 | self.set_value(key, value) 206 | 207 | def __delitem__(self, key): 208 | with self.pool.reserve() as mc: 209 | mc.delete(self._format_key(key)) 210 | 211 | def do_remove(self): 212 | with self.pool.reserve() as mc: 213 | mc.flush_all() 214 | 215 | 216 | class MemcachedContainer(Container): 217 | """Container class which invokes :class:`.MemcacheNamespaceManager`.""" 218 | namespace_class = MemcachedNamespaceManager 219 | -------------------------------------------------------------------------------- /tests/test_cache_decorator.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | import beaker.cache as cache 5 | from beaker.cache import CacheManager, cache_region, region_invalidate 6 | from beaker import util 7 | 8 | defaults = {'cache.data_dir':'./cache', 'cache.type':'dbm', 'cache.expire': 2} 9 | 10 | def teardown(): 11 | import shutil 12 | shutil.rmtree('./cache', True) 13 | 14 | @cache_region('short_term') 15 | def fred(x): 16 | return time.time() 17 | 18 | @cache_region('short_term') 19 | def george(x): 20 | return time.time() 21 | 22 | @cache_region('short_term') 23 | def albert(x): 24 | """A doc string""" 25 | return time.time() 26 | 27 | @cache_region('short_term') 28 | def alfred(x, xx, y=None): 29 | return str(time.time()) + str(x) + str(xx) + str(y) 30 | 31 | @cache_region('short_term') 32 | def alfred_self(self, xx, y=None): 33 | return str(time.time()) + str(self) + str(xx) + str(y) 34 | 35 | 36 | def make_cache_obj(**kwargs): 37 | opts = defaults.copy() 38 | opts.update(kwargs) 39 | cache = CacheManager(**util.parse_cache_config_options(opts)) 40 | return cache 41 | 42 | def make_cached_func(**opts): 43 | cache = make_cache_obj(**opts) 44 | @cache.cache() 45 | def load(person): 46 | now = datetime.now() 47 | return "Hi there %s, its currently %s" % (person, now) 48 | return cache, load 49 | 50 | def make_region_cached_func(): 51 | opts = {} 52 | opts['cache.regions'] = 'short_term, long_term' 53 | opts['cache.short_term.expire'] = '2' 54 | cache = make_cache_obj(**opts) 55 | 56 | @cache_region('short_term', 'region_loader') 57 | def load(person): 58 | now = datetime.now() 59 | return "Hi there %s, its currently %s" % (person, now) 60 | return load 61 | 62 | def make_region_cached_func_2(): 63 | opts = {} 64 | opts['cache.regions'] = 'short_term, long_term' 65 | opts['cache.short_term.expire'] = '2' 66 | cache = make_cache_obj(**opts) 67 | 68 | @cache_region('short_term') 69 | def load_person(person): 70 | now = datetime.now() 71 | return "Hi there %s, its currently %s" % (person, now) 72 | return load_person 73 | 74 | def test_check_region_decorator(): 75 | func = make_region_cached_func() 76 | result = func('Fred') 77 | assert 'Fred' in result 78 | 79 | result2 = func('Fred') 80 | assert result == result2 81 | 82 | result3 = func('George') 83 | assert 'George' in result3 84 | result4 = func('George') 85 | assert result3 == result4 86 | 87 | time.sleep(2) # Now it should have expired as cache is 2secs 88 | result2 = func('Fred') 89 | assert result != result2 90 | 91 | def test_different_default_names(): 92 | result = fred(1) 93 | time.sleep(0.1) 94 | result2 = george(1) 95 | assert result != result2 96 | 97 | def test_check_invalidate_region(): 98 | func = make_region_cached_func() 99 | result = func('Fred') 100 | assert 'Fred' in result 101 | 102 | result2 = func('Fred') 103 | assert result == result2 104 | region_invalidate(func, None, 'region_loader', 'Fred') 105 | 106 | result3 = func('Fred') 107 | assert result3 != result2 108 | 109 | result2 = func('Fred') 110 | assert result3 == result2 111 | 112 | # Invalidate a non-existent key 113 | region_invalidate(func, None, 'region_loader', 'Fredd') 114 | assert result3 == result2 115 | 116 | 117 | def test_check_invalidate_region_2(): 118 | func = make_region_cached_func_2() 119 | result = func('Fred') 120 | assert 'Fred' in result 121 | 122 | result2 = func('Fred') 123 | assert result == result2 124 | region_invalidate(func, None, 'Fred') 125 | 126 | result3 = func('Fred') 127 | assert result3 != result2 128 | 129 | result2 = func('Fred') 130 | assert result3 == result2 131 | 132 | # Invalidate a non-existent key 133 | region_invalidate(func, None, 'Fredd') 134 | assert result3 == result2 135 | 136 | def test_invalidate_cache(): 137 | cache, func = make_cached_func() 138 | val = func('foo') 139 | time.sleep(0.1) 140 | val2 = func('foo') 141 | assert val == val2 142 | 143 | cache.invalidate(func, 'foo') 144 | val3 = func('foo') 145 | assert val3 != val 146 | 147 | def test_class_key_cache(): 148 | cache = make_cache_obj() 149 | 150 | class Foo(object): 151 | @cache.cache('method') 152 | def go(self, x, y): 153 | return "hi foo" 154 | 155 | @cache.cache('standalone') 156 | def go(x, y): 157 | return "hi standalone" 158 | 159 | x = Foo().go(1, 2) 160 | y = go(1, 2) 161 | 162 | ns = go._arg_namespace 163 | assert cache.get_cache(ns).get('method 1 2') == x 164 | assert cache.get_cache(ns).get('standalone 1 2') == y 165 | 166 | def test_func_namespace(): 167 | def go(x, y): 168 | return "hi standalone" 169 | 170 | assert 'test_cache_decorator' in util.func_namespace(go) 171 | assert util.func_namespace(go).endswith('go') 172 | 173 | def test_class_key_region(): 174 | opts = {} 175 | opts['cache.regions'] = 'short_term' 176 | opts['cache.short_term.expire'] = '2' 177 | cache = make_cache_obj(**opts) 178 | 179 | class Foo(object): 180 | @cache_region('short_term', 'method') 181 | def go(self, x, y): 182 | return "hi foo" 183 | 184 | @cache_region('short_term', 'standalone') 185 | def go(x, y): 186 | return "hi standalone" 187 | 188 | x = Foo().go(1, 2) 189 | y = go(1, 2) 190 | ns = go._arg_namespace 191 | assert cache.get_cache_region(ns, 'short_term').get('method 1 2') == x 192 | assert cache.get_cache_region(ns, 'short_term').get('standalone 1 2') == y 193 | 194 | def test_classmethod_key_region(): 195 | opts = {} 196 | opts['cache.regions'] = 'short_term' 197 | opts['cache.short_term.expire'] = '2' 198 | cache = make_cache_obj(**opts) 199 | 200 | class Foo(object): 201 | @classmethod 202 | @cache_region('short_term', 'method') 203 | def go(cls, x, y): 204 | return "hi" 205 | 206 | x = Foo.go(1, 2) 207 | ns = Foo.go._arg_namespace 208 | assert cache.get_cache_region(ns, 'short_term').get('method 1 2') == x 209 | 210 | def test_class_key_region_invalidate(): 211 | opts = {} 212 | opts['cache.regions'] = 'short_term' 213 | opts['cache.short_term.expire'] = '2' 214 | cache = make_cache_obj(**opts) 215 | 216 | class Foo(object): 217 | @cache_region('short_term', 'method') 218 | def go(self, x, y): 219 | now = datetime.now() 220 | return "hi %s" % now 221 | 222 | def invalidate(self, x, y): 223 | region_invalidate(self.go, None, "method", x, y) 224 | 225 | x = Foo().go(1, 2) 226 | time.sleep(0.1) 227 | y = Foo().go(1, 2) 228 | Foo().invalidate(1, 2) 229 | z = Foo().go(1, 2) 230 | 231 | assert x == y 232 | assert x != z 233 | 234 | def test_check_region_decorator_keeps_docstring_and_name(): 235 | result = albert(1) 236 | time.sleep(0.1) 237 | result2 = albert(1) 238 | assert result == result2 239 | 240 | assert albert.__doc__ == "A doc string" 241 | assert albert.__name__ == "albert" 242 | 243 | 244 | def test_check_region_decorator_with_kwargs(): 245 | result = alfred(1, xx=5, y=3) 246 | time.sleep(0.1) 247 | 248 | result2 = alfred(1, y=3, xx=5) 249 | assert result == result2 250 | 251 | result3 = alfred(1, 5, y=5) 252 | assert result != result3 253 | 254 | result4 = alfred(1, 5, 3) 255 | assert result == result4 256 | 257 | result5 = alfred(1, 5, y=3) 258 | assert result == result5 259 | 260 | 261 | def test_check_region_decorator_with_kwargs_and_self(): 262 | result = alfred_self('fake_self', xx=5, y='blah') 263 | time.sleep(0.1) 264 | 265 | result2 = alfred_self('fake_self2', y='blah', xx=5) 266 | assert result == result2 267 | 268 | result3 = alfred_self('fake_self2', 5, y=5) 269 | assert result != result3 270 | 271 | result4 = alfred_self('fake_self2', 5, 'blah') 272 | assert result == result4 273 | 274 | result5 = alfred_self('fake_self2', 5, y='blah') 275 | assert result == result5 276 | -------------------------------------------------------------------------------- /beaker/docs/sessions.rst: -------------------------------------------------------------------------------- 1 | .. _sessions: 2 | 3 | ======== 4 | Sessions 5 | ======== 6 | 7 | About 8 | ===== 9 | 10 | Sessions provide a place to persist data in web applications, Beaker's session 11 | system simplifies session implementation details by providing WSGI middleware 12 | that handles them. 13 | 14 | All cookies are signed with an HMAC signature to prevent tampering by the 15 | client. 16 | 17 | Lazy-Loading 18 | ------------ 19 | 20 | Only when a session object is actually accessed will the session be loaded 21 | from the file-system, preventing performance hits on pages that don't use 22 | the session. 23 | 24 | Using 25 | ===== 26 | 27 | The session object provided by Beaker's 28 | :class:`~beaker.middleware.SessionMiddleware` implements a dict-style interface 29 | with a few additional object methods. Once the SessionMiddleware is in place, 30 | a session object will be made available as ``beaker.session`` in the WSGI 31 | environ. 32 | 33 | Getting data out of the session:: 34 | 35 | myvar = session['somekey'] 36 | 37 | Testing for a value:: 38 | 39 | logged_in = 'user_id' in session 40 | 41 | Adding data to the session:: 42 | 43 | session['name'] = 'Fred Smith' 44 | 45 | Complete example using a basic WSGI app with sessions:: 46 | 47 | from beaker.middleware import SessionMiddleware 48 | 49 | def simple_app(environ, start_response): 50 | # Get the session object from the environ 51 | session = environ['beaker.session'] 52 | 53 | # Check to see if a value is in the session 54 | user = 'logged_in' in session 55 | 56 | # Set some other session variable 57 | session['user_id'] = 10 58 | 59 | start_response('200 OK', [('Content-type', 'text/plain')]) 60 | return ['User is logged in: %s' % user] 61 | 62 | # Configure the SessionMiddleware 63 | session_opts = { 64 | 'session.type': 'file', 65 | 'session.cookie_expires': True, 66 | } 67 | wsgi_app = SessionMiddleware(simple_app, session_opts) 68 | 69 | Now ``wsgi_app`` is a replacement of original application ``simple_app``. 70 | You should specify it as a request handler in your WSGI configuration file. 71 | 72 | .. note:: 73 | This example does **not** actually save the session for the next request. 74 | Adding the :meth:`~beaker.session.Session.save` call explained below is 75 | required, or having the session set to auto-save. 76 | 77 | .. _cookie_attributes: 78 | 79 | Session Attributes / Keys 80 | ------------------------- 81 | 82 | Sessions have several special attributes that can be used as needed by an 83 | application. 84 | 85 | * id - Unique 40 char SHA-generated session ID 86 | * last_accessed - The last time the session was accessed before the current 87 | access, will be None if the session was just made 88 | 89 | There's several special session keys populated as well: 90 | 91 | * _accessed_time - Current accessed time of the session, when it was loaded 92 | * _creation_time - When the session was created 93 | 94 | 95 | Saving 96 | ====== 97 | 98 | Sessions can be saved using the :meth:`~beaker.session.Session.save` method 99 | on the session object:: 100 | 101 | session.save() 102 | 103 | .. warning:: 104 | 105 | Beaker relies on Python's pickle module to pickle data objects for storage 106 | in the session. Objects that cannot be pickled should **not** be stored in 107 | the session. 108 | 109 | This flags a session to be saved, and it will be stored on the chosen back-end 110 | at the end of the request. 111 | 112 | .. warning:: 113 | 114 | When using the ``memory`` backend, session will only be valid for the process 115 | that created it and will be lost when process is restarted. It is usually 116 | suggested to only use the ``memory`` backend for development and not for production. 117 | 118 | If it's necessary to immediately save the session to the back-end, the 119 | :meth:`~beaker.session.SessionObject.persist` method should be used:: 120 | 121 | session.persist() 122 | 123 | This is not usually the case however, as a session generally should not be 124 | saved should something catastrophic happen during a request. 125 | 126 | **Order Matters**: When using the Beaker middleware, you **must call save before 127 | the headers are sent to the client**. Since Beaker's middleware watches for when 128 | the ``start_response`` function is called to know that it should add its cookie 129 | header, the session must be saved before it is called. 130 | 131 | Keep in mind that Response objects in popular frameworks (WebOb, Werkzeug, 132 | etc.) call start_response immediately, so if you are using one of those 133 | objects to handle your Response, you must call .save() before the Response 134 | object is called:: 135 | 136 | # this would apply to WebOb and possibly others too 137 | from werkzeug.wrappers import Response 138 | 139 | # this will work 140 | def sessions_work(environ, start_response): 141 | environ['beaker.session']['count'] += 1 142 | resp = Response('hello') 143 | environ['beaker.session'].save() 144 | return resp(environ, start_response) 145 | 146 | # this will not work 147 | def sessions_broken(environ, start_response): 148 | environ['beaker.session']['count'] += 1 149 | resp = Response('hello') 150 | retval = resp(environ, start_response) 151 | environ['beaker.session'].save() 152 | return retval 153 | 154 | 155 | 156 | Auto-save 157 | --------- 158 | 159 | Saves can be done automatically by setting the ``auto`` configuration option 160 | for sessions. When set, calling the :meth:`~beaker.session.Session.save` method 161 | is no longer required, and the session will be saved automatically anytime it is 162 | accessed during a request. 163 | 164 | 165 | Deleting 166 | ======== 167 | 168 | Calling the :meth:`~beaker.session.Session.delete` method deletes the session 169 | from the back-end storage and sends an expiration on the cookie requesting the 170 | browser to clear it:: 171 | 172 | session.delete() 173 | 174 | This should be used at the end of a request when the session should be deleted 175 | and will not be used further in the request. 176 | 177 | If a session should be invalidated, and a new session created and used during 178 | the request, the :meth:`~beaker.session.Session.invalidate` method should be 179 | used:: 180 | 181 | session.invalidate() 182 | 183 | Removing Expired/Old Sessions 184 | ----------------------------- 185 | 186 | Beaker does **not** automatically delete expired or old cookies on any of its 187 | back-ends. This task is left up to the developer based on how sessions are 188 | being used, and on what back-end. 189 | 190 | The database backend records the last accessed time as a column in the database 191 | so a script could be run to delete session rows in the database that haven't 192 | been used in a long time. 193 | 194 | When using the file-based sessions, a script could run to remove files that 195 | haven't been touched in a long time, for example (in the session's data dir): 196 | 197 | .. code-block:: bash 198 | 199 | find . -mtime +3 -exec rm {} \; 200 | 201 | 202 | Cookie Domain and Path 203 | ====================== 204 | 205 | In addition to setting a default cookie domain with the 206 | :ref:`cookie domain setting `, the cookie's domain and 207 | path can be set dynamically for a session with the domain and path properties. 208 | 209 | These settings will persist as long as the cookie exists, or until changed. 210 | 211 | Example:: 212 | 213 | # Setting the session's cookie domain and path 214 | session.domain = '.domain.com' 215 | session.path = '/admin' 216 | 217 | 218 | Cookie-Based 219 | ============ 220 | 221 | Session can be stored purely on the client-side using cookie-based sessions. 222 | This option can be turned on by setting the session type to ``cookie``. 223 | 224 | Using cookie-based session carries the limitation of how large a cookie can 225 | be (generally 4096 bytes). An exception will be thrown should a session get 226 | too large to fit in a cookie, so using cookie-based session should be done 227 | carefully and only small bits of data should be stored in them (the users login 228 | name, admin status, etc.). 229 | 230 | Large cookies can slow down page-loads as they increase latency to every 231 | page request since the cookie is sent for every request under that domain. 232 | Static content such as images and Javascript should be served off a domain 233 | that the cookie is not valid for to prevent this. 234 | 235 | Cookie-based sessions scale easily in a clustered environment as there's no 236 | need for a shared storage system when different servers handle the same 237 | session. 238 | 239 | .. _encryption: 240 | 241 | Encryption 242 | ---------- 243 | 244 | In the event that the cookie-based sessions should also be encrypted to 245 | prevent the user from being able to decode the data (in addition to not 246 | being able to tamper with it), Beaker can use 256-bit AES encryption to 247 | secure the contents of the cookie. 248 | 249 | Depending on the Python implementation used, Beaker may require an additional 250 | library to provide AES encryption. 251 | 252 | On CPython (the regular Python), the `pycryptopp`_ library or `PyCrypto`_ library 253 | is required. 254 | 255 | On Jython, no additional packages are required, but at least on the Sun JRE, 256 | the size of the encryption key is by default limited to 128 bits, which causes 257 | generated sessions to be incompatible with those generated in CPython, and vice 258 | versa. To overcome this limitation, you need to install the unlimited strength 259 | juristiction policy files from Sun: 260 | 261 | * `Policy files for Java 5 `_ 262 | * `Policy files for Java 6 `_ 263 | 264 | .. _pycryptopp: http://pypi.python.org/pypi/pycryptopp 265 | .. _PyCrypto: http://pypi.python.org/pypi/pycrypto/2.0.1 266 | -------------------------------------------------------------------------------- /beaker/docs/caching.rst: -------------------------------------------------------------------------------- 1 | .. _caching: 2 | 3 | ======= 4 | Caching 5 | ======= 6 | 7 | About 8 | ===== 9 | 10 | Beaker's caching system was originally based off the Perl Cache::Cache module, 11 | which was ported for use in `Myghty`_. Beaker was then extracted from this 12 | code, and has been substantially rewritten and modernized. 13 | 14 | Several concepts still exist from this origin though. Beaker's caching (and 15 | its sessions, behind the scenes) utilize the concept of 16 | :term:`NamespaceManager`, and :term:`Container` objects to handle storing 17 | cached data. 18 | 19 | Each back-end utilizes a customized version of each of these objects to handle 20 | storing data appropriately depending on the type of the back-end. 21 | 22 | The :class:`~beaker.cache.CacheManager` is responsible for getting the 23 | appropriate NamespaceManager, which then stores the cached values. Each 24 | namespace corresponds to a single ``thing`` that should be cached. Usually 25 | a single ``thing`` to be cached might vary slightly depending on parameters, 26 | for example a template might need several different copies of itself stored 27 | depending on whether a user is logged in or not. Each one of these copies 28 | is then ``keyed`` under the NamespaceManager and stored in a Container. 29 | 30 | There are three schemes for using Beaker's caching, the first and more 31 | traditional style is the programmatic API. This exposes the namespace's 32 | and retrieves a :class:`~beaker.cache.Cache` object that handles storing 33 | keyed values in a NamespaceManager with Container objects. 34 | 35 | The more elegant system, introduced in Beaker 1.3, is to use the 36 | :ref:`cache decorators `, these also support the 37 | use of :term:`Cache Regions`. 38 | 39 | Introduced in Beaker 1.5 is a more flexible :func:`~beaker.cache.cache_region` 40 | decorator capable of decorating functions for use with Beaker's 41 | :ref:`caching_with_regions` **before** Beaker has been configured. This makes 42 | it possible to easily use Beaker's region caching decorator on functions in 43 | the module level. 44 | 45 | 46 | Creating the CacheManager Instance 47 | ================================== 48 | 49 | Before using Beaker's caching, an instance of the 50 | :class:`~beaker.cache.CacheManager` class should be created. All of the 51 | examples below assume that it has already been created. 52 | 53 | Creating the cache instance:: 54 | 55 | from beaker.cache import CacheManager 56 | from beaker.util import parse_cache_config_options 57 | 58 | cache_opts = { 59 | 'cache.type': 'file', 60 | 'cache.data_dir': '/tmp/cache/data', 61 | 'cache.lock_dir': '/tmp/cache/lock' 62 | } 63 | 64 | cache = CacheManager(**parse_cache_config_options(cache_opts)) 65 | 66 | Additional configuration options are documented in the :ref:`Configuration` 67 | section of the Beaker docs. 68 | 69 | 70 | Programmatic API 71 | ================ 72 | 73 | .. _programmatic: 74 | 75 | To store data for a cache value, first, a NamespaceManager has to be 76 | retrieved to manage the keys for a ``thing`` to be cached:: 77 | 78 | # Assuming that cache is an already created CacheManager instance 79 | tmpl_cache = cache.get_cache('mytemplate.html', type='dbm', expire=3600) 80 | 81 | .. note:: 82 | In addition to the defaults supplied to the 83 | :class:`~beaker.cache.CacheManager` instance, any of the Cache options 84 | can be changed on a per-namespace basis, as this example demonstrates 85 | by setting a ``type``, and ``expire`` option. 86 | 87 | Individual values should be stored using a creation function, which will 88 | be called anytime the cache has expired or a new copy needs to be made. The 89 | creation function must not accept any arguments as it won't be called with 90 | any. Options affecting the created value can be passed in by using closure 91 | scope on the creation function:: 92 | 93 | search_param = 'gophers' 94 | 95 | def get_results(): 96 | # do something to retrieve data 97 | data = get_data(search_param) 98 | return data 99 | 100 | # Cache this function, based on the search_param, using the tmpl_cache 101 | # instance from the prior example 102 | results = tmpl_cache.get(key=search_param, createfunc=get_results) 103 | 104 | Invalidating 105 | ------------ 106 | 107 | All of the values for a particular namespace can be removed by calling the 108 | :meth:`~beaker.cache.Cache.clear` method:: 109 | 110 | tmpl_cache.clear() 111 | 112 | Note that this only clears the key's in the namespace that this particular 113 | Cache instance is aware of. Therefore, it is recommended to manually clear out 114 | specific keys in a cache namespace that should be removed:: 115 | 116 | tmpl_cache.remove_value(key=search_param) 117 | 118 | 119 | Decorator API 120 | ============= 121 | 122 | .. _decorator_api: 123 | 124 | When using the decorator API, a namespace does not need to be specified and 125 | will instead be created for you with the name of the module + the name of the 126 | function that will have its output cached. 127 | 128 | Since it's possible that multiple functions in the same module might have the 129 | same name, additional arguments can be provided to the decorators that will be 130 | used in the namespace to prevent multiple functions from caching their values 131 | in the same location. 132 | 133 | For example:: 134 | 135 | # Assuming that cache is an already created CacheManager instance 136 | @cache.cache('my_search_func', expire=3600) 137 | def get_results(search_param): 138 | # do something to retrieve data 139 | data = get_data(search_param) 140 | return data 141 | 142 | results = get_results('gophers') 143 | 144 | The non-keyword arguments to the :meth:`~beaker.cache.CacheManager.cache` 145 | method are the additional ones used to ensure this function's cache results 146 | won't clash with another function in this module called ``get_results``. 147 | 148 | The cache expire argument is specified as a keyword argument. Other valid 149 | arguments to the :meth:`~beaker.cache.CacheManager.get_cache` method such 150 | as ``type`` can also be passed in. 151 | 152 | When using the decorator, the function to cache can have arguments, which will 153 | be used as the key was in the :ref:`Programmatic API ` for 154 | the data generated. 155 | 156 | .. warning:: 157 | These arguments can **not** be keyword arguments. 158 | 159 | Invalidating 160 | ------------ 161 | 162 | Since the :meth:`~beaker.cache.CacheManager.cache` decorator hides the 163 | namespace used, manually removing the key requires the use of the 164 | :meth:`~beaker.cache.CacheManager.invalidate` function. To invalidate 165 | the 'gophers' result that the prior example referred to:: 166 | 167 | cache.invalidate(get_results, 'my_search_func', 'gophers') 168 | 169 | If however, a type was specified for the cached function, the type must 170 | also be given to the :meth:`~beaker.cache.CacheManager.invalidate` 171 | function so that it can remove the value from the appropriate back-end. 172 | 173 | Example:: 174 | 175 | # Assuming that cache is an already created CacheManager instance 176 | @cache.cache('my_search_func', type="file", expire=3600) 177 | def get_results(search_param): 178 | # do something to retrieve data 179 | data = get_data(search_param) 180 | return data 181 | 182 | cache.invalidate(get_results, 'my_search_func', 'gophers', type="file") 183 | 184 | .. note:: 185 | Both the arguments used to specify the additional namespace info to the 186 | cache decorator **and** the arguments sent to the function need to be 187 | given to the :meth:`~beaker.cache.CacheManager.region_invalidate` 188 | function so that it can properly locate the namespace and cache key 189 | to remove. 190 | 191 | 192 | .. _caching_with_regions: 193 | 194 | Cache Regions 195 | ============= 196 | 197 | Rather than having to specify the expiration, or toggle the type used for 198 | caching different functions, commonly used cache parameters can be defined 199 | as :term:`Cache Regions`. These user-defined regions may be used 200 | with the :meth:`~beaker.cache.CacheManager.region` decorator rather than 201 | passing the configuration. 202 | 203 | This can be useful if there are a few common cache schemes used by an 204 | application that should be setup in a single place then used as appropriate 205 | throughout the application. 206 | 207 | Setting up cache regions is documented in the 208 | :ref:`cache region options ` section in 209 | :ref:`configuration`. 210 | 211 | Assuming a ``long_term`` and ``short_term`` region were setup, the 212 | :meth:`~beaker.cache.CacheManager.region` decorator can be used:: 213 | 214 | @cache.region('short_term', 'my_search_func') 215 | def get_results(search_param): 216 | # do something to retrieve data 217 | data = get_data(search_param) 218 | return data 219 | 220 | results = get_results('gophers') 221 | 222 | Or using the :func:`~beaker.cache.cache_region` decorator:: 223 | 224 | @cache_region('short_term', 'my_search_func') 225 | def get_results(search_param): 226 | # do something to retrieve data 227 | data = get_data(search_param) 228 | return data 229 | 230 | results = get_results('gophers') 231 | 232 | The only difference with the :func:`~beaker.cache.cache_region` decorator is 233 | that the cache does not need to be configured when it is used. This allows one 234 | to decorate functions in a module before the Beaker cache is configured. 235 | 236 | Invalidating 237 | ------------ 238 | 239 | Since the :meth:`~beaker.cache.CacheManager.region` decorator hides the 240 | namespace used, manually removing the key requires the use of the 241 | :meth:`~beaker.cache.CacheManager.region_invalidate` function. To invalidate 242 | the 'gophers' result that the prior example referred to:: 243 | 244 | cache.region_invalidate(get_results, None, 'my_search_func', 'gophers') 245 | 246 | Or when using the :func:`~beaker.cache.cache_region` decorator, the 247 | :func:`beaker.cache.region_invalidate` function should be used:: 248 | 249 | region_invalidate(get_results, None, 'my_search_func', 'gophers') 250 | 251 | .. note:: 252 | Both the arguments used to specify the additional namespace info to the 253 | cache decorator **and** the arguments sent to the function need to be 254 | given to the :meth:`~beaker.cache.CacheManager.region_invalidate` 255 | function so that it can properly locate the namespace and cache key 256 | to remove. 257 | 258 | 259 | .. _Myghty: http://www.myghty.org/ 260 | -------------------------------------------------------------------------------- /beaker/docs/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | ============= 4 | Configuration 5 | ============= 6 | 7 | Beaker can be configured several different ways, depending on how it is to be 8 | used. The most recommended style is to use a dictionary of preferences that 9 | are to be passed to either the :class:`~beaker.middleware.SessionMiddleware` or 10 | the :class:`~beaker.cache.CacheManager`. 11 | 12 | Since both Beaker's sessions and caching use the same back-end container 13 | storage system, there's some options that are applicable to both of them in 14 | addition to session and cache specific configuration. 15 | 16 | Most options can be specified as a string (necessary to config options that 17 | are setup in INI files), and will be coerced to the appropriate value. Only 18 | datetime's and timedelta's cannot be coerced and must be the actual objects. 19 | 20 | Frameworks using Beaker usually allow both caching and sessions to be 21 | configured in the same spot, Beaker assumes this condition as well and 22 | requires options for caching and sessions to be prefixed appropriately. 23 | 24 | For example, to configure the ``cookie_expires`` option for Beaker sessions 25 | below, an appropriate entry in a `Pylons`_ INI file would be:: 26 | 27 | # Setting cookie_expires = true causes Beaker to omit the 28 | # expires= field from the Set-Cookie: header, signaling the cookie 29 | # should be discarded when the browser closes. 30 | beaker.session.cookie_expires = true 31 | 32 | .. note:: 33 | 34 | When using the options in a framework like `Pylons`_ or `TurboGears2`_, these 35 | options must be prefixed by ``beaker.``, for example in a `Pylons`_ INI file:: 36 | 37 | beaker.session.data_dir = %(here)s/data/sessions/data 38 | beaker.session.lock_dir = %(here)s/data/sessions/lock 39 | 40 | Or when using stand-alone with the :class:`~beaker.middleware.SessionMiddleware`: 41 | 42 | .. code-block:: python 43 | 44 | from beaker.middleware import SessionMiddleware 45 | 46 | session_opts = { 47 | 'session.cookie_expires': True 48 | } 49 | 50 | app = SomeWSGIAPP() 51 | app = SessionMiddleware(app, session_opts) 52 | 53 | 54 | Or when using the :class:`~beaker.cache.CacheManager`: 55 | 56 | .. code-block:: python 57 | 58 | from beaker.cache import CacheManager 59 | from beaker.util import parse_cache_config_options 60 | 61 | cache_opts = { 62 | 'cache.type': 'file', 63 | 'cache.data_dir': '/tmp/cache/data', 64 | 'cache.lock_dir': '/tmp/cache/lock' 65 | } 66 | 67 | cache = CacheManager(**parse_cache_config_options(cache_opts)) 68 | 69 | .. note:: 70 | 71 | When using the CacheManager directly, all dict options must be run through the 72 | :func:`beaker.util.parse_cache_config_options` function to ensure they're valid 73 | and of the appropriate type. 74 | 75 | 76 | Options For Sessions and Caching 77 | ================================ 78 | 79 | data_dir (**optional**, string) 80 | Used with any back-end that stores its data in physical files, such as the 81 | dbm or file-based back-ends. This path should be an absolute path to the 82 | directory that stores the files. 83 | 84 | lock_dir (**required**, string) 85 | Used with every back-end, to coordinate locking. With caching, this lock 86 | file is used to ensure that multiple processes/threads aren't attempting 87 | to re-create the same value at the same time (The :term:`Dog-Pile Effect`) 88 | 89 | memcache_module (**optional**, string) 90 | One of the names ``memcache``, ``cmemcache``, ``pylibmc``, or ``auto``. 91 | Default is ``auto``. Specifies which memcached client library should 92 | be imported when using the ext:memcached backend. If left at its 93 | default of ``auto``, ``pylibmc`` is favored first, then ``cmemcache``, 94 | then ``memcache``. New in 1.5.5. 95 | 96 | type (**required**, string) 97 | The name of the back-end to use for storing the sessions or cache objects. 98 | 99 | Available back-ends supplied with Beaker: ``file``, ``dbm``, ``memory``, 100 | ``ext:memcached``, ``ext:database``, ``ext:google`` 101 | 102 | For sessions, the additional type of ``cookie`` is available which 103 | will store all the session data in the cookie itself. As such, size 104 | limitations apply (4096 bytes). 105 | 106 | Some of these back-ends require the url option as listed below. 107 | 108 | webtest_varname (**optional**, string) 109 | The name of the attribute to use when stashing the session object into 110 | the environ for use with WebTest. The name provided here is where the 111 | session object will be attached to the WebTest TestApp return value. 112 | 113 | url (**optional**, string) 114 | URL is specific to use of either ext:memcached or ext:database. When using 115 | one of those types, this option is **required**. 116 | 117 | When used with ext:memcached, this should be either a single, or 118 | semi-colon separated list of memcached servers:: 119 | 120 | session_opts = { 121 | 'session.type': 'ext:memcached', 122 | 'session.url': '127.0.0.1:11211', 123 | } 124 | 125 | When used with ext:database, this should be a valid `SQLAlchemy`_ database 126 | string. 127 | 128 | 129 | Session Options 130 | =============== 131 | 132 | The Session handling takes a variety of additional options relevant to how it 133 | stores session id's in cookies, and when using the optional encryption. 134 | 135 | auto (**optional**, bool) 136 | When set to True, the session will save itself anytime it is accessed 137 | during a request, negating the need to issue the 138 | :meth:`~beaker.session.Session.save` method. 139 | 140 | Defaults to False. 141 | 142 | cookie_expires (**optional**, bool, datetime, timedelta, int) 143 | Determines when the cookie used to track the client-side of the session 144 | will expire. When set to a boolean value, it will either expire at the 145 | end of the browsers session, or never expire. 146 | 147 | Setting to a datetime forces a hard ending time for the session (generally 148 | used for setting a session to a far off date). 149 | 150 | Setting to an integer will result in the cookie being set to expire in 151 | that many seconds. I.e. a value of ``300`` will result in the cookie being 152 | set to expire in 300 seconds. 153 | 154 | Defaults to never expiring. 155 | 156 | 157 | .. _cookie_domain_config: 158 | 159 | cookie_domain (**optional**, string) 160 | What domain the cookie should be set to. When using sub-domains, this 161 | should be set to the main domain the cookie should be valid for. For 162 | example, if a cookie should be valid under ``www.nowhere.com`` **and** 163 | ``files.nowhere.com`` then it should be set to ``.nowhere.com``. 164 | 165 | Defaults to the current domain in its entirety. 166 | 167 | Alternatively, the domain can be set dynamically on the session by 168 | calling, see :ref:`cookie_attributes`. 169 | 170 | key (**required**, string) 171 | Name of the cookie key used to save the session under. 172 | 173 | secret (**required**, string) 174 | Used with the HMAC to ensure session integrity. This value should 175 | ideally be a randomly generated string. 176 | 177 | When using in a cluster environment, the secret must be the same on 178 | every machine. 179 | 180 | secure (**optional**, bool) 181 | Whether or not the session cookie should be marked as secure. When 182 | marked as secure, browsers are instructed to not send the cookie over 183 | anything other than an SSL connection. 184 | 185 | timeout (**optional**, integer) 186 | Seconds until the session is considered invalid, after which it will 187 | be ignored and invalidated. This number is based on the time since 188 | the session was last accessed, not from when the session was created. 189 | 190 | Defaults to never expiring. 191 | 192 | 193 | Encryption Options 194 | ------------------ 195 | 196 | These options should then be used *instead* of the ``secret`` 197 | option listed above. 198 | 199 | encrypt_key (**required**, string) 200 | Encryption key to use for the AES cipher. This should be a fairly long 201 | randomly generated string. 202 | 203 | validate_key (**required**, string) 204 | Validation key used to sign the AES encrypted data. 205 | 206 | .. note:: 207 | 208 | You may need to install additional libraries to use Beaker's 209 | cookie-based session encryption. See the :ref:`encryption` section for 210 | more information. 211 | 212 | Cache Options 213 | ============= 214 | 215 | For caching, options may be directly specified on a per-use basis with the 216 | :meth:`~beaker.cache.CacheManager.cache` decorator, with the rest of these 217 | options used as fallback should one of them not be specified in the call. 218 | 219 | Only the ``lock_dir`` option is strictly required, unless using the file-based 220 | back-ends as noted with the sessions. 221 | 222 | expire (**optional**, integer) 223 | Seconds until the cache is considered old and a new value is created. 224 | 225 | 226 | Cache Region Options 227 | -------------------- 228 | 229 | .. _cache_region_options: 230 | 231 | Starting in Beaker 1.3, cache regions are now supported. These can be thought 232 | of as bundles of configuration options to apply, rather than specifying the 233 | type and expiration on a per-usage basis. 234 | 235 | enabled (**optional**, bool) 236 | Quick toggle to disable or enable caching across an entire application. 237 | 238 | This should generally be used when testing an application or in 239 | development when caching should be ignored. 240 | 241 | Defaults to True. 242 | 243 | 244 | regions (**optional**, list, tuple) 245 | Names of the regions that are to be configured. 246 | 247 | For each region, all of the other cache options are valid and will 248 | be read out of the cache options for that key. Options that are not 249 | listed under a region will be used globally in the cache unless a 250 | region specifies a different value. 251 | 252 | For example, to specify two batches of options, one called ``long-term``, 253 | and one called ``short-term``:: 254 | 255 | cache_opts = { 256 | 'cache.data_dir': '/tmp/cache/data', 257 | 'cache.lock_dir': '/tmp/cache/lock' 258 | 'cache.regions': 'short_term, long_term', 259 | 'cache.short_term.type': 'ext:memcached', 260 | 'cache.short_term.url': '127.0.0.1.11211', 261 | 'cache.short_term.expire': '3600', 262 | 'cache.long_term.type': 'file', 263 | 'cache.long_term.expire': '86400', 264 | 265 | 266 | .. _Pylons: http://pylonshq.com/ 267 | .. _TurboGears2: http://turbogears.org/2.0/ 268 | .. _SQLAlchemy: http://www.sqlalchemy.org/ 269 | .. _pycryptopp: http://pypi.python.org/pypi/pycryptopp 270 | -------------------------------------------------------------------------------- /tests/test_cookie_only.py: -------------------------------------------------------------------------------- 1 | import datetime, time 2 | import re 3 | import os 4 | 5 | import beaker.session 6 | import beaker.util 7 | from beaker.session import SignedCookie 8 | from beaker._compat import b64decode 9 | from beaker.middleware import SessionMiddleware 10 | from nose import SkipTest 11 | try: 12 | from webtest import TestApp 13 | except ImportError: 14 | raise SkipTest("webtest not installed") 15 | 16 | from beaker import crypto 17 | if not crypto.has_aes: 18 | raise SkipTest("No AES library is installed, can't test cookie-only " 19 | "Sessions") 20 | 21 | def simple_app(environ, start_response): 22 | session = environ['beaker.session'] 23 | if not session.has_key('value'): 24 | session['value'] = 0 25 | session['value'] += 1 26 | if not environ['PATH_INFO'].startswith('/nosave'): 27 | session.save() 28 | start_response('200 OK', [('Content-type', 'text/plain')]) 29 | msg = 'The current value is: %d and cookie is %s' % (session['value'], session) 30 | return [msg.encode('UTF-8')] 31 | 32 | def test_increment(): 33 | options = {'session.validate_key':'hoobermas', 'session.type':'cookie'} 34 | app = TestApp(SessionMiddleware(simple_app, **options)) 35 | res = app.get('/') 36 | assert 'current value is: 1' in res 37 | res = app.get('/') 38 | assert 'current value is: 2' in res 39 | res = app.get('/') 40 | assert 'current value is: 3' in res 41 | 42 | def test_invalid_cookie(): 43 | # This is not actually a cookie only session, but we still test the cookie part. 44 | options = {'session.validate_key':'hoobermas'} 45 | app = TestApp(SessionMiddleware(simple_app, **options)) 46 | 47 | res = app.get('/') 48 | assert 'current value is: 1' in res 49 | 50 | # Set an invalid cookie. 51 | app.set_cookie('cb_/zabbix/actionconf.php_parts', 'HI') 52 | res = app.get('/') 53 | assert 'current value is: 2' in res, res 54 | 55 | res = app.get('/') 56 | assert 'current value is: 3' in res, res 57 | 58 | def test_invalid_cookie_cookietype(): 59 | # This is not actually a cookie only session, but we still test the cookie part. 60 | options = {'session.validate_key':'hoobermas', 'session.type':'cookie'} 61 | app = TestApp(SessionMiddleware(simple_app, **options)) 62 | 63 | res = app.get('/') 64 | assert 'current value is: 1' in res 65 | 66 | # Set an invalid cookie. 67 | app.set_cookie('cb_/zabbix/actionconf.php_parts', 'HI') 68 | res = app.get('/') 69 | assert 'current value is: 2' in res, res 70 | 71 | res = app.get('/') 72 | assert 'current value is: 3' in res, res 73 | 74 | def test_json_serializer(): 75 | options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'data_serializer': 'json'} 76 | app = TestApp(SessionMiddleware(simple_app, **options)) 77 | 78 | res = app.get('/') 79 | assert 'current value is: 1' in res 80 | 81 | res = app.get('/') 82 | cookie = SignedCookie('hoobermas') 83 | session_data = cookie.value_decode(app.cookies['beaker.session.id'])[0] 84 | session_data = b64decode(session_data) 85 | data = beaker.util.deserialize(session_data, 'json') 86 | assert data['value'] == 2 87 | 88 | res = app.get('/') 89 | assert 'current value is: 3' in res 90 | 91 | def test_pickle_serializer(): 92 | options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'data_serializer': 'pickle'} 93 | app = TestApp(SessionMiddleware(simple_app, **options)) 94 | 95 | res = app.get('/') 96 | assert 'current value is: 1' in res 97 | 98 | res = app.get('/') 99 | cookie = SignedCookie('hoobermas') 100 | session_data = cookie.value_decode(app.cookies['beaker.session.id'])[0] 101 | session_data = b64decode(session_data) 102 | data = beaker.util.deserialize(session_data, 'pickle') 103 | assert data['value'] == 2 104 | 105 | res = app.get('/') 106 | assert 'current value is: 3' in res 107 | 108 | def test_expires(): 109 | options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 110 | 'session.cookie_expires': datetime.timedelta(days=1)} 111 | app = TestApp(SessionMiddleware(simple_app, **options)) 112 | res = app.get('/') 113 | assert 'expires=' in res.headers.getall('Set-Cookie')[0] 114 | assert 'current value is: 1' in res 115 | 116 | def test_different_sessions(): 117 | options = {'session.validate_key':'hoobermas', 'session.type':'cookie'} 118 | app = TestApp(SessionMiddleware(simple_app, **options)) 119 | app2 = TestApp(SessionMiddleware(simple_app, **options)) 120 | res = app.get('/') 121 | assert 'current value is: 1' in res 122 | res = app2.get('/') 123 | assert 'current value is: 1' in res 124 | res = app2.get('/') 125 | res = app2.get('/') 126 | res = app2.get('/') 127 | res2 = app.get('/') 128 | assert 'current value is: 2' in res2 129 | assert 'current value is: 4' in res 130 | 131 | def test_nosave(): 132 | options = {'session.validate_key':'hoobermas', 'session.type':'cookie'} 133 | app = TestApp(SessionMiddleware(simple_app, **options)) 134 | res = app.get('/nosave') 135 | assert 'current value is: 1' in res 136 | assert [] == res.headers.getall('Set-Cookie') 137 | res = app.get('/nosave') 138 | assert 'current value is: 1' in res 139 | 140 | res = app.get('/') 141 | assert 'current value is: 1' in res 142 | assert len(res.headers.getall('Set-Cookie')) > 0 143 | res = app.get('/') 144 | assert 'current value is: 2' in res 145 | 146 | def test_increment_with_encryption(): 147 | options = {'session.encrypt_key':'666a19cf7f61c64c', 'session.validate_key':'hoobermas', 148 | 'session.type':'cookie'} 149 | app = TestApp(SessionMiddleware(simple_app, **options)) 150 | res = app.get('/') 151 | assert 'current value is: 1' in res 152 | res = app.get('/') 153 | assert 'current value is: 2' in res 154 | res = app.get('/') 155 | assert 'current value is: 3' in res 156 | 157 | def test_different_sessions_with_encryption(): 158 | options = {'session.encrypt_key':'666a19cf7f61c64c', 'session.validate_key':'hoobermas', 159 | 'session.type':'cookie'} 160 | app = TestApp(SessionMiddleware(simple_app, **options)) 161 | app2 = TestApp(SessionMiddleware(simple_app, **options)) 162 | res = app.get('/') 163 | assert 'current value is: 1' in res 164 | res = app2.get('/') 165 | assert 'current value is: 1' in res 166 | res = app2.get('/') 167 | res = app2.get('/') 168 | res = app2.get('/') 169 | res2 = app.get('/') 170 | assert 'current value is: 2' in res2 171 | assert 'current value is: 4' in res 172 | 173 | def test_nosave_with_encryption(): 174 | options = {'session.encrypt_key':'666a19cf7f61c64c', 'session.validate_key':'hoobermas', 175 | 'session.type':'cookie'} 176 | app = TestApp(SessionMiddleware(simple_app, **options)) 177 | res = app.get('/nosave') 178 | assert 'current value is: 1' in res 179 | assert [] == res.headers.getall('Set-Cookie') 180 | res = app.get('/nosave') 181 | assert 'current value is: 1' in res 182 | 183 | res = app.get('/') 184 | assert 'current value is: 1' in res 185 | assert len(res.headers.getall('Set-Cookie')) > 0 186 | res = app.get('/') 187 | assert 'current value is: 2' in res 188 | 189 | def test_cookie_id(): 190 | options = {'session.encrypt_key':'666a19cf7f61c64c', 'session.validate_key':'hoobermas', 191 | 'session.type':'cookie'} 192 | app = TestApp(SessionMiddleware(simple_app, **options)) 193 | 194 | res = app.get('/') 195 | assert "_id':" in res 196 | sess_id = re.sub(r".*'_id': '(.*?)'.*", r'\1', res.body.decode('utf-8')) 197 | res = app.get('/') 198 | new_id = re.sub(r".*'_id': '(.*?)'.*", r'\1', res.body.decode('utf-8')) 199 | assert new_id == sess_id 200 | 201 | def test_invalidate_with_save_does_not_delete_session(): 202 | def invalidate_session_app(environ, start_response): 203 | session = environ['beaker.session'] 204 | session.invalidate() 205 | session.save() 206 | start_response('200 OK', [('Content-type', 'text/plain')]) 207 | return [('Cookie is %s' % session).encode('UTF-8')] 208 | 209 | options = {'session.encrypt_key':'666a19cf7f61c64c', 'session.validate_key':'hoobermas', 210 | 'session.type':'cookie'} 211 | app = TestApp(SessionMiddleware(invalidate_session_app, **options)) 212 | res = app.get('/') 213 | assert 'expires=' not in res.headers.getall('Set-Cookie')[0] 214 | 215 | 216 | def test_changing_encrypt_key_with_timeout(): 217 | COMMON_ENCRYPT_KEY = '666a19cf7f61c64c' 218 | DIFFERENT_ENCRYPT_KEY = 'hello-world' 219 | 220 | options = {'session.encrypt_key': COMMON_ENCRYPT_KEY, 221 | 'session.timeout': 300, 222 | 'session.validate_key': 'hoobermas', 223 | 'session.type': 'cookie'} 224 | app = TestApp(SessionMiddleware(simple_app, **options)) 225 | res = app.get('/') 226 | assert 'The current value is: 1' in res, res 227 | 228 | # Get the session cookie, so we can reuse it. 229 | cookies = res.headers['Set-Cookie'] 230 | 231 | # Check that we get the same session with the same cookie 232 | options = {'session.encrypt_key': COMMON_ENCRYPT_KEY, 233 | 'session.timeout': 300, 234 | 'session.validate_key': 'hoobermas', 235 | 'session.type': 'cookie'} 236 | app = TestApp(SessionMiddleware(simple_app, **options)) 237 | res = app.get('/', headers={'Cookie': cookies}) 238 | assert 'The current value is: 2' in res, res 239 | 240 | # Now that we are sure that it reuses the same session, 241 | # change the encrypt_key so that it is unable to understand the cookie. 242 | options = {'session.encrypt_key': DIFFERENT_ENCRYPT_KEY, 243 | 'session.timeout': 300, 244 | 'session.validate_key': 'hoobermas', 245 | 'session.type': 'cookie'} 246 | app = TestApp(SessionMiddleware(simple_app, **options)) 247 | res = app.get('/', headers={'Cookie': cookies}) 248 | 249 | # Let's check it created a new session as the old one is invalid 250 | # in the past it just crashed. 251 | assert 'The current value is: 1' in res, res 252 | 253 | 254 | def test_cookie_properly_expires(): 255 | COMMON_ENCRYPT_KEY = '666a19cf7f61c64c' 256 | 257 | options = {'session.encrypt_key': COMMON_ENCRYPT_KEY, 258 | 'session.timeout': 1, 259 | 'session.validate_key': 'hoobermas', 260 | 'session.type': 'cookie'} 261 | app = TestApp(SessionMiddleware(simple_app, **options)) 262 | res = app.get('/') 263 | assert 'The current value is: 1' in res, res 264 | 265 | res = app.get('/') 266 | assert 'The current value is: 2' in res, res 267 | 268 | # Wait session to expire and check it starts with a clean one 269 | time.sleep(1) 270 | res = app.get('/') 271 | assert 'The current value is: 1' in res, res 272 | 273 | 274 | if __name__ == '__main__': 275 | from paste import httpserver 276 | wsgi_app = SessionMiddleware(simple_app, {}) 277 | httpserver.serve(wsgi_app, host='127.0.0.1', port=8080) 278 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from beaker._compat import u_, bytes_ 3 | 4 | import os 5 | import platform 6 | import shutil 7 | import tarfile 8 | import tempfile 9 | import time 10 | from beaker.middleware import CacheMiddleware 11 | from beaker import util 12 | from beaker.cache import Cache 13 | from nose import SkipTest 14 | from beaker.util import skip_if 15 | import base64 16 | import zlib 17 | 18 | try: 19 | from webtest import TestApp 20 | except ImportError: 21 | TestApp = None 22 | 23 | # Tarballs of the output of: 24 | # >>> from beaker.cache import Cache 25 | # >>> c = Cache('test', data_dir='db', type='dbm') 26 | # >>> c['foo'] = 'bar' 27 | # in the old format, Beaker @ revision: 24f57102d310 28 | dbm_cache_tar = bytes_("""\ 29 | eJzt3EtOwkAAgOEBjTHEBDfu2ekKZ6bTTnsBL+ABzPRB4osSRBMXHsNruXDl3nMYLaEbpYRAaIn6 30 | f8kwhFcn/APLSeNTUTdZsL4/m4Pg21wSqiCt9D1PC6mUZ7Xo+bWvrHB/N3HjXk+MrrLhQ/a48HXL 31 | nv+l0vg0yYcTdznMxhdpfFvHbpj1lyv0N8oq+jdhrr/b/A5Yo79R9G9ERX8XbXgLrNHfav7/G1Hd 32 | 30XGhYPMT5JYRbELVGISGVov9SKVRaGNQj2I49TrF+8oxpJrTAMHxizob+b7ay+Y/v5lE1/AP+8v 33 | 9o5ccdsWYvdViMPpIwdCtMRsiP3yTrucd8r5pJxbz8On9/KT2uVo3H5rG1cFAAAAAOD3aIuP7lv3 34 | pRjbXgkAAAAAAFjVyc1Idc6U1lYGgbSmL0Mjpe248+PYjY87I91x/UGeb3udAAAAAACgfh+fAAAA 35 | AADgr/t5/sPFTZ5cb/38D19Lzn9pRHX/zR4CtEZ/o+nfiEX9N3kI0Gr9vWl/W0z0BwAAAAAAAAAA 36 | AAAAAAAAqPAFyOvcKA== 37 | """) 38 | dbm_cache_tar = zlib.decompress(base64.b64decode(dbm_cache_tar)) 39 | 40 | # dumbdbm format 41 | dumbdbm_cache_tar = bytes_("""\ 42 | eJzt191qgzAYBmCPvYqc2UGx+ZKY6A3scCe7gJKoha6binOD3f2yn5Ouf3TTlNH3AQlEJcE3nyGV 43 | W0RT457Jsq9W6632W0Se0JI49/1E0vCIZZPPzHt5HmzPWNQ91M1r/XbwuVP3/6nKLcq2Gey6qftl 44 | 5Z6mWA3n56/IKOQfwk7+dvwV8Iv8FSH/IPbkb4uRl8BZ+fvg/WUE8g9if/62UDZf1VlZOiqc1VSq 45 | kudGVrKgushNkYuVc5VM/Rups5vjY3wErJU6nD+Z7fyFNFpEjIf4AFeef7Jq22TOZnzOpLiJLz0d 46 | CGyE+q/scHyMk/Wv+E79G0L9hzC7JSFMpv0PN0+J4rv7xNk+iTuKh07E6aXnB9Mao/7X/fExzt// 47 | FecS9R8C9v/r9rP+l49tubnk+e/z/J8JjvMfAAAAAAAAAADAn70DFJAAwQ== 48 | """) 49 | dumbdbm_cache_tar = zlib.decompress(base64.b64decode(dumbdbm_cache_tar)) 50 | 51 | def simple_app(environ, start_response): 52 | clear = False 53 | if environ.get('beaker.clear'): 54 | clear = True 55 | cache = environ['beaker.cache'].get_cache('testcache') 56 | if clear: 57 | cache.clear() 58 | try: 59 | value = cache.get_value('value') 60 | except: 61 | value = 0 62 | cache.set_value('value', value+1) 63 | start_response('200 OK', [('Content-type', 'text/plain')]) 64 | msg = 'The current value is: %s' % cache.get_value('value') 65 | return [msg.encode('utf-8')] 66 | 67 | def cache_manager_app(environ, start_response): 68 | cm = environ['beaker.cache'] 69 | cm.get_cache('test')['test_key'] = 'test value' 70 | 71 | start_response('200 OK', [('Content-type', 'text/plain')]) 72 | yield ("test_key is: %s\n" % cm.get_cache('test')['test_key']).encode('utf-8') 73 | cm.get_cache('test').clear() 74 | 75 | try: 76 | test_value = cm.get_cache('test')['test_key'] 77 | except KeyError: 78 | yield "test_key cleared".encode('utf-8') 79 | else: 80 | test_value = cm.get_cache('test')['test_key'] 81 | yield ("test_key wasn't cleared, is: %s\n" % test_value).encode('utf-8') 82 | 83 | def test_has_key(): 84 | cache = Cache('test', data_dir='./cache', type='dbm') 85 | o = object() 86 | cache.set_value("test", o) 87 | assert cache.has_key("test") 88 | assert "test" in cache 89 | assert not cache.has_key("foo") 90 | assert "foo" not in cache 91 | cache.remove_value("test") 92 | assert not cache.has_key("test") 93 | 94 | def test_expire_changes(): 95 | cache = Cache('test_bar', data_dir='./cache', type='dbm') 96 | cache.set_value('test', 10) 97 | assert cache.has_key('test') 98 | assert cache['test'] == 10 99 | 100 | # ensure that we can change a never-expiring value 101 | cache.set_value('test', 20, expiretime=1) 102 | assert cache.has_key('test') 103 | assert cache['test'] == 20 104 | time.sleep(1) 105 | assert not cache.has_key('test') 106 | 107 | # test that we can change it before its expired 108 | cache.set_value('test', 30, expiretime=50) 109 | assert cache.has_key('test') 110 | assert cache['test'] == 30 111 | 112 | cache.set_value('test', 40, expiretime=3) 113 | assert cache.has_key('test') 114 | assert cache['test'] == 40 115 | time.sleep(3) 116 | assert not cache.has_key('test') 117 | 118 | def test_fresh_createfunc(): 119 | cache = Cache('test_foo', data_dir='./cache', type='dbm') 120 | x = cache.get_value('test', createfunc=lambda: 10, expiretime=2) 121 | assert x == 10 122 | x = cache.get_value('test', createfunc=lambda: 12, expiretime=2) 123 | assert x == 10 124 | x = cache.get_value('test', createfunc=lambda: 14, expiretime=2) 125 | assert x == 10 126 | time.sleep(2) 127 | x = cache.get_value('test', createfunc=lambda: 16, expiretime=2) 128 | assert x == 16 129 | x = cache.get_value('test', createfunc=lambda: 18, expiretime=2) 130 | assert x == 16 131 | 132 | cache.remove_value('test') 133 | assert not cache.has_key('test') 134 | x = cache.get_value('test', createfunc=lambda: 20, expiretime=2) 135 | assert x == 20 136 | 137 | def test_has_key_multicache(): 138 | cache = Cache('test', data_dir='./cache', type='dbm') 139 | o = object() 140 | cache.set_value("test", o) 141 | assert cache.has_key("test") 142 | assert "test" in cache 143 | cache = Cache('test', data_dir='./cache', type='dbm') 144 | assert cache.has_key("test") 145 | 146 | def test_unicode_keys(): 147 | cache = Cache('test', data_dir='./cache', type='dbm') 148 | o = object() 149 | cache.set_value(u_('hiŏ'), o) 150 | assert u_('hiŏ') in cache 151 | assert u_('hŏa') not in cache 152 | cache.remove_value(u_('hiŏ')) 153 | assert u_('hiŏ') not in cache 154 | 155 | def test_remove_stale(): 156 | """test that remove_value() removes even if the value is expired.""" 157 | 158 | cache = Cache('test', type='memory') 159 | o = object() 160 | cache.namespace[b'key'] = (time.time() - 60, 5, o) 161 | container = cache._get_value('key') 162 | assert not container.has_current_value() 163 | assert b'key' in container.namespace 164 | cache.remove_value('key') 165 | assert b'key' not in container.namespace 166 | 167 | # safe to call again 168 | cache.remove_value('key') 169 | 170 | def test_multi_keys(): 171 | cache = Cache('newtests', data_dir='./cache', type='dbm') 172 | cache.clear() 173 | called = {} 174 | def create_func(): 175 | called['here'] = True 176 | return 'howdy' 177 | 178 | try: 179 | cache.get_value('key1') 180 | except KeyError: 181 | pass 182 | else: 183 | raise Exception("Failed to keyerror on nonexistent key") 184 | 185 | assert 'howdy' == cache.get_value('key2', createfunc=create_func) 186 | assert called['here'] == True 187 | del called['here'] 188 | 189 | try: 190 | cache.get_value('key3') 191 | except KeyError: 192 | pass 193 | else: 194 | raise Exception("Failed to keyerror on nonexistent key") 195 | try: 196 | cache.get_value('key1') 197 | except KeyError: 198 | pass 199 | else: 200 | raise Exception("Failed to keyerror on nonexistent key") 201 | 202 | assert 'howdy' == cache.get_value('key2', createfunc=create_func) 203 | assert called == {} 204 | 205 | @skip_if(lambda: TestApp is None, "webtest not installed") 206 | def test_increment(): 207 | app = TestApp(CacheMiddleware(simple_app)) 208 | res = app.get('/', extra_environ={'beaker.type':type, 'beaker.clear':True}) 209 | assert 'current value is: 1' in res 210 | res = app.get('/') 211 | assert 'current value is: 2' in res 212 | res = app.get('/') 213 | assert 'current value is: 3' in res 214 | 215 | @skip_if(lambda: TestApp is None, "webtest not installed") 216 | def test_cache_manager(): 217 | app = TestApp(CacheMiddleware(cache_manager_app)) 218 | res = app.get('/') 219 | assert 'test_key is: test value' in res 220 | assert 'test_key cleared' in res 221 | 222 | def test_clsmap_nonexistent(): 223 | from beaker.cache import clsmap 224 | 225 | try: 226 | clsmap['fake'] 227 | assert False 228 | except KeyError: 229 | pass 230 | 231 | def test_clsmap_present(): 232 | from beaker.cache import clsmap 233 | 234 | assert clsmap['memory'] 235 | 236 | 237 | def test_legacy_cache(): 238 | cache = Cache('newtests', data_dir='./cache', type='dbm') 239 | 240 | cache.set_value('x', '1') 241 | assert cache.get_value('x') == '1' 242 | 243 | cache.set_value('x', '2', type='file', data_dir='./cache') 244 | assert cache.get_value('x') == '1' 245 | assert cache.get_value('x', type='file', data_dir='./cache') == '2' 246 | 247 | cache.remove_value('x') 248 | cache.remove_value('x', type='file', data_dir='./cache') 249 | 250 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '5') == '5' 251 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '6', type='file', data_dir='./cache') == '6' 252 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '7') == '5' 253 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '8', type='file', data_dir='./cache') == '6' 254 | time.sleep(1) 255 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '9') == '9' 256 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '10', type='file', data_dir='./cache') == '10' 257 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '11') == '9' 258 | assert cache.get_value('x', expiretime=1, createfunc=lambda: '12', type='file', data_dir='./cache') == '10' 259 | 260 | 261 | def test_upgrade(): 262 | # If we're on OSX, lets run this since its OSX dump files, otherwise 263 | # we have to skip it 264 | if platform.system() != 'Darwin': 265 | return 266 | for test in _test_upgrade_has_key, _test_upgrade_in, _test_upgrade_setitem: 267 | for mod, tar in (('dbm', dbm_cache_tar), 268 | ('dumbdbm', dumbdbm_cache_tar)): 269 | try: 270 | __import__(mod) 271 | except ImportError: 272 | continue 273 | dir = tempfile.mkdtemp() 274 | fd, name = tempfile.mkstemp(dir=dir) 275 | fp = os.fdopen(fd, 'wb') 276 | fp.write(tar) 277 | fp.close() 278 | tar = tarfile.open(name) 279 | for member in tar.getmembers(): 280 | tar.extract(member, dir) 281 | tar.close() 282 | try: 283 | test(os.path.join(dir, 'db')) 284 | finally: 285 | shutil.rmtree(dir) 286 | 287 | def _test_upgrade_has_key(dir): 288 | cache = Cache('test', data_dir=dir, type='dbm') 289 | assert cache.has_key('foo') 290 | assert cache.has_key('foo') 291 | 292 | def _test_upgrade_in(dir): 293 | cache = Cache('test', data_dir=dir, type='dbm') 294 | assert 'foo' in cache 295 | assert 'foo' in cache 296 | 297 | def _test_upgrade_setitem(dir): 298 | cache = Cache('test', data_dir=dir, type='dbm') 299 | assert cache['foo'] == 'bar' 300 | assert cache['foo'] == 'bar' 301 | 302 | 303 | def teardown(): 304 | import shutil 305 | shutil.rmtree('./cache', True) 306 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from beaker._compat import u_, pickle 3 | 4 | import sys 5 | import time 6 | import warnings 7 | 8 | from nose import SkipTest 9 | 10 | from beaker.crypto import has_aes 11 | from beaker.session import Session 12 | from beaker import util 13 | 14 | 15 | def get_session(**kwargs): 16 | """A shortcut for creating :class:`Session` instance""" 17 | options = {} 18 | options.update(**kwargs) 19 | return Session({}, **options) 20 | 21 | 22 | def test_save_load(): 23 | """Test if the data is actually persistent across requests""" 24 | session = get_session() 25 | session[u_('Suomi')] = u_('Kimi Räikkönen') 26 | session[u_('Great Britain')] = u_('Jenson Button') 27 | session[u_('Deutchland')] = u_('Sebastian Vettel') 28 | session.save() 29 | 30 | session = get_session(id=session.id) 31 | assert u_('Suomi') in session 32 | assert u_('Great Britain') in session 33 | assert u_('Deutchland') in session 34 | 35 | assert session[u_('Suomi')] == u_('Kimi Räikkönen') 36 | assert session[u_('Great Britain')] == u_('Jenson Button') 37 | assert session[u_('Deutchland')] == u_('Sebastian Vettel') 38 | 39 | 40 | def test_save_load_encryption(): 41 | """Test if the data is actually persistent across requests""" 42 | if not has_aes: 43 | raise SkipTest() 44 | session = get_session(encrypt_key='666a19cf7f61c64c', 45 | validate_key='hoobermas') 46 | session[u_('Suomi')] = u_('Kimi Räikkönen') 47 | session[u_('Great Britain')] = u_('Jenson Button') 48 | session[u_('Deutchland')] = u_('Sebastian Vettel') 49 | session.save() 50 | 51 | session = get_session(id=session.id, encrypt_key='666a19cf7f61c64c', 52 | validate_key='hoobermas') 53 | assert u_('Suomi') in session 54 | assert u_('Great Britain') in session 55 | assert u_('Deutchland') in session 56 | 57 | assert session[u_('Suomi')] == u_('Kimi Räikkönen') 58 | assert session[u_('Great Britain')] == u_('Jenson Button') 59 | assert session[u_('Deutchland')] == u_('Sebastian Vettel') 60 | 61 | 62 | def test_decryption_failure(): 63 | """Test if the data fails without the right keys""" 64 | if not has_aes: 65 | raise SkipTest() 66 | session = get_session(encrypt_key='666a19cf7f61c64c', 67 | validate_key='hoobermas') 68 | session[u_('Suomi')] = u_('Kimi Räikkönen') 69 | session[u_('Great Britain')] = u_('Jenson Button') 70 | session[u_('Deutchland')] = u_('Sebastian Vettel') 71 | session.save() 72 | 73 | session = get_session(id=session.id, encrypt_key='asfdasdfadsfsadf', 74 | validate_key='hoobermas', invalidate_corrupt=True) 75 | assert u_('Suomi') not in session 76 | assert u_('Great Britain') not in session 77 | 78 | 79 | def test_delete(): 80 | """Test :meth:`Session.delete`""" 81 | session = get_session() 82 | session[u_('Suomi')] = u_('Kimi Räikkönen') 83 | session[u_('Great Britain')] = u_('Jenson Button') 84 | session[u_('Deutchland')] = u_('Sebastian Vettel') 85 | session.delete() 86 | 87 | assert u_('Suomi') not in session 88 | assert u_('Great Britain') not in session 89 | assert u_('Deutchland') not in session 90 | 91 | 92 | def test_revert(): 93 | """Test :meth:`Session.revert`""" 94 | session = get_session() 95 | session[u_('Suomi')] = u_('Kimi Räikkönen') 96 | session[u_('Great Britain')] = u_('Jenson Button') 97 | session[u_('Deutchland')] = u_('Sebastian Vettel') 98 | session.save() 99 | 100 | session = get_session(id=session.id) 101 | del session[u_('Suomi')] 102 | session[u_('Great Britain')] = u_('Lewis Hamilton') 103 | session[u_('Deutchland')] = u_('Michael Schumacher') 104 | session[u_('España')] = u_('Fernando Alonso') 105 | session.revert() 106 | 107 | assert session[u_('Suomi')] == u_('Kimi Räikkönen') 108 | assert session[u_('Great Britain')] == u_('Jenson Button') 109 | assert session[u_('Deutchland')] == u_('Sebastian Vettel') 110 | assert u_('España') not in session 111 | 112 | 113 | def test_invalidate(): 114 | """Test :meth:`Session.invalidate`""" 115 | session = get_session() 116 | id = session.id 117 | created = session.created 118 | session[u_('Suomi')] = u_('Kimi Räikkönen') 119 | session[u_('Great Britain')] = u_('Jenson Button') 120 | session[u_('Deutchland')] = u_('Sebastian Vettel') 121 | session.invalidate() 122 | 123 | assert session.id != id 124 | assert session.created != created 125 | assert u_('Suomi') not in session 126 | assert u_('Great Britain') not in session 127 | assert u_('Deutchland') not in session 128 | 129 | 130 | def test_regenerate_id(): 131 | """Test :meth:`Session.regenerate_id`""" 132 | # new session & save 133 | session = get_session() 134 | orig_id = session.id 135 | session[u_('foo')] = u_('bar') 136 | session.save() 137 | 138 | # load session 139 | session = get_session(id=session.id) 140 | # data should still be there 141 | assert session[u_('foo')] == u_('bar') 142 | 143 | # regenerate the id 144 | session.regenerate_id() 145 | 146 | assert session.id != orig_id 147 | 148 | # data is still there 149 | assert session[u_('foo')] == u_('bar') 150 | 151 | # should be the new id 152 | assert 'beaker.session.id=%s' % session.id in session.request['cookie_out'] 153 | 154 | # get a new session before calling save 155 | bunk_sess = get_session(id=session.id) 156 | assert u_('foo') not in bunk_sess 157 | 158 | # save it 159 | session.save() 160 | 161 | # make sure we get the data back 162 | session = get_session(id=session.id) 163 | assert session[u_('foo')] == u_('bar') 164 | 165 | 166 | def test_timeout(): 167 | """Test if the session times out properly""" 168 | session = get_session(timeout=2) 169 | id = session.id 170 | created = session.created 171 | session[u_('Suomi')] = u_('Kimi Räikkönen') 172 | session[u_('Great Britain')] = u_('Jenson Button') 173 | session[u_('Deutchland')] = u_('Sebastian Vettel') 174 | session.save() 175 | 176 | session = get_session(id=session.id, timeout=2) 177 | assert session.id == id 178 | assert session.created == created 179 | assert session[u_('Suomi')] == u_('Kimi Räikkönen') 180 | assert session[u_('Great Britain')] == u_('Jenson Button') 181 | assert session[u_('Deutchland')] == u_('Sebastian Vettel') 182 | 183 | time.sleep(2) 184 | session = get_session(id=session.id, timeout=2) 185 | assert session.id != id 186 | assert session.created != created 187 | assert u_('Suomi') not in session 188 | assert u_('Great Britain') not in session 189 | assert u_('Deutchland') not in session 190 | 191 | 192 | def test_cookies_enabled(): 193 | """ 194 | Test if cookies are sent out properly when ``use_cookies`` 195 | is set to ``True`` 196 | """ 197 | session = get_session(use_cookies=True) 198 | assert 'cookie_out' in session.request 199 | assert session.request['set_cookie'] == False 200 | 201 | session.domain = 'example.com' 202 | session.path = '/example' 203 | assert session.request['set_cookie'] == True 204 | assert 'beaker.session.id=%s' % session.id in session.request['cookie_out'] 205 | assert 'Domain=example.com' in session.request['cookie_out'] 206 | assert 'Path=/' in session.request['cookie_out'] 207 | 208 | session = get_session(use_cookies=True) 209 | session.save() 210 | assert session.request['set_cookie'] == True 211 | assert 'beaker.session.id=%s' % session.id in session.request['cookie_out'] 212 | 213 | session = get_session(use_cookies=True, id=session.id) 214 | session.delete() 215 | assert session.request['set_cookie'] == True 216 | assert 'beaker.session.id=%s' % session.id in session.request['cookie_out'] 217 | assert 'expires=' in session.request['cookie_out'] 218 | 219 | # test for secure 220 | session = get_session(use_cookies=True, secure=True) 221 | cookie = session.request['cookie_out'].lower() # Python3.4.3 outputs "Secure", while previous output "secure" 222 | assert 'secure' in cookie, cookie 223 | 224 | # test for httponly 225 | class ShowWarning(object): 226 | def __init__(self): 227 | self.msg = None 228 | def __call__(self, message, category, filename, lineno, file=None, line=None): 229 | self.msg = str(message) 230 | orig_sw = warnings.showwarning 231 | sw = ShowWarning() 232 | warnings.showwarning = sw 233 | session = get_session(use_cookies=True, httponly=True) 234 | if sys.version_info < (2, 6): 235 | assert sw.msg == 'Python 2.6+ is required to use httponly' 236 | else: 237 | # Python3.4.3 outputs "HttpOnly", while previous output "httponly" 238 | cookie = session.request['cookie_out'].lower() 239 | assert 'httponly' in cookie, cookie 240 | warnings.showwarning = orig_sw 241 | 242 | def test_cookies_disabled(): 243 | """ 244 | Test that no cookies are sent when ``use_cookies`` is set to ``False`` 245 | """ 246 | session = get_session(use_cookies=False) 247 | assert 'set_cookie' not in session.request 248 | assert 'cookie_out' not in session.request 249 | 250 | session.save() 251 | assert 'set_cookie' not in session.request 252 | assert 'cookie_out' not in session.request 253 | 254 | session = get_session(use_cookies=False, id=session.id) 255 | assert 'set_cookie' not in session.request 256 | assert 'cookie_out' not in session.request 257 | 258 | session.delete() 259 | assert 'set_cookie' not in session.request 260 | assert 'cookie_out' not in session.request 261 | 262 | 263 | def test_file_based_replace_optimization(): 264 | """Test the file-based backend with session, 265 | which includes the 'replace' optimization. 266 | 267 | """ 268 | 269 | session = get_session(use_cookies=False, type='file', 270 | data_dir='./cache') 271 | 272 | session['foo'] = 'foo' 273 | session['bar'] = 'bar' 274 | session.save() 275 | 276 | session = get_session(use_cookies=False, type='file', 277 | data_dir='./cache', id=session.id) 278 | assert session['foo'] == 'foo' 279 | assert session['bar'] == 'bar' 280 | 281 | session['bar'] = 'bat' 282 | session['bat'] = 'hoho' 283 | session.save() 284 | 285 | session.namespace.do_open('c', False) 286 | session.namespace['test'] = 'some test' 287 | session.namespace.do_close() 288 | 289 | session = get_session(use_cookies=False, type='file', 290 | data_dir='./cache', id=session.id) 291 | 292 | session.namespace.do_open('r', False) 293 | assert session.namespace['test'] == 'some test' 294 | session.namespace.do_close() 295 | 296 | assert session['foo'] == 'foo' 297 | assert session['bar'] == 'bat' 298 | assert session['bat'] == 'hoho' 299 | session.save() 300 | 301 | # the file has been replaced, so our out-of-session 302 | # key is gone 303 | session.namespace.do_open('r', False) 304 | assert 'test' not in session.namespace 305 | session.namespace.do_close() 306 | 307 | 308 | def test_invalidate_corrupt(): 309 | session = get_session(use_cookies=False, type='file', 310 | data_dir='./cache') 311 | session['foo'] = 'bar' 312 | session.save() 313 | 314 | f = open(session.namespace.file, 'w') 315 | f.write("crap") 316 | f.close() 317 | 318 | util.assert_raises( 319 | pickle.UnpicklingError, 320 | get_session, 321 | use_cookies=False, type='file', 322 | data_dir='./cache', id=session.id 323 | ) 324 | 325 | session = get_session(use_cookies=False, type='file', 326 | invalidate_corrupt=True, 327 | data_dir='./cache', id=session.id) 328 | assert "foo" not in dict(session) 329 | -------------------------------------------------------------------------------- /beaker/synchronization.py: -------------------------------------------------------------------------------- 1 | """Synchronization functions. 2 | 3 | File- and mutex-based mutual exclusion synchronizers are provided, 4 | as well as a name-based mutex which locks within an application 5 | based on a string name. 6 | 7 | """ 8 | 9 | import os 10 | import sys 11 | import tempfile 12 | 13 | try: 14 | import threading as _threading 15 | except ImportError: 16 | import dummy_threading as _threading 17 | 18 | # check for fcntl module 19 | try: 20 | sys.getwindowsversion() 21 | has_flock = False 22 | except: 23 | try: 24 | import fcntl 25 | has_flock = True 26 | except ImportError: 27 | has_flock = False 28 | 29 | from beaker import util 30 | from beaker.exceptions import LockError 31 | 32 | __all__ = ["file_synchronizer", "mutex_synchronizer", "null_synchronizer", 33 | "NameLock", "_threading"] 34 | 35 | 36 | class NameLock(object): 37 | """a proxy for an RLock object that is stored in a name based 38 | registry. 39 | 40 | Multiple threads can get a reference to the same RLock based on the 41 | name alone, and synchronize operations related to that name. 42 | 43 | """ 44 | locks = util.WeakValuedRegistry() 45 | 46 | class NLContainer(object): 47 | def __init__(self, reentrant): 48 | if reentrant: 49 | self.lock = _threading.RLock() 50 | else: 51 | self.lock = _threading.Lock() 52 | 53 | def __call__(self): 54 | return self.lock 55 | 56 | def __init__(self, identifier=None, reentrant=False): 57 | if identifier is None: 58 | self._lock = NameLock.NLContainer(reentrant) 59 | else: 60 | self._lock = NameLock.locks.get(identifier, NameLock.NLContainer, 61 | reentrant) 62 | 63 | def acquire(self, wait=True): 64 | return self._lock().acquire(wait) 65 | 66 | def release(self): 67 | self._lock().release() 68 | 69 | 70 | _synchronizers = util.WeakValuedRegistry() 71 | 72 | 73 | def _synchronizer(identifier, cls, **kwargs): 74 | return _synchronizers.sync_get((identifier, cls), cls, identifier, **kwargs) 75 | 76 | 77 | def file_synchronizer(identifier, **kwargs): 78 | if not has_flock or 'lock_dir' not in kwargs: 79 | return mutex_synchronizer(identifier) 80 | else: 81 | return _synchronizer(identifier, FileSynchronizer, **kwargs) 82 | 83 | 84 | def mutex_synchronizer(identifier, **kwargs): 85 | return _synchronizer(identifier, ConditionSynchronizer, **kwargs) 86 | 87 | 88 | class null_synchronizer(object): 89 | """A 'null' synchronizer, which provides the :class:`.SynchronizerImpl` interface 90 | without any locking. 91 | 92 | """ 93 | def acquire_write_lock(self, wait=True): 94 | return True 95 | 96 | def acquire_read_lock(self): 97 | pass 98 | 99 | def release_write_lock(self): 100 | pass 101 | 102 | def release_read_lock(self): 103 | pass 104 | acquire = acquire_write_lock 105 | release = release_write_lock 106 | 107 | 108 | class SynchronizerImpl(object): 109 | """Base class for a synchronization object that allows 110 | multiple readers, single writers. 111 | 112 | """ 113 | def __init__(self): 114 | self._state = util.ThreadLocal() 115 | 116 | class SyncState(object): 117 | __slots__ = 'reentrantcount', 'writing', 'reading' 118 | 119 | def __init__(self): 120 | self.reentrantcount = 0 121 | self.writing = False 122 | self.reading = False 123 | 124 | def state(self): 125 | if not self._state.has(): 126 | state = SynchronizerImpl.SyncState() 127 | self._state.put(state) 128 | return state 129 | else: 130 | return self._state.get() 131 | state = property(state) 132 | 133 | def release_read_lock(self): 134 | state = self.state 135 | 136 | if state.writing: 137 | raise LockError("lock is in writing state") 138 | if not state.reading: 139 | raise LockError("lock is not in reading state") 140 | 141 | if state.reentrantcount == 1: 142 | self.do_release_read_lock() 143 | state.reading = False 144 | 145 | state.reentrantcount -= 1 146 | 147 | def acquire_read_lock(self, wait=True): 148 | state = self.state 149 | 150 | if state.writing: 151 | raise LockError("lock is in writing state") 152 | 153 | if state.reentrantcount == 0: 154 | x = self.do_acquire_read_lock(wait) 155 | if (wait or x): 156 | state.reentrantcount += 1 157 | state.reading = True 158 | return x 159 | elif state.reading: 160 | state.reentrantcount += 1 161 | return True 162 | 163 | def release_write_lock(self): 164 | state = self.state 165 | 166 | if state.reading: 167 | raise LockError("lock is in reading state") 168 | if not state.writing: 169 | raise LockError("lock is not in writing state") 170 | 171 | if state.reentrantcount == 1: 172 | self.do_release_write_lock() 173 | state.writing = False 174 | 175 | state.reentrantcount -= 1 176 | 177 | release = release_write_lock 178 | 179 | def acquire_write_lock(self, wait=True): 180 | state = self.state 181 | 182 | if state.reading: 183 | raise LockError("lock is in reading state") 184 | 185 | if state.reentrantcount == 0: 186 | x = self.do_acquire_write_lock(wait) 187 | if (wait or x): 188 | state.reentrantcount += 1 189 | state.writing = True 190 | return x 191 | elif state.writing: 192 | state.reentrantcount += 1 193 | return True 194 | 195 | acquire = acquire_write_lock 196 | 197 | def do_release_read_lock(self): 198 | raise NotImplementedError() 199 | 200 | def do_acquire_read_lock(self): 201 | raise NotImplementedError() 202 | 203 | def do_release_write_lock(self): 204 | raise NotImplementedError() 205 | 206 | def do_acquire_write_lock(self): 207 | raise NotImplementedError() 208 | 209 | 210 | class FileSynchronizer(SynchronizerImpl): 211 | """A synchronizer which locks using flock(). 212 | 213 | """ 214 | def __init__(self, identifier, lock_dir): 215 | super(FileSynchronizer, self).__init__() 216 | self._filedescriptor = util.ThreadLocal() 217 | 218 | if lock_dir is None: 219 | lock_dir = tempfile.gettempdir() 220 | else: 221 | lock_dir = lock_dir 222 | 223 | self.filename = util.encoded_path( 224 | lock_dir, 225 | [identifier], 226 | extension='.lock' 227 | ) 228 | 229 | def _filedesc(self): 230 | return self._filedescriptor.get() 231 | _filedesc = property(_filedesc) 232 | 233 | def _open(self, mode): 234 | filedescriptor = self._filedesc 235 | if filedescriptor is None: 236 | filedescriptor = os.open(self.filename, mode) 237 | self._filedescriptor.put(filedescriptor) 238 | return filedescriptor 239 | 240 | def do_acquire_read_lock(self, wait): 241 | filedescriptor = self._open(os.O_CREAT | os.O_RDONLY) 242 | if not wait: 243 | try: 244 | fcntl.flock(filedescriptor, fcntl.LOCK_SH | fcntl.LOCK_NB) 245 | return True 246 | except IOError: 247 | os.close(filedescriptor) 248 | self._filedescriptor.remove() 249 | return False 250 | else: 251 | fcntl.flock(filedescriptor, fcntl.LOCK_SH) 252 | return True 253 | 254 | def do_acquire_write_lock(self, wait): 255 | filedescriptor = self._open(os.O_CREAT | os.O_WRONLY) 256 | if not wait: 257 | try: 258 | fcntl.flock(filedescriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) 259 | return True 260 | except IOError: 261 | os.close(filedescriptor) 262 | self._filedescriptor.remove() 263 | return False 264 | else: 265 | fcntl.flock(filedescriptor, fcntl.LOCK_EX) 266 | return True 267 | 268 | def do_release_read_lock(self): 269 | self._release_all_locks() 270 | 271 | def do_release_write_lock(self): 272 | self._release_all_locks() 273 | 274 | def _release_all_locks(self): 275 | filedescriptor = self._filedesc 276 | if filedescriptor is not None: 277 | fcntl.flock(filedescriptor, fcntl.LOCK_UN) 278 | os.close(filedescriptor) 279 | self._filedescriptor.remove() 280 | 281 | 282 | class ConditionSynchronizer(SynchronizerImpl): 283 | """a synchronizer using a Condition.""" 284 | 285 | def __init__(self, identifier): 286 | super(ConditionSynchronizer, self).__init__() 287 | 288 | # counts how many asynchronous methods are executing 289 | self.async = 0 290 | 291 | # pointer to thread that is the current sync operation 292 | self.current_sync_operation = None 293 | 294 | # condition object to lock on 295 | self.condition = _threading.Condition(_threading.Lock()) 296 | 297 | def do_acquire_read_lock(self, wait=True): 298 | self.condition.acquire() 299 | try: 300 | # see if a synchronous operation is waiting to start 301 | # or is already running, in which case we wait (or just 302 | # give up and return) 303 | if wait: 304 | while self.current_sync_operation is not None: 305 | self.condition.wait() 306 | else: 307 | if self.current_sync_operation is not None: 308 | return False 309 | 310 | self.async += 1 311 | finally: 312 | self.condition.release() 313 | 314 | if not wait: 315 | return True 316 | 317 | def do_release_read_lock(self): 318 | self.condition.acquire() 319 | try: 320 | self.async -= 1 321 | 322 | # check if we are the last asynchronous reader thread 323 | # out the door. 324 | if self.async == 0: 325 | # yes. so if a sync operation is waiting, notifyAll to wake 326 | # it up 327 | if self.current_sync_operation is not None: 328 | self.condition.notifyAll() 329 | elif self.async < 0: 330 | raise LockError("Synchronizer error - too many " 331 | "release_read_locks called") 332 | finally: 333 | self.condition.release() 334 | 335 | def do_acquire_write_lock(self, wait=True): 336 | self.condition.acquire() 337 | try: 338 | # here, we are not a synchronous reader, and after returning, 339 | # assuming waiting or immediate availability, we will be. 340 | 341 | if wait: 342 | # if another sync is working, wait 343 | while self.current_sync_operation is not None: 344 | self.condition.wait() 345 | else: 346 | # if another sync is working, 347 | # we dont want to wait, so forget it 348 | if self.current_sync_operation is not None: 349 | return False 350 | 351 | # establish ourselves as the current sync 352 | # this indicates to other read/write operations 353 | # that they should wait until this is None again 354 | self.current_sync_operation = _threading.currentThread() 355 | 356 | # now wait again for asyncs to finish 357 | if self.async > 0: 358 | if wait: 359 | # wait 360 | self.condition.wait() 361 | else: 362 | # we dont want to wait, so forget it 363 | self.current_sync_operation = None 364 | return False 365 | finally: 366 | self.condition.release() 367 | 368 | if not wait: 369 | return True 370 | 371 | def do_release_write_lock(self): 372 | self.condition.acquire() 373 | try: 374 | if self.current_sync_operation is not _threading.currentThread(): 375 | raise LockError("Synchronizer error - current thread doesnt " 376 | "have the write lock") 377 | 378 | # reset the current sync operation so 379 | # another can get it 380 | self.current_sync_operation = None 381 | 382 | # tell everyone to get ready 383 | self.condition.notifyAll() 384 | finally: 385 | # everyone go !! 386 | self.condition.release() 387 | -------------------------------------------------------------------------------- /tests/test_memcached.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from beaker._compat import u_ 3 | 4 | import mock 5 | import os 6 | 7 | from beaker.cache import clsmap, Cache, CacheManager, util 8 | from beaker.middleware import CacheMiddleware, SessionMiddleware 9 | from beaker.exceptions import InvalidCacheBackendError 10 | from beaker.util import parse_cache_config_options 11 | from nose import SkipTest 12 | import unittest 13 | 14 | try: 15 | from webtest import TestApp 16 | except ImportError: 17 | TestApp = None 18 | 19 | try: 20 | from beaker.ext import memcached 21 | client = memcached._load_client() 22 | except InvalidCacheBackendError: 23 | raise SkipTest("an appropriate memcached backend is not installed") 24 | 25 | mc_url = '127.0.0.1:11211' 26 | 27 | c =client.Client([mc_url]) 28 | c.set('x', 'y') 29 | if not c.get('x'): 30 | raise SkipTest("Memcached is not running at %s" % mc_url) 31 | 32 | def teardown(): 33 | import shutil 34 | shutil.rmtree('./cache', True) 35 | 36 | def simple_session_app(environ, start_response): 37 | session = environ['beaker.session'] 38 | sess_id = environ.get('SESSION_ID') 39 | if environ['PATH_INFO'].startswith('/invalid'): 40 | # Attempt to access the session 41 | id = session.id 42 | session['value'] = 2 43 | else: 44 | if sess_id: 45 | session = session.get_by_id(sess_id) 46 | if not session: 47 | start_response('200 OK', [('Content-type', 'text/plain')]) 48 | return ["No session id of %s found." % sess_id] 49 | if not session.has_key('value'): 50 | session['value'] = 0 51 | session['value'] += 1 52 | if not environ['PATH_INFO'].startswith('/nosave'): 53 | session.save() 54 | start_response('200 OK', [('Content-type', 'text/plain')]) 55 | return ['The current value is: %d, session id is %s' % (session['value'], 56 | session.id)] 57 | 58 | def simple_app(environ, start_response): 59 | extra_args = {} 60 | clear = False 61 | if environ.get('beaker.clear'): 62 | clear = True 63 | extra_args['type'] = 'ext:memcached' 64 | extra_args['url'] = mc_url 65 | extra_args['data_dir'] = './cache' 66 | cache = environ['beaker.cache'].get_cache('testcache', **extra_args) 67 | if clear: 68 | cache.clear() 69 | try: 70 | value = cache.get_value('value') 71 | except: 72 | value = 0 73 | cache.set_value('value', value+1) 74 | start_response('200 OK', [('Content-type', 'text/plain')]) 75 | return ['The current value is: %s' % cache.get_value('value')] 76 | 77 | 78 | def using_none_app(environ, start_response): 79 | extra_args = {} 80 | clear = False 81 | if environ.get('beaker.clear'): 82 | clear = True 83 | extra_args['type'] = 'ext:memcached' 84 | extra_args['url'] = mc_url 85 | extra_args['data_dir'] = './cache' 86 | cache = environ['beaker.cache'].get_cache('testcache', **extra_args) 87 | if clear: 88 | cache.clear() 89 | try: 90 | value = cache.get_value('value') 91 | except: 92 | value = 10 93 | cache.set_value('value', None) 94 | start_response('200 OK', [('Content-type', 'text/plain')]) 95 | return ['The current value is: %s' % value] 96 | 97 | 98 | def cache_manager_app(environ, start_response): 99 | cm = environ['beaker.cache'] 100 | cm.get_cache('test')['test_key'] = 'test value' 101 | 102 | start_response('200 OK', [('Content-type', 'text/plain')]) 103 | yield "test_key is: %s\n" % cm.get_cache('test')['test_key'] 104 | cm.get_cache('test').clear() 105 | 106 | try: 107 | test_value = cm.get_cache('test')['test_key'] 108 | except KeyError: 109 | yield "test_key cleared" 110 | else: 111 | yield "test_key wasn't cleared, is: %s\n" % \ 112 | cm.get_cache('test')['test_key'] 113 | 114 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 115 | def test_session(): 116 | app = TestApp(SessionMiddleware(simple_session_app, data_dir='./cache', type='ext:memcached', url=mc_url)) 117 | res = app.get('/') 118 | assert 'current value is: 1' in res 119 | res = app.get('/') 120 | assert 'current value is: 2' in res 121 | res = app.get('/') 122 | assert 'current value is: 3' in res 123 | 124 | 125 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 126 | def test_session_invalid(): 127 | app = TestApp(SessionMiddleware(simple_session_app, data_dir='./cache', type='ext:memcached', url=mc_url)) 128 | res = app.get('/invalid', headers=dict(Cookie='beaker.session.id=df7324911e246b70b5781c3c58328442; Path=/')) 129 | assert 'current value is: 2' in res 130 | 131 | 132 | def test_has_key(): 133 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 134 | o = object() 135 | cache.set_value("test", o) 136 | assert cache.has_key("test") 137 | assert "test" in cache 138 | assert not cache.has_key("foo") 139 | assert "foo" not in cache 140 | cache.remove_value("test") 141 | assert not cache.has_key("test") 142 | 143 | def test_dropping_keys(): 144 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 145 | cache.set_value('test', 20) 146 | cache.set_value('fred', 10) 147 | assert cache.has_key('test') 148 | assert 'test' in cache 149 | assert cache.has_key('fred') 150 | 151 | # Directly nuke the actual key, to simulate it being removed by memcached 152 | cache.namespace.mc.delete('test_test') 153 | assert not cache.has_key('test') 154 | assert cache.has_key('fred') 155 | 156 | # Nuke the keys dict, it might die, who knows 157 | cache.namespace.mc.delete('test:keys') 158 | assert cache.has_key('fred') 159 | 160 | # And we still need clear to work, even if it won't work well 161 | cache.clear() 162 | 163 | def test_deleting_keys(): 164 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 165 | cache.set_value('test', 20) 166 | 167 | # Nuke the keys dict, it might die, who knows 168 | cache.namespace.mc.delete('test:keys') 169 | 170 | assert cache.has_key('test') 171 | 172 | # make sure we can still delete keys even though our keys dict got nuked 173 | del cache['test'] 174 | 175 | assert not cache.has_key('test') 176 | 177 | def test_has_key_multicache(): 178 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 179 | o = object() 180 | cache.set_value("test", o) 181 | assert cache.has_key("test") 182 | assert "test" in cache 183 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 184 | assert cache.has_key("test") 185 | 186 | def test_unicode_keys(): 187 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 188 | o = object() 189 | cache.set_value(u_('hiŏ'), o) 190 | assert u_('hiŏ') in cache 191 | assert u_('hŏa') not in cache 192 | cache.remove_value(u_('hiŏ')) 193 | assert u_('hiŏ') not in cache 194 | 195 | def test_long_unicode_keys(): 196 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 197 | o = object() 198 | long_str = u_('Очень длинная строка, которая не влезает в сто двадцать восемь байт и поэтому не проходит ограничение в check_key, что очень прискорбно, не правда ли, друзья? Давайте же скорее исправим это досадное недоразумение!') 199 | cache.set_value(long_str, o) 200 | assert long_str in cache 201 | cache.remove_value(long_str) 202 | assert long_str not in cache 203 | 204 | def test_spaces_in_unicode_keys(): 205 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 206 | o = object() 207 | cache.set_value(u_('hi ŏ'), o) 208 | assert u_('hi ŏ') in cache 209 | assert u_('hŏa') not in cache 210 | cache.remove_value(u_('hi ŏ')) 211 | assert u_('hi ŏ') not in cache 212 | 213 | def test_spaces_in_keys(): 214 | cache = Cache('test', data_dir='./cache', url=mc_url, type='ext:memcached') 215 | cache.set_value("has space", 24) 216 | assert cache.has_key("has space") 217 | assert 24 == cache.get_value("has space") 218 | cache.set_value("hasspace", 42) 219 | assert cache.has_key("hasspace") 220 | assert 42 == cache.get_value("hasspace") 221 | 222 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 223 | def test_increment(): 224 | app = TestApp(CacheMiddleware(simple_app)) 225 | res = app.get('/', extra_environ={'beaker.clear':True}) 226 | assert 'current value is: 1' in res 227 | res = app.get('/') 228 | assert 'current value is: 2' in res 229 | res = app.get('/') 230 | assert 'current value is: 3' in res 231 | 232 | app = TestApp(CacheMiddleware(simple_app)) 233 | res = app.get('/', extra_environ={'beaker.clear':True}) 234 | assert 'current value is: 1' in res 235 | res = app.get('/') 236 | assert 'current value is: 2' in res 237 | res = app.get('/') 238 | assert 'current value is: 3' in res 239 | 240 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 241 | def test_cache_manager(): 242 | app = TestApp(CacheMiddleware(cache_manager_app)) 243 | res = app.get('/') 244 | assert 'test_key is: test value' in res 245 | assert 'test_key cleared' in res 246 | 247 | @util.skip_if(lambda: TestApp is None, "webtest not installed") 248 | def test_store_none(): 249 | app = TestApp(CacheMiddleware(using_none_app)) 250 | res = app.get('/', extra_environ={'beaker.clear':True}) 251 | assert 'current value is: 10' in res 252 | res = app.get('/') 253 | assert 'current value is: None' in res 254 | 255 | class TestPylibmcInit(unittest.TestCase): 256 | def setUp(self): 257 | 258 | from beaker.ext import memcached 259 | try: 260 | import pylibmc as memcache 261 | except: 262 | import memcache 263 | memcached._client_libs['pylibmc'] = memcached.pylibmc = memcache 264 | from contextlib import contextmanager 265 | class ThreadMappedPool(dict): 266 | "a mock of pylibmc's ThreadMappedPool" 267 | 268 | def __init__(self, master): 269 | self.master = master 270 | 271 | @contextmanager 272 | def reserve(self): 273 | yield self.master 274 | memcache.ThreadMappedPool = ThreadMappedPool 275 | 276 | def test_uses_pylibmc_client(self): 277 | from beaker.ext import memcached 278 | cache = Cache('test', data_dir='./cache', 279 | memcache_module='pylibmc', 280 | url=mc_url, type="ext:memcached") 281 | assert isinstance(cache.namespace, memcached.PyLibMCNamespaceManager) 282 | 283 | def test_dont_use_pylibmc_client(self): 284 | from beaker.ext.memcached import _load_client 285 | load_mock = mock.Mock() 286 | load_mock.return_value = _load_client('memcache') 287 | with mock.patch('beaker.ext.memcached._load_client', load_mock): 288 | cache = Cache('test', data_dir='./cache', url=mc_url, type="ext:memcached") 289 | assert not isinstance(cache.namespace, memcached.PyLibMCNamespaceManager) 290 | assert isinstance(cache.namespace, memcached.MemcachedNamespaceManager) 291 | 292 | def test_client(self): 293 | cache = Cache('test', data_dir='./cache', url=mc_url, type="ext:memcached", 294 | protocol='binary') 295 | o = object() 296 | cache.set_value("test", o) 297 | assert cache.has_key("test") 298 | assert "test" in cache 299 | assert not cache.has_key("foo") 300 | assert "foo" not in cache 301 | cache.remove_value("test") 302 | assert not cache.has_key("test") 303 | 304 | def test_client_behaviors(self): 305 | config = { 306 | 'cache.lock_dir':'./lock', 307 | 'cache.data_dir':'./cache', 308 | 'cache.type':'ext:memcached', 309 | 'cache.url':mc_url, 310 | 'cache.memcache_module':'pylibmc', 311 | 'cache.protocol':'binary', 312 | 'cache.behavior.ketama': 'True', 313 | 'cache.behavior.cas':False, 314 | 'cache.behavior.receive_timeout':'3600', 315 | 'cache.behavior.send_timeout':1800, 316 | 'cache.behavior.tcp_nodelay':1, 317 | 'cache.behavior.auto_eject':"0" 318 | } 319 | cache_manager = CacheManager(**parse_cache_config_options(config)) 320 | cache = cache_manager.get_cache('test_behavior', expire=6000) 321 | 322 | with cache.namespace.pool.reserve() as mc: 323 | assert "ketama" in mc.behaviors 324 | assert mc.behaviors["ketama"] == 1 325 | assert "cas" in mc.behaviors 326 | assert mc.behaviors["cas"] == 0 327 | assert "receive_timeout" in mc.behaviors 328 | assert mc.behaviors["receive_timeout"] == 3600 329 | assert "send_timeout" in mc.behaviors 330 | assert mc.behaviors["send_timeout"] == 1800 331 | assert "tcp_nodelay" in mc.behaviors 332 | assert mc.behaviors["tcp_nodelay"] == 1 333 | assert "auto_eject" in mc.behaviors 334 | assert mc.behaviors["auto_eject"] == 0 335 | 336 | def test_pylibmc_pool_sharing(self): 337 | from beaker.ext import memcached 338 | cache_1a = Cache('test_1a', data_dir='./cache', 339 | memcache_module='pylibmc', 340 | url=mc_url, type="ext:memcached") 341 | cache_1b = Cache('test_1b', data_dir='./cache', 342 | memcache_module='pylibmc', 343 | url=mc_url, type="ext:memcached") 344 | cache_2 = Cache('test_2', data_dir='./cache', 345 | memcache_module='pylibmc', 346 | url='127.0.0.1:11212', type="ext:memcached") 347 | 348 | assert (cache_1a.namespace.pool is cache_1b.namespace.pool) 349 | assert (cache_1a.namespace.pool is not cache_2.namespace.pool) 350 | 351 | --------------------------------------------------------------------------------