Skip to content

Commit

Permalink
Some API ref
Browse files Browse the repository at this point in the history
  • Loading branch information
uranusjr committed Sep 9, 2017
1 parent b72f408 commit 9725ba9
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 28 deletions.
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -31,7 +31,7 @@ docs:
lint:
pipenv run flake8 --isolated

open:
open: docs
open docs/build/html/index.html

test: lint
Expand Down
7 changes: 5 additions & 2 deletions docs/source/conf.py
Expand Up @@ -19,12 +19,14 @@
#
import os

# import sys
# sys.path.insert(0, os.path.abspath('.'))
import sys


PROJECT_ROOT = os.path.abspath(os.path.join(__file__, '..', '..', '..'))

# Make the module available for autodoc.
sys.path.insert(0, PROJECT_ROOT)


# -- General configuration ------------------------------------------------

Expand All @@ -37,6 +39,7 @@
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
11 changes: 6 additions & 5 deletions docs/source/index.rst
Expand Up @@ -59,9 +59,10 @@ Table of Contents
==================

.. toctree::
:maxdepth: 2
:maxdepth: 2

installation
quickstart
inspirations
indices-and-tables
installation
quickstart
references
inspirations
indices-and-tables
6 changes: 6 additions & 0 deletions docs/source/references.rst
@@ -0,0 +1,6 @@
API References
===============

.. toctree::
references/databases
references/records
9 changes: 9 additions & 0 deletions docs/source/references/databases.rst
@@ -0,0 +1,9 @@
Databases
=========

.. autosummary::
sqlian.standard.Database


.. autoclass:: sqlian.standard.Database
:members:
12 changes: 12 additions & 0 deletions docs/source/references/records.rst
@@ -0,0 +1,12 @@
Records
=======

.. autosummary::
sqlian.records.Record
sqlian.records.RecordCollection


.. automodule:: sqlian.records
:members:
:special-members:
:exclude-members: __weakref__
66 changes: 62 additions & 4 deletions sqlian/records.py
@@ -1,3 +1,12 @@
"""The record API and accompanying classes.
Things in this module are strongly inspired by the `Recrods library by Kenneth
Reitz <https://github.com/kennethreitz/records>`__, but with some subtle
differences on how things interact with the database.
.. currentmodule:: sqlian.records
"""

import collections
import json

Expand All @@ -9,6 +18,9 @@

class Record(object):
"""A single row of data from a database.
You typically don't need to create instances of this class, but interact
with instances returned by a :class:`sqlian.standard.Database` query.
"""
def __init__(self, keys, values):
self._keys = keys
Expand All @@ -21,11 +33,16 @@ def __repr__(self):
))))

def __len__(self):
"""How many columns there are in this row.
"""
return len(self._keys)

def __eq__(self, other):
# Two records are equal if their keys and values match.
# Ordering matters!
"""Test record equality.
Two records are equal if their keys and values both match.
Ordering matters.
"""
try:
other_keys = other.keys()
other_vals = other.values()
Expand All @@ -34,6 +51,14 @@ def __eq__(self, other):
return self.keys() == other_keys and self.values() == other_vals

def __getitem__(self, key):
"""Access content in the record.
Records support both numeric (sequence-like) and string (mapping-like)
indexing.
Slicing is not supported. Use the :meth:`values` method, which returns
a slicible object.
"""
# Numeric indexing.
if isinstance(key, six.integer_types):
if key < len(self._keys):
Expand All @@ -46,25 +71,49 @@ def __getitem__(self, key):
raise KeyError(key)

def __getattr__(self, key):
"""Access content in the record.
This works like the string indexing of :meth:`__getitem__` to provide
a simpler syntax under common usage. Always prefer :meth:`__getitem__`
(the square bracket syntax) if you want to access columns with a
variable.
"""
if key in self._key_indexes:
return self._values[self._key_indexes[key]]
raise AttributeError(
"'Record' object has no attribute {!r}".format(key),
)

def get(self, key, default=None):
"""Get an item in the record, return `default` on failure.
This works similarly with the standard mapping interface, but also
supports numeric indexing.
"""
try:
return self[key]
except KeyError:
except (KeyError, IndexError):
return default

def keys(self):
"""Returns a sequence containing keys (column names) in this row.
This works similarly with the standard mapping interface.
"""
return tuple(self._keys)

def values(self):
"""Returns a sequence containing values in this row.
This works similarly with the standard mapping interface.
"""
return tuple(self._values)

def items(self):
"""Returns an iterable of 2-tuples containing keys and values.
This works similarly with the standard mapping interface.
"""
return zip(self._keys, self._values)


Expand Down Expand Up @@ -93,6 +142,10 @@ def next(self):

class RecordCollection(object):
"""A sequence of records.
Record collections are backed by record *generators*. Results are fetched
on demand. This class conforms to the standard sequence interface, and can
be seamlessly treated as such.
"""
def __init__(self, record_generator):
self._row_gen = record_generator
Expand All @@ -101,7 +154,12 @@ def __init__(self, record_generator):

