#!/usr/bin/env python
##
## This file is part of OpenSIPS CLI
## (see https://github.com/OpenSIPS/opensips-cli).
##
## 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, see <http://www.gnu.org/licenses/>.
##

from opensipscli.logger import logger
from opensipscli.config import cfg
import re

try:
    import sqlalchemy
    import sqlalchemy_utils
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy import Column, Date, Integer, String, Boolean
    from sqlalchemy.orm import sessionmaker, deferred
    from sqlalchemy.engine.url import make_url
    sqlalchemy_available = True
    logger.debug("SQLAlchemy version: ", sqlalchemy.__version__)
except ImportError:
    logger.info("sqlalchemy and sqlalchemy_utils are not available!")
    sqlalchemy_available = False

SUPPORTED_BACKENDS = [
    "mysql",
    "postgres",
    "sqlite",
    "oracle",
]

"""
SQLAlchemy: Classes for ORM handling
"""
if sqlalchemy_available:
    Base = declarative_base()
    
    class Roles(Base):
        """
        Postgres: Roles database
        """
        __tablename__ = 'pg_roles'
    
        oid = Column(Integer, primary_key=True)
        rolname = Column(String)
        rolsuper = deferred(Column(Boolean), group='options')
        rolinherit = deferred(Column(Boolean), group='options')
        rolcreaterole = deferred(Column(Boolean), group='options')
        rolcreatedb = deferred(Column(Boolean), group='options')
        rolcanlogin = deferred(Column(Boolean), group='options')
        rolreplication = deferred(Column(Boolean), group='options')
        rolconnlimit = deferred(Column(Integer))
        rolpassword = Column(String)
        rolvaliduntil = deferred(Column(Date))
        rolbypassrls = deferred(Column(Boolean))
        rolconfig = deferred(Column(String))
    
        def __repr__(self):
            """
            returns a string from an arbitrary object
            """
            return self.shape

class osdbError(Exception):
    """
    OSDB: error handler
    """
    pass

class osdbConnectError(osdbError):
    """
    OSDB: connecton error handler
    """
    pass

class osdbArgumentError(osdbError):
    """
    OSDB: argument error handler
    """
    pass

class osdbNoSuchModuleError(osdbError):
    """
    OSDB: module error handler
    """
    pass

class osdbModuleAlreadyExistsError(osdbError):
    """
    OSDB: module error handler
    """
    pass

class osdbAccessDeniedError(osdbError):
    """
    OSDB: module error handler
    """
    pass


