save point (see note)

Added initial database schema and ability to install it, added init-db command
to pyramid scaffold.
This commit is contained in:
Lance Edgar 2012-03-24 12:15:11 -05:00
parent 925cd30b96
commit 8ceb98baf4
29 changed files with 1116 additions and 191 deletions

View file

@ -29,7 +29,7 @@
from edbob._version import __version__
from edbob.core import *
from edbob.times import *
from edbob.time import *
from edbob.files import *
from edbob.modules import *
from edbob.configuration import *

View file

@ -157,7 +157,7 @@ Try '%(name)s help <command>' for more help.""" % self
# Initialize everything...
if not args.no_init:
edbob.init(*(args.config_paths or []))
edbob.init(self.name, *(args.config_paths or []))
# Command line logging flags should override config.
if args.verbose:

View file

@ -32,52 +32,77 @@ from sqlalchemy.orm import sessionmaker
import edbob
__all__ = ['engines', 'engine', 'Session', 'metadata',
'get_setting', 'save_setting']
inited = False
engines = None
engine = None
Session = sessionmaker()
metadata = None
def init():
def init(config):
"""
Called whenever ``'edbob.db'`` is configured to be auto-initialized.
Initializes the database connection(s); called by :func:`edbob.init()` if
config includes something like::
This function is responsible for establishing the primary database engine
(a ``sqlalchemy.Engine`` instance, read from config), and extending the
root ``edbob`` namespace with the ORM classes (``Person``, ``User``, etc.),
as well as a few other things, e.g. ``engine``, ``Session`` and
``metadata``.
.. highlight:: ini
In addition to this, if a connection to the primary database can be
obtained, it will be consulted to see which extensions are active within
it. If any are found, edbob's ORM will be extended in-place accordingly.
[edbob]
init = ['edbob.db']
[edbob.db]
sqlalchemy.urls = {
'default': 'postgresql://user:pass@localhost/edbob,
}
This function reads connection info from ``config`` and builds a dictionary
or :class:`sqlalchemy.Engine` instances accordingly. It also extends the
root ``edbob`` namespace with the ORM classes (:class:`edbob.Person`,
:class:`edbob.User`, etc.), as well as a few other things
(e.g. :attr:`edbob.engine`, :attr:`edbob.Session`, :attr:`edbob.metadata`).
"""
config = edbob.config.get_dict('edbob.db')
engine = engine_from_config(config)
edbob.graft(edbob, locals(), 'engine')
Session.configure(bind=engine)
edbob.graft(edbob, globals(), 'Session')
import edbob.db
from edbob.db import classes
from edbob.db import enum
from edbob.db.model import get_metadata
metadata = get_metadata(bind=engine)
edbob.graft(edbob, locals(), 'metadata')
from edbob.db.mappers import make_mappers
make_mappers(metadata)
from edbob.db.extensions import extend_framework
from edbob.db.ext import extend_framework
global inited, engines, engine, metadata
keys = config.get('edbob.db', 'sqlalchemy.keys')
if keys:
keys = keys.split()
else:
keys = ['default']
engines = {}
cfg = config.get_dict('edbob.db')
for key in keys:
try:
engines[key] = engine_from_config(cfg, 'sqlalchemy.%s.' % key)
except KeyError:
if key == 'default':
try:
engines[key] = engine_from_config(cfg)
except KeyError:
pass
engine = engines.get('default')
if engine:
Session.configure(bind=engine)
metadata = get_metadata(bind=engine)
make_mappers(metadata)
extend_framework()
# Note that we extend the framework before we graft the 'classes' module
# contents, since extensions may graft things to that module.
import edbob.db.classes as classes
edbob.graft(edbob, edbob.db)
edbob.graft(edbob, classes)
# Same goes for the enum module.
import edbob.db.enum as enum
edbob.graft(edbob, enum)
# Add settings functions.
edbob.graft(edbob, globals(), ('get_setting', 'save_setting'))
inited = True
def get_setting(name, session=None):

View file

@ -26,15 +26,48 @@
``edbob.db.auth`` -- Authentication & Authorization
"""
import bcrypt
from sqlalchemy.orm import object_session
from edbob.db.classes import Role, User
import edbob
from edbob.db import needs_session
from edbob.db.classes import Permission, Role, User
def get_administrator(session):
class BcryptAuthenticator(edbob.Object):
"""
Returns a :class:`edbob.Role` instance representing the "Administrator"
role, attached to the given ``session``.
Authentication with py-bcrypt (Blowfish).
"""
def populate_user(self, user, password):
user.salt = bcrypt.gensalt()
user.password = bcrypt.hashpw(password, user.salt)
def authenticate_user(self, user, password):
return bcrypt.hashpw(password, user.salt) == user.password
@needs_session
def authenticate_user(session, username, password):
"""
Attempts to authenticate with ``username`` and ``password``. If successful,
returns the :class:`edbob.User` instance; otherwise returns ``None``.
"""
user = session.query(User).filter_by(username=username).first()
if not user:
return None
auth = BcryptAuthenticator()
if not auth.authenticate_user(user, password):
return None
return user
def administrator_role(session):
"""
Returns the "Administrator" :class:`edbob.Role` instance, attached to the
given ``session``.
"""
uuid = 'd937fa8a965611dfa0dd001143047286'
@ -59,7 +92,7 @@ def has_permission(obj, perm):
elif isinstance(obj, Role):
roles = [obj]
else:
raise TypeError, "You must pass either a User or Role for 'obj'; got: %s" % repr(obj)
raise TypeError("You must pass either a User or Role for 'obj'; got: %s" % repr(obj))
session = object_session(obj)
assert session
admin = get_administrator(session)

