TODO NOW:
+- ImportError is too broad a catch for make() -- see patch from afarrell
+
- The calling web code invocations are a mess, with stubs living
in the install, deploy modules and the real deal living in util. Furthermore,
we use the scripts-specific heuristic to determine where the app
lives, and the only reason my test scripts work is because they
get manually fed the domain and path by my environment variables.
- We will record the URL used for the initial installation, and save it in
- .scripts/url. If autodetection in either direction is
- available, we verify this value against the actual file path the installation
- lives in (for the scripts case, we can do a file-level comparison because we
- know the web root of any given file). If they mismatch, we error out
- and have someone manually resolve the problem. If autodetection is not
- available, we use the saved .scripts/url for operations.
+ Use system similar to database, with option for explicit override,
+ but otherwise attempting to determine from ambient code
+
+- Setting PATH and WIZARD_SRV_PATH from a test script: mention in docs
+- If you try to do an install on scripts w/o sql, it will sign you up but fail to write
+ the sql.cnf file. This sucks.
-- wizard install wordpress should ask for password
-- Test code should auto-nuke the database using `wizard remove` before doing a new install
+- wizard install wordpress should ask for password. One problem with this is that
+ Wordpress will still send mail with the wrong username and password, so Wordpress
+ will need to be patched to not do that. Alternatively we can initally set the admin
+ email to a null address and then fix it manually.
- git diff :1:$file :2:$file to find out what the user did, or is it :3:?
- php.ini needs to get substituted!
- --raw parameter for install which means an arbitrary commit can be installed
# If false, no module index is generated.
#latex_use_modindex = True
-intersphinx_mapping = {'http://docs.python.org/dev': None}
+intersphinx_mapping = {
+ 'http://docs.python.org/dev': None,
+ 'http://www.sqlalchemy.org/docs/05/index.html': None,
+ }
todo_include_todos = True
these arguments inside :meth:`~wizard.app.Application.install` via
the ``options`` value.
+In particular, ``options.dsn`` is a :class:`sqlalchemy.engine.url.URL`
+which contains member variables such as :meth:`~sqlalchemy.engine.url.URL.username`,
+:meth:`~sqlalchemy.engine.url.URL.password`, :meth:`~sqlalchemy.engine.url.URL.host` and
+:meth:`~sqlalchemy.engine.url.URL.database` which you can use to pass in POST.
+
Some tips and tricks for writing :meth:`wizard.app.Application.install`:
* Some configuration file generators will get unhappy if the
* You should log any web page output using :func:`logging.debug`.
* If you need to manually manipulate the database afterwards, you
- can use :func:`wizard.sql.mysql_connect` (passing it ``options``)
+ can use :func:`wizard.sql.connect` (passing it ``options.dsn``)
to get a `SQLAlchemy metadata object
<http://www.sqlalchemy.org/docs/05/sqlexpression.html>`_, which can
consequently be queried. For convenience, we've bound metadata
env WIZARD_DEBUG=1 ./test-install-wordpress.sh
+The test scripts will try to conserve databases by running ``wizard remove`` on the
+old directory, but this requires :meth:`~wizard.app.remove` be implemented.
+Most of the time (namely, for single database setups), this simple template will suffice:
+
+.. code-block:: python
+
+ class Application(app.Application):
+ # ...
+ def remove(self, deployment)
+ app.remove_database(deployment)
+
Versioning config
-----------------
Here is a sample file::
+ MYSQL_ARGS="-uroot -ppassword"
+
export WIZARD_WEB_HOST="localhost"
export WIZARD_WEB_PATH="/wizard/tests/$TESTDIR"
- export WIZARD_MYSQL_HOST="localhost"
- export WIZARD_MYSQL_USER="root"
- export WIZARD_MYSQL_PASSWORD="password"
- export WIZARD_MYSQL_DB="wizard_test_$TESTID"
+ export WIZARD_DSN="mysql://root:password@localhost/wizard_test_$TESTID"
export WIZARD_EMAIL="bob@example.com"
-You will need to specify all of these environment variables. :envvar:`WIZARD_WEB_HOST`
-and :envvar:`WIZARD_WEB_PATH` indicate Wizard's configuration with respect to
-your web server. ``http://$WIZARD_WEB_HOST/$WIZARD_WEB_PATH`` will be the location
-that a newly installed application will be accessible. You will notice that
-we used :envvar:`TESTDIR`; this is the directory that will be created in
-:file:`tests` for the application. :envvar:`WIZARD_MYSQL_HOST`,
-:envvar:`WIZARD_MYSQL_USER`, :envvar:`WIZARD_MYSQL_PASSWORD` and :envvar:`WIZARD_MYSQL_DB`
-are standard configuration variables for accessing a local MySQL database. You can use
-:envvar:`TESTID` to uniquely identify any particular test. Finally, :envvar:`WIZARD_EMAIL`
-is any email address you own that will be configured as an administrative email.
+You will need to specify all of these environment variables. Those prefixed
+with ``WIZARD`` are directly used by Wizard, while the ``MYSQL`` environment
+variables are used if a test script wants to interactive directly with a
+MYSQL database. We don't quite have a good story for alternative databases
+in test scripts.
-It may be useful to include a little bit of code to handle dropping and creating
-databases. Here is a sample::
+* :envvar:`WIZARD_WEB_HOST` and :envvar:`WIZARD_WEB_PATH` indicate Wizard's
+ configuration with respect to your web server.
+ ``http://$WIZARD_WEB_HOST/$WIZARD_WEB_PATH`` will be the location that a newly
+ installed application will be accessible. You will notice that we used
+ :envvar:`TESTDIR`; this is the directory that will be created in :file:`tests`
+ for the application.
+* :envvar:`WIZARD_DSN` is the database source name, which
+ should be used to access a MySQL database (as Wizard does not contain support
+ for any other database system yet.) You can use :envvar:`TESTID` to uniquely
+ identify any particular test.
+* :envvar:`WIZARD_EMAIL` is any email address you
+ own that will be configured as an administrative email.
- MYSQL_ARGS="-uroot -prootpassword"
- mysql $MYSQL_ARGS -e "DROP DATABASE \`$WIZARD_MYSQL_DB\`;" || true
- mysql $MYSQL_ARGS -e "CREATE DATABASE \`$WIZARD_MYSQL_DB\`;"
echo "BOOM" > "$FROB"
# destroy the database
-mysql $MYSQL_ARGS -e "DROP DATABASE \`$WIZARD_MYSQL_DB\`;"
-mysql $MYSQL_ARGS -e "CREATE DATABASE \`$WIZARD_MYSQL_DB\`;"
+mysql $MYSQL_ARGS -e "DROP DATABASE \``wizard database .`\`;"
+mysql $MYSQL_ARGS -e "CREATE DATABASE \``wizard database .`\`;"
BACKUP=`wizard restore | head -n1`
wizard restore "$BACKUP"
if [ -e "$TESTDIR" ]; then
echo "Removing previous $TESTDIR folder..."
- rm -Rf "$TESTDIR"
+ wizard remove "$TESTDIR" || rm -Rf "$TESTDIR"
fi
import shlex
import logging
import shutil
+import sqlalchemy
import wizard
from wizard import resolve, scripts, shell, util
#: Instance of :class:`wizard.install.ArgSchema` that defines the arguments
#: this application requires.
install_schema = None
+ #: Name of the database that this application uses, i.e. ``mysql`` or
+ #: ``postgres``. If we end up supporting multiple databases for a single
+ #: application, there should also be a value for this in
+ #: :class:`wizard.deploy.Deployment`; the value here is merely the preferred
+ #: value.
+ database = None
def __init__(self, name):
self.name = name
self.versions = {}
for k,extractor in self.extractors.items():
result[k] = extractor(deployment)
return result
+ def dsn(self, deployment):
+ """
+ Returns the deployment specific database URL. Uses the override file
+ in :file:`.scripts` if it exists, and otherwise attempt to extract the
+ variables from the source files.
+
+ Under some cases, the database URL will contain only the database
+ property, and no other values. This indicates that the actual DSN
+ should be determined from the environment.
+
+ This function might return ``None``.
+
+ .. note::
+
+ We are allowed to batch these two together, because the full precedence
+ chain for determining the database of an application combines these
+ two together. If this was not the case, we would have to call
+ :meth:`databaseUrlFromOverride` and :meth:`databaseUrlFromExtract` manually.
+ """
+ url = self.dsnFromOverride(deployment)
+ if url:
+ return url
+ return self.dsnFromExtract(deployment)
+ def dsnFromOverride(self, deployment):
+ """
+ Extracts database URL from an explicit dsn override file.
+ """
+ try:
+ return sqlalchemy.engine.url.make_url(open(deployment.dsn_file).read().strip())
+ except IOError:
+ return None
+ def dsnFromExtract(self, deployment):
+ """
+ Extracts database URL from a deployment, and returns them as
+ a :class:`sqlalchemy.engine.url.URL`. Returns ``None`` if we
+ can't figure it out: i.e. the conventional variables are not defined
+ for this application.
+ """
+ if not self.database:
+ return None
+ vars = self.extract(deployment)
+ names = ("WIZARD_DBSERVER", "WIZARD_DBUSER", "WIZARD_DBPASSWORD", "WIZARD_DBNAME")
+ host, user, password, database = (shlex.split(vars[x])[0] if vars[x] is not None else None for x in names)
+ # XXX: You'd have to put support for an explicit different database
+ # type here
+ return sqlalchemy.engine.url.URL(self.database, username=user, password=password, host=host, database=database)
def parametrize(self, deployment, ref_deployment):
"""
Takes a generic source checkout and parametrizes it according to the
should provide an implementation.
"""
raise NotImplementedError
+ def remove(self, deployment, options):
+ """
+ Run for 'wizard remove' to delete all database and non-local
+ file data. This assumes that the current working directory is
+ the deployment. Subclasses should provide an implementation.
+ """
+ raise NotImplementedError
def detectVersion(self, deployment):
"""
Checks source files to determine the version manually. This assumes
return subs
return h
-# XXX: rename to show that it's mysql specific
def backup_database(outdir, deployment):
"""
- Generic database backup function for MySQL. Assumes that ``WIZARD_DBNAME``
- is extractable, and that :func:`wizard.scripts.get_sql_credentials`
- works.
+ Generic database backup function for MySQL.
"""
+ # XXX: Change this once deployments support multiple dbs
+ if deployment.application.database == "mysql":
+ return backup_mysql_database(outdir, deployment)
+ else:
+ raise NotImplementedError
+
+def backup_mysql_database(outdir, deployment):
sh = shell.Shell()
outfile = os.path.join(outdir, "db.sql")
try:
- sh.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment))
+ sh.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
sh.call("gzip", "--best", outfile)
except shell.CallError as e:
shutil.rmtree(outdir)
def restore_database(backup_dir, deployment):
"""
- Generic database restoration function for MySQL. See :func:`backup_database`
- for the assumptions that we make.
+ Generic database restoration function for MySQL.
"""
+ # XXX: see backup_database
+ if deployment.application.database == "mysql":
+ return restore_mysql_database(backup_dir, deployment)
+ else:
+ raise NotImplementedError
+
+def restore_mysql_database(backup_dir, deployment):
sh = shell.Shell()
if not os.path.exists(backup_dir):
raise RestoreFailure("Backup %s doesn't exist", backup_dir.rpartition("/")[2])
sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
sh.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
sql.seek(0)
- sh.call("mysql", *get_mysql_args(deployment), stdin=sql)
+ sh.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
sql.close()
-def get_mysql_args(d):
+def remove_database(deployment):
+ """
+ Generic database removal function.
+ """
+ engine = sqlalchemy.create_engine(deployment.dsn)
+ engine.execute("DROP DATABASE `%s`" % deployment.dsn.database)
+
+def get_mysql_args(dsn):
"""
Extracts arguments that would be passed to the command line mysql utility
from a deployment.
"""
- # XXX: add support for getting these out of options
- vars = d.extract()
- if 'WIZARD_DBNAME' not in vars:
- raise BackupFailure("Could not determine database name")
- triplet = scripts.get_sql_credentials(vars)
args = []
- if triplet is not None:
- server, user, password = triplet
- args += ["-h", server, "-u", user, "-p" + password]
- name = shlex.split(vars['WIZARD_DBNAME'])[0]
- args.append(name)
+ if dsn.host:
+ args += ["-h", dsn.host]
+ if dsn.username:
+ args += ["-u", dsn.username]
+ if dsn.password:
+ args += ["-p" + dsn.password]
+ args += [dsn.database]
return args
class Error(wizard.Error):
ERROR: Restore script failed, details:
%s""" % self.details
+
+class RemoveFailure(Failure):
+ """Remove script failed."""
+ #: String details of failure
+ details = None
+ def __init__(self, details):
+ self.details = details
+ def __str__(self):
+ return """
+
+ERROR: Remove script failed, details:
+
+%s""" % self.details
})
class Application(app.Application):
+ database = "mysql"
parametrized_files = ['LocalSettings.php'] + php.parametrized_files
deprecated_keys = set(['WIZARD_IP']) | php.deprecated_keys
extractors = app.make_extractors(seed)
extractors.update(php.extractors)
substitutions = app.make_substitutions(seed)
substitutions.update(php.substitutions)
- install_schema = install.ArgSchema("mysql", "admin", "email", "title")
+ install_schema = install.ArgSchema("db", "admin", "email", "title")
def checkConfig(self, deployment):
return os.path.isfile("LocalSettings.php")
def detectVersion(self, deployment):
'Sitename': options.title,
'EmergencyContact': options.email,
'LanguageCode': 'en',
- 'DBserver': options.mysql_host,
- 'DBname': options.mysql_db,
- 'DBuser': options.mysql_user,
- 'DBpassword': options.mysql_password,
- 'DBpassword2': options.mysql_password,
+ 'DBserver': options.dsn.host,
+ 'DBname': options.dsn.database,
+ 'DBuser': options.dsn.username,
+ 'DBpassword': options.dsn.password,
+ 'DBpassword2': options.dsn.password,
'defaultEmail': options.email,
'SysopName': options.admin_name,
'SysopPass': options.admin_password,
app.backup_database(backup_dir, deployment)
def restore(self, deployment, backup_dir, options):
app.restore_database(backup_dir, deployment)
+ def remove(self, deployment, options):
+ app.remove_database(deployment)
Application.resolutions = {
'LocalSettings.php': [
})
class Application(app.Application):
+ database = "mysql"
parametrized_files = ['wp-config.php'] + php.parametrized_files
extractors = app.make_extractors(seed)
extractors.update(php.extractors)
substitutions = app.make_substitutions(seed)
substitutions.update(php.substitutions)
- install_schema = install.ArgSchema("mysql", "email", "title")
+ install_schema = install.ArgSchema("db", "email", "title")
deprecated_keys = set(['WIZARD_SECRETKEY'])
def download(self, version):
return "http://wordpress.org/wordpress-%s.tar.gz" % version
util.soft_unlink("wp-config.php")
post_setup_config = {
- 'dbhost': options.mysql_host,
- 'uname': options.mysql_user,
- 'dbname': options.mysql_db,
- 'pwd': options.mysql_password,
+ 'dbhost': options.dsn.host,
+ 'uname': options.dsn.username,
+ 'dbname': options.dsn.database,
+ 'pwd': options.dsn.password,
'prefix': '',
'submit': 'Submit',
'step': '2',
raise app.InstallFailure()
# not sure what to do about this
- meta = sql.mysql_connect(options)
+ meta = sql.connect(options.dsn)
wp_options = meta.tables["wp_options"]
wp_options.update().where(wp_options.c.option_name == 'siteurl').values(option_value=options.web_path).execute()
wp_options.update().where(wp_options.c.option_name == 'home').values(option_value="http://%s%s" % (options.web_host, options.web_path)).execute() # XXX: what if missing leading slash; this should be put in a function
app.backup_database(backup_dir, deployment)
def restore(self, deployment, backup_dir, options):
app.restore_database(backup_dir, deployment)
+ def remove(self, deployment, options):
+ app.remove_database(deployment)
--- /dev/null
+from wizard import deploy, command
+
+def main(argv, baton):
+ options, args = parse_args(argv, baton)
+ dir = args[0]
+ deployment = deploy.ProductionCopy(dir)
+ print deployment.dsn.database
+
+def parse_args(argv, baton):
+ usage = """usage: %prog database DIR
+
+Prints the name of the database an application is using.
+Maybe in the future this will print more information."""
+ parser = command.WizardOptionParser(usage)
+ options, args = parser.parse_all(argv)
+ if len(args) > 1:
+ parser.error("too many arguments")
+ if len(args) == 0:
+ parser.error("must specify directory")
+ return options, args
# get configuration
schema = application.install_schema
- schema.commit(dir)
+ schema.commit(application, dir)
options = None
opthandler = installopt.Controller(dir, schema)
parser = command.WizardOptionParser("""usage: %%prog install %s DIR [ -- SETUPARGS ]
--- /dev/null
+import shutil
+
+from wizard import command, deploy, shell
+
+def main(argv, baton):
+ options, args = parse_args(argv, baton)
+ dir = args[0]
+ shell.drop_priviledges(dir, options.log_file)
+ deployment = deploy.ProductionCopy(dir)
+ deployment.verify()
+ deployment.remove(options)
+ shutil.rmtree(dir)
+
+def parse_args(argv, baton):
+ usage = """usage: %prog remove DIR
+
+Removes an autoinstall directory, deleting any databases along
+with it. Will refuse to remove a non-autoinstall directory. Be
+careful: this will also destroy all backups!"""
+ parser = command.WizardOptionParser(usage)
+ options, args = parser.parse_all(argv)
+ if len(args) > 1:
+ parser.error("too many arguments")
+ if len(args) == 0:
+ parser.error("must specify directory")
+ return options, args
+
import datetime
import wizard
-from wizard import app, git, old_log, scripts, shell, util
+from wizard import app, git, old_log, scripts, shell, sql, util
## -- Global Functions --
# some cache variables
self._read_cache = {}
self._old_log = None
+ self._dsn = None
def invalidateCache(self):
"""
Invalidates all cached variables. This currently applies to
"""The absolute path of the ``.scripts/version`` file."""
return os.path.join(self.scripts_dir, 'version')
@property
+ def dsn_file(self):
+ """The absolute path of the :file:`.scripts/dsn` override file."""
+ return os.path.join(self.scripts_dir, 'dsn')
+ @property
def application(self):
"""The :class:`app.Application` of this deployment."""
return self.app_version.application
appname = shell.Shell().eval("git", "config", "remote.origin.url").rpartition("/")[2].partition(".")[0]
self._app_version = app.ApplicationVersion.make(appname, "unknown")
return self._app_version
+ @property
+ def dsn(self):
+ if not self._dsn:
+ self._dsn = sql.fill_url(self.application.dsn(self))
+ return self._dsn
@staticmethod
def parse(line):
"""
"""
backup_dir = os.path.join(".scripts", "backups", backup)
return self.application.restore(self, backup_dir, options)
+ @chdir_to_location
+ def remove(self, options):
+ """
+ Deletes all non-local or non-filesystem data (such as databases) that
+ this application uses.
+ """
+ self.application.remove(self, options)
def verifyWeb(self):
"""
Checks if the autoinstall is viewable from the web.
import os
import logging
+import sqlalchemy
import wizard
from wizard import scripts, shell, util
+def dsn_callback(options):
+ if not isinstance(options.dsn, sqlalchemy.engine.url.URL):
+ options.dsn = sqlalchemy.engine.url.make_url(options.dsn)
+
# XXX: This is in the wrong place
def fetch(options, path, post=None):
"""
"""
return {
'web': WebArgSet(),
- 'mysql': MysqlArgSet(),
+ 'db': DbArgSet(),
'admin': AdminArgSet(),
'email': EmailArgSet(),
'title': TitleArgSet(),
may create an appropriate database for the user.
"""
side_effects = True
- provides = frozenset(["mysql_host", "mysql_user", "mysql_password", "mysql_db"])
- def __init__(self, dir):
+ provides = frozenset(["dsn"])
+ def __init__(self, application, dir):
+ self.application = application
self.dir = dir
def prepare(self):
"""Uses :func:`wizard.scripts.get_sql_credentials`"""
- self._triplet = scripts.get_sql_credentials()
- if not self._triplet:
+ if self.application.database != "mysql":
+ raise StrategyFailed
+ try:
+ self._triplet = shell.Shell().eval("/mit/scripts/sql/bin/get-password").split()
+ except shell.CallError:
raise StrategyFailed
self._username = os.getenv('USER')
if self._username is None:
def execute(self, options):
"""Creates a new database for the user using ``get-next-database`` and ``create-database``."""
sh = shell.Shell()
- options.mysql_host, options.mysql_user, options.mysql_password = self._triplet
+ host, username, password = self._triplet
# race condition
- options.mysql_db = self._username + '+' + sh.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir))
- sh.call("/mit/scripts/sql/bin/create-database", options.mysql_db)
+ database = self._username + '+' + sh.eval("/mit/scripts/sql/bin/get-next-database", os.path.basename(self.dir))
+ sh.call("/mit/scripts/sql/bin/create-database", name)
+ options.dsn = sqlalchemy.engine.url.URL("mysql", username=username, password=password, host=host, database=database)
class ScriptsEmailStrategy(Strategy):
"""Performs script specific guess for email."""
type = None
#: If true, is a password
password = False
+ #: Callback that this argument wants to get run on options after finished
+ callback = None
@property
def envname(self):
"""Name of the environment variable containing this arg."""
"""
#: The :class:`Arg` objects that compose this argument set.
args = None
+ # XXX: probably could also use a callback attribute
def __init__(self):
self.args = []
Arg("web_path", type="PATH", help="Relative path to your application root"),
]
-class MysqlArgSet(ArgSet):
- """Common arguments for applications that use a MySQL database."""
+class DbArgSet(ArgSet):
+ """Common arguments for applications that use a database."""
def __init__(self):
self.args = [
- Arg("mysql_host", type="HOST", help="Host that your MySQL server lives on"),
- Arg("mysql_db", type="DB", help="Name of the database to populate"),
- Arg("mysql_user", type="USER", help="Name of user to access database with"),
- Arg("mysql_password", type="PWD", password=True, help="Password of the database user"),
+ Arg("dsn", type="DSN", help="Database Source Name, i.e. mysql://user:pass@host/dbname", callback=dsn_callback),
]
class AdminArgSet(ArgSet):
def add(self, arg):
"""Adds an argument to our schema."""
self.args[arg.name] = arg
- def commit(self, dir):
+ def commit(self, application, dir):
"""Populates :attr:`strategies` and :attr:`provides`"""
self.strategies = []
self.provides = set()
raw_strategies = [
EnvironmentStrategy(self),
ScriptsWebStrategy(dir),
- ScriptsMysqlStrategy(dir),
+ ScriptsMysqlStrategy(application, dir),
ScriptsEmailStrategy(),
]
for arg in self.args.values():
Load values from strategy. Must be called after :meth:`commit`. We
omit strategies whose provided variables are completely specified
already. Will raise :exc:`MissingRequiredParam` if strategies aren't
- sufficient to fill all options.
+ sufficient to fill all options. It will then run any callbacks on
+ arguments.
"""
unfilled = set(name for name in self.args if getattr(options, name) is None)
missing = unfilled - self.provides
if getattr(options, name) is not None:
logging.warning("Overriding pre-specified value for %s", name)
strategy.execute(options)
+ for arg in self.args.values():
+ if arg.callback is None:
+ continue
+ arg.callback(options)
class Error(wizard.Error):
"""Base error class for this module."""
import wizard
from wizard import shell, util
-def get_sql_credentials(vars=None):
- """
- Attempts to determine a user's MySQL credentials. They are
- returned as a three-tuple (host, user, password).
- """
- sh = shell.Shell()
- host = os.getenv("WIZARD_MYSQL_HOST")
- user = os.getenv("WIZARD_MYSQL_USER")
- password = os.getenv("WIZARD_MYSQL_PASSWORD")
- if host is not None and user is not None and password is not None:
- return (host, user, password)
- # XXX: this is very fragile
- elif vars and "WIZARD_DBSERVER" in vars and "WIZARD_DBUSER" in vars and "WIZARD_DBPASSWORD" in vars:
- return (shlex.split(vars[x])[0] for x in ("WIZARD_DBSERVER", "WIZARD_DBUSER", "WIZARD_DBPASSWORD"))
- try:
- tuple = sh.eval("/mit/scripts/sql/bin/get-password").split()
- if len(tuple) == 3:
- return tuple
- return None
- except shell.CallError:
- return None
-
def get_web_host_and_path(dir=None):
"""
Attempts to determine webhost and path for the current directory
import sqlalchemy
-def mysql_connect(options):
+# We're going to use sqlalchemy.engine.url.URL as our database
+# info intermediate object
+
+def connect(url):
"""Convenience method for connecting to a MySQL database."""
- engine = sqlalchemy.create_engine(sqlalchemy.engine.url.URL(
- "mysql",
- username=options.mysql_user,
- password=options.mysql_password,
- host=options.mysql_host,
- database=options.mysql_db,
- ))
+ engine = sqlalchemy.create_engine(url)
meta = sqlalchemy.MetaData()
meta.bind = engine
meta.reflect()
return meta
+
+def fill_url(url):
+ """
+ If the URL has a database name but no other values, it will
+ use the global configuration, and then try the database name.
+ """
+ if not url:
+ return None
+ if not url.database:
+ # it's hopeless
+ return url
+ # omitted port and query
+ if any((url.host, url.username, url.password)):
+ # don't try for defaults if a few of these were set
+ return url
+ # this is hook stuff
+ if url.driver == "mysql":
+ try:
+ url.host, url.username, url.password = sh.eval("/mit/scripts/sql/bin/get-password").split()
+ return url
+ except shell.CallError:
+ pass
+ dsn = os.getenv("WIZARD_DSN")
+ old_url = url
+ url = sqlalchemy.engine.url.make_url(dsn)
+ url.database = old_url.database
+ return url