@classmethod
def from_cursor(cls, cursor):
"""A shorthand to create a `RecordCollection` from a DB-API 2.0 cursor.
"""Create a :class:`RecordCollection` from a DB-API 2.0 cursor.
This method automatically extract useful information for DB-API 2.0
to generate records. Discrepencies in various interfaces are generally
taken care of by this method, and you should use it instead of the
basic constructor when returning records for a database query.
"""
if isinstance(cursor.description, collections.Sequence):
keys = tuple(desc[0] for desc in cursor.description)
Expand Down
99 changes: 83 additions & 16 deletions sqlian/standard/databases.py
Expand Up @@ -6,17 +6,29 @@


class Database(object):
"""A database connection.
This class provides a wrapper to a `DB-API 2.0`_ Connection instance,
offering additional SQL-building methods alongside with the standard API.
Keyord arguments passed to the constructor are used to create the
underlying `Connection` instance. See documentation of the underlying
DB-API 2.0 module for appropriate parameters to pass.
Instances of this class implement the context manager interface. The
instance itself is assigned to the **as** expression, and the connection
is closed when the context manager exists, committing done automatically
if there are no exceptions.
.. _`DB-API 2.0`: https://www.python.org/dev/peps/pep-0249
"""
def __init__(self, **kwargs):
"""__init__(
self, database,
host=None, port=None, user=None, password=None,
params=None, options=None)
"""
dbapi = self.get_dbapi2()
self.populate_dbapi2_members(dbapi)
self._conn = self.create_connection(dbapi, **kwargs)
self._engine = self.engine_class()
# Typical signature:
# __init__(
# self, database, host=None, port=None, user=None, password=None,
# params=None, options=None)
self._conn = self.create_connection(**kwargs)
self.engine = self.engine_class()

def __repr__(self):
return '<Database open={}>'.format(self.is_open())
Expand All @@ -25,8 +37,18 @@ def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.commit()
self.close()

@property
def connection(self):
"""The underlying connection object. This property is read-only.
:returns: A DB-API 2.0 Connection object.
"""
return self._conn

def get_dbapi2(self):
return importlib.import_module(self.dbapi2_module_name)

Expand All @@ -39,43 +61,88 @@ def populate_dbapi2_members(self, dbapi):
if all_names is None or name in all_names:
setattr(self, name, member)

def create_connection(self, dbapi, **kwargs):
def create_connection(self, **kwargs):
"""Creates a connection.
If you're implementing a wrapper not conforming to DB-API 2.0, you
should implement this method to override the default behavior, which
depends on the API.
Keyword arguments to this method are passed directly from the class
constructor.
:returns: A DB-API 2.0 interface object.
"""
dbapi = self.get_dbapi2()
self.populate_dbapi2_members(dbapi)
return dbapi.connect(**kwargs)

def is_open(self):
"""Whether the connection is open.
:rtype: bool
"""
return self._conn is not None

# DB-API 2.0 interface.

def close(self):
"""Close the connection.
This method exists to conform to DB-API 2.0.
"""
self._conn.close()
self._conn = None

def commit(self):
"""Commit any pending transaction to the database.
This method exists to conform to DB-API 2.0.
"""
self._conn.commit()

def rollback(self):
"""Rollback pending transaction.
This method exists to conform to DB-API 2.0. Behavior of calling this
method on a database not supporting transactions is undefined.
"""
self._conn.rollback()

def cursor(self):
"""Return a new Cursor Object using the connection.
This method exists to conform to DB-API 2.0.
"""
return self._conn.cursor()

# Things!

def execute_statement(self, name, args, kwargs):
statement = getattr(self._engine, name)(*args, **kwargs)
def execute_statement(self, statement, args, kwargs):
"""Build a statement, and execute it on the connection.
:rtype: RecordCollection
"""
cursor = self._conn.cursor()
cursor.execute(statement)
return RecordCollection.from_cursor(cursor)

def select(self, *args, **kwargs):
return self.execute_statement('select', args, kwargs)
"""Build and execute a SELECT statement.
"""
return self.execute_statement(self.engine.select, args, kwargs)

def insert(self, *args, **kwargs):
return self.execute_statement('insert', args, kwargs)
"""Build and execute an INSERT statement.
"""
return self.execute_statement(self.engine.insert, args, kwargs)

def update(self, *args, **kwargs):
return self.execute_statement('update', args, kwargs)
"""Build and execute an UPDATE statement.
"""
return self.execute_statement(self.engine.update, args, kwargs)

def delete(self, *args, **kwargs):
return self.execute_statement('delete', args, kwargs)
"""Build and execute a DELETE statement.
"""
return self.execute_statement(self.engine.delete, args, kwargs)

0 comments on commit 9725ba9

Please sign in to comment.