36
edbob/db/enum.py Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``edbob.db.enum`` -- Enumerations
"""
USER_ACTIVE = 1
USER_INACTIVE = 2
USER_STATUS = {
USER_ACTIVE : "active",
USER_INACTIVE : "inactive",
}

65
edbob/db/exceptions.py Normal file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``edbob.db.exceptions`` -- Database Exceptions
"""
class CoreSchemaAlreadyInstalled(Exception):
"""
Raised when a request is made to install the core schema to a database, but
it is already installed there.
"""
def __init__(self, installed_version):
self.installed_version = installed_version
def __str__(self):
return "Core schema already installed (version %s)" % self.installed_version
class CoreSchemaNotInstalled(Exception):
"""
Raised when a request is made which requires the core schema to be present
in a database, yet such is not the case.
"""
def __init__(self, engine):
self.engine = engine
def __str__(self):
return "Core schema not installed: %s" % str(self.engine)
class ExtensionNotFound(Exception):
"""
Raised when an extension is requested which cannot be located.
"""
def __init__(self, name):
self.name = name
def __str__(self):
return "Extension not found: %s" % self.name

426
edbob/db/extensions.py Normal file
View file

@ -0,0 +1,426 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``edbob.db.extensions`` -- Database Extensions
"""
import logging
# from pkg_resources import iter_entry_points
import sqlalchemy.exc
# from sqlalchemy.orm import clear_mappers
# import migrate.versioning.api
# from migrate.versioning.schema import ControlledSchema
# import rattail
# from rattail.db import exc as exceptions
# from rattail.db import Session
# from rattail.db.classes import ActiveExtension
# from rattail.db.mappers import make_mappers
# from rattail.db.model import get_metadata
# from rattail.db.util import get_repository_path, get_repository_version
import edbob
import edbob.db
from edbob.db import exceptions
from edbob.db import Session
from edbob.db.classes import ActiveExtension
from edbob.db.util import get_database_version
from edbob.util import requires_impl
log = logging.getLogger(__name__)
available_extensions = edbob.entry_point_map('edbob.db.extensions')
active_extensions = {}
class Extension(edbob.Object):
"""
Base class for schema/ORM extensions.
"""
# Set this to a list of strings (extension names) as needed within your
# derived class.
required_extensions = []
@property
@requires_impl(is_property=True)
def name(self):
"""
The name of the extension.
"""
pass
@property
@requires_impl(is_property=True)
def schema(self):
"""
Should return a reference to the extension's ``schema`` module, which
is assumed to be a SQLAlchemy-Migrate repository.
"""
pass
def add_class(self, cls):
"""
Convenience method for use in :meth:`extend_classes()`.
"""
from edbob.db import classes
name = cls.__name__
edbob.graft(classes, {name:cls}, name)
def extend_classes(self):
"""
Any extra classes provided by the extension should be added to the ORM
whenever this method is called.
Note that the :meth:`add_class()` convenience method is designed to be
used when adding classes.
"""
pass
def extend_mappers(self, metadata):
"""
All SQLAlchemy mapping to be done by the extension should be done
within this method.
Any extra classes the extension provides will typically be mapped here.
Any manipulation the extension needs to perform on the ``edbob`` core
ORM should be done here as well.
"""
pass
def get_metadata(self):
"""
Should return a :class:`sqlalchemy.MetaData` instance containing the
schema definition for the extension, or ``None``.
"""
return None
def remove_class(self, name):
"""
Convenience method for use in :meth:`restore_classes()`.
"""
from edbob.db import classes
if name in classes.__all__:
classes.__all__.remove(name)
if hasattr(classes, name):
del classes.__dict__[name]
def restore_classes(self):
"""
This method should remove any extra classes which were added within
:meth:`extend_classes()`. Note that there is a :meth:`remove_class()`
method for convenience in doing so.
"""
pass
# def activate_extension(extension, engine=None):
# """
# Activates the :class:`RattailExtension` instance represented by
# ``extension`` (which can be the actual instance, or the extension's name)
# by installing its schema and registering it within the database, and
# immediately applies it to the current ORM API.
# If ``engine`` is not provided, then ``rattail.engine`` is assumed.
# """
# if engine is None:
# engine = rattail.engine
# if not isinstance(extension, RattailExtension):
# extension = get_extension(extension)
# log.info("Activating extension: %s" % extension.name)
# install_extension_schema(extension, engine)
# session = Session(bind=engine)
# if not session.query(ActiveExtension).get(extension.name):
# session.add(ActiveExtension(name=extension.name))
# session.commit()
# session.close()
# merge_extension_metadata(extension)
# extension.extend_classes()
# extension.extend_mappers(rattail.metadata)
# _active_extensions[extension.name] = extension
# def deactivate_extension(extension, engine=None):
# """
# Uninstalls an extension's schema from the primary database, and immediately
# requests it to restore the ORM API.
# If ``engine`` is not provided, then ``rattail.engine`` is assumed.
# """
# if engine is None:
# engine = rattail.engine
# if not isinstance(extension, RattailExtension):
# extension = get_extension(extension)
# log.info("Deactivating extension: %s" % extension.name)
# if extension.name in _active_extensions:
# del _active_extensions[extension.name]
# session = Session()
# ext = session.query(ActiveExtension).get(extension.name)
# if ext:
# session.delete(ext)
# session.commit()
# session.close()
# uninstall_extension_schema(extension, engine)
# unmerge_extension_metadata(extension)
# extension.restore_classes()
# clear_mappers()
# make_mappers(rattail.metadata)
# for name in sorted(_active_extensions, extension_sorter(_active_extensions)):
# _active_extensions[name].extend_mappers(rattail.metadata)
def extend_framework():
"""
Attempts to connect to the primary database and, if successful, inspects it
to determine which extensions are active within it. Any such extensions
found will be used to extend the ORM/API in-place.
"""
engine = edbob.db.engine
# Check primary database connection.
try:
engine.connect()
except sqlalchemy.exc.OperationalError:
return
# Check database version to see if core schema is installed.
try:
db_version = get_database_version(engine)
except exceptions.CoreSchemaNotInstalled:
return
# Since extensions may depend on one another, we must first retrieve the
# list of active extensions' names from the database and *then* sort them
# according to their stated dependencies. (This information is only known
# after instantiating the extensions.)
session = Session()
try:
active_extensions = session.query(ActiveExtension).all()
except sqlalchemy.exc.ProgrammingError:
session.close()
return
extensions = {}
for ext in active_extensions:
extensions[ext.name] = get_extension(ext.name)
session.close()
for name in sorted(extensions, extension_sorter(extensions)):
ext = extensions[name]
log.info("Applying active extension: %s" % name)
merge_extension_metadata(ext)
ext.extend_classes()
ext.extend_mappers(rattail.metadata)
active_extensions[name] = ext
# def extension_active(extension):
# """
# Returns boolean indicating whether or not the given ``extension`` is active
# within the current database.
# """
# if not isinstance(extension, RattailExtension):
# extension = get_extension(extension)
# return extension.name in _active_extensions
def extension_sorter(extensions):
"""
Returns a function to be used for sorting extensions according to their
inter-dependencies. ``extensions`` should be a dictionary containing the
extensions which are to be sorted.
"""
def sorter(name_x, name_y):
ext_x = extensions[name_x]
ext_y = extensions[name_y]
if name_y in ext_x.required_extensions:
return 1
if name_x in ext_y.required_extensions:
return -1
if ext_x.required_extensions and not ext_y.required_extensions:
return 1
if ext_y.required_extensions and not ext_x.required_extensions:
return -1
return 0
return sorter
def get_extension(name):
"""
Returns a :class:`Extension` instance, according to ``name``. An error is
raised if the extension cannot be found.
"""
if name in available_extensions:
return available_extensions[name]()
raise exceptions.ExtensionNotFound(name)
# def install_extension_schema(extension, engine=None):
# """
# Installs an extension's schema to the database and adds version control for
# it.
# """
# if engine is None:
# engine = rattail.engine
# # Extensionls aren't required to provide metadata...
# ext_meta = extension.get_metadata()
# if not ext_meta:
# return
# # ...but if they do they must also provide a SQLAlchemy-Migrate repository!
# assert extension.schema, "Extension does not implement 'schema': %s" % extension.name
# meta = rattail.metadata
# for table in meta.sorted_tables:
# table.tometadata(ext_meta)
# for table in ext_meta.sorted_tables:
# if table.name not in meta.tables:
# table.create(bind=engine, checkfirst=True)
# migrate.versioning.api.version_control(
# str(engine.url), get_repository_path(extension), get_repository_version(extension))
def merge_extension_metadata(ext):
"""
Merges an extension's metadata with the global ``edbob.db.metadata``
instance.
.. note::
``edbob`` uses this internally; you should not need to.
"""
ext_meta = ext.get_metadata()
if not ext_meta:
return
meta = edbob.db.metadata
for table in meta.sorted_tables:
table.tometadata(ext_meta)
for table in ext_meta.sorted_tables:
if table.name not in meta.tables:
table.tometadata(meta)
# def uninstall_extension_schema(extension, engine=None):
# """
# Uninstalls an extension's tables from the database represented by
# ``engine`` (or ``rattail.engine`` if none is provided), and removes
# SQLAlchemy-Migrate version control for the extension.
# """
# if engine is None:
# engine = rattail.engine
# ext_meta = extension.get_metadata()
# if not ext_meta:
# return
# schema = ControlledSchema(engine, get_repository_path(extension))
# engine.execute(schema.table.delete().where(
# schema.table.c.repository_id == schema.repository.id))
# meta = get_metadata()
# for table in meta.sorted_tables:
# table.tometadata(ext_meta)
# for table in reversed(ext_meta.sorted_tables):
# if table.name not in meta.tables:
# table.drop(bind=engine)
# def unmerge_extension_metadata(extension):
# """
# Removes an extension's metadata from the global ``rattail.metadata``
# instance.
# """
# ext_meta = extension.get_metadata()
# if not ext_meta:
# return
# meta = rattail.metadata
# ext_tables = ext_meta.tables.keys()
# for table in reversed(meta.sorted_tables):
# if table.name in ext_tables:
# meta.remove(table)
# # def merge_extension_permissions(extension):
# # '''
# # Helper function to merge an extension's permission definitions with those of
# # the framework. (This should only be called by the framework itself.)
# # '''
# # from rattail.v1.perms import permissions
# # log.debug('Merging permissions from extension: %s' % extension.name)
# # for group_name in extension.permissions:
# # if group_name not in permissions:
# # permissions[group_name] = extension.permissions[group_name]
# # elif extension.permissions[group_name][0] != permissions[group_name][0]:
# # log.warning("Extension '%s' tries to override UUID of permission group '%s' (but is denied)" % (
# # extension.name, group_name))
# # else:
# # # Extensions may override permission group display names.
# # if extension.permissions[group_name][1]:
# # permissions[group_name][1] = extension.permissions[group_name][1]
# # perms = permissions[group_name][2]
# # ext_perms = extension.permissions[group_name][2]
# # for perm_name in ext_perms:
# # if perm_name not in perms:
# # perms[perm_name] = ext_perms[perm_name]
# # elif ext_perms[perm_name][0] != perms[perm_name][0]:
# # log.warning("Extension '%s' tries to override UUID of permission '%s' (but is denied)" % (
# # extension.name, '.'.join((group_name, perm_name))))
# # else:
# # # Extensions may override permission display names.
# # if ext_perms[perm_name][1]:
# # perms[perm_name][1] = ext_perms[perm_name][1]

