diff --git a/setup.py b/setup.py index e38a9a09d..2cb27f517 100644 --- a/setup.py +++ b/setup.py @@ -34,8 +34,7 @@ author_email='catherine.devlin@gmail.com', url='pypi.python.org/pypi/ipython-sql', license='MIT', - packages=find_packages('src'), - package_dir = {'': 'src'}, + packages=['sql'], include_package_data=True, zip_safe=False, install_requires=install_requires, diff --git a/src/sql/__init__.py b/sql/__init__.py similarity index 100% rename from src/sql/__init__.py rename to sql/__init__.py diff --git a/src/sql/column_guesser.py b/sql/column_guesser.py similarity index 100% rename from src/sql/column_guesser.py rename to sql/column_guesser.py diff --git a/src/sql/connection.py b/sql/connection.py similarity index 91% rename from src/sql/connection.py rename to sql/connection.py index 48a19db42..2e11bcb77 100644 --- a/src/sql/connection.py +++ b/sql/connection.py @@ -12,10 +12,10 @@ def __init__(self, connect_str=None): engine = sqlalchemy.create_engine(connect_str) except: # TODO: bare except; but what's an ArgumentError? print(self.tell_format()) - raise + raise self.metadata = sqlalchemy.MetaData(bind=engine) self.name = self.assign_name(engine) - self.session = engine.connect() + self.session = engine.connect() self.connections[self.name] = self self.connections[str(self.metadata.bind.url)] = self Connection.current = self @@ -25,7 +25,7 @@ def get(cls, descriptor): cls.current = descriptor elif descriptor: conn = cls.connections.get(descriptor) or \ - cls.connections.get(descriptor.lower()) + (type(descriptor) == str and cls.connections.get(descriptor.lower())) if conn: cls.current = conn else: diff --git a/src/sql/magic.py b/sql/magic.py similarity index 84% rename from src/sql/magic.py rename to sql/magic.py index 40ae86f24..64297f40d 100644 --- a/src/sql/magic.py +++ b/sql/magic.py @@ -18,13 +18,13 @@ class SqlMagic(Magics, Configurable): autolimit = Int(0, config=True, help="Automatically limit the size of the returned result sets") style = Unicode('DEFAULT', config=True, help="Set the table printing style to any of prettytable's defined styles (currently DEFAULT, MSWORD_FRIENDLY, PLAIN_COLUMNS, RANDOM)") short_errors = Bool(True, config=True, help="Don't display the full traceback on SQL Programming Error") - displaylimit = Int(0, config=True, help="Automatic,ally limit the number of rows displayed (full result set is still stored)") + displaylimit = Int(0, config=True, help="Automatically limit the number of rows displayed (full result set is still stored)") autopandas = Bool(False, config=True, help="Return Pandas DataFrames instead of regular result sets") feedback = Bool(True, config=True, help="Print number of rows affected by DML") - dsn_filename = Unicode('odbc.ini', config=True, help="Path to DSN file. \ - When the first argument is of the form [section], \ - a sqlalchemy connection string is formed from the \ - matching section in the DSN file.") + dsn_filename = Unicode('odbc.ini', config=True, help='Path to DSN file.' \ + ' When the first argument is of the form [section],' \ + ' a sqlalchemy connection string is formed from the' \ + ' matching section in the DSN file.') def __init__(self, shell): Configurable.__init__(self, config=shell.config) @@ -65,7 +65,7 @@ def execute(self, line, cell='', local_ns={}): user_ns = self.shell.user_ns user_ns.update(local_ns) - parsed = sql.parse.parse('%s\n%s' % (line, cell), self) + parsed = sql.parse.parse('%s\n%s' % (line, cell), self, user_ns) conn = sql.connection.Connection.get(parsed['connection']) try: result = sql.run.run(conn, parsed['sql'], self, user_ns) diff --git a/sql/parse.py b/sql/parse.py new file mode 100644 index 000000000..ca248cfd5 --- /dev/null +++ b/sql/parse.py @@ -0,0 +1,39 @@ +from ConfigParser import ConfigParser +from sqlalchemy.engine.url import URL +from ast import literal_eval + + +def parse(cell, config, user_ns={}): + parts = [part.strip() for part in cell.split(None, 1)] + if not parts: + return {'connection': '', 'sql': ''} + if parts[0].startswith('[') and parts[0].endswith(']'): + part0 = parts[0].lstrip('[').rstrip(']').split(',') + section = part0[0] + var_names = part0[1:] + parser = ConfigParser() + parser.read(config.dsn_filename) + cfg_dict = dict(parser.items(section)) + if 'query' in cfg_dict.keys(): + cfg_dict['query'] = literal_eval(cfg_dict['query']) + + # TODO currently the url has to be turned into a string object + # so that the Connection object can find it in its dictionary + connection = str(URL(**cfg_dict)) + sql = parts[1].strip() if len(parts) > 1 else '' + for var_name in var_names or []: + var_name = var_name.strip() + var_val = user_ns.get(var_name) + if var_val: + sql = sql.replace(':{}'.format(var_name), str(var_val)) + elif '@' in parts[0] or '://' in parts[0]: + connection = parts[0].strip() + if len(parts) > 1: + sql = parts[1].strip() + else: + sql = '' + else: + connection = '' + sql = cell.strip() + return {'connection': connection, + 'sql': sql} diff --git a/src/sql/run.py b/sql/run.py similarity index 94% rename from src/sql/run.py rename to sql/run.py index 3f2b719a5..28133c7c6 100644 --- a/src/sql/run.py +++ b/sql/run.py @@ -64,11 +64,11 @@ def __repr__(self): return 'CSV results at %s' % os.path.join(os.path.abspath('.'), self.file_path) def _repr_html_(self): return 'CSV results' % os.path.join('.', 'files', self.file_path) - + class ResultSet(list, ColumnGuesserMixin): """ Results of a SQL query. - + Can access rows listwise, or by string value of leftmost column. """ def __init__(self, sqlaproxy, sql, config): @@ -124,23 +124,23 @@ def DataFrame(self): return frame def pie(self, key_word_sep=" ", title=None, **kwargs): """Generates a pylab pie chart from the result set. - + ``matplotlib`` must be installed, and in an IPython Notebook, inlining must be on:: - + %%matplotlib inline - - Values (pie slice sizes) are taken from the + + Values (pie slice sizes) are taken from the rightmost column (numerical values required). All other columns are used to label the pie slices. - + Parameters ---------- key_word_sep: string used to separate column values from each other in pie labels title: Plot title, defaults to name of value column - Any additional keyword arguments will be passsed + Any additional keyword arguments will be passsed through to ``matplotlib.pylab.pie``. """ self.guess_pie_columns(xlabel_sep=key_word_sep) @@ -148,23 +148,23 @@ def pie(self, key_word_sep=" ", title=None, **kwargs): pie = plt.pie(self.ys[0], labels=self.xlabels, **kwargs) plt.title(title or self.ys[0].name) return pie - + def plot(self, title=None, **kwargs): """Generates a pylab plot from the result set. - + ``matplotlib`` must be installed, and in an IPython Notebook, inlining must be on:: - + %%matplotlib inline - + The first and last columns are taken as the X and Y values. Any columns between are ignored. - + Parameters ---------- title: Plot title, defaults to names of Y value columns - Any additional keyword arguments will be passsed + Any additional keyword arguments will be passsed through to ``matplotlib.pylab.plot``. """ import matplotlib.pylab as plt @@ -178,25 +178,25 @@ def plot(self, title=None, **kwargs): plt.title(title or ylabel) plt.ylabel(ylabel) return plot - + def bar(self, key_word_sep = " ", title=None, **kwargs): """Generates a pylab bar plot from the result set. - + ``matplotlib`` must be installed, and in an IPython Notebook, inlining must be on:: - + %%matplotlib inline - + The last quantitative column is taken as the Y values; - all other columns are combined to label the X axis. - + all other columns are combined to label the X axis. + Parameters ---------- title: Plot title, defaults to names of Y value columns key_word_sep: string used to separate column values from each other in labels - - Any additional keyword arguments will be passsed + + Any additional keyword arguments will be passsed through to ``matplotlib.pylab.bar``. """ import matplotlib.pylab as plt @@ -207,8 +207,8 @@ def bar(self, key_word_sep = " ", title=None, **kwargs): rotation=45) plt.xlabel(self.xlabel) plt.ylabel(self.ys[0].name) - return plot - + return plot + def csv(self, filename=None, **format_params): """Generate results in comma-separated form. Write to ``filename`` if given. Any other parameterw will be passed on to csv.writer.""" @@ -227,8 +227,8 @@ def csv(self, filename=None, **format_params): return CsvResultDescriptor(filename) else: return outfile.getvalue() - - + + def interpret_rowcount(rowcount): if rowcount < 0: result = 'Done.' @@ -252,4 +252,4 @@ def run(conn, sql, config, user_namespace): #returning only last result, intentionally else: return 'Connected: %s' % conn.name - \ No newline at end of file + diff --git a/src/sql/parse.py b/src/sql/parse.py deleted file mode 100644 index bd2afcd2b..000000000 --- a/src/sql/parse.py +++ /dev/null @@ -1,27 +0,0 @@ -from ConfigParser import ConfigParser -from sqlalchemy.engine.url import URL - - -def parse(cell, config): - parts = [part.strip() for part in cell.split(None, 1)] - if not parts: - return {'connection': '', 'sql': ''} - if parts[0].startswith('[') and parts[0].endswith(']'): - section = parts[0].lstrip('[').rstrip(']') - parser = ConfigParser() - parser.read(config.dsn_filename) - cfg_dict = dict(parser.items(section)) - - connection = str(URL(**cfg_dict)) - sql = parts[1] if len(parts) > 1 else '' - elif '@' in parts[0] or '://' in parts[0]: - connection = parts[0] - if len(parts) > 1: - sql = parts[1] - else: - sql = '' - else: - connection = '' - sql = cell - return {'connection': connection.strip(), - 'sql': sql.strip()} diff --git a/src/tests/run_tests.sh b/tests/run_tests.sh similarity index 100% rename from src/tests/run_tests.sh rename to tests/run_tests.sh diff --git a/src/tests/test_column_guesser.py b/tests/test_column_guesser.py similarity index 100% rename from src/tests/test_column_guesser.py rename to tests/test_column_guesser.py diff --git a/src/tests/test_magic.py b/tests/test_magic.py similarity index 100% rename from src/tests/test_magic.py rename to tests/test_magic.py diff --git a/src/tests/test_parse.py b/tests/test_parse.py similarity index 100% rename from src/tests/test_parse.py rename to tests/test_parse.py