diff --git a/ttfrog/db/__init__.py b/ttfrog/db/__init__.py index aece833..e69de29 100644 --- a/ttfrog/db/__init__.py +++ b/ttfrog/db/__init__.py @@ -1,4 +0,0 @@ -from .manager import db, session - - -__ALL__ = [db, session] diff --git a/ttfrog/db/bootstrap.py b/ttfrog/db/bootstrap.py index a8c0cb2..6b7a27c 100644 --- a/ttfrog/db/bootstrap.py +++ b/ttfrog/db/bootstrap.py @@ -1,17 +1,20 @@ import logging +import transaction -from ttfrog.db import db, session +from ttfrog.db.manager import db +from ttfrog.db import schema +from sqlalchemy.exc import IntegrityError # move this to json or whatever data = { - 'ancestry': [ - {'name': 'human'}, - {'name': 'dragonborn'}, - {'name': 'tiefling'}, + 'Ancestry': [ + {'id': 1, 'name': 'human'}, + {'id': 2, 'name': 'dragonborn'}, + {'id': 3, 'name': 'tiefling'}, ], - 'character': [ - {'name': 'Sabetha', 'ancestry_name': 'tiefling', 'level': 10, 'str': 10, 'dex': 10, 'con': 10, 'int': 10, 'wis': 10, 'cha': 10}, + 'Character': [ + {'id': 1, 'name': 'Sabetha', 'ancestry': 'tiefling', 'level': 10, 'str': 10, 'dex': 10, 'con': 10, 'int': 10, 'wis': 10, 'cha': 10}, ] } @@ -20,23 +23,21 @@ def bootstrap(): """ Initialize the database with source data. Idempotent; will skip anything that already exists. """ - db.init_model() - for table_name, table in db.tables.items(): - if table_name not in data: - logging.debug("No bootstrap data for table {table_name}; skipping.") - continue - for rec in data[table_name]: - stmt = table.insert().values(**rec).prefix_with("OR IGNORE") - result, error = db.execute(stmt) - if error: - raise RuntimeError(error) + db.init() + for table, records in data.items(): + model = getattr(schema, table) - rec['id'] = result.inserted_primary_key[0] - if rec['id'] == 0: - logging.info(f"Skipped existing {table_name} {rec}") - continue - - if 'slug' in table.columns: - rec['slug'] = db.slugify(rec) - db.update(table, **rec) - logging.info(f"Created {table_name} {rec}") + for rec in records: + with transaction.manager as tx: + obj = model(**rec) + db.session.add(obj) + obj.slug = db.slugify(rec) + try: + tx.commit() + except IntegrityError as e: + tx.abort() + if 'UNIQUE constraint failed' in str(e): + logging.info(f"Skipping existing {table} {rec}") + continue + raise + logging.info(f"Created {table} {rec}") diff --git a/ttfrog/db/manager.py b/ttfrog/db/manager.py index 54bab4e..d81a220 100644 --- a/ttfrog/db/manager.py +++ b/ttfrog/db/manager.py @@ -1,15 +1,21 @@ +import transaction import base64 import hashlib import logging from functools import cached_property +from pyramid_sqlalchemy import Session +from pyramid_sqlalchemy import init_sqlalchemy +from pyramid_sqlalchemy import metadata as _metadata + from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.exc import IntegrityError from ttfrog.path import database -from ttfrog.db.schema import metadata +import ttfrog.db.schema + +ttfrog.db.schema class SQLDatabaseManager: @@ -22,30 +28,34 @@ class SQLDatabaseManager: @cached_property def engine(self): - return create_engine(self.url, future=True) + return create_engine(self.url) @cached_property - def DBSession(self): - maker = sessionmaker(bind=self.engine, future=True, autoflush=True) - return scoped_session(maker) + def session(self): + return Session + + @cached_property + def metadata(self): + return _metadata @cached_property def tables(self): - return dict((t.name, t) for t in metadata.sorted_tables) + return dict((t.name, t) for t in self.metadata.sorted_tables) def query(self, *args, **kwargs): - return self.DBSession.query(*args, **kwargs) + return self.session.query(*args, **kwargs) def execute(self, statement) -> tuple: - logging.debug(statement) + logging.info(statement) result = None error = None try: - result = self.DBSession.execute(statement) - self.DBSession.commit() + with transaction.manager as tx: + result = self.session.execute(statement) + tx.commit() except IntegrityError as exc: logging.error(exc) - error = "An error occurred when saving changes." + error = "I AM ERROR." return result, error def insert(self, table, **kwargs) -> tuple: @@ -57,10 +67,6 @@ class SQLDatabaseManager: stmt = table.update().values(**kwargs).where(table.columns.id == primary_key) return self.execute(stmt) - def init_model(self, engine=None): - metadata.create_all(bind=engine or self.engine) - return self.DBSession - def slugify(self, rec: dict) -> str: """ Create a uniquish slug from a dictionary. @@ -68,6 +74,9 @@ class SQLDatabaseManager: sha1bytes = hashlib.sha1(str(rec['id']).encode()) return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10] + def init(self): + init_sqlalchemy(self.engine) + self.metadata.create_all(self.engine) def __getattr__(self, name: str): try: @@ -75,14 +84,5 @@ class SQLDatabaseManager: except KeyError: raise AttributeError(f"{self} does not contain the attribute '{name}'.") - def __enter__(self): - self.init_model(self.engine) - return self - - def __exit__(self, exc_type, exc_value, traceback): - if self.DBSession: - self.DBSession.close() - db = SQLDatabaseManager() -session = db.DBSession diff --git a/ttfrog/db/schema.py b/ttfrog/db/schema.py index 0eca673..bf831be 100644 --- a/ttfrog/db/schema.py +++ b/ttfrog/db/schema.py @@ -9,29 +9,27 @@ from sqlalchemy import CheckConstraint # from sqlalchemy import PrimaryKeyConstraint # from sqlalchemy import DateTime -metadata = MetaData() +from pyramid_sqlalchemy import BaseObject -Ancestry = Table( - "ancestry", - metadata, - Column("id", Integer, primary_key=True, autoincrement=True), - Column("name", String, index=True, unique=True), - Column("slug", String, index=True, unique=True), - Column("description", UnicodeText), -) +class Ancestry(BaseObject): + __tablename__ = "ancestry" -Character = Table( - "character", - metadata, - Column("id", Integer, primary_key=True, autoincrement=True), - Column("slug", String, index=True, unique=True), - Column("ancestry_name", Integer, ForeignKey("ancestry.name")), - Column("name", String), - Column("level", Integer, CheckConstraint('level > 0 AND level <= 20')), - Column("str", Integer, CheckConstraint('str >=0')), - Column("dex", Integer, CheckConstraint('dex >=0')), - Column("con", Integer, CheckConstraint('con >=0')), - Column("int", Integer, CheckConstraint('int >=0')), - Column("wis", Integer, CheckConstraint('wis >=0')), - Column("cha", Integer, CheckConstraint('cha >=0')), -) + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String, index=True, unique=True) + slug = Column(String, index=True, unique=True) + + +class Character(BaseObject): + __tablename__ = "character" + + id = Column(Integer, primary_key=True, autoincrement=True) + slug = Column(String, index=True, unique=True) + ancestry = Column(String, ForeignKey("ancestry.name")) + name = Column(String) + level = Column(Integer, CheckConstraint('level > 0 AND level <= 20')) + str = Column(Integer, CheckConstraint('str >=0')) + dex = Column(Integer, CheckConstraint('dex >=0')) + con = Column(Integer, CheckConstraint('con >=0')) + int = Column(Integer, CheckConstraint('int >=0')) + wis = Column(Integer, CheckConstraint('wis >=0')) + cha = Column(Integer, CheckConstraint('cha >=0')) diff --git a/ttfrog/webserver/application.py b/ttfrog/webserver/application.py index 2d3a5c3..1e0bbff 100644 --- a/ttfrog/webserver/application.py +++ b/ttfrog/webserver/application.py @@ -1,62 +1,24 @@ import logging -from tg import MinimalApplicationConfigurator -from tg.configurator.components.statics import StaticsConfigurationComponent -from tg.configurator.components.sqlalchemy import SQLAlchemyConfigurationComponent -from tg.util.bunch import Bunch - from wsgiref.simple_server import make_server -import webhelpers2 -import tw2.core +from pyramid.config import Configurator -from ttfrog.webserver.controllers.root import RootController -from ttfrog.db import db -import ttfrog.path +from ttfrog.db.manager import db +from ttfrog.webserver.routes import routes -def app_globals(): - return Bunch - - -def application(): - """ - Create a TurboGears2 application - """ - - config = MinimalApplicationConfigurator() - config.register(StaticsConfigurationComponent) - config.register(SQLAlchemyConfigurationComponent) - config.update_blueprint({ - - # rendering - 'root_controller': RootController(), - 'default_renderer': 'jinja', - 'renderers': ['jinja'], - 'tg.jinja_filters': {}, - 'auto_reload_templates': True, - - # helpers - 'app_globals': app_globals, - 'helpers': webhelpers2, - 'use_toscawidgets2': True, - - # assets - 'serve_static': True, - 'paths': { - 'static_files': ttfrog.path.static_files(), - 'templates': [ttfrog.path.templates()], - }, - - # db - 'use_sqlalchemy': True, +def configuration(): + config = Configurator(settings={ 'sqlalchemy.url': db.url, - 'model': db, }) - - # wrap the core wsgi app in a ToscaWidgets2 app - return tw2.core.make_middleware(config.make_wsgi_app(), default_engine='jinja') + config.include('pyramid_tm') + config.include('pyramid_sqlalchemy') + return config def start(host: str, port: int, debug: bool = False) -> None: logging.debug(f"Configuring webserver with {host=}, {port=}, {debug=}") - make_server(host, int(port), application()).serve_forever() + config = configuration() + config.include(routes) + config.scan('ttfrog.webserver.views') + make_server(host, int(port), config.make_wsgi_app()).serve_forever() diff --git a/ttfrog/webserver/routes.py b/ttfrog/webserver/routes.py new file mode 100644 index 0000000..b91c70d --- /dev/null +++ b/ttfrog/webserver/routes.py @@ -0,0 +1,2 @@ +def routes(config): + config.add_route('index', '/') diff --git a/ttfrog/webserver/views.py b/ttfrog/webserver/views.py new file mode 100644 index 0000000..73383ed --- /dev/null +++ b/ttfrog/webserver/views.py @@ -0,0 +1,10 @@ +from pyramid.response import Response +from pyramid.view import view_config +from ttfrog.db.manager import db +from ttfrog.db.schema import Ancestry + + +@view_config(route_name='index') +def index(request): + ancestries = [a.name for a in db.session.query(Ancestry).all()] + return Response(','.join(ancestries))