View file

@ -28,7 +28,7 @@
from sqlalchemy.orm import mapper, relationship
import edbob.db.classes as c
from edbob.db import classes as c
def make_mappers(metadata):
@ -60,17 +60,6 @@ def make_mappers(metadata):
c.Person, t['people'],
properties=dict(
customers=relationship(
c.Customer,
backref='person',
),
employee=relationship(
c.Employee,
back_populates='person',
uselist=False,
),
user=relationship(
c.User,
back_populates='person',

View file

@ -28,7 +28,7 @@
from sqlalchemy import *
from edbob import get_uuid
from edbob.sqlalchemy import table_with_uuid
def get_metadata(*args, **kwargs):
@ -66,9 +66,8 @@ def get_metadata(*args, **kwargs):
return None
return '%(first_name)s %(last_name)s' % locals()
people = Table(
people = table_with_uuid(
'people', metadata,
Column('uuid', String(32), primary_key=True, default=get_uuid),
Column('first_name', String(50)),
Column('last_name', String(50)),
Column('display_name', String(100), default=get_person_display_name),
@ -80,9 +79,8 @@ def get_metadata(*args, **kwargs):
Column('permission', String(50), primary_key=True),
)
roles = Table(
roles = table_with_uuid(
'roles', metadata,
Column('uuid', String(32), primary_key=True, default=get_uuid),
Column('name', String(25), nullable=False, unique=True),
)
@ -92,16 +90,14 @@ def get_metadata(*args, **kwargs):
Column('value', Text),
)
users = Table(
users = table_with_uuid(
'users', metadata,
Column('uuid', String(32), primary_key=True, default=get_uuid),
Column('username', String(25), nullable=False, unique=True),
Column('person_uuid', String(32), ForeignKey('people.uuid')),
)
users_roles = Table(
users_roles = table_with_uuid(
'users_roles', metadata,
Column('uuid', String(32), primary_key=True, default=get_uuid),
Column('user_uuid', String(32), ForeignKey('users.uuid')),
Column('role_uuid', String(32), ForeignKey('roles.uuid')),
)

View file

@ -1,84 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``edbob.db.perms`` -- Roles & Permissions
"""
from sqlalchemy.orm import object_session
from edbob.db.classes import Role, User, Permission
def get_administrator(session):
"""
Returns the "Administrator" :class:`rattail.db.classes.Role` instance,
attached to the given ``session``.
"""
uuid = 'd937fa8a965611dfa0dd001143047286'
admin = session.query(Role).get(uuid)
if admin:
return admin
admin = Role(uuid=uuid, name='Administrator')
session.add(admin)
return admin
# def has_permission(object_, permission, session=None):
# '''
# Checks the given ``object_`` (which may be either a :class:`rattail.v1.User` or
# a :class:`rattail.v1.Role`) and returns a boolean indicating whether or not the
# object is allowed the given permission. ``permission`` may be either a
# :class:`rattail.v1.Permission` instance, or the fully-qualified name of one.
# If ``object_`` is ``None``, the permission check is made against the special
# "(Anybody)" role.
# '''
def has_permission(obj, perm):
"""
Checks the given ``obj`` (which may be either a
:class:`rattail.db.classes.User`` or :class:`rattail.db.classes.Role`
instance), and returns a boolean indicating whether or not the object is
allowed the given permission. ``perm`` should be a fully-qualified
permission name, e.g. ``'employees.admin'``.
"""
if isinstance(obj, User):
roles = obj.roles
elif isinstance(obj, Role):
roles = [obj]
else:
raise TypeError, "You must pass either a User or Role for 'obj'; got: %s" % repr(obj)
session = object_session(obj)
assert session
admin = get_administrator(session)
for role in roles:
if role is admin:
return True
for permission in role.permissions:
if permission == perm:
return True
return False

4
edbob/db/schema/README Normal file
View file

@ -0,0 +1,4 @@
This is a database migration repository.
More information at
http://code.google.com/p/sqlalchemy-migrate/

View file

View file

@ -0,0 +1,5 @@
#!/usr/bin/env python
from migrate.versioning.shell import main
if __name__ == '__main__':
main(debug='False')

View file

@ -0,0 +1,25 @@
[db_settings]
# Used to identify which repository this database is versioned under.
# You can use the name of your project.
repository_id=edbob
# The name of the database table used to track the schema version.
# This name shouldn't already be used by your project.
# If this is changed once a database is under version control, you'll need to
# change the table name in each database too.
version_table=migrate_version
# When committing a change script, Migrate will attempt to generate the
# sql for all supported databases; normally, if one of them fails - probably
# because you don't have that database installed - it is ignored and the
# commit continues, perhaps ending successfully.
# Databases in this list MUST compile successfully during a commit, or the
# entire commit will fail. List the databases your application will actually
# be using to ensure your updates to that database work properly.
# This must be a list; example: ['postgres','sqlite']
required_dbs=[]
# When creating new change scripts, Migrate will stamp the new script with
# a version number. By default this is latest_version + 1. You can set this
# to 'true' to tell Migrate to use the UTC timestamp instead.
use_timestamp_numbering=False

View file

163
edbob/db/util.py Normal file
View file

@ -0,0 +1,163 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``edbob.db.util`` -- Database Utilities
"""
import os.path
import sqlalchemy.exc
import migrate.versioning.api
import migrate.exceptions
# import rattail
# from rattail.db import exc as exceptions
# from rattail.db import Session
# from rattail.db.classes import Role
# from rattail.db.model import get_metadata
# from rattail.db.perms import get_administrator
import edbob.db
from edbob.db import exceptions
from edbob.db.model import get_metadata
# def core_schema_installed(engine=None):
# """
# Returns boolean indicating whether or not the core schema has been
# installed to the database represented by ``engine``. If ``engine`` is not
# provided, then ``rattail.engine`` will be assumed.
# """
# if engine is None:
# engine = rattail.engine
# try:
# get_database_version(engine)
# except exceptions.CoreSchemaNotInstalled:
# return False
# return True
def get_database_version(engine=None, extension=None):
"""
Returns a SQLAlchemy-Migrate version number found in the database
represented by ``engine``.
If no engine is provided, :attr:`edbob.db.engine` is assumed.
If ``extension`` is provided, the version for its schema is returned;
otherwise the core schema is assumed.
"""
if engine is None:
engine = edbob.db.engine
try:
version = migrate.versioning.api.db_version(
str(engine.url), get_repository_path(extension))
except (sqlalchemy.exc.NoSuchTableError,
migrate.exceptions.DatabaseNotControlledError):
raise exceptions.CoreSchemaNotInstalled(engine)
return version
def get_repository_path(extension=None):
"""
Returns the absolute filesystem path to the SQLAlchemy-Migrate repository
for ``extension``.
If no extension is provided, ``edbob``'s core repository is assumed.
"""
if not extension:
from edbob.db import schema
return os.path.dirname(schema.__file__)
return os.path.dirname(extension.schema.__file__)
def get_repository_version(extension=None):
"""
Returns the version of the SQLAlchemy-Migrate repository for ``extension``.
If no extension is provided, ``edbob``'s core repository is assumed.
"""
return migrate.versioning.api.version(get_repository_path(extension))
def install_core_schema(engine=None):
"""
Installs the core schema to the database represented by ``engine``.
If no engine is provided, :attr:`edbob.db.engine` is assumed.
"""
if not engine:
engine = edbob.db.engine
# Try to connect in order to force an error, if applicable.
conn = engine.connect()
conn.close()
# Check DB version to see if core schema is already installed.
try:
db_version = get_database_version(engine)
except exceptions.CoreSchemaNotInstalled:
pass
else:
raise exceptions.CoreSchemaAlreadyInstalled(db_version)
# Create tables for core schema.
metadata = get_metadata()
metadata.create_all(bind=engine)
# Add versioning for core schema.
migrate.versioning.api.version_control(
str(engine.url), get_repository_path(), get_repository_version())
# WTF
# session = Session(bind=engine)
# get_administrator(session)
# session.commit()
# session.close()
# def upgrade_schema(extension=None, engine=None):
# """
# Upgrades a schema within the database represented by ``engine`` (or
# ``rattail.engine`` if none is provided). If ``extension`` is provided,
# then its schema will be upgraded; otherwise the core is assumed.
# """
# if engine is None:
# engine = rattail.engine
# repo_version = get_repository_version(extension)
# db_version = get_database_version(engine, extension)
# if db_version < repo_version:
# migrate.versioning.api.upgrade(str(engine.url), get_repository_path(extension), repo_version)

View file

@ -27,13 +27,14 @@
"""
import os
# import locale
import logging
from edbob.configuration import AppConfigParser
from edbob.configuration import default_system_paths, default_user_paths
from edbob.core import graft
from edbob.times import set_timezone
import edbob
from edbob.configuration import (
AppConfigParser,
default_system_paths,
default_user_paths,
)
__all__ = ['init']
@ -83,19 +84,15 @@ def init(appname='edbob', *args, **kwargs):
config.read(paths, recurse=not shell)
config.configure_logging()
# loc = config.get('edbob', 'locale')
# if loc:
# locale.setlocale(locale.LC_ALL, loc)
# log.info("Set locale to '%s'" % loc)
default_modules = 'edbob.time'
modules = config.get('edbob', 'init', default=default_modules)
if modules:
for name in modules.split(','):
name = name.strip()
module = __import__(name, globals(), locals(), fromlist=['init'])
getattr(module, 'init')(config)
# config.inited.append(name)
tz = config.get('edbob', 'timezone')
if tz:
set_timezone(tz)
log.info("Set timezone to '%s'" % tz)
else:
log.warning("No timezone configured; falling back to US/Central")
set_timezone('US/Central')
import edbob
graft(edbob, locals(), 'config')
# config.inited.append('edbob')
edbob.graft(edbob, locals(), 'config')
edbob.inited = True

View file

@ -5,10 +5,15 @@
"""
import os.path
import edbob
import pyramid_beaker
from pyramid.config import Configurator
import edbob
from {{package}}._version import __version__
from {{package}}.db import DBSession
def main(global_config, **settings):
"""
@ -39,7 +44,7 @@ def main(global_config, **settings):
config.set_session_factory(session_factory)
pyramid_beaker.set_cache_regions_from_settings(settings)
# Initialize edbob
# Configure edbob
edbob.basic_logging()
edbob.init('{{package}}', os.path.abspath(settings['edbob.config']))

View file

@ -0,0 +1,82 @@
#!/usr/bin/env python
"""
``{{package}}.commands`` -- Console Commands
"""
import sys
import edbob
from edbob import commands
from {{package}} import __version__
class Command(commands.Command):
"""
The primary command for {{project}}.
"""
name = '{{package}}'
version = __version__
description = "{{project}}"
long_description = ''
class InitDatabaseCommand(commands.Subcommand):
"""
Initializes the database. This is meant to be leveraged as part of setting
up the application. The database used by this command will be determined
by config, for example::
.. highlight:: ini
[edbob.db]
sqlalchemy.url = postgresql://user:pass@localhost/{{package}}
"""
name = 'init-db'
description = "Initialize the database"
def run(self, args):
from edbob.db import engine, Session
from edbob.db.util import install_core_schema
from edbob.db.exceptions import CoreSchemaAlreadyInstalled
# Install core schema to database.
try:
install_core_schema(engine)
except CoreSchemaAlreadyInstalled, err:
print err
return
from edbob.db.classes import Role, User
from edbob.db.auth import administrator_role
session = Session()
# Create 'admin' user with full rights.
admin = User(username='admin', password='admin')
admin.roles.append(administrator_role(session))
session.add(admin)
# Do any other bootstrapping you like here...
session.commit()
session.close()
print "Initialized database %s" % engine.url
def main(*args):
"""
The primary entry point for the command system.
"""
if args:
args = list(args)
else:
args = sys.argv[1:]
cmd = Command()
cmd.run(*args)

View file

@ -0,0 +1,11 @@
#!/usr/bin/env python
"""
``{{package}}.db`` -- Database Stuff
"""
from sqlalchemy.orm import scoped_session, sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))

View file

@ -5,9 +5,13 @@
Welcome to the {{project}} project.
Installation
------------
Getting Started
---------------
Install the project with::
- cd <directory containing this file>
$ pip install {{package}}
- $venv/bin/python setup.py develop
- $venv/bin/populate_{{package}} development.ini
- $venv/bin/pserve --reload development.ini

View file

@ -51,6 +51,9 @@ port = 6543
[edbob]
include_config = ['%(here)s/production.ini']
[edbob.db]
sqlalchemy.url = sqlite:///{{package}}.sqlite
####################
# logging

View file

@ -41,9 +41,12 @@ port = 6543
####################
[edbob]
timezone = US/Central
init = edbob.time, edbob.db
# shell.python = ipython
[edbob.db]
sqlalchemy.url = postgresql://user:pass@localhost/{{package}}
[edbob.mail]
smtp.server = localhost
# smtp.username = user
@ -56,6 +59,9 @@ recipients.default = [
]
subject.default = Message from {{project}}
[edbob.time]
timezone = US/Central
####################
# logging

View file

@ -39,12 +39,19 @@ requires = [
#
# package # low high
'decorator', # 3.3.2
'edbob', # 0.1a1
'Mako', # 0.6.2
'pyramid', # 1.3b2
'pyramid_beaker', # 0.6.1
'pyramid_debugtoolbar', # 1.0
'pyramid_tm', # 0.3
'SQLAlchemy', # 0.7.6
'Tempita', # 0.5.1
'transaction', # 1.2.0
'waitress', # 0.8.1
'WebHelpers', # 1.3
'zope.sqlalchemy', # 0.7
]
@ -77,6 +84,12 @@ setup(
zip_safe = False,
entry_points = """
[console_scripts]
{{package}} = {{package}}.commands:main
[{{package}}.commands]
init-db = {{package}}.commands:InitDatabaseCommand
[paste.app_factory]
main = {{package}}:main

View file

@ -26,21 +26,56 @@
``edbob.pyramid.subscribers`` -- Subscribers
"""
from pyramid.security import authenticated_userid
# from sqlahelper import get_session
import edbob
from edbob.db.auth import has_permission
from edbob.pyramid import helpers
def add_renderer_globals(event):
def before_render(event):
"""
Adds goodies to the global template renderer context.
Adds goodies to the global template renderer context:
* ``h``
* ``url``
* ``edbob``
"""
renderer_globals = event
renderer_globals['h'] = helpers
renderer_globals['edbob'] = edbob
renderer_globals['url'] = event['request'].route_url
renderer_globals['edbob'] = edbob
def context_found(event):
"""
This hook attaches the :class:`edbob.User` instance for the currently
logged-in user to the request (if there is one) as ``request.user``.
Also adds a ``has_perm()`` function to the request, which is a shortcut for
:func:`edbob.db.auth.has_permission()`.
"""
def has_perm_func(request):
def has_perm(perm):
if not request.current_user:
return False
return has_permission(request.current_user, perm)
return has_perm
request = event.request
request.user = None
request.has_perm = has_perm_func(request)
uuid = authenticated_userid(request)
if uuid:
request.user = get_session().query(rattail.User).get(uuid)
def includeme(config):
config.add_subscriber('edbob.pyramid.subscribers:add_renderer_globals',
config.add_subscriber('edbob.pyramid.subscribers:before_render',
'pyramid.events.BeforeRender')
config.add_subscriber('edbob.pyramid.subscribers.context_found',
'pyramid.events.ContextFound')

View file

@ -31,6 +31,9 @@ import os.path
from pyramid.response import Response
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from edbob.db.auth import authenticate_user
_here = os.path.join(os.path.dirname(__file__), os.pardir)
@ -51,6 +54,29 @@ def home(context, request):
@view_config(route_name='login', renderer='login.mako')
def login(context, request):
"""
The login view, responsible for displaying and handling the login form.
"""
if request.params.get('referer'):
referer = request.params['referer']
elif request.session.get('referer'):
referer = request.session.pop('referer')
else:
referer = request.referer or request.route_url('home')
# if request.current_user:
# return HTTPFound(location=referer)
# form = Form(self.request, schema=UserLogin)
# if form.validate():
# user = authenticate_user(self.Session(), form.data['username'], form.data['password'])
# if user:
# self.request.session.flash("%s logged in at %s" % (
# user.display_name,
# datetime.datetime.now().strftime("%I:%M %p")))
# headers = remember(self.request, user.uuid)
# return HTTPFound(location=referer, headers=headers)
# self.request.session.flash("Invalid username or password.")
# return {'form':FormRenderer(form), 'referer':referer}
return {}

View file

@ -26,6 +26,15 @@
``edbob.sqlalchemy`` -- SQLAlchemy Stuff
"""
from __future__ import absolute_import
from sqlalchemy import Table, Column, String
from edbob.core import get_uuid
__all__ = ['getset_factory', 'table_with_uuid']
def getset_factory(collection_class, proxy):
"""
@ -37,3 +46,29 @@ def getset_factory(collection_class, proxy):
return getattr(obj, proxy.value_attr)
setter = lambda obj, val: setattr(obj, proxy.value_attr, val)
return getter, setter
def table_with_uuid(name, metadata, *args, **kwargs):
"""
Convenience function to abstract the addition of the ``uuid`` column to a
new table. Can be used to replace this::
.. highlight:: python
Table(
'things', metadata,
Column('uuid', String(32), primary_key=True, default=get_uuid),
Column('name', String(50)),
)
...with this::
table_with_uuid(
'things', metadata,
Column('name', String(50)),
)
"""
return Table(name, metadata,
Column('uuid', String(32), primary_key=True, default=get_uuid),
*args, **kwargs)

View file

@ -23,17 +23,35 @@
################################################################################
"""
``edbob.times`` -- Date & Time Utilities
``edbob.time`` -- Date & Time Utilities
"""
import datetime
import pytz
import logging
__all__ = ['local_time', 'set_timezone', 'utc_time']
_timezone = None
log = logging.getLogger(__name__)
timezone = None
def init(config):
"""
Initializes the time framework. Currently this only sets the local
timezone according to config.
"""
tz = config.get('edbob.time', 'timezone')
if tz:
set_timezone(tz)
log.info("Set timezone to '%s'" % tz)
else:
log.warning("No timezone configured; falling back to US/Central")
set_timezone('US/Central')
def local_time(timestamp=None):
"""
@ -51,11 +69,11 @@ def local_time(timestamp=None):
should none be specified. ``timestamp`` will be returned unchanged.
"""
if _timezone:
if timezone:
if timestamp is None:
timestamp = datetime.datetime.utcnow()
timestamp = pytz.utc.localize(timestamp)
return timestamp.astimezone(_timezone)
return timestamp.astimezone(timezone)
if timestamp is None:
timestamp = datetime.datetime.now()
@ -67,21 +85,25 @@ def set_timezone(tz):
Sets edbob's notion of the "local" timezone. ``tz`` should be an Olson
name.
.. highlight:: ini
You usually don't need to call this yourself, since it's called by
:func:`edbob.init()` whenever ``edbob.conf`` includes a timezone::
:func:`edbob.init()` whenever the config file includes a timezone (but
only as long as ``edbob.time`` is configured to be initialized)::
.. highlight:: ini
[edbob]
init = ['edbob.time']
[edbob.time]
timezone = US/Central
"""
global _timezone
global timezone
if tz is None:
_timezone = None
timezone = None
else:
_timezone = pytz.timezone(tz)
timezone = pytz.timezone(tz)
def utc_time(timestamp=None):

View file

@ -99,15 +99,18 @@ setup(
extras_require = {
#
# Same guidelines apply to the extra dependencies:
# Same guidelines apply to the extra dependency versions.
# 'db': [
# #
# # package # low high
# #
# 'SQLAlchemy', # 0.6.7
# 'sqlalchemy-migrate', # 0.6.1
# ],
'db': [
#
# package # low high
#
'decorator', # 3.3.2
'py-bcrypt', # 0.2
'SQLAlchemy', # 0.7.6
'sqlalchemy-migrate', # 0.7.2
'Tempita', # 0.5.1
],
'docs': [
#