This commit is contained in:
Friedrich Lindenberg 2016-04-10 10:25:14 +03:00
parent b64837e2e2
commit bdd937e2c9
3 changed files with 101 additions and 57 deletions

View File

@ -24,9 +24,12 @@ log = logging.getLogger(__name__)
class Database(object): class Database(object):
"""A database object represents a SQL database with multiple tables."""
def __init__(self, url, schema=None, reflect_metadata=True, def __init__(self, url, schema=None, reflect_metadata=True,
engine_kwargs=None, reflect_views=True, engine_kwargs=None, reflect_views=True,
ensure_schema=True, row_type=row_type): ensure_schema=True, row_type=row_type):
"""Configure and connect to the database."""
if engine_kwargs is None: if engine_kwargs is None:
engine_kwargs = {} engine_kwargs = {}
@ -60,14 +63,14 @@ class Database(object):
@property @property
def executable(self): def executable(self):
""" The current connection or engine against which statements """Connection or engine against which statements will be executed."""
will be executed. """
if hasattr(self.local, 'connection'): if hasattr(self.local, 'connection'):
return self.local.connection return self.local.connection
return self.engine return self.engine
@property @property
def op(self): def op(self):
"""Get an alembic operations context."""
ctx = MigrationContext.configure(self.engine) ctx = MigrationContext.configure(self.engine)
return Operations(ctx) return Operations(ctx)
@ -95,11 +98,13 @@ class Database(object):
del self.local.connection del self.local.connection
def begin(self): def begin(self):
""" Enter a transaction explicitly. No data will be written """
until the transaction has been committed. Enter a transaction explicitly.
No data will be written until the transaction has been committed.
**NOTICE:** Schema modification operations, such as the creation **NOTICE:** Schema modification operations, such as the creation
of tables or columns will not be part of the transactional context.""" of tables or columns will not be part of the transactional context.
"""
if not hasattr(self.local, 'connection'): if not hasattr(self.local, 'connection'):
self.local.connection = self.engine.connect() self.local.connection = self.engine.connect()
if not hasattr(self.local, 'tx'): if not hasattr(self.local, 'tx'):
@ -109,24 +114,32 @@ class Database(object):
self.local.lock_count.append(0) self.local.lock_count.append(0)
def commit(self): def commit(self):
""" Commit the current transaction, making all statements executed """
since the transaction was begun permanent. """ Commit the current transaction.
Make all statements executed since the transaction was begun permanent.
"""
if hasattr(self.local, 'tx') and self.local.tx: if hasattr(self.local, 'tx') and self.local.tx:
self.local.tx[-1].commit() self.local.tx[-1].commit()
self._dispose_transaction() self._dispose_transaction()
def rollback(self): def rollback(self):
""" Roll back the current transaction, discarding all statements """
executed since the transaction was begun. """ Roll back the current transaction.
Discard all statements executed since the transaction was begun.
"""
if hasattr(self.local, 'tx') and self.local.tx: if hasattr(self.local, 'tx') and self.local.tx:
self.local.tx[-1].rollback() self.local.tx[-1].rollback()
self._dispose_transaction() self._dispose_transaction()
def __enter__(self): def __enter__(self):
"""Start a transaction."""
self.begin() self.begin()
return self return self
def __exit__(self, error_type, error_value, traceback): def __exit__(self, error_type, error_value, traceback):
"""End a transaction by committing or rolling back."""
if error_type is None: if error_type is None:
try: try:
self.commit() self.commit()
@ -138,26 +151,27 @@ class Database(object):
@property @property
def tables(self): def tables(self):
""" """Get a listing of all tables that exist in the database."""
Get a listing of all tables that exist in the database.
"""
return list(self._tables.keys()) return list(self._tables.keys())
def __contains__(self, member): def __contains__(self, member):
"""Check if the given table name exists in the database."""
return member in self.tables return member in self.tables
def _valid_table_name(self, table_name): def _valid_table_name(self, table_name):
""" Check if the table name is obviously invalid. """ """Check if the table name is obviously invalid."""
if table_name is None or not len(table_name.strip()): if table_name is None or not len(table_name.strip()):
raise ValueError("Invalid table name: %r" % table_name) raise ValueError("Invalid table name: %r" % table_name)
return table_name.strip() return table_name.strip()
def create_table(self, table_name, primary_id='id', primary_type='Integer'): def create_table(self, table_name, primary_id='id', primary_type='Integer'):
""" """
Creates a new table. The new table will automatically have an `id` column Create a new table.
unless specified via optional parameter primary_id, which will be used
as the primary key of the table. Automatic id is set to be an The new table will automatically have an `id` column unless specified via
auto-incrementing integer, while the type of custom primary_id can be a optional parameter primary_id, which will be used as the primary key of the
table. Automatic id is set to be an auto-incrementing integer, while the
type of custom primary_id can be a
String or an Integer as specified with primary_type flag. The default String or an Integer as specified with primary_type flag. The default
length of String is 255. The caller can specify the length. length of String is 255. The caller can specify the length.
The caller will be responsible for the uniqueness of manual primary_id. The caller will be responsible for the uniqueness of manual primary_id.
@ -206,10 +220,11 @@ class Database(object):
def load_table(self, table_name): def load_table(self, table_name):
""" """
Loads a table. This will fail if the tables does not already Load a table.
exist in the database. If the table exists, its columns will be
reflected and are available on the :py:class:`Table <dataset.Table>` This will fail if the tables does not already exist in the database. If the
object. table exists, its columns will be reflected and are available on the
:py:class:`Table <dataset.Table>` object.
Returns a :py:class:`Table <dataset.Table>` instance. Returns a :py:class:`Table <dataset.Table>` instance.
:: ::
@ -228,6 +243,7 @@ class Database(object):
self._release() self._release()
def update_table(self, table_name): def update_table(self, table_name):
"""Reload a table schema from the database."""
table_name = self._valid_table_name(table_name) table_name = self._valid_table_name(table_name)
self.metadata = MetaData(schema=self.schema) self.metadata = MetaData(schema=self.schema)
self.metadata.bind = self.engine self.metadata.bind = self.engine
@ -238,8 +254,9 @@ class Database(object):
def get_table(self, table_name, primary_id='id', primary_type='Integer'): def get_table(self, table_name, primary_id='id', primary_type='Integer'):
""" """
Smart wrapper around *load_table* and *create_table*. Either loads a table Smart wrapper around *load_table* and *create_table*.
or creates it if it doesn't exist yet.
Either loads a table or creates it if it doesn't exist yet.
For short-hand to create a table with custom id and type using [], where For short-hand to create a table with custom id and type using [], where
table_name, primary_id, and primary_type are specified as a tuple table_name, primary_id, and primary_type are specified as a tuple
@ -263,12 +280,14 @@ class Database(object):
self._release() self._release()
def __getitem__(self, table_name): def __getitem__(self, table_name):
"""Get a given table."""
return self.get_table(table_name) return self.get_table(table_name)
def query(self, query, **kw): def query(self, query, **kw):
""" """
Run a statement on the database directly, allowing for the Run a statement on the database directly.
execution of arbitrary read/write queries. A query can either be
Allows for the execution of arbitrary read/write queries. A query can either be
a plain text string, or a `SQLAlchemy expression <http://docs.sqlalchemy.org/en/latest/core/tutorial.html#selecting>`_. a plain text string, or a `SQLAlchemy expression <http://docs.sqlalchemy.org/en/latest/core/tutorial.html#selecting>`_.
If a plain string is passed in, it will be converted to an expression automatically. If a plain string is passed in, it will be converted to an expression automatically.
@ -288,4 +307,5 @@ class Database(object):
row_type=self.row_type) row_type=self.row_type)
def __repr__(self): def __repr__(self):
"""Text representation contains the URL."""
return '<Database(%s)>' % safe_url(self.url) return '<Database(%s)>' % safe_url(self.url)

