def cli(ctx, debug): if ctx.obj is None: ctx.obj = {} ctx.obj['backend'] = PostgresqlBackend(parse_uri(settings.POSTGRES_DSN), migration_table="yoyo_migrations") if debug: loglevel = logging.DEBUG else: loglevel = logging.INFO logging.basicConfig(level=loglevel, format='%(message)s')
def _parse_uri(uri): """Patch original yoyo's parse_uri() function. This patch adds the client_flag CLIENT_MULTI_STATEMENTS for mysql backends powered by the pymysql driver. This is used to execute multiple queries in a single migration step instead of going through the hassle of breaking up some steps that don't make sense to break up in the first place. """ result = parse_uri(uri) if result.scheme == "mysql": result.args["client_flag"] = CLIENT.MULTI_STATEMENTS return result
def test_it_parses_all_fields(self): parsed = parse_uri("protocol://*****:*****@server:666/db?k=1") assert tuple(parsed) == ( "protocol", "scott", "tiger", "server", 666, "db", { "k": "1" }, )
def test_connections(get_dbapi_module): from yoyo import backends u = parse_uri("odbc://*****:*****@db.example.org:42/northwind?foo=bar") cases = [ ( backends.ODBCBackend, "pyodbc", call("UID=scott;PWD=tiger;ServerName=db.example.org;" "Port=42;Database=northwind;foo=bar"), ), ( backends.MySQLBackend, "pymysql", call( user="******", passwd="tiger", host="db.example.org", port=42, db="northwind", foo="bar", ), ), ( backends.SQLiteBackend, "sqlite3", call( "northwind", detect_types=get_dbapi_module("sqlite3").PARSE_DECLTYPES, ), ), ( backends.PostgresqlBackend, "psycopg2", call( user="******", password="******", port=42, host="db.example.org", dbname="northwind", foo="bar", ), ), ] for cls, driver_module, connect_args in cases: cls(u, "_yoyo_migration") assert get_dbapi_module.call_args == call(driver_module) assert get_dbapi_module().connect.call_args == connect_args
def _get_backend(uri, migration_table=default_migration_table): """Patch of yoyo's original get_backend() function. Original implementation is here: https://hg.sr.ht/~olly/yoyo/browse/yoyo/connections.py?rev=387b00ee5596b63cb93bf79fa3d4112576679bc0#L82 Patching is needed for the same reason documented elsewhere in this codebase: https://github.com/lekha/jeopardy/commit/76e8b7ebd8abb0e2ad6ff7547ad74ae7b60d1690 """ if uri.startswith("mysql://"): parsed_uri = parse_uri(uri) parsed_uri.args["client_flag"] = CLIENT.MULTI_STATEMENTS backend = MySQLBackend(parsed_uri, migration_table) else: backend = get_backend(uri, migration_table) return backend
def get_backend(args, config): try: dburi = args.database except AttributeError: dburi = config.get("DEFAULT", "database") try: migration_table = args.migration_table except AttributeError: migration_table = config.get("DEFAULT", "migration_table") if dburi is None: raise InvalidArgument("Please specify a database uri") try: if args.prompt_password: password = getpass("Password for %s: " % dburi) parsed = connections.parse_uri(dburi) dburi = parsed._replace(password=password).uri except AttributeError: pass return connections.get_backend(dburi, migration_table)
def test_it_returns_absolute_paths_for_sqlite(self): assert parse_uri("sqlite:////foo/bar.db").database == "/foo/bar.db"
def test_it_requires_scheme(self): with pytest.raises(BadConnectionURI): parse_uri("//scott:tiger@localhost/db")
def test_it_parses_escaped_username(self): parsed = parse_uri("protocol://scott%40example.org:tiger@localhost/db") assert parsed.username == "*****@*****.**"
def _test_parse_uri(connection_string, expected_uri_tuple): uri_tuple = parse_uri(connection_string) assert isinstance(uri_tuple, tuple) assert (uri_tuple == expected_uri_tuple)
def main(argv=None): argparser = make_argparser() args = argparser.parse_args(argv) if args.verbosity_level: verbosity_level = args.verbosity_level else: verbosity_level = args.verbose verbosity_level = min(verbosity_level, max(verbosity_levels)) verbosity_level = max(verbosity_level, min(verbosity_levels)) configure_logging(verbosity_level) command = args.command migrations_dir = os.path.normpath(os.path.abspath(args.migrations_dir)) dburi = args.database config_path = os.path.join(migrations_dir, '.yoyo-migrate') config = readconfig(config_path) if dburi is None and args.cache: try: logger.debug("Looking up connection string for %r", migrations_dir) dburi = config.get('DEFAULT', 'dburi') except (ValueError, NoSectionError, NoOptionError): pass if args.migration_table: migration_table = args.migration_table else: try: migration_table = config.get('DEFAULT', 'migration_table') except (ValueError, NoSectionError, NoOptionError): migration_table = None # Earlier versions had a bug where the migration_table could be set to the # string 'None'. if migration_table in (None, 'None'): migration_table = default_migration_table config.set('DEFAULT', 'migration_table', migration_table) if dburi is None: argparser.error("Please specify command, migrations directory and " "database connection string arguments") if args.prompt_password: password = getpass('Password for %s: ' % dburi) scheme, username, _, host, port, database, db_params = parse_uri(dburi) dburi = unparse_uri( (scheme, username, password, host, port, database, db_params)) # Cache the database this migration set is applied to so that subsequent # runs don't need the dburi argument. Don't cache anything in batch mode - # we can't prompt to find the user's preference. if args.cache and not args.batch: if not config.has_option('DEFAULT', 'dburi'): response = prompt( "Save connection string to %s for future migrations?\n" "This is saved in plain text and " "contains your database password." % (config_path, ), "yn") if response == 'y': config.set('DEFAULT', 'dburi', dburi) elif config.get('DEFAULT', 'dburi') != dburi: response = prompt( "Specified connection string differs from that saved in %s. " "Update saved connection string?" % (config_path, ), "yn") if response == 'y': config.set('DEFAULT', 'dburi', dburi) config.set('DEFAULT', 'migration_table', migration_table) saveconfig(config, config_path) conn, paramstyle = connect(dburi) migrations = read_migrations(conn, paramstyle, migrations_dir, migration_table=migration_table) if args.match: migrations = migrations.filter( lambda m: re.search(args.match, m.id) is not None) if not args.all: if command in ['apply']: migrations = migrations.to_apply() elif command in ['reapply', 'rollback']: migrations = migrations.to_rollback() if not args.batch: migrations = prompt_migrations(conn, paramstyle, migrations, command) if not args.batch and migrations: if prompt( command.title() + plural(len(migrations), " %d migration", " %d migrations") + " to %s?" % dburi, "Yn") != 'y': return 0 if command == 'reapply': migrations.rollback(args.force) migrations.apply(args.force) elif command == 'apply': migrations.apply(args.force) elif command == 'rollback': migrations.rollback(args.force)
def main(argv=None): argparser = make_argparser() args = argparser.parse_args(argv) if args.verbosity_level: verbosity_level = args.verbosity_level else: verbosity_level = args.verbose verbosity_level = min(verbosity_level, max(verbosity_levels)) verbosity_level = max(verbosity_level, min(verbosity_levels)) configure_logging(verbosity_level) command = args.command migrations_dir = os.path.normpath(os.path.abspath(args.migrations_dir)) dburi = args.database config_path = os.path.join(migrations_dir, '.yoyo-migrate') config = readconfig(config_path) if dburi is None and args.cache: try: logger.debug("Looking up connection string for %r", migrations_dir) dburi = config.get('DEFAULT', 'dburi') except (ValueError, NoSectionError, NoOptionError): pass if args.migration_table: migration_table = args.migration_table else: try: migration_table = config.get('DEFAULT', 'migration_table') except (ValueError, NoSectionError, NoOptionError): migration_table = None # Earlier versions had a bug where the migration_table could be set to the # string 'None'. if migration_table in (None, 'None'): migration_table = default_migration_table config.set('DEFAULT', 'migration_table', migration_table) if dburi is None: argparser.error( "Please specify command, migrations directory and " "database connection string arguments" ) if args.prompt_password: password = getpass('Password for %s: ' % dburi) scheme, username, _, host, port, database, db_params = parse_uri(dburi) dburi = unparse_uri((scheme, username, password, host, port, database, db_params)) # Cache the database this migration set is applied to so that subsequent # runs don't need the dburi argument. Don't cache anything in batch mode - # we can't prompt to find the user's preference. if args.cache and not args.batch: if not config.has_option('DEFAULT', 'dburi'): response = prompt( "Save connection string to %s for future migrations?\n" "This is saved in plain text and " "contains your database password." % (config_path,), "yn" ) if response == 'y': config.set('DEFAULT', 'dburi', dburi) elif config.get('DEFAULT', 'dburi') != dburi: response = prompt( "Specified connection string differs from that saved in %s. " "Update saved connection string?" % (config_path,), "yn" ) if response == 'y': config.set('DEFAULT', 'dburi', dburi) config.set('DEFAULT', 'migration_table', migration_table) saveconfig(config, config_path) conn, paramstyle = connect(dburi) migrations = read_migrations(conn, paramstyle, migrations_dir, migration_table=migration_table) if args.match: migrations = migrations.filter( lambda m: re.search(args.match, m.id) is not None) if not args.all: if command in ['apply']: migrations = migrations.to_apply() elif command in ['reapply', 'rollback']: migrations = migrations.to_rollback() if not args.batch: migrations = prompt_migrations(conn, paramstyle, migrations, command) if not args.batch and migrations: if prompt(command.title() + plural(len(migrations), " %d migration", " %d migrations") + " to %s?" % dburi, "Yn") != 'y': return 0 if command == 'reapply': migrations.rollback(args.force) migrations.apply(args.force) elif command == 'apply': migrations.apply(args.force) elif command == 'rollback': migrations.rollback(args.force)