''' metadata.py Everything needed to interact with the metadata database; namely SQL ORM objects, a context generator for the actual database, and a handful of convenience functions. ''' import contextlib import uuid import sqlalchemy import sqlalchemy.ext.declarative import sqlalchemy.orm # TODO: l o g g i n g HASH_CHUNK_SIZE = 10485760 # 10mb _db_session_maker = sqlalchemy.orm.sessionmaker() _engine = None _configured = False # TODO: Support DAT credit, DAT filenames, and checking DAT completeness. #DatData = collections.namedtuple('DatData', 'UUID, name, website, version, image_list') class MetadataDBSessionException(Exception): '''This exception is raised when something goes wrong with a database session.''' def _uuidgen(): return str(uuid.uuid4()) _SQLBase = sqlalchemy.ext.declarative.declarative_base() class Release(_SQLBase): '''SQLAlchemy ORM class for ROM image metadata.''' __tablename__ = 'images' id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.Sequence('image_id_sequence'), primary_key=True) uuid = sqlalchemy.Column(sqlalchemy.String, nullable=False, default=_uuidgen) sha1sum = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False) format = sqlalchemy.Column(sqlalchemy.String, nullable=False) region = sqlalchemy.Column(sqlalchemy.String) version = sqlalchemy.Column(sqlalchemy.String, nullable=False) disambiguation = sqlalchemy.Column(sqlalchemy.String) release_group_id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey('release_groups.id')) release_group = sqlalchemy.orm.relationship('ReleaseGroup', back_populates='releases') def __repr__(self): if self.release_group is None: release_group_name = '[Orphan]' else: release_group_name = self.release_group.name return ('ROM Image: id: %s, uuid: %s, sha1sum: %s, release-group: %s, region: %s, ' 'version: %s, disambiguation: %s' % ( self.id, self.uuid, self.sha1sum, release_group_name, self.region, self.version, self.disambiguation)) class ReleaseGroup(_SQLBase): '''SQLAlchemy ORM class for release group metadata.''' __tablename__ = 'release_groups' id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.Sequence('image_id_sequence'), primary_key=True) uuid = sqlalchemy.Column(sqlalchemy.String, nullable=False, default=_uuidgen) name = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False) platform_id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey('platforms.id')) platform = sqlalchemy.orm.relationship('Platform', back_populates='release_groups') releases = sqlalchemy.orm.relationship('Release', back_populates='release_group') def __repr__(self): return 'Release Group: id: %s, uuid: %s, name: %s, platform:%s' % (self.id, self.uuid, self.name, self.platform.fullname) class Platform(_SQLBase): '''SQLAlchemy ORM class for platform metadata.''' __tablename__ = 'platforms' id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.Sequence('platform_id_sequence'), primary_key=True) uuid = sqlalchemy.Column(sqlalchemy.String, nullable=False, default=_uuidgen) fullname = sqlalchemy.Column(sqlalchemy.String, nullable=False) shortcode = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False) release_groups = sqlalchemy.orm.relationship('ReleaseGroup', order_by=ReleaseGroup.id, back_populates='platform') def __repr__(self): return 'Platform: id: %s, uuid: %s, fullname: %s, shortcode: %s' % (self.id, self.uuid, self.fullname, self.shortcode) def configure(db_path): ''' Configure and initialize the database for the entire module. Currently, only SQLite is supported. db_path: Path for the SQLite database ''' global _configured global _engine _engine = sqlalchemy.create_engine('sqlite:///%s' % db_path) _SQLBase.metadata.create_all(_engine) _db_session_maker.configure(bind=_engine) _configured = True print('Database configured.') # TODO: Passing the session object is a little clunky. Maybe there's a way to infer it somehow? def search(session, table_object, **constraints): ''' Search the database for entries matching the given constraints. session: SQLAlchemy session, presumably from get_db_session table_object: SQLAlchemy ORM table object, defined in the file above constraints: key-value pairs to match against specific fields in the database Note: Currently, only the query.ilike method is supported. This is intended to eventually support the entire range of available filters. ''' query = session.query(table_object) for key, value in constraints.items(): query = query.filter(getattr(table_object, key).ilike('%%%s%%' % value)) item_list = [] for item in query: item_list.append(item) return item_list @contextlib.contextmanager def get_db_session(): '''Get a SQLAlchemy database session with a proper context object. ''' # TODO: There's probably a more reliable way of knowing whether the database was configured. if not _configured: raise MetadataDBSessionException('Tried to get session without configuring a database.') session = _db_session_maker() try: yield session except: # TODO: Decide which exceptions to handle/eat here and which ones belong in UI. # This one is okay to put off until you start really building UI. session.rollback() raise else: session.commit() finally: session.close()