View File

@ -14,8 +14,10 @@ log = logging.getLogger(__name__)
class Table(object): class Table(object):
"""Represents a table in a database and exposes common operations."""
def __init__(self, database, table): def __init__(self, database, table):
"""Initialise the table from database schema."""
self.indexes = dict((i.name, i) for i in table.indexes) self.indexes = dict((i.name, i) for i in table.indexes)
self.database = database self.database = database
self.table = table self.table = table
@ -23,9 +25,7 @@ class Table(object):
@property @property
def columns(self): def columns(self):
""" """Get a listing of all columns that exist in the table."""
Get a listing of all columns that exist in the table.
"""
return list(self.table.columns.keys()) return list(self.table.columns.keys())
@property @property
@ -34,9 +34,9 @@ class Table(object):
def drop(self): def drop(self):
""" """
Drop the table from the database, deleting both the schema Drop the table from the database.
and all the contents within it.
Delete both the schema and all the contents within it.
Note: the object will raise an Exception if you use it after Note: the object will raise an Exception if you use it after
dropping the table. If you want to re-create the table, make dropping the table. If you want to re-create the table, make
sure to get a fresh instance from the :py:class:`Database <dataset.Database>`. sure to get a fresh instance from the :py:class:`Database <dataset.Database>`.
@ -55,6 +55,7 @@ class Table(object):
def insert(self, row, ensure=None, types={}): def insert(self, row, ensure=None, types={}):
""" """
Add a row (type: dict) by inserting it into the table. Add a row (type: dict) by inserting it into the table.
If ``ensure`` is set, any of the keys of the row are not If ``ensure`` is set, any of the keys of the row are not
table columns, they will be created automatically. table columns, they will be created automatically.
@ -80,9 +81,11 @@ class Table(object):
def insert_many(self, rows, chunk_size=1000, ensure=None, types={}): def insert_many(self, rows, chunk_size=1000, ensure=None, types={}):
""" """
Add many rows at a time, which is significantly faster than adding Add many rows at a time.
them one by one. Per default the rows are processed in chunks of
1000 per commit, unless you specify a different ``chunk_size``. This is significantly faster than adding them one by one. Per default
the rows are processed in chunks of 1000 per commit, unless you specify
a different ``chunk_size``.
See :py:meth:`insert() <dataset.Table.insert>` for details on See :py:meth:`insert() <dataset.Table.insert>` for details on
the other parameters. the other parameters.
@ -112,9 +115,10 @@ class Table(object):
def update(self, row, keys, ensure=None, types={}): def update(self, row, keys, ensure=None, types={}):
""" """
Update a row in the table. The update is managed via Update a row in the table.
the set of column names stated in ``keys``: they will be
used as filters for the data to be updated, using the values The update is managed via the set of column names stated in ``keys``:
they will be used as filters for the data to be updated, using the values
in ``row``. in ``row``.
:: ::
@ -154,8 +158,10 @@ class Table(object):
def upsert(self, row, keys, ensure=None, types={}): def upsert(self, row, keys, ensure=None, types={}):
""" """
An UPSERT is a smart combination of insert and update. If rows with matching ``keys`` exist An UPSERT is a smart combination of insert and update.
they will be updated, otherwise a new row is inserted in the table.
If rows with matching ``keys`` exist they will be updated, otherwise a
new row is inserted in the table.
:: ::
data = dict(id=10, title='I am a banana!') data = dict(id=10, title='I am a banana!')
@ -190,9 +196,12 @@ class Table(object):
return self.insert(row, ensure=ensure, types=types) return self.insert(row, ensure=ensure, types=types)
def delete(self, *_clauses, **_filter): def delete(self, *_clauses, **_filter):
""" Delete rows from the table. Keyword arguments can be used """
to add column-based filters. The filter criterion will always
be equality: Delete rows from the table.
Keyword arguments can be used to add column-based filters. The filter
criterion will always be equality:
.. code-block:: python .. code-block:: python
@ -242,6 +251,7 @@ class Table(object):
def create_column(self, name, type): def create_column(self, name, type):
""" """
Explicitely create a new column ``name`` of a specified type. Explicitely create a new column ``name`` of a specified type.
``type`` must be a `SQLAlchemy column type <http://docs.sqlalchemy.org/en/rel_0_8/core/types.html>`_. ``type`` must be a `SQLAlchemy column type <http://docs.sqlalchemy.org/en/rel_0_8/core/types.html>`_.
:: ::
@ -263,7 +273,8 @@ class Table(object):
def drop_column(self, name): def drop_column(self, name):
""" """
Drop the column ``name`` Drop the column ``name``.
:: ::
table.drop_column('created_at') table.drop_column('created_at')
@ -284,7 +295,9 @@ class Table(object):
def create_index(self, columns, name=None): def create_index(self, columns, name=None):
""" """
Create an index to speed up queries on a table. If no ``name`` is given a random name is created. Create an index to speed up queries on a table.
If no ``name`` is given a random name is created.
:: ::
table.create_index(['name', 'country']) table.create_index(['name', 'country'])
@ -319,7 +332,10 @@ class Table(object):
def find_one(self, *args, **kwargs): def find_one(self, *args, **kwargs):
""" """
Get a single result from the table.
Works just like :py:meth:`find() <dataset.Table.find>` but returns one result, or None. Works just like :py:meth:`find() <dataset.Table.find>` but returns one result, or None.
:: ::
row = table.find_one(country='United States') row = table.find_one(country='United States')
@ -339,7 +355,9 @@ class Table(object):
def find(self, *_clauses, **kwargs): def find(self, *_clauses, **kwargs):
""" """
Performs a simple search on the table. Simply pass keyword arguments as ``filter``. Perform a simple search on the table.
Simply pass keyword arguments as ``filter``.
:: ::
results = table.find(country='France') results = table.find(country='France')
@ -359,7 +377,8 @@ class Table(object):
results = table.find(order_by=['country', '-year']) results = table.find(order_by=['country', '-year'])
For more complex queries, please use :py:meth:`db.query() <dataset.Database.query>` For more complex queries, please use :py:meth:`db.query() <dataset.Database.query>`
instead.""" instead.
"""
_limit = kwargs.pop('_limit', None) _limit = kwargs.pop('_limit', None)
_offset = kwargs.pop('_offset', 0) _offset = kwargs.pop('_offset', 0)
_step = kwargs.pop('_step', 5000) _step = kwargs.pop('_step', 5000)
@ -398,20 +417,17 @@ class Table(object):
row_type=self.database.row_type, step=_step) row_type=self.database.row_type, step=_step)
def count(self, *args, **kwargs): def count(self, *args, **kwargs):
""" """Return the count of results for the given filter set."""
Return the count of results for the given filter set (same filter options as with ``find()``).
"""
return self.find(*args, return_count=True, **kwargs) return self.find(*args, return_count=True, **kwargs)
def __len__(self): def __len__(self):
""" """Return the number of rows in the table."""
Returns the number of rows in the table.
"""
return self.count() return self.count()
def distinct(self, *args, **_filter): def distinct(self, *args, **_filter):
""" """
Returns all rows of a table, but removes rows in with duplicate values in ``columns``. Return all rows of a table, but remove rows in with duplicate values in ``columns``.
Interally this creates a `DISTINCT statement <http://www.w3schools.com/sql/sql_distinct.asp>`_. Interally this creates a `DISTINCT statement <http://www.w3schools.com/sql/sql_distinct.asp>`_.
:: ::
@ -442,7 +458,10 @@ class Table(object):
return self.database.query(q) return self.database.query(q)
def __getitem__(self, item): def __getitem__(self, item):
""" This is an alias for distinct which allows the table to be queried as using """
Get distinct column values.
This is an alias for distinct which allows the table to be queried as using
square bracket syntax. square bracket syntax.
:: ::
# Same as distinct: # Same as distinct:
@ -454,15 +473,19 @@ class Table(object):
def all(self): def all(self):
""" """
Returns all rows of the table as simple dictionaries. This is simply a shortcut Return all rows of the table as simple dictionaries.
to *find()* called with no arguments.
This is simply a shortcut to *find()* called with no arguments.
:: ::
rows = table.all()""" rows = table.all()
"""
return self.find() return self.find()
def __iter__(self): def __iter__(self):
""" """
Return all rows of the table as simple dictionaries.
Allows for iterating over all rows in the table without explicetly Allows for iterating over all rows in the table without explicetly
calling :py:meth:`all() <dataset.Table.all>`. calling :py:meth:`all() <dataset.Table.all>`.
:: ::
@ -473,4 +496,5 @@ class Table(object):
return self.all() return self.all()
def __repr__(self): def __repr__(self):
"""Get table representation."""
return '<Table(%s)>' % self.table.name return '<Table(%s)>' % self.table.name

View File

@ -83,7 +83,7 @@ class ResultIter(object):
def safe_url(url): def safe_url(url):
""" Remove password from printed connection URLs. """ """Remove password from printed connection URLs."""
parsed = urlparse(url) parsed = urlparse(url)
if parsed.password is not None: if parsed.password is not None:
pwd = ':%s@' % parsed.password pwd = ':%s@' % parsed.password