Commit 28ae352e authored by Georges Dubus's avatar Georges Dubus

Added support for postgres:// uri.

Support for mysql and oracle should be easy to add from here.
parent 87be3603
...@@ -97,3 +97,4 @@ Contributors ...@@ -97,3 +97,4 @@ Contributors
------------ ------------
- Chris McDonough, 2011/08/18 - Chris McDonough, 2011/08/18
- Georges Dubus 2012/05/27
...@@ -43,9 +43,10 @@ passing to the ``ZODB.DB.DB`` constructor. For example: ...@@ -43,9 +43,10 @@ passing to the ``ZODB.DB.DB`` constructor. For example:
URI Schemes URI Schemes
----------- -----------
The URI schemes currently recognized in the ``zodbconn.uri`` setting are The URI schemes currently recognized in the ``zodbconn.uri`` setting
``file://``, ``zeo://``, ``zconfig://`` and ``memory://``. Documentation for are ``file://``, ``zeo://``, ``zconfig://``, ``memory://``
these URI scheme syntaxes are below. ``postgres://``. Documentation for these URI scheme syntaxes are
below.
``file://`` URI scheme ``file://`` URI scheme
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
...@@ -286,6 +287,110 @@ An example that combines a dbname with a query string:: ...@@ -286,6 +287,110 @@ An example that combines a dbname with a query string::
memory://storagename?connection_cache_size=100&database_name=fleeb memory://storagename?connection_cache_size=100&database_name=fleeb
``postgres://`` URI scheme
~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``postgres://`` URI scheme can be passed as ``zodbconn.uri`` to
create a RelStorage PostgresSQL database factory. The uri should
contain the user, the password, the host, the port and the db name
e.g.::
postgres://someuser:somepass@somehost:5432/somedb?connection_cache_size=20000
The URI scheme also accepts query string arguments. The query string
arguments honored by this scheme are as follows.
RelStorage-constructor related
++++++++++++++++++++++++++++++
These arguments generally inform the RelStorage constructor about
values of the same names.
poll_interval
int
cache_local_mb
int
commit_lock_timeout
int
commit_lock_id
int
read_only
boolean
shared_blob_dir
boolean
keep_history
boolean
pack_gc
boolean
pack_dry_run
boolean
strict_tpc
boolean
create
boolean
blob_cache_size
bytesize
blob_cache_size_check
bytesize
blob_cache_chunk_size
bytesize
replica_timeout
float
pack_batch_timeout
float
pack_duty_cycle
float
pack_max_delay
float
name
string
blob_dir
string
replica_conf
string
cache_module_name
string
cache_prefix
string
cache_delta_size_limit
string
cache_servers
string of the for "first,second,third"
Misc
++++
demostorage
boolean (if true, wrap RelStorage in a DemoStorage)
Connection-related
++++++++++++++++++
These arguments relate to connections created from the database.
connection_cache_size
integer (default 10000)
connection_pool_size
integer (default 7)
Database-related
++++++++++++++++
These arguments relate to the database (as opposed to storage)
settings.
database_name
string
Example
+++++++
An example that combines a path with a query string::
postgres://someuser:somepass@somehost:5432/somedb?connection_cache_size=20000
More Information More Information
---------------- ----------------
......
...@@ -12,7 +12,7 @@ except: ...@@ -12,7 +12,7 @@ except:
README = '' README = ''
CHANGES = '' CHANGES = ''
requires = ['ZODB3'] requires = ['ZODB3', 'RelStorage',]
tests_require = requires + ['mock'] tests_require = requires + ['mock']
setup(name='zodburi', setup(name='zodburi',
...@@ -40,5 +40,6 @@ setup(name='zodburi', ...@@ -40,5 +40,6 @@ setup(name='zodburi',
file = zodburi.resolvers:file_storage_resolver file = zodburi.resolvers:file_storage_resolver
zconfig = zodburi.resolvers:zconfig_resolver zconfig = zodburi.resolvers:zconfig_resolver
memory = zodburi.resolvers:mapping_storage_resolver memory = zodburi.resolvers:mapping_storage_resolver
postgres = zodburi.resolvers:postgresql_resolver
""" """
) )
...@@ -10,6 +10,9 @@ from ZODB.DemoStorage import DemoStorage ...@@ -10,6 +10,9 @@ from ZODB.DemoStorage import DemoStorage
from ZODB.MappingStorage import MappingStorage from ZODB.MappingStorage import MappingStorage
from ZODB.blob import BlobStorage from ZODB.blob import BlobStorage
import ZConfig import ZConfig
from relstorage.adapters.postgresql import PostgreSQLAdapter
from relstorage.options import Options
from relstorage.storage import RelStorage
from zodburi.datatypes import convert_bytesize from zodburi.datatypes import convert_bytesize
from zodburi.datatypes import convert_int from zodburi.datatypes import convert_int
...@@ -19,6 +22,8 @@ class Resolver(object): ...@@ -19,6 +22,8 @@ class Resolver(object):
_int_args = () _int_args = ()
_string_args = () _string_args = ()
_bytesize_args = () _bytesize_args = ()
_float_args = ()
_tuple_args = ()
def interpret_kwargs(self, kw): def interpret_kwargs(self, kw):
unused = kw.copy() unused = kw.copy()
...@@ -197,7 +202,70 @@ class ZConfigURIResolver(object): ...@@ -197,7 +202,70 @@ class ZConfigURIResolver(object):
return factory.open, dbkw return factory.open, dbkw
# Not a real resolver, but we use interpret_kwargs
class PostgreSQLAdapterHelper(Resolver):
_int_args = ('connect_timeout', )
_string_args = ('ssl_mode', )
def __call__(self, parsed_uri, kw):
dsn_args = [
('dbname', parsed_uri.path[1:]),
('user', parsed_uri.username),
('password', parsed_uri.password),
('host', parsed_uri.hostname),
('port', parsed_uri.port)
]
kw, unused = self.interpret_kwargs(kw)
dsn_args.extend(kw.items())
dsn = ' '.join("%s='%s'"%arg for arg in dsn_args)
def factory(options):
return PostgreSQLAdapter(dsn=dsn, options=options)
return factory, unused
# The relstorage support is inspired by django-zodb.
# Oracle and mysql should be easily implementable from here
class RelStorageURIResolver(Resolver):
_int_args = ('poll_interval', 'cache_local_mb', 'commit_lock_timeout',
'commit_lock_id', 'read_only', 'shared_blob_dir',
'keep_history', 'pack_gc', 'pack_dry_run', 'strict_tpc',
'create', 'demostorage',)
_string_args = ('name', 'blob_dir', 'replica_conf', 'cache_module_name',
'cache_prefix', 'cache_delta_size_limit')
_bytesize_args = ('blob_cache_size', 'blob_cache_size_check',
'blob_cache_chunk_size')
_float_args = ('replica_timeout', 'pack_batch_timeout', 'pack_duty_cycle',
'pack_max_delay')
_tuple_args = ('cache_servers',)
def __init__(self, adapter_helper):
self.adapter_helper = adapter_helper
def __call__(self, uri):
uri = uri.replace('postgres://', 'http://', 1)
parsed_uri = urlparse.urlsplit(uri)
kw = dict(cgi.parse_qsl(parsed_uri.query))
adapter_factory, kw = self.adapter_helper(parsed_uri, kw)
kw, unused = self.interpret_kwargs(kw)
demostorage = kw.pop('demostorage', False)
options = Options(**kw)
def factory():
adapter = adapter_factory(options)
storage = RelStorage(adapter=adapter, options=options)
if demostorage:
storage = DemoStorage(base=storage)
return storage
return factory, unused
client_storage_resolver = ClientStorageURIResolver() client_storage_resolver = ClientStorageURIResolver()
file_storage_resolver = FileStorageURIResolver() file_storage_resolver = FileStorageURIResolver()
zconfig_resolver = ZConfigURIResolver() zconfig_resolver = ZConfigURIResolver()
mapping_storage_resolver = MappingStorageURIResolver() mapping_storage_resolver = MappingStorageURIResolver()
\ No newline at end of file postgresql_resolver = RelStorageURIResolver(PostgreSQLAdapterHelper())
\ No newline at end of file
...@@ -47,6 +47,32 @@ class Base: ...@@ -47,6 +47,32 @@ class Base:
for name, value in args.items(): for name, value in args.items():
self.assertEqual(value, 'string') self.assertEqual(value, 'string')
def test_float_args(self):
resolver = self._makeOne()
names = list(resolver._float_args)
kwargs = {}
for name in names:
kwargs[name] = '3.14'
args = resolver.interpret_kwargs(kwargs)[0]
keys = args.keys()
keys.sort()
self.assertEqual(keys, names)
for name, value in args.items():
self.assertEqual(value, 3.14)
def test_tuple_args(self):
resolver = self._makeOne()
names = list(resolver._tuple_args)
kwargs = {}
for name in names:
kwargs[name] = 'first,second,third'
args = resolver.interpret_kwargs(kwargs)[0]
keys = args.keys()
keys.sort()
self.assertEqual(keys, names)
for name, value in args.items():
self.assertEqual(value, ('first', 'second', 'third'))
class TestFileStorageURIResolver(Base, unittest.TestCase): class TestFileStorageURIResolver(Base, unittest.TestCase):
def _getTargetClass(self): def _getTargetClass(self):
...@@ -387,6 +413,88 @@ class TestMappingStorageURIResolver(Base, unittest.TestCase): ...@@ -387,6 +413,88 @@ class TestMappingStorageURIResolver(Base, unittest.TestCase):
self.assertEqual(storage.__name__, 'storagename') self.assertEqual(storage.__name__, 'storagename')
class TestPostgreSQLURIResolver(unittest.TestCase):
def _getTargetClass(self):
from zodburi.resolvers import RelStorageURIResolver
return RelStorageURIResolver
def _makeOne(self):
from zodburi.resolvers import PostgreSQLAdapterHelper
klass = self._getTargetClass()
return klass(PostgreSQLAdapterHelper())
def setUp(self):
# relstorage.options.Options is little more than a dict.
# We make it comparable to simplify the tests.
from relstorage.options import Options
Options.__eq__ = lambda s, o: vars(s) == vars(o)
def test_bool_args(self):
resolver = self._makeOne()
f = resolver.interpret_kwargs
kwargs = f({'read_only':'1'})
self.assertEqual(kwargs[0], {'read_only':1})
kwargs = f({'read_only':'true'})
self.assertEqual(kwargs[0], {'read_only':1})
kwargs = f({'read_only':'on'})
self.assertEqual(kwargs[0], {'read_only':1})
kwargs = f({'read_only':'off'})
self.assertEqual(kwargs[0], {'read_only':0})
kwargs = f({'read_only':'no'})
self.assertEqual(kwargs[0], {'read_only':0})
kwargs = f({'read_only':'false'})
self.assertEqual(kwargs[0], {'read_only':0})
@mock.patch('zodburi.resolvers.PostgreSQLAdapter')
@mock.patch('zodburi.resolvers.RelStorage')
def test_call(self, RelStorage, PostgreSQLAdapter):
from relstorage.options import Options
resolver = self._makeOne()
factory, dbkw = resolver('postgres://someuser:somepass@somehost:5432/somedb?read_only=1')
factory()
expected_options = Options(read_only=1)
PostgreSQLAdapter.assert_called_once_with(dsn="dbname='somedb' user='someuser' password='somepass' "
"host='somehost' port='5432'",
options=expected_options)
RelStorage.assert_called_once_with(adapter=PostgreSQLAdapter(), options=expected_options)
@mock.patch('zodburi.resolvers.PostgreSQLAdapter')
@mock.patch('zodburi.resolvers.RelStorage')
def test_call_adapter_options(self, RelStorage, PostgreSQLAdapter):
from relstorage.options import Options
resolver = self._makeOne()
factory, dbkw = resolver('postgres://someuser:somepass@somehost:5432/somedb?read_only=1'
'&connect_timeout=10')
factory()
expected_options = Options(read_only=1)
PostgreSQLAdapter.assert_called_once_with(dsn="dbname='somedb' user='someuser' password='somepass' "
"host='somehost' port='5432' connect_timeout='10'",
options=expected_options)
RelStorage.assert_called_once_with(adapter=PostgreSQLAdapter(), options=expected_options)
@mock.patch('zodburi.resolvers.PostgreSQLAdapter')
@mock.patch('zodburi.resolvers.RelStorage')
def test_invoke_factory_demostorage(self, RelStorage, PostgreSQLAdapter):
from ZODB.DemoStorage import DemoStorage
resolver = self._makeOne()
factory, dbkw = resolver('postgres://someuser:somepass@somehost:5432/somedb?read_only=1'
'&demostorage=true')
self.assertTrue(isinstance(factory(), DemoStorage))
def test_dbargs(self):
resolver = self._makeOne()
factory, dbkw = resolver('postgres://someuser:somepass@somehost:5432/somedb?read_only=1&'
'connection_pool_size=1&'
'connection_cache_size=1&'
'database_name=dbname')
self.assertEqual(dbkw, {'connection_pool_size': '1',
'connection_cache_size': '1',
'database_name': 'dbname'})
class TestEntryPoints(unittest.TestCase): class TestEntryPoints(unittest.TestCase):
def test_it(self): def test_it(self):
...@@ -397,6 +505,7 @@ class TestEntryPoints(unittest.TestCase): ...@@ -397,6 +505,7 @@ class TestEntryPoints(unittest.TestCase):
('zeo', resolvers.ClientStorageURIResolver), ('zeo', resolvers.ClientStorageURIResolver),
('file', resolvers.FileStorageURIResolver), ('file', resolvers.FileStorageURIResolver),
('zconfig', resolvers.ZConfigURIResolver), ('zconfig', resolvers.ZConfigURIResolver),
('postgres', resolvers.RelStorageURIResolver),
] ]
for name, cls in expected: for name, cls in expected:
target = load_entry_point('zodburi', 'zodburi.resolvers', name) target = load_entry_point('zodburi', 'zodburi.resolvers', name)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment