#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # mysqlred.py # # Copyright 2017 Jonathan Golder # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # # """ Provides interface classes for communication of redundances bot with mysql-db """ import atexit # noqa import pywikibot # noqa from pywikibot import config import jogobot from sqlalchemy import ( create_engine, Column, Integer, String, Text, DateTime, ForeignKey ) from sqlalchemy import text # noqa from sqlalchemy.engine.url import URL from sqlalchemy.ext.declarative import ( declarative_base, declared_attr, has_inherited_table ) from sqlalchemy.ext.mutable import MutableComposite, MutableSet from sqlalchemy.orm import sessionmaker, relationship, composite from sqlalchemy.orm.collections import attribute_mapped_collection import sqlalchemy.types as types Base = declarative_base() url = URL( "mysql+pymysql", username=config.db_username, password=config.db_password, host=config.db_hostname_format.format('tools'), port=config.db_port, database=( config.db_username + jogobot.config['redundances']['db_suffix'] ), query={'charset': 'utf8'} ) engine = create_engine(url, echo=False) Session = sessionmaker(bind=engine) session = Session() family = pywikibot.Site().family.dbName(pywikibot.Site().code) class Mysql(object): session = session @declared_attr def _tableprefix(cls): return family + "_" @declared_attr def _tablesuffix(cls): return "s" @declared_attr def __tablename__(cls): if has_inherited_table(cls): return None name = cls.__name__[len("Mysql"):].lower() return cls._tableprefix + name + cls._tablesuffix def changedp(self): return self.session.is_modified(self) class MutableSet(MutableSet): """ Extended version of the mutable set for our states """ def has(self, item): """ Check if item is in set @param item Item to check """ return item in self def add(self, item): """ Extended add method, which only result in changed object if there is really an item added. @param item Item to add """ if item not in self: super().add(item) def discard(self, item): """ Wrapper for extended remove below @param item Item to discard """ self.remove(item) def remove(self, item, weak=True ): """ Extended remove method, which only results in changed object if there is really an item removed. Additionally, combine remove and discard! @param item Item to remove/discard @param weak Set to false to use remove, else discard behavior """ if item in self: if weak: super().discard(item) else: super().remove(item) class ColumnList( list, MutableComposite ): """ Combines multiple Colums into a list like object """ def __init__( self, *columns ): """ Wrapper to the list constructor deciding whether we have initialization with individual params per article or with an iterable. """ # Individual params per article (from db), first one is a str if isinstance( columns[0], str ) or \ isinstance( columns[0], MutableSet ) or columns[0] is None: super().__init__( columns ) # Iterable articles list else: super().__init__( columns[0] ) def __setitem__(self, key, value): """ The MutableComposite class needs to be noticed about changes in our component. So we tweak the setitem process. """ # set the item super().__setitem__( key, value) # alert all parents to the change self.changed() def __composite_values__(self): """ The Composite method needs to have this method to get the items for db. """ return self class Status( types.TypeDecorator ): impl = types.String def process_bind_param(self, value, dialect): """ Returns status as commaseparated string (to save in DB) @returns Raw status string @rtype str """ if isinstance(value, MutableSet): return ",".join( value ) elif isinstance(value, String ) or value is None: return value else: raise TypeError( "Value should be an instance of one of {0:s},".format( str( [type(MutableSet()), type(String()), type(None)] ) ) + "given value was an instance of {1:s}".format( str(type(value))) ) def process_result_value(self, value, dialect): """ Sets status based on comma separated list @param raw_status Commaseparated string of stati (from DB) @type raw_status str """ if value: return MutableSet( value.strip().split(",")) else: return MutableSet([]) def copy(self, **kw): return Status(self.impl.length) class MysqlRedFam( Mysql, Base ): famhash = Column( String(64), primary_key=True, unique=True ) __article0 = Column('article0', String(255), nullable=False ) __article1 = Column('article1', String(255), nullable=False ) __article2 = Column('article2', String(255), nullable=True ) __article3 = Column('article3', String(255), nullable=True ) __article4 = Column('article4', String(255), nullable=True ) __article5 = Column('article5', String(255), nullable=True ) __article6 = Column('article6', String(255), nullable=True ) __article7 = Column('article7', String(255), nullable=True ) __articlesList = composite( ColumnList, __article0, __article1, __article2, __article3, __article4, __article5, __article6, __article7 ) heading = Column( Text, nullable=False ) redpageid = Column( Integer, ForeignKey( family + "_redpages.pageid" ), nullable=False ) beginning = Column( DateTime, nullable=False ) ending = Column( DateTime, nullable=True ) _status = Column( 'status', MutableSet.as_mutable(Status(255)), nullable=True ) __article0_status = Column( 'article0_status', MutableSet.as_mutable(Status(64)), nullable=True ) __article1_status = Column( 'article1_status', MutableSet.as_mutable(Status(64)), nullable=True ) __article2_status = Column( 'article2_status', MutableSet.as_mutable(Status(64)), nullable=True ) __article3_status = Column( 'article3_status', MutableSet.as_mutable(Status(64)), nullable=True ) __article4_status = Column( 'article4_status', MutableSet.as_mutable(Status(64)), nullable=True ) __article5_status = Column( 'article5_status', MutableSet.as_mutable(Status(64)), nullable=True ) __article6_status = Column( 'article6_status', MutableSet.as_mutable(Status(64)), nullable=True ) __article7_status = Column( 'article7_status', MutableSet.as_mutable(Status(64)), nullable=True ) __articlesStatus = composite( ColumnList, __article0_status, __article1_status, __article2_status, __article3_status, __article4_status, __article5_status, __article6_status, __article7_status ) redpage = relationship( "MysqlRedPage", enable_typechecks=False, back_populates="redfams" ) @property def articlesList(self): """ List of articles belonging to the redfam """ return self.__articlesList @articlesList.setter def articlesList(self, articlesList): # Make sure to always have full length for complete overwrites while( len(articlesList) < 8 ): articlesList.append(None) self.__articlesList = ColumnList(articlesList) @property def status( self ): """ Current fam status """ return self._status @status.setter def status( self, status ): if status: self._status = MutableSet( status ) else: self._status = MutableSet() @property def articlesStatus(self): """ List of status strings/sets for the articles of the redfam """ return self.__articlesStatus @articlesStatus.setter def articlesStatus(self, articlesStatus): self.__articlesStatus = ColumnList(articlesStatus) class MysqlRedPage( Mysql, Base ): pageid = Column( Integer, unique=True, primary_key=True ) revid = Column( Integer, unique=True, nullable=False ) pagetitle = Column( String(255), nullable=False ) __status = Column( 'status', MutableSet.as_mutable(Status(255)), nullable=True ) redfams = relationship( "MysqlRedFam", enable_typechecks=False, back_populates="redpage", order_by=MysqlRedFam.famhash, collection_class=attribute_mapped_collection("famhash") ) @property def status( self ): """ Current fam status """ return self.__status @status.setter def status( self, status ): if status: self.__status = MutableSet( status ) else: self.__status = MutableSet() Base.metadata.create_all(engine) class MysqlRedError(Exception): """ Basic Exception class for this module """ pass class MysqlRedConnectionError(MysqlRedError): """ Raised if there are Errors with Mysql-Connections """ pass