class osdb(object):
    """
    Class: object store database
    """
    def __init__(self, db_url, db_name):
        """
        constructor
        """
        self.db_url = db_url
        self.db_name = db_name
        self.dialect = osdb.get_dialect(db_url)
        self.Session = sessionmaker()
        self.__engine = None
        self.__conn = None

	    # TODO: do this only for SQLAlchemy
        try:
            if self.dialect == "postgresql":
                self.__engine = sqlalchemy.create_engine(db_url, isolation_level='AUTOCOMMIT')
            else:
                self.__engine = sqlalchemy.create_engine(db_url)

            logger.debug("connecting to %s", db_url)
            self.__conn = self.__engine.connect().\
                    execution_options(autocommit=True)
            # connect the Session object to our engine
            self.Session.configure(bind=self.__engine)
            # instanciate the Session object
            self.__session = self.Session()
        except sqlalchemy.exc.OperationalError as se:
            if self.dialect == "mysql":
                try:
                    if int(se.args[0].split(",")[0].split("(")[2]) in [
                            2006, # MySQL
                            1044, # MariaDB
                            1045, # MariaDB
                            ]:
                        raise osdbAccessDeniedError
                except osdbAccessDeniedError:
                    raise
                except:
                    logger.error("unexpected parsing exception")
            elif self.dialect == "postgres" and \
                    (("authentication" in se.args[0] and "failed" in se.args[0]) or \
                     ("no password supplied" in se.args[0])):
                raise osdbAccessDeniedError

            raise osdbError("unable to connect to the database")
        except sqlalchemy.exc.NoSuchModuleError:
            raise osdbError("cannot handle {} dialect".
                    format(self.dialect))
        except sqlalchemy.exc.ArgumentError:
            raise osdbArgumentError("bad DB URL: {}".format(
                self.db_url))

    def alter_role(self, role_name, role_options=None, role_password=None):
        """
        alter attributes of a role object
        """
        # TODO: is any other dialect using the "role" concept?
        if self.dialect != "postgres":
            return False

        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")
            return False

        if not role_options is None:
            sqlcmd = "ALTER ROLE {} WITH {}".format(role_name, role_options)
            msg = "Alter role '{}' with options '{}'". \
                format(role_name, role_options, self.db_name)
        if not role_password is None:
            sqlcmd  += " PASSWORD '{}'".format(role_password)
            msg += " and password '********'"
        msg += " on database '{}'".format(self.db_name)
        try:
            result = self.__conn.execute(sqlcmd)
            if result:
                logger.info( "{} was successfull".format(msg))
        except:
            logger.error("%s failed", msg)
            return False
        return

    def connect(self, db_name=None):
        """
        connect to database
        """
        if db_name is not None:
            self.db_name = db_name
		# TODO: do this only for SQLAlchemy

        try:
            if self.dialect == "postgres":
                self.db_url = self.set_url_db(self.db_url, self.db_name)
                if sqlalchemy_utils.database_exists(self.db_url) is True:
                    engine = sqlalchemy.create_engine(self.db_url, isolation_level='AUTOCOMMIT')
                    if self.__conn:
                        self.__conn.close()
                    self.__conn = engine.connect()
                    # connect the Session object to our engine
                    self.Session.configure(bind=self.__engine)
                    # instanciate the Session object
                    self.session = self.Session()
                    logger.debug("connected to database URL '%s'", self.db_url)
            else:
                self.__conn.execute("USE {}".format(self.db_name))
        except Exception as e:
            logger.error("failed to connect to %s", self.db_url)
            logger.error(e)
            return False

        return True

    def create(self, db_name=None):
        """
        create a database object
        """
        if db_name is None:
            db_name = self.db_name
        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")

        logger.debug("Create Database '%s' for dialect '%s' ...",
                     self.db_name, self.dialect)

        # all good - it's time to create the database
        if self.dialect == "postgres":
            self.__conn.connection.connection.set_isolation_level(0)
            try:
                self.__conn.execute("CREATE DATABASE {}".format(self.db_name))
                self.__conn.connection.connection.set_isolation_level(1)
            except sqlalchemy.exc.OperationalError as se:
                logger.error("cannot create database: {}!".format(se))
                return False
        else:
            self.__conn.execute("CREATE DATABASE {}".format(self.db_name))

        logger.debug("success")
        return True

    def create_module(self, import_file):
        """
        create a module object
        """
        self.exec_sql_file(import_file)

    def ensure_user(self, db_url):
        url = make_url(db_url)
        if url.password is None:
            logger.error("database URL does not include a password")
            return False

        if url.drivername.lower() == "mysql":
            sqlcmd = "CREATE USER IF NOT EXISTS '{}' IDENTIFIED BY '{}'".format(
                        url.username, url.password)
            try:
                result = self.__conn.execute(sqlcmd)
                if result:
                    logger.info("created user '%s'", url.username)
            except:
                logger.error("failed to create user '%s'", url.username)
                return False

            if url.username == 'root':
                logger.debug("skipping password change for root user")
            else:
                """
                Query compatibility facts when changing a MySQL user password:
                 - SET PASSWORD syntax has diverged between MySQL and MariaDB
                 - ALTER USER syntax is not supported in MariaDB < 10.2
                """

                # try MariaDB syntax first
                sqlcmd = "SET PASSWORD FOR '{}' = PASSWORD('{}')".format(
                            url.username, url.password)
                try:
                    result = self.__conn.execute(sqlcmd)
                    if result:
                        logger.info("set password '%s%s%s' for '%s' (MariaDB)",
                            url.password[0] if len(url.password) >= 1 else '',
                            (len(url.password) - 2) * '*',
                            url.password[-1] if len(url.password) >= 2 else '',
                            url.username)
                except sqlalchemy.exc.ProgrammingError as se:
                    try:
                        if int(se.args[0].split(",")[0].split("(")[2]) == 1064:
                            # syntax error!  OK, now try Oracle MySQL syntax
                            sqlcmd = "ALTER USER '{}' IDENTIFIED BY '{}'".format(
                                        url.username, url.password)
                            result = self.__conn.execute(sqlcmd)
                            if result:
                                logger.info("set password '%s%s%s' for '%s' (MySQL)",
                                    url.password[0] if len(url.password) >= 1 else '',
                                    (len(url.password) - 2) * '*',
                                    url.password[-1] if len(url.password) >= 2 else '',
                                    url.username)
                    except:
                        logger.exception("failed to set password for '%s'", url.username)
                        return False
                except:
                    logger.exception("failed to set password for '%s'", url.username)
                    return False

            sqlcmd = "GRANT ALL ON {}.* TO '{}'".format(self.db_name, url.username)
            try:
                result = self.__conn.execute(sqlcmd)
                if result:
                    logger.info("granted access to user '%s' on DB '%s'",
                                url.username, self.db_name)
            except:
                logger.exception("failed to grant access to '%s' on DB '%s'",
                                url.username, self.db_name)
                return False

            sqlcmd = "FLUSH PRIVILEGES"
            try:
                result = self.__conn.execute(sqlcmd)
                logger.info("flushed privileges")
            except:
                logger.exception("failed to flush privileges")
                return False

        elif url.drivername.lower() == "postgres":
            if not self.exists_role(url.username):
                logger.info("creating role %s", url.username)
                if not self.create_role(url.username, url.password):
                    logger.error("failed to create role %s", url.username)

            self.create_role(url.username, url.password, update=True)

            sqlcmd = "GRANT ALL PRIVILEGES ON DATABASE {} TO {}".format(
                        self.db_name, url.username)
            logger.info(sqlcmd)

            try:
                result = self.__conn.execute(sqlcmd)
                if result:
                    logger.debug("... OK")
            except:
                logger.error("failed to grant ALL to '%s' on db '%s'",
                        url.username, self.db_name)
                return False

        return True

    def create_role(self, role_name, role_password, update=False,
                    role_options="NOCREATEDB NOCREATEROLE LOGIN"):
        """
        create a role object (PostgreSQL secific)
        """
        # TODO: is any other dialect using the "role" concept?
        if self.dialect != "postgres":
            return False

        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")

        if update:
            sqlcmd = "ALTER USER {} WITH PASSWORD '{}' {}".format(
                    role_name, role_password, role_options)
        else:
            sqlcmd = "CREATE ROLE {} WITH {} PASSWORD '{}'".format(
                    role_name, role_options, role_password)
        logger.info(sqlcmd)

        try:
            result = self.__conn.execute(sqlcmd)
            if result:
                logger.info("role '{}' with options '{}' created".
                    format(role_name, role_options))
        except Exception as e:
            logger.exception(e)
            logger.error("creation of new role '%s' with options '%s' failed",
                    role_name, role_options)
            return False
        return result

    def delete(self, table, filter_keys=None):
        """
        delete a table object from a database
        """
        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")

        where_str = self.get_where(filter_keys)
        statement = "DELETE FROM {}{}".format(table, where_str)
        try:
            self.__conn.execute(statement)
        except sqlalchemy.exc.SQLAlchemyError as ex:
            logger.error("cannot execute query: {}".format(statement))
            logger.error(ex)
            return False
        return True

    def destroy(self):
       """
       decontructor of a database object
       """
       # TODO: do this only for SQLAlchemy
       if not self.__conn:
            return
       self.__conn.close()

    def drop(self):
        """
        drop a database object
        """
        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")
        database_url = self.set_url_db(self.db_url, self.db_name)
        try:
            sqlalchemy_utils.drop_database(database_url)
            logger.debug("database '%s' dropped", self.db_name)
            return True
        except sqlalchemy.exc.NoSuchModuleError as me:
            logger.error("cannot check if database {} exists: {}".
                    format(self.db_name, me))
            raise osdbError("cannot handle {} dialect".
                    format(self.dialect)) from None

    def drop_role(self, role_name):
        """
        drop a role object (PostgreSQL specific)
        """
        # TODO: is any other dialect using the "role" concept?
        if self.dialect != "postgres":
            return False

        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")
            return False

        logger.debug("Role '%s' will be dropped", role_name)

        sqlcmd = "DROP ROLE IF EXISTS {}".format(role_name)
        try:
            result = self.__conn.execute(sqlcmd)
            if result:
                logger.debug("Role '%s' dropped", role_name)
        except:
            logger.error("dropping role '%s' failed", role_name)
            return False
        return

    def entry_exists(self, table, constraints):
        """
        check for existence of table constraints
        """
        ret = self.find(table, "count(*)", constraints)
        if ret and ret.first()[0] != 0:
            return True
        return False

    def exec_sql_file(self, sql_file):
        """
        deploy given sql file
        """
        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")

        with open(sql_file, 'r') as f:
            if sql_file.endswith("-migrate.sql"):
                try:
                    sql = f.read()

                    # the DELIMITER thingies are only useful to mysql shell client
                    sql = re.sub(r'DELIMITER .*\n', '', sql)
                    sql = re.sub(r'\$\$', ';', sql)

                    # DROP/CREATE PROCEDURE statements seem to only work separately
                    sql = re.sub(r'DROP PROCEDURE .*\n', '', sql)

                    self.__conn.execute(sql)
                except sqlalchemy.exc.IntegrityError as ie:
                    raise osdbError("cannot deploy {} file: {}".
                            format(sql_file, ie)) from None
            else:
                for sql in f.read().split(";"):
                    sql = sql.strip()
                    if not sql:
                        continue
                    try:
                        self.__conn.execute(sql)
                    except sqlalchemy.exc.IntegrityError as ie:
                        raise osdbModuleAlreadyExistsError(
                            "cannot deploy {} file: {}".format(sql_file, ie)) from None

    def exists(self, db=None):
        """
        check for existence of a database object
        """
        check_db = db if db is not None else self.db_name
        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            return False

        database_url = self.set_url_db(self.db_url, check_db)
        logger.debug("check database URL '{}'".format(database_url))

        try:
            if sqlalchemy_utils.database_exists(database_url):
                logger.debug("DB '{}' exists".format(check_db))
                return True
        except sqlalchemy.exc.NoSuchModuleError as me:
            logger.error("cannot check if database {} exists: {}".
                    format(check_db, me))
            raise osdbError("cannot handle {} dialect".
                    format(self.dialect)) from None

        logger.debug("DB does not exist")
        return False

    def exists_role(self, role_name=None):
        """
        check for existence of a role object (PostgreSQL specific)
        """
        # TODO: is any other dialect using the "role" concept?
        if self.dialect != "postgres":
            return False

        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")
            return False

        if role_name is None:
            role_name = 'opensips'

        filter_args = {'rolname': role_name}
        logger.debug("filter argument: '%s'", filter_args)

        role_count = self.__session.query(Roles).\
                filter_by(**filter_args).\
                count()
        logger.debug("Number of matching role instances: '%s'", role_count)

        if role_count >= 1:
            logger.debug("Role instance '%s' exists", role_name)
            return True
        else:
            logger.debug("Role instance '%s' does not exist", role_name)
            return False

    def find(self, table, fields, filter_keys):
        """
        match fields in a given table
        """
       # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")
        if not fields:
            fields = ['*']
        elif type(fields) != list:
            fields = [ fields ]

        where_str = self.get_where(filter_keys)
        statement = "SELECT {} FROM {}{}".format(
                ", ".join(fields),
                table,
                where_str)
        try:
            result = self.__conn.execute(statement)
        except sqlalchemy.exc.SQLAlchemyError as ex:
            logger.error("cannot execute query: {}".format(statement))
            logger.error(ex)
            return None
        return result

    def get_dialect(url):
        """
        extract database dialect from an url
        """
        return url.split('://')[0]

    def get_where(self, filter_keys):
        """
        construct a sql 'where clause' from given filter keys
        """
        if filter_keys:
            where_str = ""
            for k, v in filter_keys.items():
                where_str += " AND {} = ".format(k)
                if type(v) == int:
                    where_str += v
                else:
                    where_str += "'{}'".format(
                            v.translate(str.maketrans({'\'': '\\\''})))
            if where_str != "":
                where_str = " WHERE " + where_str[5:]
        else:
            where_str = ""
        return where_str

    def get_role(self, role_name="opensips"):
        """
        get attibutes of a role object (PostgreSQL specific)
        """
        # TODO: is any other dialect using the "role" concept?
        if self.dialect != "postgres":
            return False

        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")
            return False

        # query elements for the given role
        role_element = self.__session.query(Roles).\
            filter(Roles.rolname == role_name).all()

        # create a dictionary and output the key-value pairs
        for row in role_element:
            #print ("role: ", row.rolname, "(password:", row.rolpassword, "canlogin:", row.rolcanlogin, ")")
            dict = self.row2dict(row)
        for key in sorted(dict, key=lambda k: dict[k], reverse=True):
            print (key + ": " + dict[key])
        logger.debug("role_elements: %s", dict)

    def grant_db_options(self, role_name="opensips", role_options="ALL PRIVILEGES"):
        """
        assign attibutes to a role object (PostgreSQL specific)
        """
        # TODO: is any other dialect using the "role" concept?
        if self.dialect != "postgres":
            return False

        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")

        return True

    def grant_table_options(self, role, table, privs="ALL PRIVILEGES"):
        if self.dialect != "postgres":
            return False

        if not self.__conn:
            raise osdbError("connection not available")

        sqlcmd = "GRANT {} ON TABLE {} TO {}".format(privs, table, role)
        logger.info(sqlcmd)

        try:
            result = self.__conn.execute(sqlcmd)
        except Exception as e:
            logger.exception(e)
            logger.error("failed to grant '%s' to '%s' on table '%s'",
                         privs, role, table)
            return False

        return True

    def has_sqlalchemy():
        """
        check for usability of the SQLAlchemy modules
        """
        return sqlalchemy_available

    def has_dialect(dialect):
        """
        check for support of a given database dialect via SQLAlchemy
        """
        # TODO: do this only for SQLAlchemy
        try:
            sqlalchemy.create_engine('{}://'.format(dialect))
        except sqlalchemy.exc.NoSuchModuleError:
            return False
        return True

    def insert(self, table, keys):
        """
        insert values into table
        """
        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")

        values = ""
        for v in keys.values():
            values += ", "
            if type(v) == int:
                values += v
            else:
                values += "'{}'".format(
                        v.translate(str.maketrans({'\'': '\\\''})))
        statement = "INSERT INTO {} ({}) VALUES ({})".format(
                table, ", ".join(keys.keys()), values[2:])
        try:
            result = self.__conn.execute(statement)
        except sqlalchemy.exc.SQLAlchemyError as ex:
            logger.error("cannot execute query: {}".format(statement))
            logger.error(ex)
            return False
        return result

    def migrate(self, proc_suffix, migrate_scripts, old_db, new_db, tables=[]):
        """
        migrate from source to destination database using SQL schema files
        @flavour: values should resemble: '2.4_to_3.0', '3.0_to_3.1'
        @sp_suffix: stored procedure name suffix, specific to each migration
        """

        if self.dialect != "mysql":
            logger.error("Table data migration is only supported for MySQL!")
            return

        proc_db_migrate = 'OSIPS_DB_MIGRATE_{}'.format(proc_suffix)
        proc_tb_migrate = 'OSIPS_TB_COPY_{}'.format(proc_suffix)

        self.connect(old_db)

        # separately drop DB/table migration stored procedures if already
        # present, since there are issues with multiple statements in 1 import
        try:
            self.__conn.execute(sqlalchemy.sql.text(
                "DROP PROCEDURE IF EXISTS {}".format(proc_db_migrate)).
                    execution_options(autocommit=True))

            self.__conn.execute(sqlalchemy.sql.text(
                "DROP PROCEDURE IF EXISTS {}".format(proc_tb_migrate)).
                    execution_options(autocommit=True))
        except:
            logger.exception("Failed to drop migration stored procedures!")

        for ms in migrate_scripts:
            logger.debug("Importing {}...".format(ms))
            self.exec_sql_file(ms)

        if tables:
            for tb in tables:
                logger.info("Migrating {} data... ".format(tb))
                try:
                    self.__conn.execute(sqlalchemy.sql.text(
                        "CALL {}.{}('{}', '{}', '{}')".format(
                            old_db, proc_tb_migrate, old_db, new_db, tb)))
                except Exception as e:
                    logger.exception(e)
                    logger.error("Failed to migrate '{}' table data, ".format(tb) +
                                    "see above errors!")
        else:
            try:
                self.__conn.execute(sqlalchemy.sql.text(
                    "CALL {}.{}('{}', '{}')".format(
                        old_db, proc_db_migrate, old_db, new_db)))
            except Exception as e:
                logger.exception(e)
                logger.error("Failed to migrate database!")

        print("Finished copying OpenSIPS table data " +
                "into database '{}'!".format(new_db))

    def row2dict(self, row):
        """
        convert SQL table row to python dict
        """
        dict = {}
        for column in row.__table__.columns:
            dict[column.name] = str(getattr(row, column.name))

        return dict

    def update(self, table, update_keys, filter_keys=None):
        """
        update table
        """
        # TODO: do this only for SQLAlchemy
        if not self.__conn:
            raise osdbError("connection not available")

        update_str = ""
        for k, v in update_keys.items():
            update_str += ", {} = ".format(k)
            if type(v) == int:
                update_str += v
            else:
                update_str += "'{}'".format(
                        v.translate(str.maketrans({'\'': '\\\''})))
        where_str = self.get_where(filter_keys)
        statement = "UPDATE {} SET {}{}".format(table,
                update_str[2:], where_str)
        try:
            result = self.__conn.execute(statement)
        except sqlalchemy.exc.SQLAlchemyError as ex:
            logger.error("cannot execute query: {}".format(statement))
            logger.error(ex)
            return False
        return result


    @staticmethod
    def get_db_engine():
        if cfg.exists('database_admin_url'):
            engine = osdb.get_url_driver(cfg.get('database_admin_url'))
        elif cfg.exists('database_url'):
            engine = osdb.get_url_driver(cfg.get('database_url'))
        else:
            engine = "mysql"

        if engine not in SUPPORTED_BACKENDS:
            logger.error("bad database engine ({}), supported: {}".format(
                         engine, " ".join(SUPPORTED_BACKENDS)))
            return None
        return engine


    @staticmethod
    def get_db_host():
        if cfg.exists('database_admin_url'):
            return osdb.get_url_host(cfg.get('database_admin_url'))
        elif cfg.exists('database_url'):
            return osdb.get_url_host(cfg.get('database_url'))

        return "localhost"


    @staticmethod
    def set_url_db(url, db):
        """
        Force a given database @url string to include the given @db
        """
        at_idx = url.find('@')
        if at_idx < 0:
            logger.error("Bad database URL: {}, missing host part".format(url))
            return None

        db_idx = url.find('/', at_idx)
        if db_idx < 0:
            return url + '/' + db
        else:
            return url[:db_idx+1] + db


    @staticmethod
    def set_url_driver(url, driver):
        return driver + url[url.find(':'):]


    @staticmethod
    def set_url_password(url, password):
        url = make_url(url)
        url.password = password
        return str(url)


    @staticmethod
    def set_url_host(url, host):
        url = make_url(url)
        url.host = host
        return str(url)


    @staticmethod
    def get_url_driver(url, capitalize=False):
        if capitalize:
            driver = make_url(url).drivername.lower()
            capitalized = {
                'mysql': 'MySQL',
                'postgres': 'PostgreSQL',
                'sqlite': 'SQLite',
                'oracle': 'Oracle',
                }
            return capitalized.get(driver, driver.capitalize())
        else:
            return make_url(url).drivername.lower()


    @staticmethod
    def get_url_user(url):
        return make_url(url).username


    @staticmethod
    def get_url_pswd(url):
        return make_url(url).password


    @staticmethod
    def get_url_host(url):
        return make_url(url).host