From 6da5cc5b47dad363215bdc42378b5a2c43c7d72e Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Mon, 18 Dec 2023 01:09:04 +0000 Subject: [PATCH 01/29] add basic spark support to library --- src/sql/command.py | 4 +- src/sql/connection/__init__.py | 2 + src/sql/connection/connection.py | 68 ++++++++++++++++++++++++++++++++ src/sql/run/run.py | 6 +++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/sql/command.py b/src/sql/command.py index 7b7bd168b..894ad10e3 100644 --- a/src/sql/command.py +++ b/src/sql/command.py @@ -5,7 +5,7 @@ from sql import parse, exceptions from sql.store import store -from sql.connection import ConnectionManager, is_pep249_compliant +from sql.connection import ConnectionManager, is_pep249_compliant, is_spark from sql.util import validate_nonidentifier_connection @@ -49,7 +49,7 @@ def __init__(self, magic, user_ns, line, cell) -> None: if ( one_arg and self.args.line[0] in user_ns - and (isinstance(user_ns[self.args.line[0]], Engine) or is_dbapi_connection_) + and (isinstance(user_ns[self.args.line[0]], Engine) or is_dbapi_connection_ or is_spark) ): line_for_command = [] add_conn = True diff --git a/src/sql/connection/__init__.py b/src/sql/connection/__init__.py index 7c48e624b..259b549d4 100644 --- a/src/sql/connection/__init__.py +++ b/src/sql/connection/__init__.py @@ -2,7 +2,9 @@ ConnectionManager, SQLAlchemyConnection, DBAPIConnection, + SparkConnectConnection, is_pep249_compliant, + is_spark, PLOOMBER_DOCS_LINK_STR, default_alias_for_engine, ResultSetCollection, diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 678ca30de..203462082 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -5,6 +5,7 @@ from difflib import get_close_matches import atexit from functools import partial +import pyspark import sqlalchemy from sqlalchemy.engine import Engine @@ -21,6 +22,7 @@ import sqlparse from ploomber_core.exceptions import modify_exceptions +from pyspark.sql import SparkSession from sql.store import store from sql.telemetry import telemetry @@ -257,6 +259,8 @@ def set( ) elif is_pep249_compliant(descriptor): cls.current = DBAPIConnection(descriptor, config=config, alias=alias) + elif is_spark(descriptor): + cls.current = SparkConnectConnection(descriptor) else: existing = rough_dict_get(cls.connections, descriptor) if existing and existing.alias == alias: @@ -1060,6 +1064,68 @@ def to_table(self, table_name, data_frame, if_exists, index, schema=None): ) +class SparkConnectConnection(AbstractConnection): + + @telemetry.log_call("SparkConnectConnection", payload=True) + def __init__(self, payload, connection: SparkSession, alias=None, config=None): + self._driver = None + + # TODO: implement the dialect blacklist and add unit tests + self._requires_manual_commit = True if config is None else config.autocommit + + self._connection = connection + self._connection_class_name = type(connection).__name__ + + # calling init from AbstractConnection must be the last thing we do as it + # register the connection + super().__init__(self._connection_class_name) + + # TODO: delete this + self.name = self._connection_class_name + + @property + def dialect(self): + """Returns a string with the SQL dialect name""" + return "spark" + + + def raw_execute(self, query, parameters=None): + """Run the query without any pre-processing""" + return self._connection.sql(query) + + + def _get_database_information(self): + """ + Get the dialect, driver, and database server version info of current + connection + """ + return { + "dialect": self.dialect, + "driver": self._connection_class_name, + "server_version_info": None, + } + + @property + def url(self): + """Returns None since DBAPI connections don't have a url""" + return None + + @property + def connection_sqlalchemy(self): + """ + Raises NotImplementedError since DBAPI connections don't have a SQLAlchemy + connection object + """ + raise NotImplementedError( + "This feature is only available for SQLAlchemy connections" + ) + + def to_table(self, table_name, data_frame, if_exists, index, schema=None): + raise exceptions.NotImplementedError( + "--persist/--persist-replace is not available for DBAPI connections" + " (only available for SQLAlchemy connections)" + ) + def _check_if_duckdb_dbapi_connection(conn): """Check if the connection is a native duckdb connection""" # NOTE: duckdb defines df and pl to efficiently convert results to @@ -1153,6 +1219,8 @@ def is_pep249_compliant(conn): return True +def is_spark(ins): + return isinstance(ins,pyspark.sql.connect.session.SparkSession) or isinstance(ins,pyspark.sql.SparkSession) def default_alias_for_engine(engine): if not engine.url.username: diff --git a/src/sql/run/run.py b/src/sql/run/run.py index 11312c450..a55c2c96d 100644 --- a/src/sql/run/run.py +++ b/src/sql/run/run.py @@ -49,6 +49,9 @@ def run_statements(conn, sql, config, parameters=None): if first_word.startswith("\\") and is_postgres_or_redshift(conn.dialect): result = handle_postgres_special(conn, statement) + if is_spark(conn.dialect): + return conn.raw_execute(statement, parameters=parameters) + # regular query else: result = conn.raw_execute(statement, parameters=parameters) @@ -68,6 +71,9 @@ def is_postgres_or_redshift(dialect): """Checks if dialect is postgres or redshift""" return "postgres" in str(dialect) or "redshift" in str(dialect) +def is_spark(dialect): + return "spark" in str(dialect) + def select_df_type(resultset, config): """ From 280b646023037a9cab4313a853b8042b6120f10a Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Mon, 18 Dec 2023 22:00:02 +0000 Subject: [PATCH 02/29] adding tests --- setup.py | 1 + src/sql/connection/connection.py | 34 +++++++++++++++++++-------- src/tests/integration/conftest.py | 8 +++++++ src/tests/test_connection.py | 38 +++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 2e1b87ad3..7d65357ae 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ "pandas", # previously pinned to 2.0.3 "polars==0.17.2", # 04/18/23 this breaks our CI "pyarrow", + "pyspark" "invoke", "pkgmt", "twine", diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 203462082..3f3e58f42 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -5,7 +5,6 @@ from difflib import get_close_matches import atexit from functools import partial -import pyspark import sqlalchemy from sqlalchemy.engine import Engine @@ -17,12 +16,18 @@ InternalError, ProgrammingError, ) + +try: + from pyspark.sql.connect.session import SparkSession as CSparkSession + from pyspark.sql import SparkSession +except ModuleNotFoundError: + CSparkSession = None + SparkSession = None from IPython.core.error import UsageError import sqlglot import sqlparse from ploomber_core.exceptions import modify_exceptions -from pyspark.sql import SparkSession from sql.store import store from sql.telemetry import telemetry @@ -1065,9 +1070,15 @@ def to_table(self, table_name, data_frame, if_exists, index, schema=None): class SparkConnectConnection(AbstractConnection): + + is_dbapi_connection = False @telemetry.log_call("SparkConnectConnection", payload=True) - def __init__(self, payload, connection: SparkSession, alias=None, config=None): + def __init__(self, payload, connection, alias=None, config=None): + try: + payload["engine"] = type(connection) + except Exception as e: + payload["engine_parsing_error"] = str(e) self._driver = None # TODO: implement the dialect blacklist and add unit tests @@ -1080,7 +1091,7 @@ def __init__(self, payload, connection: SparkSession, alias=None, config=None): # register the connection super().__init__(self._connection_class_name) - # TODO: delete this + self.name = self._connection_class_name @property @@ -1102,18 +1113,18 @@ def _get_database_information(self): return { "dialect": self.dialect, "driver": self._connection_class_name, - "server_version_info": None, + "server_version_info": self._connection.version(), } @property def url(self): - """Returns None since DBAPI connections don't have a url""" + """Returns None since Spark connections don't have a url""" return None @property def connection_sqlalchemy(self): """ - Raises NotImplementedError since DBAPI connections don't have a SQLAlchemy + Raises NotImplementedError since Spark connections don't have a SQLAlchemy connection object """ raise NotImplementedError( @@ -1122,9 +1133,14 @@ def connection_sqlalchemy(self): def to_table(self, table_name, data_frame, if_exists, index, schema=None): raise exceptions.NotImplementedError( - "--persist/--persist-replace is not available for DBAPI connections" + "--persist/--persist-replace is not available for Spark connections" " (only available for SQLAlchemy connections)" ) + + def close(self): + """Close the connection""" + # NOTE: spark is often shared outside sql, allow user to manage closure + pass def _check_if_duckdb_dbapi_connection(conn): """Check if the connection is a native duckdb connection""" @@ -1220,7 +1236,7 @@ def is_pep249_compliant(conn): return True def is_spark(ins): - return isinstance(ins,pyspark.sql.connect.session.SparkSession) or isinstance(ins,pyspark.sql.SparkSession) + return (CSparkSession is not None and isinstance(ins,CSparkSession)) or (SparkSession is not None and isinstance(ins,SparkSession)) def default_alias_for_engine(engine): if not engine.url.username: diff --git a/src/tests/integration/conftest.py b/src/tests/integration/conftest.py index e0a0b5e7f..0a13810f1 100644 --- a/src/tests/integration/conftest.py +++ b/src/tests/integration/conftest.py @@ -2,6 +2,7 @@ from pathlib import Path import shutil import pandas as pd +from pyspark.sql import SparkSession import pytest from sqlalchemy import MetaData, Table, create_engine, text import uuid @@ -287,6 +288,13 @@ def setup_duckDB_native(test_table_name_dict): yield conn conn.close() +@pytest.fixture(scope="session") +def setup_spark(): + spark = SparkSession.Builder.master("local").getOrCreate() + yield spark + spark.stop() + + def load_generic_testing_data_duckdb_native(ip, test_table_name_dict): ip.run_cell("import pandas as pd") diff --git a/src/tests/test_connection.py b/src/tests/test_connection.py index 1329eebca..396ec168e 100644 --- a/src/tests/test_connection.py +++ b/src/tests/test_connection.py @@ -13,6 +13,8 @@ from sqlalchemy.engine import Engine from sqlalchemy import exc +import pyspark + from sql.connection import connection as connection_module import sql.connection from sql.connection import ( @@ -21,6 +23,7 @@ ConnectionManager, is_pep249_compliant, default_alias_for_engine, + is_spark, ResultSetCollection, detect_duckdb_summarize_or_select, ) @@ -40,6 +43,10 @@ def mock_database(monkeypatch, cleanup): monkeypatch.setattr(Engine, "connect", Mock()) monkeypatch.setattr(sqlalchemy, "create_engine", Mock()) +@pytest.fixture +def mock_spark(monkeypatch,cleanup): + monkeypatch.setitem(sys.modules, "pyspark.sql.SparkSession", Mock()) + @pytest.fixture def mock_postgres(monkeypatch, cleanup): @@ -456,6 +463,22 @@ def test_properties(mock_postgres): def test_is_pep249_compliant(conn, expected): assert is_pep249_compliant(conn) is expected +@pytest.mark.parametrize( + "descriptor, expected", + [ + [sqlite3.connect(""), False], + [duckdb.connect(""), False], + [create_engine("sqlite://"), False], + [Mock(spec=pyspark.sql.SparkSession), True], + [Mock(spec=pyspark.sql.connect.session.SparkSession), True], + [None, False], + [object(), False], + ["not_a_valid_connection", False], + [0, False], + ] +) +def test_is_spark(descriptor, expected): + assert is_spark(descriptor) is expected def test_close_all(ip_empty, monkeypatch): connections = {} @@ -589,6 +612,21 @@ def test_set_dbapi(monkeypatch, callable_, key): assert connections == {key: conn} assert ConnectionManager.current == conn +@pytest.mark.parametrize( + "spark, key", + [ + [Mock(name="SparkSession",spec=pyspark.sql.SparkSession), "Mock"], + [Mock(name="SparkSession",spec=pyspark.sql.connect.session.SparkSession), "Mock"], + ], +) +def test_set_spark(monkeypatch, spark, key): + connections = {} + monkeypatch.setattr(ConnectionManager, "connections", connections) + + conn = ConnectionManager.set(spark, displaycon=False) + + assert connections == {key: conn} + assert ConnectionManager.current == conn def test_set_with_alias(monkeypatch): connections = {} From 145ae6ab02aad58370cdfd632732ccfc630a197c Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Mon, 18 Dec 2023 22:06:33 +0000 Subject: [PATCH 03/29] formatting --- setup.py | 3 +-- src/sql/command.py | 6 +++++- src/sql/connection/connection.py | 17 +++++++++-------- src/sql/run/run.py | 1 + src/tests/integration/conftest.py | 2 +- src/tests/test_connection.py | 16 ++++++++++++---- 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/setup.py b/setup.py index 7d65357ae..f7f213405 100644 --- a/setup.py +++ b/setup.py @@ -35,8 +35,7 @@ "pandas", # previously pinned to 2.0.3 "polars==0.17.2", # 04/18/23 this breaks our CI "pyarrow", - "pyspark" - "invoke", + "pyspark" "invoke", "pkgmt", "twine", # tests diff --git a/src/sql/command.py b/src/sql/command.py index 894ad10e3..c5a664c73 100644 --- a/src/sql/command.py +++ b/src/sql/command.py @@ -49,7 +49,11 @@ def __init__(self, magic, user_ns, line, cell) -> None: if ( one_arg and self.args.line[0] in user_ns - and (isinstance(user_ns[self.args.line[0]], Engine) or is_dbapi_connection_ or is_spark) + and ( + isinstance(user_ns[self.args.line[0]], Engine) + or is_dbapi_connection_ + or is_spark + ) ): line_for_command = [] add_conn = True diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 3f3e58f42..54b0b4ff1 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1070,9 +1070,8 @@ def to_table(self, table_name, data_frame, if_exists, index, schema=None): class SparkConnectConnection(AbstractConnection): - is_dbapi_connection = False - + @telemetry.log_call("SparkConnectConnection", payload=True) def __init__(self, payload, connection, alias=None, config=None): try: @@ -1091,20 +1090,17 @@ def __init__(self, payload, connection, alias=None, config=None): # register the connection super().__init__(self._connection_class_name) - self.name = self._connection_class_name - + @property def dialect(self): """Returns a string with the SQL dialect name""" return "spark" - def raw_execute(self, query, parameters=None): """Run the query without any pre-processing""" return self._connection.sql(query) - def _get_database_information(self): """ Get the dialect, driver, and database server version info of current @@ -1136,12 +1132,13 @@ def to_table(self, table_name, data_frame, if_exists, index, schema=None): "--persist/--persist-replace is not available for Spark connections" " (only available for SQLAlchemy connections)" ) - + def close(self): """Close the connection""" # NOTE: spark is often shared outside sql, allow user to manage closure pass + def _check_if_duckdb_dbapi_connection(conn): """Check if the connection is a native duckdb connection""" # NOTE: duckdb defines df and pl to efficiently convert results to @@ -1235,8 +1232,12 @@ def is_pep249_compliant(conn): return True + def is_spark(ins): - return (CSparkSession is not None and isinstance(ins,CSparkSession)) or (SparkSession is not None and isinstance(ins,SparkSession)) + return (CSparkSession is not None and isinstance(ins, CSparkSession)) or ( + SparkSession is not None and isinstance(ins, SparkSession) + ) + def default_alias_for_engine(engine): if not engine.url.username: diff --git a/src/sql/run/run.py b/src/sql/run/run.py index a55c2c96d..f59ef96d2 100644 --- a/src/sql/run/run.py +++ b/src/sql/run/run.py @@ -71,6 +71,7 @@ def is_postgres_or_redshift(dialect): """Checks if dialect is postgres or redshift""" return "postgres" in str(dialect) or "redshift" in str(dialect) + def is_spark(dialect): return "spark" in str(dialect) diff --git a/src/tests/integration/conftest.py b/src/tests/integration/conftest.py index 0a13810f1..d92fe4d70 100644 --- a/src/tests/integration/conftest.py +++ b/src/tests/integration/conftest.py @@ -288,12 +288,12 @@ def setup_duckDB_native(test_table_name_dict): yield conn conn.close() + @pytest.fixture(scope="session") def setup_spark(): spark = SparkSession.Builder.master("local").getOrCreate() yield spark spark.stop() - def load_generic_testing_data_duckdb_native(ip, test_table_name_dict): diff --git a/src/tests/test_connection.py b/src/tests/test_connection.py index 396ec168e..b22b03c32 100644 --- a/src/tests/test_connection.py +++ b/src/tests/test_connection.py @@ -43,8 +43,9 @@ def mock_database(monkeypatch, cleanup): monkeypatch.setattr(Engine, "connect", Mock()) monkeypatch.setattr(sqlalchemy, "create_engine", Mock()) + @pytest.fixture -def mock_spark(monkeypatch,cleanup): +def mock_spark(monkeypatch, cleanup): monkeypatch.setitem(sys.modules, "pyspark.sql.SparkSession", Mock()) @@ -463,6 +464,7 @@ def test_properties(mock_postgres): def test_is_pep249_compliant(conn, expected): assert is_pep249_compliant(conn) is expected + @pytest.mark.parametrize( "descriptor, expected", [ @@ -475,11 +477,12 @@ def test_is_pep249_compliant(conn, expected): [object(), False], ["not_a_valid_connection", False], [0, False], - ] + ], ) def test_is_spark(descriptor, expected): assert is_spark(descriptor) is expected + def test_close_all(ip_empty, monkeypatch): connections = {} monkeypatch.setattr(ConnectionManager, "connections", connections) @@ -612,11 +615,15 @@ def test_set_dbapi(monkeypatch, callable_, key): assert connections == {key: conn} assert ConnectionManager.current == conn + @pytest.mark.parametrize( "spark, key", [ - [Mock(name="SparkSession",spec=pyspark.sql.SparkSession), "Mock"], - [Mock(name="SparkSession",spec=pyspark.sql.connect.session.SparkSession), "Mock"], + [Mock(name="SparkSession", spec=pyspark.sql.SparkSession), "Mock"], + [ + Mock(name="SparkSession", spec=pyspark.sql.connect.session.SparkSession), + "Mock", + ], ], ) def test_set_spark(monkeypatch, spark, key): @@ -628,6 +635,7 @@ def test_set_spark(monkeypatch, spark, key): assert connections == {key: conn} assert ConnectionManager.current == conn + def test_set_with_alias(monkeypatch): connections = {} monkeypatch.setattr(ConnectionManager, "connections", connections) From 7cd9e6118427e5f9e000630b6163194db47789eb Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Mon, 18 Dec 2023 22:28:16 +0000 Subject: [PATCH 04/29] add spark connection --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f7f213405..28092dd88 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,8 @@ "pandas", # previously pinned to 2.0.3 "polars==0.17.2", # 04/18/23 this breaks our CI "pyarrow", - "pyspark" "invoke", + "pyspark", + "invoke", "pkgmt", "twine", # tests From 0f6a3284858c5371b1ceed700905fde828227186 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Mon, 18 Dec 2023 22:28:26 +0000 Subject: [PATCH 05/29] add spark connection --- src/tests/integration/test_connection.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tests/integration/test_connection.py b/src/tests/integration/test_connection.py index 5d867b6e6..aa4c4a8bf 100644 --- a/src/tests/integration/test_connection.py +++ b/src/tests/integration/test_connection.py @@ -8,7 +8,7 @@ import pytest -from sql.connection import SQLAlchemyConnection, DBAPIConnection, ConnectionManager +from sql.connection import SQLAlchemyConnection, DBAPIConnection, ConnectionManager, SparkConnectConnection from sql import _testing from sql.connection import connection @@ -92,6 +92,11 @@ def test_connection_properties(dynamic_db, request, Constructor, alias, dialect) partial(DBAPIConnection, alias="another-alias"), "another-alias", ], + [ + "setup_spark", + SparkConnectConnection, + "SparkSession" + ] ], ) def test_connection_identifiers( From 0b7e3bfe6cb54d411dea3e3b0296f0409e2830de Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Mon, 18 Dec 2023 23:36:21 +0000 Subject: [PATCH 06/29] fixed test and formating --- setup.py | 2 +- src/sql/command.py | 2 +- src/tests/integration/test_connection.py | 13 +++++++------ src/tests/test_magic.py | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 28092dd88..50c5a19ce 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ "pandas", # previously pinned to 2.0.3 "polars==0.17.2", # 04/18/23 this breaks our CI "pyarrow", - "pyspark", + "pyspark", "invoke", "pkgmt", "twine", diff --git a/src/sql/command.py b/src/sql/command.py index c5a664c73..675420d74 100644 --- a/src/sql/command.py +++ b/src/sql/command.py @@ -52,7 +52,7 @@ def __init__(self, magic, user_ns, line, cell) -> None: and ( isinstance(user_ns[self.args.line[0]], Engine) or is_dbapi_connection_ - or is_spark + or is_spark(user_ns[self.args.line[0]]) ) ): line_for_command = [] diff --git a/src/tests/integration/test_connection.py b/src/tests/integration/test_connection.py index aa4c4a8bf..63dc71ac9 100644 --- a/src/tests/integration/test_connection.py +++ b/src/tests/integration/test_connection.py @@ -8,7 +8,12 @@ import pytest -from sql.connection import SQLAlchemyConnection, DBAPIConnection, ConnectionManager, SparkConnectConnection +from sql.connection import ( + SQLAlchemyConnection, + DBAPIConnection, + ConnectionManager, + SparkConnectConnection, +) from sql import _testing from sql.connection import connection @@ -92,11 +97,7 @@ def test_connection_properties(dynamic_db, request, Constructor, alias, dialect) partial(DBAPIConnection, alias="another-alias"), "another-alias", ], - [ - "setup_spark", - SparkConnectConnection, - "SparkSession" - ] + ["setup_spark", SparkConnectConnection, "SparkSession"], ], ) def test_connection_identifiers( diff --git a/src/tests/test_magic.py b/src/tests/test_magic.py index c2dd8f155..0502bfb9a 100644 --- a/src/tests/test_magic.py +++ b/src/tests/test_magic.py @@ -1702,6 +1702,7 @@ def test_persist_uses_error_handling_method(ip, monkeypatch, cell): ip.push({"df": df}) conn = ConnectionManager.current + print(conn) execute_with_error_handling_mock = Mock(wraps=conn._execute_with_error_handling) monkeypatch.setattr( conn, "_execute_with_error_handling", execute_with_error_handling_mock From e60f0ea718f7f18b8c35126c87a62c4a2aad5e15 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Tue, 19 Dec 2023 00:56:51 +0000 Subject: [PATCH 07/29] added docs --- doc/_toc.yml | 1 + doc/integrations/spark.ipynb | 867 +++++++++++++++++++++++++++++++ src/sql/connection/connection.py | 2 +- 3 files changed, 869 insertions(+), 1 deletion(-) create mode 100644 doc/integrations/spark.ipynb diff --git a/doc/_toc.yml b/doc/_toc.yml index 2d9850b10..f667e807b 100644 --- a/doc/_toc.yml +++ b/doc/_toc.yml @@ -43,6 +43,7 @@ parts: - file: integrations/duckdb-native - file: integrations/compatibility - file: integrations/chdb + - file: integrations/spark - caption: API Reference chapters: diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb new file mode 100644 index 000000000..2e518be11 --- /dev/null +++ b/doc/integrations/spark.ipynb @@ -0,0 +1,867 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Spark\n", + "\n", + "This tutorial will show you how to get a Spark instance up and running locally to test JupySQL. You can run this in a Jupyter notebook. We'll use Spark Connect which is the new thin client for Spark" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pre-requisites\n", + "\n", + "To run this tutorial, you need to install following Python packages:" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install jupysql pyspark==3.4.1 arrow pyarrow==12.0.1 pandas --quiet" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Start Spark instance\n", + "\n", + "We fetch the official image, create a new database, and user (this will take a few seconds)." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4a7133e0b70b9435d575ef2eda12644a589ab78ef5800f4d95775377c13ef18c\n" + ] + } + ], + "source": [ + "%%bash\n", + "docker run -p 15002:15002 -p 4040:4040 -d --name spark wh1isper/sparglim-server" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our database is running, let's load some data!" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load sample data\n", + "\n", + "Now, let's fetch some sample data. We'll be using the [NYC taxi dataset](https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page):" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from pyspark.sql.connect.session import SparkSession\n", + "\n", + "spark = SparkSession.builder.remote(\"sc://localhost\").getOrCreate()\n", + "\n", + "df = pd.read_parquet(\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2021-01.parquet\"\n", + ")\n", + "sparkDf = spark.createDataFrame(df.head(10000))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set eagerEval on to print dataframes" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def __pretty_(self, p, cycle):\n", + " self.show(truncate=False)\n", + "\n", + "\n", + "from pyspark.sql.connect.dataframe import DataFrame\n", + "\n", + "DataFrame._repr_pretty_ = __pretty_\n", + "spark.conf.set(\"spark.sql.repl.eagerEval.enabled\", True)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add dataset to temporary view to allow querying" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sparkDf.createOrReplaceTempView(\"taxi\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Query\n", + "\n", + "Now, let's start JupySQL, authenticate, and query the data!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The sql extension is already loaded. To reload it, use:\n", + " %reload_ext sql\n" + ] + } + ], + "source": [ + "%load_ext sql" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%sql spark" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{important}\n", + "If the cell above fails, you might have some missing packages. Message us on [Slack](https://ploomber.io/community) and we'll help you!\n", + "```" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "List the tables in the database:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+---------+--------+-----------+\n", + "|namespace|viewName|isTemporary|\n", + "+---------+--------+-----------+\n", + "| |taxi |true |\n", + "+---------+--------+-----------+\n", + "\n" + ] + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%sql show views in default" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "List columns in the taxi table:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "root\n", + " |-- VendorID: long (nullable = true)\n", + " |-- tpep_pickup_datetime: timestamp (nullable = true)\n", + " |-- tpep_dropoff_datetime: timestamp (nullable = true)\n", + " |-- passenger_count: double (nullable = true)\n", + " |-- trip_distance: double (nullable = true)\n", + " |-- RatecodeID: double (nullable = true)\n", + " |-- store_and_fwd_flag: string (nullable = true)\n", + " |-- PULocationID: long (nullable = true)\n", + " |-- DOLocationID: long (nullable = true)\n", + " |-- payment_type: long (nullable = true)\n", + " |-- fare_amount: double (nullable = true)\n", + " |-- extra: double (nullable = true)\n", + " |-- mta_tax: double (nullable = true)\n", + " |-- tip_amount: double (nullable = true)\n", + " |-- tolls_amount: double (nullable = true)\n", + " |-- improvement_surcharge: double (nullable = true)\n", + " |-- total_amount: double (nullable = true)\n", + " |-- congestion_surcharge: double (nullable = true)\n", + " |-- airport_fee: double (nullable = true)\n", + "\n" + ] + } + ], + "source": [ + "df = %sql select * from taxi\n", + "df.printSchema()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Query our data:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------+\n", + "|count(1)|\n", + "+--------+\n", + "|10000 |\n", + "+--------+\n", + "\n" + ] + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%sql\n", + "SELECT COUNT(*) FROM taxi" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Parameterize queries" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "threshold = 10" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------+\n", + "|count(1)|\n", + "+--------+\n", + "|9476 |\n", + "+--------+\n", + "\n" + ] + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%sql\n", + "SELECT COUNT(*) FROM taxi\n", + "WHERE trip_distance < {{threshold}}" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "threshold = 0.5" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------+\n", + "|count(1)|\n", + "+--------+\n", + "|642 |\n", + "+--------+\n", + "\n" + ] + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%sql\n", + "SELECT COUNT(*) FROM taxi\n", + "WHERE trip_distance < {{threshold}}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CTEs" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Skipping execution..." + ], + "text/plain": [ + "Skipping execution..." + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%sql --save many_passengers --no-execute\n", + "SELECT *\n", + "FROM taxi\n", + "WHERE passenger_count > 3\n", + "-- remove top 1% outliers for better visualization\n", + "AND trip_distance < 18.93" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+------------------+------------------+------------------+\n", + "|min(trip_distance)|avg(trip_distance)|max(trip_distance)|\n", + "+------------------+------------------+------------------+\n", + "|0.0 |3.1091381872213963|18.46 |\n", + "+------------------+------------------+------------------+\n", + "\n" + ] + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%sql --save trip_stats --with many_passengers\n", + "SELECT MIN(trip_distance), AVG(trip_distance), MAX(trip_distance)\n", + "FROM many_passengers" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is what JupySQL executes:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WITH `many_passengers` AS (\n", + "SELECT *\n", + "FROM taxi\n", + "WHERE passenger_count > 3\n", + "\n", + "AND trip_distance < 18.93)\n", + "SELECT MIN(trip_distance), AVG(trip_distance), MAX(trip_distance)\n", + "FROM many_passengers\n" + ] + } + ], + "source": [ + "query = %sqlcmd snippets trip_stats\n", + "print(query)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The %sqlplot magic command currently does not directly support the `--schema` option for specifying the schema name. To work around this, you can specify the schema in the SQL query itself." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAHHCAYAAABeLEexAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABJeUlEQVR4nO3dfXzO9f////sxsxlzHHO2zd6GiTCht3nLIiXL1DoRKhET5a22Cjn9KSelJj4lOqFT04mc9I5ETtacFSMmJ4mR1Ihtiu2wxTbb6/dHl72+jracrNkxXrfr5fK65Hg9H8fzeLyOl9q919lshmEYAgAAsDAPdzcAAADgbgQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQioAJo2LChBgwY4O42rnrTpk1To0aNVKlSJV1//fWX5TNsNpsmTpx4Wea+WLfccotuueUWt/YAXGkIREAZi4+Pl81m07Zt20ocv+WWW3Tdddf948/58ssv3f6D90qyevVqjRo1Sh06dNCcOXP04osvFqtZt26dbDbbRS3lZcCAAS6f6+vrq0aNGqlXr1763//+p8LCwjL5nE2bNmnixInKzMwsk/mAK42nuxsAIKWkpMjD49L+/+TLL7/UG2+8QSi6SGvWrJGHh4fee+89eXl5lVjTvHlzffjhhy7rxo4dK19fX40bN+6iPuf06dPy9Czb/7R6e3vr3XffNef/5Zdf9MUXX6hXr1665ZZb9Pnnn8tut5v1q1evvuTP2LRpkyZNmqQBAwbIz8+vrFoHrhgEIqAC8Pb2dncLlywnJ0fVqlVzdxsXLSMjQz4+Pn8bhiQpICBADz30kMu6KVOmqHbt2sXWn6uwsFB5eXmqUqWKqlSpUmY9F/H09Cz2+ZMnT9aUKVM0duxYPfroo1qwYIE5dr5tBFAyTpkBFcBfryHKz8/XpEmT1KRJE1WpUkW1atVSx44dlZCQIOnP0yhvvPGGJJV4GicnJ0dPP/20goOD5e3traZNm+r//u//ZBiGy+eePn1aTz75pGrXrq3q1avr7rvv1q+//lrsOpiJEyfKZrPphx9+UJ8+fVSjRg117NhRkrRr1y4NGDBAjRo1UpUqVRQYGKiBAwfq999/d/msojn279+vhx56SA6HQ3Xq1NGzzz4rwzB0+PBh3XPPPbLb7QoMDNTLL798Ud/d2bNn9fzzz+uaa66Rt7e3GjZsqP/v//v/lJuba9bYbDbNmTNHOTk55ncVHx9/UfOXxGazKTY2Vh9//LFatGghb29vrVy50hwr6bvbt2+f7r//ftntdtWqVUtPPfWUzpw5U+oeJGnMmDHq2rWrFi1apP3795vrS7qG6LXXXlOLFi1UtWpV1ahRQ23bttW8efPMHkeOHClJCgkJMb+jn3/+WZI0Z84c3XrrrfL395e3t7dCQ0M1a9asYv00bNhQd955p7755hu1a9dOVapUUaNGjfTBBx8Uq83MzNSwYcPUsGFDeXt7q169eurfv79+++03syY3N1cTJkxQ48aN5e3treDgYI0aNcpl3wJlhSNEwGWSlZXl8h/3Ivn5+Rd878SJExUXF6dHHnlE7dq1k9Pp1LZt27R9+3bddttt+u9//6ujR48qISGh2CkewzB09913a+3atRo0aJCuv/56rVq1SiNHjtSvv/6q6dOnm7UDBgzQwoUL1a9fP7Vv317r169XVFTU3/Z13333qUmTJnrxxRfNcJWQkKCffvpJDz/8sAIDA7Vnzx69/fbb2rNnjzZv3lzsepsHHnhAzZs315QpU7R8+XJNnjxZNWvW1FtvvaVbb71VL730kj7++GONGDFC//nPf9SpU6fzflePPPKI5s6dq169eunpp5/Wli1bFBcXp71792rx4sWSpA8//FBvv/22vv32W/PU04033njB/XA+a9as0cKFCxUbG6vatWurYcOG562///771bBhQ8XFxWnz5s2aOXOmTp48WWJYuBT9+vXT6tWrlZCQoGuvvbbEmnfeeUdPPvmkevXqZQaxXbt2acuWLerTp4969Oih/fv365NPPtH06dNVu3ZtSVKdOnUkSbNmzVKLFi109913y9PTU1988YUef/xxFRYWKiYmxuWzfvzxR/Xq1UuDBg1SdHS03n//fQ0YMEBhYWFq0aKFJCk7O1s33XST9u7dq4EDB6pNmzb67bfftHTpUh05ckS1a9dWYWGh7r77bn3zzTcaPHiwmjdvrt27d2v69Onav3+/lixZ8o++N6AYA0CZmjNnjiHpvEuLFi1c3tOgQQMjOjrafN26dWsjKirqvJ8TExNjlPSv8JIlSwxJxuTJk13W9+rVy7DZbMaPP/5oGIZhJCcnG5KMoUOHutQNGDDAkGRMmDDBXDdhwgRDkvHggw8W+7w//vij2LpPPvnEkGRs2LCh2ByDBw821509e9aoV6+eYbPZjClTppjrT548afj4+Lh8JyXZsWOHIcl45JFHXNaPGDHCkGSsWbPGXBcdHW1Uq1btvPOVpEWLFsbNN9/ssk6S4eHhYezZs6dY/d99d3fffbdL3eOPP25IMnbu3Hnez79Q3999950hyRg2bJi57uabb3bp+Z577in2d+6vpk2bZkgyDh06VGyspH0cGRlpNGrUyGVdgwYNiu33jIwMw9vb23j66afNdePHjzckGZ999lmxeQsLCw3DMIwPP/zQ8PDwML7++muX8dmzZxuSjI0bN553e4BLxSkz4DJ54403lJCQUGxp1arVBd/r5+enPXv26MCBA5f8uV9++aUqVaqkJ5980mX9008/LcMwtGLFCkkyT/E8/vjjLnVPPPHE3849ZMiQYut8fHzMP585c0a//fab2rdvL0navn17sfpHHnnE/HOlSpXUtm1bGYahQYMGmev9/PzUtGlT/fTTT3/bi/TntkrS8OHDXdY//fTTkqTly5ef9/3/xM0336zQ0NCLrv/rkZSi77loG0rL19dXknTq1Km/rfHz89ORI0e0devWUn3Gufu46MjnzTffrJ9++klZWVkutaGhobrpppvM13Xq1Cm2L//3v/+pdevWuvfee4t9VtERxUWLFql58+Zq1qyZfvvtN3O59dZbJUlr164t1bYAf4dTZsBl0q5dO7Vt27bY+ho1apR4Ku1czz33nO655x5de+21uu6669StWzf169fvosLUL7/8oqCgIFWvXt1lffPmzc3xon96eHgoJCTEpa5x48Z/O/dfayXpxIkTmjRpkubPn6+MjAyXsb/+sJSk+vXru7x2OByqUqWKeZrm3PV/vQ7pr4q24a89BwYGys/Pz9zWy6Gk7+J8mjRp4vL6mmuukYeHh3mdTmllZ2dLUrH9fa7Ro0frq6++Urt27dS4cWN17dpVffr0UYcOHS7qMzZu3KgJEyYoKSlJf/zxh8tYVlaWHA6H+fqv+1f68+/8yZMnzdcHDx5Uz549z/uZBw4c0N69e83Tdn/1179rwD9FIAIqoE6dOungwYP6/PPPtXr1ar377ruaPn26Zs+e7XKEpbyde6SgyP33369NmzZp5MiRuv766+Xr66vCwkJ169atxGfkVKpU6aLWSSp2EfjfKc/nAhUp6bu4FGXV8/fffy/p/EG2efPmSklJ0bJly7Ry5Ur973//05tvvqnx48dr0qRJ553/4MGD6tKli5o1a6ZXXnlFwcHB8vLy0pdffqnp06cX28f/dF8WKSwsVMuWLfXKK6+UOB4cHHxJ8wEXQiACKqiaNWvq4Ycf1sMPP6zs7Gx16tRJEydONAPR3/1AbdCggb766iudOnXK5ajBvn37zPGifxYWFurQoUMuRy9+/PHHi+7x5MmTSkxM1KRJkzR+/HhzfWlO9ZVG0TYcOHDAPAImSenp6crMzDS3tSI4cOCAy1GlH3/8UYWFhRe8GPtCPvzwQ9lsNt12223nratWrZoeeOABPfDAA8rLy1OPHj30wgsvaOzYsapSpcrf/n364osvlJubq6VLl7oc/fknp6yuueYaM8idr2bnzp3q0qWLWwIvrIdriIAK6K+ninx9fdW4cWOX242LngH01ycL33HHHSooKNDrr7/usn769Omy2Wy6/fbbJUmRkZGSpDfffNOl7rXXXrvoPouOBvz1//5fffXVi57jn7jjjjtK/Lyiowrnu2OuvBU9JqFI0fdctD9KY8qUKVq9erUeeOCBYqfkzvXXv09eXl4KDQ2VYRjmXY9/9/eppH2clZWlOXPmlLrvnj17aufOneZdgOcq+pz7779fv/76q955551iNadPn1ZOTk6pPx8oCUeIgAooNDRUt9xyi8LCwlSzZk1t27ZNn376qWJjY82asLAwSdKTTz6pyMhIVapUSb1799Zdd92lzp07a9y4cfr555/VunVrrV69Wp9//rmGDh2qa665xnx/z5499eqrr+r33383b7svep7Nxfxfud1uV6dOnTR16lTl5+frX//6l1avXq1Dhw5dhm+luNatWys6Olpvv/22MjMzdfPNN+vbb7/V3Llz1b17d3Xu3Llc+rgYhw4d0t13361u3bopKSlJH330kfr06aPWrVtf8L1nz57VRx99JOnPC9d/+eUXLV26VLt27VLnzp319ttvn/f9Xbt2VWBgoDp06KCAgADt3btXr7/+uqKiosyjiEV/n8aNG6fevXurcuXKuuuuu9S1a1d5eXnprrvu0n//+19lZ2frnXfekb+/v44dO1aq72LkyJH69NNPdd9992ngwIEKCwvTiRMntHTpUs2ePVutW7dWv379tHDhQg0ZMkRr165Vhw4dVFBQoH379mnhwoVatWpVidfoAaXmvhvcgKtT0W33W7duLXH85ptvvuBt95MnTzbatWtn+Pn5GT4+PkazZs2MF154wcjLyzNrzp49azzxxBNGnTp1DJvN5nIL/qlTp4xhw4YZQUFBRuXKlY0mTZoY06ZNM29pLpKTk2PExMQYNWvWNHx9fY3u3bsbKSkphiSX2+CLbh0/fvx4se05cuSIce+99xp+fn6Gw+Ew7rvvPuPo0aN/e/v5X+f4u9vKS/qeSpKfn29MmjTJCAkJMSpXrmwEBwcbY8eONc6cOXNRn3Mhf3fbfUxMTIn1f7fdP/zwg9GrVy+jevXqRo0aNYzY2Fjj9OnTF/z86Ohol0c2VK1a1WjYsKHRs2dP49NPPzUKCgqKveevt92/9dZbRqdOnYxatWoZ3t7exjXXXGOMHDnSyMrKcnnf888/b/zrX/8yPDw8XG7BX7p0qdGqVSujSpUqRsOGDY2XXnrJeP/994vdpt+gQYMSHxfx134MwzB+//13IzY21vjXv/5leHl5GfXq1TOio6ON3377zazJy8szXnrpJaNFixaGt7e3UaNGDSMsLMyYNGlSsd6Bf8pmGJd4pRuAq9qOHTv073//Wx999JH69u3r7naueBMnTtSkSZN0/PjxYnfSAag4uIYIsLDTp08XW/fqq6/Kw8Pjgk+IBoCrCdcQARY2depUJScnq3PnzvL09NSKFSu0YsUKDR48mNuaAVgKgQiwsBtvvFEJCQl6/vnnlZ2drfr162vixIkaN26cu1sDgHLFNUQAAMDyuIYIAABYHoEIAABYHtcQXYTCwkIdPXpU1atX5xHyAABcIQzD0KlTpxQUFCQPj/MfAyIQXYSjR49yxw0AAFeow4cPq169euetIRBdhKJH2x8+fFh2u93N3QAAgIvhdDoVHBzs8ouu/w6B6CIUnSaz2+0EIgAArjAXc7kLF1UDAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADLIxABAADL83R3A5Aajlnu7hYu2c9TotzdAgAAZYYjRAAAwPIIRAAAwPIIRAAAwPLcGogKCgr07LPPKiQkRD4+Prrmmmv0/PPPyzAMs8YwDI0fP15169aVj4+PIiIidODAAZd5Tpw4ob59+8put8vPz0+DBg1Sdna2S82uXbt00003qUqVKgoODtbUqVPLZRsBAEDF59ZA9NJLL2nWrFl6/fXXtXfvXr300kuaOnWqXnvtNbNm6tSpmjlzpmbPnq0tW7aoWrVqioyM1JkzZ8yavn37as+ePUpISNCyZcu0YcMGDR482Bx3Op3q2rWrGjRooOTkZE2bNk0TJ07U22+/Xa7bCwAAKiabce7hmHJ25513KiAgQO+99565rmfPnvLx8dFHH30kwzAUFBSkp59+WiNGjJAkZWVlKSAgQPHx8erdu7f27t2r0NBQbd26VW3btpUkrVy5UnfccYeOHDmioKAgzZo1S+PGjVNaWpq8vLwkSWPGjNGSJUu0b9++C/bpdDrlcDiUlZUlu91e5t8Dd5kBAFD2LuXnt1uPEN14441KTEzU/v37JUk7d+7UN998o9tvv12SdOjQIaWlpSkiIsJ8j8Ph0A033KCkpCRJUlJSkvz8/MwwJEkRERHy8PDQli1bzJpOnTqZYUiSIiMjlZKSopMnTxbrKzc3V06n02UBAABXL7c+h2jMmDFyOp1q1qyZKlWqpIKCAr3wwgvq27evJCktLU2SFBAQ4PK+gIAAcywtLU3+/v4u456enqpZs6ZLTUhISLE5isZq1KjhMhYXF6dJkyaV0VYCAICKzq1HiBYuXKiPP/5Y8+bN0/bt2zV37lz93//9n+bOnevOtjR27FhlZWWZy+HDh93aDwAAuLzceoRo5MiRGjNmjHr37i1JatmypX755RfFxcUpOjpagYGBkqT09HTVrVvXfF96erquv/56SVJgYKAyMjJc5j179qxOnDhhvj8wMFDp6ekuNUWvi2rO5e3tLW9v77LZSAAAUOG59QjRH3/8IQ8P1xYqVaqkwsJCSVJISIgCAwOVmJhojjudTm3ZskXh4eGSpPDwcGVmZio5OdmsWbNmjQoLC3XDDTeYNRs2bFB+fr5Zk5CQoKZNmxY7XQYAAKzHrYHorrvu0gsvvKDly5fr559/1uLFi/XKK6/o3nvvlSTZbDYNHTpUkydP1tKlS7V79271799fQUFB6t69uySpefPm6tatmx599FF9++232rhxo2JjY9W7d28FBQVJkvr06SMvLy8NGjRIe/bs0YIFCzRjxgwNHz7cXZsOAAAqELeeMnvttdf07LPP6vHHH1dGRoaCgoL03//+V+PHjzdrRo0apZycHA0ePFiZmZnq2LGjVq5cqSpVqpg1H3/8sWJjY9WlSxd5eHioZ8+emjlzpjnucDi0evVqxcTEKCwsTLVr19b48eNdnlUEAACsy63PIbpS8Byi4ngOEQCgortinkMEAABQERCIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5RGIAACA5bk1EDVs2FA2m63YEhMTI0k6c+aMYmJiVKtWLfn6+qpnz55KT093mSM1NVVRUVGqWrWq/P39NXLkSJ09e9alZt26dWrTpo28vb3VuHFjxcfHl9cmAgCAK4BbA9HWrVt17Ngxc0lISJAk3XfffZKkYcOG6YsvvtCiRYu0fv16HT16VD169DDfX1BQoKioKOXl5WnTpk2aO3eu4uPjNX78eLPm0KFDioqKUufOnbVjxw4NHTpUjzzyiFatWlW+GwsAACosm2EYhrubKDJ06FAtW7ZMBw4ckNPpVJ06dTRv3jz16tVLkrRv3z41b95cSUlJat++vVasWKE777xTR48eVUBAgCRp9uzZGj16tI4fPy4vLy+NHj1ay5cv1/fff29+Tu/evZWZmamVK1deVF9Op1MOh0NZWVmy2+1lvt0Nxywv8zkvt5+nRLm7BQAAzutSfn5XmGuI8vLy9NFHH2ngwIGy2WxKTk5Wfn6+IiIizJpmzZqpfv36SkpKkiQlJSWpZcuWZhiSpMjISDmdTu3Zs8esOXeOopqiOUqSm5srp9PpsgAAgKtXhQlES5YsUWZmpgYMGCBJSktLk5eXl/z8/FzqAgIClJaWZtacG4aKxovGzlfjdDp1+vTpEnuJi4uTw+Ewl+Dg4H+6eQAAoAKrMIHovffe0+23366goCB3t6KxY8cqKyvLXA4fPuzulgAAwGXk6e4GJOmXX37RV199pc8++8xcFxgYqLy8PGVmZrocJUpPT1dgYKBZ8+2337rMVXQX2rk1f70zLT09XXa7XT4+PiX24+3tLW9v73+8XQAA4MpQIY4QzZkzR/7+/oqK+n8X6oaFhaly5cpKTEw016WkpCg1NVXh4eGSpPDwcO3evVsZGRlmTUJCgux2u0JDQ82ac+coqimaAwAAwO2BqLCwUHPmzFF0dLQ8Pf/fASuHw6FBgwZp+PDhWrt2rZKTk/Xwww8rPDxc7du3lyR17dpVoaGh6tevn3bu3KlVq1bpmWeeUUxMjHmEZ8iQIfrpp580atQo7du3T2+++aYWLlyoYcOGuWV7AQBAxeP2U2ZfffWVUlNTNXDgwGJj06dPl4eHh3r27Knc3FxFRkbqzTffNMcrVaqkZcuW6bHHHlN4eLiqVaum6OhoPffcc2ZNSEiIli9frmHDhmnGjBmqV6+e3n33XUVGRpbL9gEAgIqvQj2HqKLiOUTF8RwiAEBFd0U+hwgAAMBdCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDyCEQAAMDy3B6Ifv31Vz300EOqVauWfHx81LJlS23bts0cNwxD48ePV926deXj46OIiAgdOHDAZY4TJ06ob9++stvt8vPz06BBg5Sdne1Ss2vXLt10002qUqWKgoODNXXq1HLZPgAAUPG5NRCdPHlSHTp0UOXKlbVixQr98MMPevnll1WjRg2zZurUqZo5c6Zmz56tLVu2qFq1aoqMjNSZM2fMmr59+2rPnj1KSEjQsmXLtGHDBg0ePNgcdzqd6tq1qxo0aKDk5GRNmzZNEydO1Ntvv12u2wsAAComm2EYhrs+fMyYMdq4caO+/vrrEscNw1BQUJCefvppjRgxQpKUlZWlgIAAxcfHq3fv3tq7d69CQ0O1detWtW3bVpK0cuVK3XHHHTpy5IiCgoI0a9YsjRs3TmlpafLy8jI/e8mSJdq3b98F+3Q6nXI4HMrKypLdbi+jrf9/Go5ZXuZzXm4/T4lydwsAAJzXpfz8dusRoqVLl6pt27a677775O/vr3//+9965513zPFDhw4pLS1NERER5jqHw6EbbrhBSUlJkqSkpCT5+fmZYUiSIiIi5OHhoS1btpg1nTp1MsOQJEVGRiolJUUnT54s1ldubq6cTqfLAgAArl5uDUQ//fSTZs2apSZNmmjVqlV67LHH9OSTT2ru3LmSpLS0NElSQECAy/sCAgLMsbS0NPn7+7uMe3p6qmbNmi41Jc1x7mecKy4uTg6Hw1yCg4PLYGsBAEBF5dZAVFhYqDZt2ujFF1/Uv//9bw0ePFiPPvqoZs+e7c62NHbsWGVlZZnL4cOH3doPAAC4vNwaiOrWravQ0FCXdc2bN1dqaqokKTAwUJKUnp7uUpOenm6OBQYGKiMjw2X87NmzOnHihEtNSXOc+xnn8vb2lt1ud1kAAMDVy62BqEOHDkpJSXFZt3//fjVo0ECSFBISosDAQCUmJprjTqdTW7ZsUXh4uCQpPDxcmZmZSk5ONmvWrFmjwsJC3XDDDWbNhg0blJ+fb9YkJCSoadOmLne0AQAAa3JrIBo2bJg2b96sF198UT/++KPmzZunt99+WzExMZIkm82moUOHavLkyVq6dKl2796t/v37KygoSN27d5f05xGlbt266dFHH9W3336rjRs3KjY2Vr1791ZQUJAkqU+fPvLy8tKgQYO0Z88eLViwQDNmzNDw4cPdtekAAKAC8XTnh//nP//R4sWLNXbsWD333HMKCQnRq6++qr59+5o1o0aNUk5OjgYPHqzMzEx17NhRK1euVJUqVcyajz/+WLGxserSpYs8PDzUs2dPzZw50xx3OBxavXq1YmJiFBYWptq1a2v8+PEuzyoCAADW5dbnEF0peA5RcTyHCABQ0V0xzyECAACoCAhEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8twaiCZOnCibzeayNGvWzBw/c+aMYmJiVKtWLfn6+qpnz55KT093mSM1NVVRUVGqWrWq/P39NXLkSJ09e9alZt26dWrTpo28vb3VuHFjxcfHl8fmAQCAK0SpAtFPP/1UZg20aNFCx44dM5dvvvnGHBs2bJi++OILLVq0SOvXr9fRo0fVo0cPc7ygoEBRUVHKy8vTpk2bNHfuXMXHx2v8+PFmzaFDhxQVFaXOnTtrx44dGjp0qB555BGtWrWqzLYBAABc2TxL86bGjRvr5ptv1qBBg9SrVy9VqVKl9A14eiowMLDY+qysLL333nuaN2+ebr31VknSnDlz1Lx5c23evFnt27fX6tWr9cMPP+irr75SQECArr/+ej3//PMaPXq0Jk6cKC8vL82ePVshISF6+eWXJUnNmzfXN998o+nTpysyMrLUfQMAgKtHqY4Qbd++Xa1atdLw4cMVGBio//73v/r2229L1cCBAwcUFBSkRo0aqW/fvkpNTZUkJScnKz8/XxEREWZts2bNVL9+fSUlJUmSkpKS1LJlSwUEBJg1kZGRcjqd2rNnj1lz7hxFNUVzlCQ3N1dOp9NlAQAAV69SBaLrr79eM2bM0NGjR/X+++/r2LFj6tixo6677jq98sorOn78+EXNc8MNNyg+Pl4rV67UrFmzdOjQId100006deqU0tLS5OXlJT8/P5f3BAQEKC0tTZKUlpbmEoaKxovGzlfjdDp1+vTpEvuKi4uTw+Ewl+Dg4IvaHgAAcGX6RxdVe3p6qkePHlq0aJFeeukl/fjjjxoxYoSCg4PVv39/HTt27Lzvv/3223XfffepVatWioyM1JdffqnMzEwtXLjwn7T1j40dO1ZZWVnmcvjwYbf2AwAALq9/FIi2bdumxx9/XHXr1tUrr7yiESNG6ODBg0pISNDRo0d1zz33XNJ8fn5+uvbaa/Xjjz8qMDBQeXl5yszMdKlJT083rzkKDAwsdtdZ0esL1djtdvn4+JTYh7e3t+x2u8sCAACuXqUKRK+88opatmypG2+8UUePHtUHH3ygX375RZMnT1ZISIhuuukmxcfHa/v27Zc0b3Z2tg4ePKi6desqLCxMlStXVmJiojmekpKi1NRUhYeHS5LCw8O1e/duZWRkmDUJCQmy2+0KDQ01a86do6imaA4AAIBS3WU2a9YsDRw4UAMGDFDdunVLrPH399d777133nlGjBihu+66Sw0aNNDRo0c1YcIEVapUSQ8++KAcDocGDRqk4cOHq2bNmrLb7XriiScUHh6u9u3bS5K6du2q0NBQ9evXT1OnTlVaWpqeeeYZxcTEyNvbW5I0ZMgQvf766xo1apQGDhyoNWvWaOHChVq+fHlpNh0AAFyFShWIDhw4cMEaLy8vRUdHn7fmyJEjevDBB/X777+rTp066tixozZv3qw6depIkqZPny4PDw/17NlTubm5ioyM1Jtvvmm+v1KlSlq2bJkee+wxhYeHq1q1aoqOjtZzzz1n1oSEhGj58uUaNmyYZsyYoXr16undd9/llnsAAGCyGYZhXOqb5syZI19fX913330u6xctWqQ//vjjgkHoSuN0OuVwOJSVlXVZridqOObKO1r185Qod7cAAMB5XcrP71JdQxQXF6fatWsXW+/v768XX3yxNFMCAAC4TakCUWpqqkJCQoqtb9CggflgRQAAgCtFqQKRv7+/du3aVWz9zp07VatWrX/cFAAAQHkqVSB68MEH9eSTT2rt2rUqKChQQUGB1qxZo6eeekq9e/cu6x4BAAAuq1LdZfb888/r559/VpcuXeTp+ecUhYWF6t+/P9cQAQCAK06pApGXl5cWLFig559/Xjt37pSPj49atmypBg0alHV/AAAAl12pAlGRa6+9Vtdee21Z9QIAAOAWpQpEBQUFio+PV2JiojIyMlRYWOgyvmbNmjJpDgAAoDyUKhA99dRTio+PV1RUlK677jrZbLay7gsAAKDclCoQzZ8/XwsXLtQdd9xR1v0AAACUu1Lddu/l5aXGjRuXdS8AAABuUapA9PTTT2vGjBkqxa9BAwAAqHBKdcrsm2++0dq1a7VixQq1aNFClStXdhn/7LPPyqQ5AACA8lCqQOTn56d77723rHsBAABwi1IFojlz5pR1HwAAAG5TqmuIJOns2bP66quv9NZbb+nUqVOSpKNHjyo7O7vMmgMAACgPpTpC9Msvv6hbt25KTU1Vbm6ubrvtNlWvXl0vvfSScnNzNXv27LLuEwAA4LIp1RGip556Sm3bttXJkyfl4+Njrr/33nuVmJhYZs0BAACUh1IdIfr666+1adMmeXl5uaxv2LChfv311zJpDAAAoLyU6ghRYWGhCgoKiq0/cuSIqlev/o+bAgAAKE+lCkRdu3bVq6++ar622WzKzs7WhAkT+HUeAADgilOqU2Yvv/yyIiMjFRoaqjNnzqhPnz46cOCAateurU8++aSsewQAALisShWI6tWrp507d2r+/PnatWuXsrOzNWjQIPXt29fl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3X777apfv75OnTqlefPmad26dVq1apUcDocGDRqk4cOHq2bNmrLb7XriiScUHh6u9u3bS5K6du2q0NBQ9evXT1OnTlVaWpqeeeYZxcTEmEd4hgwZotdff12jRo3SwIEDtWbNGi1cuFDLl195d3YBAIDLw62BKCMjQ/3799exY8fkcDjUqlUrrVq1Srfddpskafr06fLw8FDPnj2Vm5uryMhIvfnmm+b7K1WqpGXLlumxxx5TeHi4qlWrpujoaD333HNmTUhIiJYvX65hw4ZpxowZqlevnt59911FRkaW+/YCAICKqcI9h6gi4jlExfEcIgBARXdFPocIAADAXQhEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8twaiOLi4vSf//xH1atXl7+/v7p3766UlBSXmjNnzigmJka1atWSr6+vevbsqfT0dJea1NRURUVFqWrVqvL399fIkSN19uxZl5p169apTZs28vb2VuPGjRUfH3+5Nw8AAFwh3BqI1q9fr5iYGG3evFkJCQnKz89X165dlZOTY9YMGzZMX3zxhRYtWqT169fr6NGj6tGjhzleUFCgqKgo5eXladOmTZo7d67i4+M1fvx4s+bQoUOKiopS586dtWPHDg0dOlSPPPKIVq1aVa7bCwAAKiabYRiGu5socvz4cfn7+2v9+vXq1KmTsrKyVKdOHc2bN0+9evWSJO3bt0/NmzdXUlKS2rdvrxUrVujOO+/U0aNHFRAQIEmaPXu2Ro8erePHj8vLy0ujR4/W8uXL9f3335uf1bt3b2VmZmrlypUX7MvpdMrhcCgrK0t2u73Mt7vhmOVlPufl9vOUKHe3AADAeV3Kz+8KdQ1RVlaWJKlmzZqSpOTkZOXn5ysiIsKsadasmerXr6+kpCRJUlJSklq2bGmGIUmKjIyU0+nUnj17zJpz5yiqKZoDAABYm6e7GyhSWFiooUOHqkOHDrruuuskSWlpafLy8pKfn59LbUBAgNLS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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "result = %sql SELECT trip_distance FROM taxi\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "data = result.toPandas()\n", + "\n", + "plt.hist(data[\"trip_distance\"])\n", + "plt.xlabel(\"Trip Distance\")\n", + "plt.ylabel(\"Frequency\")\n", + "plt.title(\"Histogram of Trip Distance\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjIAAAHHCAYAAACle7JuAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA6fklEQVR4nO3deVxWZf7/8fcNCrK7sTYqGBo2on7VSlFMciGyhZCa0iY1aybHNm21zWVKyyW1zZaZsmXUJr+ok2VpJsokNqVpuaMjaYGopSwuiNzn94c/7q93oIHecrjw9Xw8ziPOda77nA9g3O/7Otc5x2FZliUAAAADedldAAAAwNkiyAAAAGMRZAAAgLEIMgAAwFgEGQAAYCyCDAAAMBZBBgAAGIsgAwAAjEWQAQAAxiLIAPWcw+HQuHHj7C7Dzddff62EhAQFBATI4XBo/fr1Hj/G0KFDFR0d7fH91sTs2bPlcDiUm5trax1AfUaQAc5SxZvUqUtYWJiSkpK0ZMkSu8s7Z5s3b9a4ceM8/iZcVlamm266Sb/88oumT5+u9957T61atarULzo6utLPt6pl9uzZHq3vdDIzM92O6+vrq/DwcPXu3VsTJ07U/v37PXKcI0eOaNy4ccrMzPTI/oD6roHdBQCmmzBhgmJiYmRZlgoKCjR79mxdc801+uijj3TttdfaXd5Z27x5s8aPH6/evXt7dGRj586d+uGHH/Tmm2/qzjvvPG2/GTNmqKSkxLX+ySefaO7cuZo+fbqaN2/uak9ISKjy9W+++aacTqfH6q5w33336bLLLlN5ebn279+v1atXa+zYsXrhhRf0z3/+U1dddZWr7x//+Efdcsst8vX1rfb+jxw5ovHjx0uSevfu7enygXqHIAOco5SUFHXt2tW1Pnz4cIWHh2vu3LlGB5nzZd++fZKkxo0bn7Ffamqq2/revXs1d+5cpaamnjFYHT58WAEBAWrYsOE5Vlq1xMREpaenu7Vt2LBB/fv318CBA7V582ZFRkZKkry9veXt7X1e6gBwEqeWAA9r3Lix/Pz81KCB++eEw4cP68EHH1SLFi3k6+urSy65RFOnTlXFA+iPHj2quLg4xcXF6ejRo67X/fLLL4qMjFRCQoLKy8slnZz/ERgYqP/+979KTk5WQECAoqKiNGHCBFXngfbffvutUlJSFBwcrMDAQPXp00dr1qxxbZ89e7ZuuukmSVJSUpLrdMpvne744osvlJiYqICAADVu3Fg33HCDtmzZ4to+dOhQXXnllZKkm266SQ6H45xGHSp+Djt37tQ111yjoKAgDR482LXt1MCTm5srh8OhqVOnavr06WrVqpX8/Px05ZVXauPGjWddgyR17NhRM2bM0KFDh/Tyyy+72quaI/PNN98oOTlZzZs3l5+fn2JiYnTHHXe4agwNDZUkjR8/3vVzr5jj9N1332no0KFq3bq1GjVqpIiICN1xxx36+eef3eoZN26cHA6HduzYoaFDh6px48YKCQnRsGHDdOTIkUr1v//++7r88svl7++vJk2aqFevXlq6dKlbnyVLlrh+t0FBQRowYIA2bdp0Tj83wBMYkQHOUWFhoQ4cOCDLsrRv3z699NJLKikp0W233ebqY1mWrr/+eq1YsULDhw9Xp06d9Nlnn+nhhx/WTz/9pOnTp8vPz0/vvPOOevTooSeeeEIvvPCCJGnkyJEqLCzU7Nmz3T7dl5eX6+qrr1a3bt00efJkffrppxo7dqxOnDihCRMmnLbeTZs2KTExUcHBwXrkkUfUsGFDvf766+rdu7dWrlypK664Qr169dJ9992nF198UY8//rjatWsnSa7/VuXzzz9XSkqKWrdurXHjxuno0aN66aWX1KNHD61bt07R0dH685//rIsuukgTJ050naIJDw8/p5//iRMnlJycrJ49e2rq1Kny9/c/Y/93331XxcXFGjlypI4dO6aZM2fqqquu0vfff39OtaSnp2v48OFaunSpnn322Sr77Nu3T/3791doaKgee+wxNW7cWLm5ucrIyJAkhYaGatasWRoxYoRuvPFGpaWlSZI6dOggSVq2bJn++9//atiwYYqIiNCmTZv0xhtvaNOmTVqzZo0cDofb8W6++WbFxMRo0qRJWrdunf72t78pLCxMzz//vKvP+PHjNW7cOCUkJGjChAny8fHRV199pS+++EL9+/eXJL333nsaMmSIkpOT9fzzz+vIkSOaNWuWevbsqW+//db2SdW4wFkAzsrbb79tSaq0+Pr6WrNnz3bru3DhQkuS9cwzz7i1p6enWw6Hw9qxY4erbcyYMZaXl5e1atUq68MPP7QkWTNmzHB73ZAhQyxJ1r333utqczqd1oABAywfHx9r//79rnZJ1tixY13rqamplo+Pj7Vz505XW15enhUUFGT16tXL1VZx7BUrVlTr59GpUycrLCzM+vnnn11tGzZssLy8vKzbb7/d1bZixQpLkvXhhx9Wa78VpkyZYkmydu3a5Wqr+Dk89thjlfoPGTLEatWqlWt9165dliTLz8/P+vHHH13tX331lSXJGjVq1BmPX526O3bsaDVp0sS1XvFvpKLmBQsWWJKsr7/++rT72L9/f6XfWYUjR45Uaps7d64lyVq1apWrbezYsZYk64477nDre+ONN1rNmjVzrefk5FheXl7WjTfeaJWXl7v1dTqdlmVZVnFxsdW4cWPrrrvuctu+d+9eKyQkpFI7UNs4tQSco1deeUXLli3TsmXL9P777yspKUl33nmn61O2dHKiqre3t+677z631z744IOyLMvtKqdx48bp97//vYYMGaK//OUvuvLKKyu9rsI999zj+trhcOiee+7R8ePH9fnnn1fZv7y8XEuXLlVqaqpat27tao+MjNSgQYP073//W0VFRTX+GeTn52v9+vUaOnSomjZt6mrv0KGD+vXrp08++aTG+6yJESNGVLtvamqqLrroItf65ZdfriuuuMIjNQYGBqq4uPi02yvmBS1evFhlZWU13r+fn5/r62PHjunAgQPq1q2bJGndunWV+t99991u64mJifr5559dv+OFCxfK6XTq6aeflpeX+9tBxejOsmXLdOjQId166606cOCAa/H29tYVV1yhFStW1Pj7ADyJIAOco8svv1x9+/ZV3759NXjwYH388ce69NJLXaFCkn744QdFRUUpKCjI7bUVp2p++OEHV5uPj4/eeust7dq1S8XFxXr77bcrnTKQJC8vL7cwIklt27aVpNNeMr1//34dOXJEl1xySaVt7dq1k9Pp1J49e6r/zf9/FfWfbr8HDhzQ4cOHa7zf6mjQoIF+97vfVbt/mzZtKrW1bdvWI5eZl5SUVPodn+rKK6/UwIEDNX78eDVv3lw33HCD3n77bZWWllZr/7/88ovuv/9+hYeHy8/PT6GhoYqJiZF08hTnr7Vs2dJtvUmTJpKkgwcPSjp5BZmXl5cuvfTS0x4zJydHknTVVVcpNDTUbVm6dKlr8jZgF+bIAB7m5eWlpKQkzZw5Uzk5Ofr9739f43189tlnkk5+6s7JyXG9WaEyX1/fSqMJdigrK9P27dvVvn370/ZxOByaP3++1qxZo48++kifffaZ7rjjDk2bNk1r1qxRYGDgGY9x8803a/Xq1Xr44YfVqVMnBQYGyul06uqrr67yUvPTXTFlVWNCeIWK/b733nuKiIiotP3Xk9qB2sa/QOA8OHHihCS57oPSqlUrff755youLnb7xL5161bX9grfffedJkyYoGHDhmn9+vW688479f333yskJMTtGE6nU//9739dozCStH37dkk67eTL0NBQ+fv7a9u2bZW2bd26VV5eXmrRooUkVTkKdDoV9Z9uv82bN1dAQEC193c+VYwwnGr79u3nPGF1/vz5Onr0qJKTk3+zb7du3dStWzc9++yzmjNnjgYPHqx58+bpzjvvPO3P/eDBg1q+fLnGjx+vp59+2tVe1fdTXRdffLGcTqc2b96sTp06nbaPJIWFhalv375nfSzgfLH/YwxQz5SVlWnp0qXy8fFxnTq65pprVF5e7nZpriRNnz5dDodDKSkprtcOHTpUUVFRmjlzpmbPnq2CggKNGjWqymOduj/LsvTyyy+rYcOG6tOnT5X9vb291b9/fy1atMjtVEpBQYHmzJmjnj17Kjg4WJJcwePQoUO/+T1HRkaqU6dOeuedd9z6b9y4UUuXLtU111zzm/uoLQsXLtRPP/3kWv/Pf/6jr776yvU7OBsbNmzQAw88oCZNmmjkyJGn7Xfw4MFKoyEVAaLi9FLFVVe//rlXjK78+vUzZsw467pTU1Pl5eWlCRMmVBrRqThOcnKygoODNXHixCrn9XjqjsbA2WJEBjhHS5YscY2s7Nu3T3PmzFFOTo4ee+wxVyi47rrrlJSUpCeeeEK5ubnq2LGjli5dqkWLFumBBx5wfep95plntH79ei1fvlxBQUHq0KGDnn76aT355JNKT093CwSNGjXSp59+qiFDhuiKK67QkiVL9PHHH+vxxx933YukKs8884yWLVumnj176i9/+YsaNGig119/XaWlpZo8ebKrX6dOneTt7a3nn39ehYWF8vX11VVXXaWwsLAq9ztlyhSlpKSoe/fuGj58uOvy65CQkDr1rKfY2Fj17NlTI0aMUGlpqWbMmKFmzZrpkUceqdbrs7KydOzYMZWXl+vnn3/Wl19+qX/9618KCQnRggULqjz9UuGdd97Rq6++qhtvvFEXX3yxiouL9eabbyo4ONj1u/Xz89Oll16qDz74QG3btlXTpk3Vvn17tW/fXr169dLk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aty48W/ud+jQoTp06JAWLlx41rUBqNt41hIAl/z8fNfXH3zwgZ5++mlt27bN1RYYGOj62rIslZeXKzAw0K39XGzbtk3BwcEqKirSunXrNHnyZP39739XZmam4uPjJUkhISEeORaA+oFTSwBcIiIiXEtISIgcDodrfevWrQoKCtKSJUvUpUsX+fr66t///nelU0sVp37Gjx+v0NBQBQcH6+6779bx48d/8/hhYWGKiIhQ27Ztdcstt+jLL79UaGioRowYUWn/FebPn6/4+Hj5+fmpWbNm6tu3rw4fPqxx48bpnXfe0aJFi+RwOORwOJSZmSlJevTRR9W2bVv5+/urdevWeuqpp1RWVubaZ8X39N577yk6OlohISG65ZZbVFxc7OrjdDo1efJkxcbGytfXVy1bttSzzz7r2r5nzx7dfPPNaty4sZo2baobbrhBubm5Nf+lADgjggyAGnnsscf03HPPacuWLerQoUOVfZYvX64tW7YoMzNTc+fOVUZGhsaPH1/jY/n5+enuu+/Wl19+qX379lXanp+fr1tvvVV33HGH63hpaWmyLEsPPfSQbr75Zl199dXKz89Xfn6+EhISJElBQUGaPXu2Nm/erJkzZ+rNN9/U9OnT3fa9c+dOLVy4UIsXL9bixYu1cuVKPffcc67tY8aM0XPPPaennnpKmzdv1pw5cxQeHi5JKisrU3JysoKCgpSVlaUvv/xSgYGBuvrqq6sV6ABUH6eWANTIhAkT1K9fvzP28fHx0VtvvSV/f3/9/ve/14QJE/Twww/rr3/9q7y8avb5KS4uTpKUm5ursLAwt235+fk6ceKE0tLS1KpVK0lynYKSTgah0tJSRUREuL3uySefdH0dHR2thx56SPPmzdMjjzzianc6nZo9e7aCgoIkSX/84x+1fPlyPfvssyouLtbMmTP18ssva8iQIZKkiy++WD179pR08rSc0+nU3/72N9e8o7fffluNGzdWZmam+vfvX6OfAYDTI8gAqJGuXbv+Zp+OHTvK39/ftd69e3eVlJRoz549rsBRXRXXI1Q1Ebljx47q06eP4uPjlZycrP79+ys9PV1NmjQ54z4/+OADvfjii9q5c6dKSkp04sQJBQcHu/WJjo52hRhJioyMdI0KbdmyRaWlperTp0+V+9+wYYN27Njh9npJOnbsmHbu3Pnb3zSAauPUEoAaCQgIqNXjbdmyRdLJYPFr3t7eWrZsmZYsWaJLL71UL730ki655BLt2rXrtPvLzs7W4MGDdc0112jx4sX69ttv9cQTT1Q65dOwYUO3dYfDIafTKenkSM+ZlJSUqEuXLlq/fr3bsn37dg0aNKg63zaAaiLIAPC4DRs26OjRo671NWvWKDAwUC1atKjRfo4ePao33nhDvXr1UmhoaJV9HA6HevToofHjx+vbb7+Vj4+PFixYIOnkKa7y8nK3/qtXr1arVq30xBNPqGvXrmrTpo1++OGHGtXVpk0b+fn5afny5VVu79y5s3JychQWFqbY2Fi3hauuAM8iyADwuOPHj2v48OHavHmzPvnkE40dO1b33HPPb86P2bdvn/bu3aucnBzNmzdPPXr00IEDBzRr1qwq+3/11VeaOHGivvnmG+3evVsZGRnav3+/2rVrJ+nkKM53332nbdu26cCBAyorK1ObNm20e/duzZs3Tzt37tSLL77oCj7V1ahRIz366KN65JFH9O6772rnzp1as2aN/v73v0uSBg8erObNm+uGG25QVlaWdu3apczMTN1333368ccfa3QsAGfGHBkAHtenTx+1adNGvXr1UmlpqW699dZKN9KryiWXXCKHw6HAwEC1bt1a/fv31+jRoytN1q0QHBysVatWacaMGSoqKlKrVq00bdo0paSkSJLuuusuZWZmqmvXriopKdGKFSt0/fXXa9SoUbrnnntUWlqqAQMG6KmnnqpWfad66qmn1KBBAz399NPKy8tTZGSk7r77bkmSv7+/Vq1apUcffVRpaWkqLi7WRRddpD59+lSaiwPg3HBnXwAexd10AdQmTi0BAABjEWQAAICxOLUEAACMxYgMAAAwFkEGAAAYiyADAACMRZABAADGIsgAAABjEWQAAICxCDIAAMBYBBkAAGAsggwAADDW/wPY4fOD7IJuHgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "result = %sql SELECT trip_distance FROM taxi\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "data = result.toPandas()\n", + "\n", + "plt.boxplot(data[\"trip_distance\"])\n", + "plt.xlabel(\"Trip Distance\")\n", + "plt.ylabel(\"Value\")\n", + "plt.title(\"Boxplot of Trip Distance\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Persist - Not Supported" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Clean up\n", + "\n", + "To stop and remove the container:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n", + "4a7133e0b70b wh1isper/sparglim-server \"tini -- sparglim-se…\" 4 minutes ago Up 4 minutes 0.0.0.0:4040->4040/tcp, 0.0.0.0:15002->15002/tcp spark\n", + "f019407c6426 docker.dev.slicelife.com/onelogin-aws-assume-role:stable \"onelogin-aws-assume…\" 2 weeks ago Up 2 weeks heuristic_tu\n" + ] + } + ], + "source": [ + "! docker container ls" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%capture out\n", + "! docker container ls --filter ancestor=wh1isper/sparglim-server --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Container id: 4a7133e0b70b\n" + ] + } + ], + "source": [ + "container_id = out.stdout.strip()\n", + "print(f\"Container id: {container_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4a7133e0b70b\n" + ] + } + ], + "source": [ + "! docker container stop {container_id}" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4a7133e0b70b\n" + ] + } + ], + "source": [ + "! docker container rm {container_id}" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + }, + "myst": { + "html_meta": { + "description lang=en": "Query a PostgreSQL database from Jupyter via JupySQL", + "keywords": "jupyter, sql, jupysql, postgres", + "property=og:locale": "en_US" + } + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 54b0b4ff1..7262065c7 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1109,7 +1109,7 @@ def _get_database_information(self): return { "dialect": self.dialect, "driver": self._connection_class_name, - "server_version_info": self._connection.version(), + "server_version_info": self._connection.version, } @property From c4acca13ef17395b5f16a091dc3ca0188821b32d Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Tue, 19 Dec 2023 01:09:52 +0000 Subject: [PATCH 08/29] exclude execution --- doc/conf.py | 1 + doc/integrations/spark.ipynb | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 5e1792154..39080d77d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -27,6 +27,7 @@ "integrations/oracle.ipynb", "integrations/snowflake.ipynb", "integrations/redshift.ipynb", + "integrations/spark.ipynb", ] nb_execution_in_temp = True nb_execution_show_tb = True diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index 2e518be11..481a8da6d 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -831,7 +831,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "jupysql", "language": "python", "name": "python3" }, @@ -854,6 +854,11 @@ "property=og:locale": "en_US" } }, + "vscode": { + "interpreter": { + "hash": "8de7291ac4f217ed756f77e1d71d41823fff9c4ffb13df0a183e9309929ad9aa" + } + }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, From 96846d4ae46eeffe2b8e735bb1c4111e273ccc6b Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Tue, 19 Dec 2023 09:57:14 +0000 Subject: [PATCH 09/29] documentation updates --- doc/integrations/compatibility.md | 15 ++++ doc/integrations/spark.ipynb | 111 +++++++++++++----------------- src/tests/test_magic.py | 1 - 3 files changed, 61 insertions(+), 66 deletions(-) diff --git a/doc/integrations/compatibility.md b/doc/integrations/compatibility.md index 4e6b36432..d26199150 100644 --- a/doc/integrations/compatibility.md +++ b/doc/integrations/compatibility.md @@ -114,4 +114,19 @@ These table reflects the compatibility status of JupySQL `>=0.7` - Listing tables with `%sqlcmd tables` ✅ - Listing columns with `%sqlcmd columns` ✅ - Parametrized SQL queries via `{{parameter}}` ✅ +- Interactive SQL queries via `--interact` ✅ + +## Spark + +- Running queries with `%%sql` ✅ +- CTEs with `%%sql --save NAME` ✅ +- Plotting with `%%sqlplot boxplot` ❌ +- Plotting with `%%sqlplot bar` ❌ +- Plotting with `%%sqlplot pie` ❌ +- Plotting with `%%sqlplot histogram` ❌ +- Plotting with `ggplot` API ❌ +- Profiling tables with `%sqlcmd profile` ❌ +- Listing tables with `%sqlcmd tables` ❌ +- Listing columns with `%sqlcmd columns` ❌ +- Parametrized SQL queries via `{{parameter}}` ✅ - Interactive SQL queries via `--interact` ✅ \ No newline at end of file diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index 481a8da6d..ee2451124 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -7,7 +7,7 @@ "source": [ "# Spark\n", "\n", - "This tutorial will show you how to get a Spark instance up and running locally to test JupySQL. You can run this in a Jupyter notebook. We'll use Spark Connect which is the new thin client for Spark" + "This tutorial will show you how to get a Spark instance up and running locally to integrate with JupySQL. You can run this in a Jupyter notebook. We'll use [Spark Connect](https://spark.apache.org/docs/latest/api/python/getting_started/quickstart_connect.html) which is the new thin client for Spark" ] }, { @@ -36,7 +36,7 @@ } ], "source": [ - "%pip install jupysql pyspark==3.4.1 arrow pyarrow==12.0.1 pandas --quiet" + "%pip install jupysql pyspark==3.4.1 arrow pyarrow==12.0.1 pandas grpc-status --quiet" ] }, { @@ -51,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 26, "metadata": { "tags": [] }, @@ -60,7 +60,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "4a7133e0b70b9435d575ef2eda12644a589ab78ef5800f4d95775377c13ef18c\n" + "8e3831298eb9dce9d26b2f2d3df5a1a64634a19976f4eaeabf92c45afd478a39\n" ] } ], @@ -89,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 27, "metadata": { "tags": [] }, @@ -110,12 +110,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Set eagerEval on to print dataframes" + "Set [eagerEval](https://spark.apache.org/docs/latest/api/python/getting_started/quickstart_df.html#Viewing-Data) on to print dataframes, This makes Spark print dataframes eagerly in notebook environments, rather than it's default lazy execution which requires .show() to see the data. In Spark 3.4.1 we need to override, as below, but in 3.5.0 it will print in html. " ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -134,12 +134,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Add dataset to temporary view to allow querying" + "Add dataset to temporary view to allow querying:" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 29, "metadata": { "tags": [] }, @@ -160,7 +160,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 30, "metadata": { "tags": [] }, @@ -180,7 +180,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 31, "metadata": { "tags": [] }, @@ -209,46 +209,34 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 46, "metadata": { "tags": [] }, "outputs": [ { - "data": { - "text/html": [ - "Running query in 'SparkSession'" - ], - "text/plain": [ - "Running query in 'SparkSession'" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+---------+--------+-----------+\n", - "|namespace|viewName|isTemporary|\n", - "+---------+--------+-----------+\n", - "| |taxi |true |\n", - "+---------+--------+-----------+\n", - "\n" + "ename": "NotImplementedError", + "evalue": "This feature is only available for SQLAlchemy connections", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNotImplementedError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[46], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mget_ipython\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_line_magic\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43msqlcmd\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtables\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/homebrew/anaconda3/envs/jupysql/lib/python3.10/site-packages/IPython/core/interactiveshell.py:2456\u001b[0m, in \u001b[0;36mInteractiveShell.run_line_magic\u001b[0;34m(self, magic_name, line, _stack_depth)\u001b[0m\n\u001b[1;32m 2454\u001b[0m kwargs[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlocal_ns\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_local_scope(stack_depth)\n\u001b[1;32m 2455\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbuiltin_trap:\n\u001b[0;32m-> 2456\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mfn\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2458\u001b[0m \u001b[38;5;66;03m# The code below prevents the output from being displayed\u001b[39;00m\n\u001b[1;32m 2459\u001b[0m \u001b[38;5;66;03m# when using magics with decorator @output_can_be_silenced\u001b[39;00m\n\u001b[1;32m 2460\u001b[0m \u001b[38;5;66;03m# when the last Python token in the expression is a ';'.\u001b[39;00m\n\u001b[1;32m 2461\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(fn, magic\u001b[38;5;241m.\u001b[39mMAGIC_OUTPUT_CAN_BE_SILENCED, \u001b[38;5;28;01mFalse\u001b[39;00m):\n", + "File \u001b[0;32m~/scripts/jupysql/src/sql/magic_cmd.py:104\u001b[0m, in \u001b[0;36mSqlCmdMagic._validate_execute_inputs\u001b[0;34m(self, line)\u001b[0m\n\u001b[1;32m 101\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m command \u001b[38;5;129;01min\u001b[39;00m COMMANDS_SQLALCHEMY_ONLY:\n\u001b[1;32m 102\u001b[0m support_only_sql_alchemy_connection(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%sqlcmd \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcommand\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 104\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcommand\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mothers\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 105\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 106\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m exceptions\u001b[38;5;241m.\u001b[39mUsageError(\n\u001b[1;32m 107\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%sqlcmd has no command: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcommand\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 108\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mValid commands are: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[1;32m 109\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m, \u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(AVAILABLE_SQLCMD_COMMANDS)\n\u001b[1;32m 110\u001b[0m )\n\u001b[1;32m 111\u001b[0m )\n", + "File \u001b[0;32m~/scripts/jupysql/src/sql/magic_cmd.py:132\u001b[0m, in \u001b[0;36mSqlCmdMagic.execute\u001b[0;34m(self, cmd_name, others, cell, local_ns)\u001b[0m\n\u001b[1;32m 130\u001b[0m cmd \u001b[38;5;241m=\u001b[39m router\u001b[38;5;241m.\u001b[39mget(cmd_name)\n\u001b[1;32m 131\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m cmd:\n\u001b[0;32m--> 132\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mcmd\u001b[49m\u001b[43m(\u001b[49m\u001b[43mothers\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/scripts/jupysql/src/sql/cmd/tables.py:30\u001b[0m, in \u001b[0;36mtables\u001b[0;34m(others)\u001b[0m\n\u001b[1;32m 26\u001b[0m parser\u001b[38;5;241m.\u001b[39madd_argument(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m-s\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m--schema\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28mtype\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mstr\u001b[39m, help\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSchema name\u001b[39m\u001b[38;5;124m\"\u001b[39m, required\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[1;32m 28\u001b[0m args \u001b[38;5;241m=\u001b[39m parser\u001b[38;5;241m.\u001b[39mparse_args(others)\n\u001b[0;32m---> 30\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43minspect\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_table_names\u001b[49m\u001b[43m(\u001b[49m\u001b[43mschema\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43margs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mschema\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/homebrew/anaconda3/envs/jupysql/lib/python3.10/site-packages/ploomber_core/telemetry/telemetry.py:679\u001b[0m, in \u001b[0;36mTelemetry.log_call.._log_call..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 677\u001b[0m result \u001b[38;5;241m=\u001b[39m func(_payload, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 678\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 679\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 680\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m 681\u001b[0m metadata_error \u001b[38;5;241m=\u001b[39m {\n\u001b[1;32m 682\u001b[0m \u001b[38;5;66;03m# can we log None to posthog?\u001b[39;00m\n\u001b[1;32m 683\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mgetattr\u001b[39m(e, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype_\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m),\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 686\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39m_payload,\n\u001b[1;32m 687\u001b[0m }\n", + "File \u001b[0;32m~/scripts/jupysql/src/sql/inspect.py:483\u001b[0m, in \u001b[0;36mget_table_names\u001b[0;34m(schema)\u001b[0m\n\u001b[1;32m 480\u001b[0m \u001b[38;5;129m@telemetry\u001b[39m\u001b[38;5;241m.\u001b[39mlog_call()\n\u001b[1;32m 481\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_table_names\u001b[39m(schema\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 482\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Get table names for a given connection\"\"\"\u001b[39;00m\n\u001b[0;32m--> 483\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mTables\u001b[49m\u001b[43m(\u001b[49m\u001b[43mschema\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/scripts/jupysql/src/sql/inspect.py:38\u001b[0m, in \u001b[0;36mTables.__init__\u001b[0;34m(self, schema, conn)\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, schema\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, conn\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m---> 38\u001b[0m inspector \u001b[38;5;241m=\u001b[39m \u001b[43m_get_inspector\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconn\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_table \u001b[38;5;241m=\u001b[39m PrettyTable()\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_table\u001b[38;5;241m.\u001b[39mfield_names \u001b[38;5;241m=\u001b[39m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mName\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n", + "File \u001b[0;32m~/scripts/jupysql/src/sql/inspect.py:21\u001b[0m, in \u001b[0;36m_get_inspector\u001b[0;34m(conn)\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m exceptions\u001b[38;5;241m.\u001b[39mRuntimeError(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNo active connection\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 20\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 21\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m inspect(\u001b[43mConnectionManager\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcurrent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconnection_sqlalchemy\u001b[49m)\n", + "File \u001b[0;32m~/scripts/jupysql/src/sql/connection/connection.py:1126\u001b[0m, in \u001b[0;36mSparkConnectConnection.connection_sqlalchemy\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1120\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[1;32m 1121\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mconnection_sqlalchemy\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 1122\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1123\u001b[0m \u001b[38;5;124;03m Raises NotImplementedError since Spark connections don't have a SQLAlchemy\u001b[39;00m\n\u001b[1;32m 1124\u001b[0m \u001b[38;5;124;03m connection object\u001b[39;00m\n\u001b[1;32m 1125\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1126\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mNotImplementedError\u001b[39;00m(\n\u001b[1;32m 1127\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThis feature is only available for SQLAlchemy connections\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1128\u001b[0m )\n", + "\u001b[0;31mNotImplementedError\u001b[0m: This feature is only available for SQLAlchemy connections" ] - }, - { - "data": { - "text/plain": [] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "%sql show views in default" + "%sqlcmd tables" ] }, { @@ -261,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 33, "metadata": { "tags": [] }, @@ -321,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 34, "metadata": { "tags": [] }, @@ -354,7 +342,7 @@ "data": { "text/plain": [] }, - "execution_count": 9, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -374,7 +362,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 35, "metadata": { "tags": [] }, @@ -385,7 +373,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 36, "metadata": { "tags": [] }, @@ -418,7 +406,7 @@ "data": { "text/plain": [] }, - "execution_count": 11, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } @@ -431,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 37, "metadata": { "tags": [] }, @@ -442,7 +430,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 38, "metadata": { "tags": [] }, @@ -475,7 +463,7 @@ "data": { "text/plain": [] }, - "execution_count": 14, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -496,7 +484,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 39, "metadata": { "tags": [] }, @@ -537,7 +525,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 40, "metadata": { "tags": [] }, @@ -570,7 +558,7 @@ "data": { "text/plain": [] }, - "execution_count": 17, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } @@ -591,7 +579,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 41, "metadata": { "tags": [] }, @@ -629,12 +617,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The %sqlplot magic command currently does not directly support the `--schema` option for specifying the schema name. To work around this, you can specify the schema in the SQL query itself." + "The %sqlplot magic command currently supported by the spark connection but you can still plot as like this:" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 45, "metadata": { "tags": [] }, @@ -720,13 +708,6 @@ "plt.show()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Persist - Not Supported" - ] - }, { "attachments": {}, "cell_type": "markdown", @@ -831,7 +812,7 @@ ], "metadata": { "kernelspec": { - "display_name": "jupysql", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/src/tests/test_magic.py b/src/tests/test_magic.py index 0502bfb9a..c2dd8f155 100644 --- a/src/tests/test_magic.py +++ b/src/tests/test_magic.py @@ -1702,7 +1702,6 @@ def test_persist_uses_error_handling_method(ip, monkeypatch, cell): ip.push({"df": df}) conn = ConnectionManager.current - print(conn) execute_with_error_handling_mock = Mock(wraps=conn._execute_with_error_handling) monkeypatch.setattr( conn, "_execute_with_error_handling", execute_with_error_handling_mock From c0723d749a5368a99d740f0b7171f7c82659e11d Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Tue, 19 Dec 2023 10:13:56 +0000 Subject: [PATCH 10/29] adjust doc string for close --- doc/integrations/spark.ipynb | 50 ++++++++++++++++++++------------ src/sql/connection/connection.py | 3 +- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index ee2451124..55ea62da5 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -209,34 +209,46 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 47, "metadata": { "tags": [] }, "outputs": [ { - "ename": "NotImplementedError", - "evalue": "This feature is only available for SQLAlchemy connections", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNotImplementedError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[46], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mget_ipython\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_line_magic\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43msqlcmd\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtables\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m/opt/homebrew/anaconda3/envs/jupysql/lib/python3.10/site-packages/IPython/core/interactiveshell.py:2456\u001b[0m, in \u001b[0;36mInteractiveShell.run_line_magic\u001b[0;34m(self, magic_name, line, _stack_depth)\u001b[0m\n\u001b[1;32m 2454\u001b[0m kwargs[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlocal_ns\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_local_scope(stack_depth)\n\u001b[1;32m 2455\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbuiltin_trap:\n\u001b[0;32m-> 2456\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mfn\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2458\u001b[0m \u001b[38;5;66;03m# The code below prevents the output from being displayed\u001b[39;00m\n\u001b[1;32m 2459\u001b[0m \u001b[38;5;66;03m# when using magics with decorator @output_can_be_silenced\u001b[39;00m\n\u001b[1;32m 2460\u001b[0m \u001b[38;5;66;03m# when the last Python token in the expression is a ';'.\u001b[39;00m\n\u001b[1;32m 2461\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(fn, magic\u001b[38;5;241m.\u001b[39mMAGIC_OUTPUT_CAN_BE_SILENCED, \u001b[38;5;28;01mFalse\u001b[39;00m):\n", - "File \u001b[0;32m~/scripts/jupysql/src/sql/magic_cmd.py:104\u001b[0m, in \u001b[0;36mSqlCmdMagic._validate_execute_inputs\u001b[0;34m(self, line)\u001b[0m\n\u001b[1;32m 101\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m command \u001b[38;5;129;01min\u001b[39;00m COMMANDS_SQLALCHEMY_ONLY:\n\u001b[1;32m 102\u001b[0m support_only_sql_alchemy_connection(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%sqlcmd \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcommand\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 104\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcommand\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mothers\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 105\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 106\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m exceptions\u001b[38;5;241m.\u001b[39mUsageError(\n\u001b[1;32m 107\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m%sqlcmd has no command: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcommand\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 108\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mValid commands are: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[1;32m 109\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m, \u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(AVAILABLE_SQLCMD_COMMANDS)\n\u001b[1;32m 110\u001b[0m )\n\u001b[1;32m 111\u001b[0m )\n", - "File \u001b[0;32m~/scripts/jupysql/src/sql/magic_cmd.py:132\u001b[0m, in \u001b[0;36mSqlCmdMagic.execute\u001b[0;34m(self, cmd_name, others, cell, local_ns)\u001b[0m\n\u001b[1;32m 130\u001b[0m cmd \u001b[38;5;241m=\u001b[39m router\u001b[38;5;241m.\u001b[39mget(cmd_name)\n\u001b[1;32m 131\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m cmd:\n\u001b[0;32m--> 132\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mcmd\u001b[49m\u001b[43m(\u001b[49m\u001b[43mothers\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/scripts/jupysql/src/sql/cmd/tables.py:30\u001b[0m, in \u001b[0;36mtables\u001b[0;34m(others)\u001b[0m\n\u001b[1;32m 26\u001b[0m parser\u001b[38;5;241m.\u001b[39madd_argument(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m-s\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m--schema\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28mtype\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mstr\u001b[39m, help\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSchema name\u001b[39m\u001b[38;5;124m\"\u001b[39m, required\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[1;32m 28\u001b[0m args \u001b[38;5;241m=\u001b[39m parser\u001b[38;5;241m.\u001b[39mparse_args(others)\n\u001b[0;32m---> 30\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43minspect\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_table_names\u001b[49m\u001b[43m(\u001b[49m\u001b[43mschema\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43margs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mschema\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m/opt/homebrew/anaconda3/envs/jupysql/lib/python3.10/site-packages/ploomber_core/telemetry/telemetry.py:679\u001b[0m, in \u001b[0;36mTelemetry.log_call.._log_call..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 677\u001b[0m result \u001b[38;5;241m=\u001b[39m func(_payload, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[1;32m 678\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 679\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 680\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m 681\u001b[0m metadata_error \u001b[38;5;241m=\u001b[39m {\n\u001b[1;32m 682\u001b[0m \u001b[38;5;66;03m# can we log None to posthog?\u001b[39;00m\n\u001b[1;32m 683\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mgetattr\u001b[39m(e, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype_\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m),\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 686\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39m_payload,\n\u001b[1;32m 687\u001b[0m }\n", - "File \u001b[0;32m~/scripts/jupysql/src/sql/inspect.py:483\u001b[0m, in \u001b[0;36mget_table_names\u001b[0;34m(schema)\u001b[0m\n\u001b[1;32m 480\u001b[0m \u001b[38;5;129m@telemetry\u001b[39m\u001b[38;5;241m.\u001b[39mlog_call()\n\u001b[1;32m 481\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_table_names\u001b[39m(schema\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 482\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Get table names for a given connection\"\"\"\u001b[39;00m\n\u001b[0;32m--> 483\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mTables\u001b[49m\u001b[43m(\u001b[49m\u001b[43mschema\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/scripts/jupysql/src/sql/inspect.py:38\u001b[0m, in \u001b[0;36mTables.__init__\u001b[0;34m(self, schema, conn)\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, schema\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, conn\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m---> 38\u001b[0m inspector \u001b[38;5;241m=\u001b[39m \u001b[43m_get_inspector\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconn\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_table \u001b[38;5;241m=\u001b[39m PrettyTable()\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_table\u001b[38;5;241m.\u001b[39mfield_names \u001b[38;5;241m=\u001b[39m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mName\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n", - "File \u001b[0;32m~/scripts/jupysql/src/sql/inspect.py:21\u001b[0m, in \u001b[0;36m_get_inspector\u001b[0;34m(conn)\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m exceptions\u001b[38;5;241m.\u001b[39mRuntimeError(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNo active connection\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 20\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 21\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m inspect(\u001b[43mConnectionManager\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcurrent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconnection_sqlalchemy\u001b[49m)\n", - "File \u001b[0;32m~/scripts/jupysql/src/sql/connection/connection.py:1126\u001b[0m, in \u001b[0;36mSparkConnectConnection.connection_sqlalchemy\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1120\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[1;32m 1121\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mconnection_sqlalchemy\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 1122\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1123\u001b[0m \u001b[38;5;124;03m Raises NotImplementedError since Spark connections don't have a SQLAlchemy\u001b[39;00m\n\u001b[1;32m 1124\u001b[0m \u001b[38;5;124;03m connection object\u001b[39;00m\n\u001b[1;32m 1125\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1126\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mNotImplementedError\u001b[39;00m(\n\u001b[1;32m 1127\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThis feature is only available for SQLAlchemy connections\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1128\u001b[0m )\n", - "\u001b[0;31mNotImplementedError\u001b[0m: This feature is only available for SQLAlchemy connections" + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+---------+--------+-----------+\n", + "|namespace|viewName|isTemporary|\n", + "+---------+--------+-----------+\n", + "| |taxi |true |\n", + "+---------+--------+-----------+\n", + "\n" ] + }, + { + "data": { + "text/plain": [] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "%sqlcmd tables" + "%sql show views in default" ] }, { diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 7262065c7..1c04ecc44 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1134,8 +1134,7 @@ def to_table(self, table_name, data_frame, if_exists, index, schema=None): ) def close(self): - """Close the connection""" - # NOTE: spark is often shared outside sql, allow user to manage closure + """Override of the abstract close as SparkSession is usually shared with pyspark""" pass From 3de5f615a6f1af942a96f78fd67d70e98bca644f Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Tue, 19 Dec 2023 10:19:38 +0000 Subject: [PATCH 11/29] add generic --- src/sql/connection/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 1c04ecc44..bf7bc44e0 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1088,7 +1088,7 @@ def __init__(self, payload, connection, alias=None, config=None): # calling init from AbstractConnection must be the last thing we do as it # register the connection - super().__init__(self._connection_class_name) + super().__init__(alias=alias or self._connection_class_name) self.name = self._connection_class_name From 0b968dc37b6f6925836d43938ac5d5e3ecf37a1a Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Tue, 19 Dec 2023 14:00:12 +0000 Subject: [PATCH 12/29] integrated better with existing functionality --- doc/integrations/compatibility.md | 15 +- doc/integrations/spark.ipynb | 776 +++++++++++++++++++++++++----- src/sql/connection/connection.py | 4 +- src/sql/magic.py | 8 + src/sql/plot.py | 2 +- src/sql/run/run.py | 5 +- src/sql/run/sparkdataframe.py | 42 ++ 7 files changed, 721 insertions(+), 131 deletions(-) create mode 100644 src/sql/run/sparkdataframe.py diff --git a/doc/integrations/compatibility.md b/doc/integrations/compatibility.md index d26199150..1771823f9 100644 --- a/doc/integrations/compatibility.md +++ b/doc/integrations/compatibility.md @@ -120,13 +120,14 @@ These table reflects the compatibility status of JupySQL `>=0.7` - Running queries with `%%sql` ✅ - CTEs with `%%sql --save NAME` ✅ -- Plotting with `%%sqlplot boxplot` ❌ -- Plotting with `%%sqlplot bar` ❌ -- Plotting with `%%sqlplot pie` ❌ -- Plotting with `%%sqlplot histogram` ❌ -- Plotting with `ggplot` API ❌ -- Profiling tables with `%sqlcmd profile` ❌ +- Plotting with `%%sqlplot boxplot` ✅ +- Plotting with `%%sqlplot bar` ✅ +- Plotting with `%%sqlplot pie` ✅ +- Plotting with `%%sqlplot histogram` ✅ +- Plotting with `ggplot` ✅ +- Profiling tables with `%sqlcmd profile` ✅ - Listing tables with `%sqlcmd tables` ❌ - Listing columns with `%sqlcmd columns` ❌ - Parametrized SQL queries via `{{parameter}}` ✅ -- Interactive SQL queries via `--interact` ✅ \ No newline at end of file +- Interactive SQL queries via `--interact` ✅ +- Persiting Dataframes via `--persist` ❌ \ No newline at end of file diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index 55ea62da5..90e296902 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 14, "metadata": { "tags": [] }, @@ -36,7 +36,7 @@ } ], "source": [ - "%pip install jupysql pyspark==3.4.1 arrow pyarrow==12.0.1 pandas grpc-status --quiet" + "%pip install jupysql pyspark==3.4.1 arrow pyarrow==12.0.1 pandas grpcio-status --quiet" ] }, { @@ -51,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 15, "metadata": { "tags": [] }, @@ -60,7 +60,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "8e3831298eb9dce9d26b2f2d3df5a1a64634a19976f4eaeabf92c45afd478a39\n" + "12f699ee8e8e35ab10186f3c39024a7e443691bb4213e56ca3c2e90cd80daf1b\n" ] } ], @@ -89,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 16, "metadata": { "tags": [] }, @@ -115,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -139,7 +139,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 18, "metadata": { "tags": [] }, @@ -160,7 +160,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 19, "metadata": { "tags": [] }, @@ -180,7 +180,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 20, "metadata": { "tags": [] }, @@ -209,10 +209,80 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 22, "metadata": { "tags": [] }, + "outputs": [ + { + "data": { + "text/html": [ + "Running query in 'SparkSession'" + ], + "text/plain": [ + "Running query in 'SparkSession'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namespaceviewNameisTemporary
taxiTrue
" + ], + "text/plain": [ + "+-----------+----------+-------------+\n", + "| namespace | viewName | isTemporary |\n", + "+-----------+----------+-------------+\n", + "| | taxi | True |\n", + "+-----------+----------+-------------+" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%sql show views in default" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can turn on `lazy_spark` to avoid executing spark plan and return a Spark Dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "%config SqlMagic.lazy_spark = True" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, "outputs": [ { "data": { @@ -242,7 +312,7 @@ "data": { "text/plain": [] }, - "execution_count": 47, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -251,6 +321,15 @@ "%sql show views in default" ] }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "%config SqlMagic.lazy_spark = False" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -261,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 25, "metadata": { "tags": [] }, @@ -308,7 +387,7 @@ ], "source": [ "df = %sql select * from taxi\n", - "df.printSchema()" + "df.sqlaproxy.dataframe.printSchema()" ] }, { @@ -321,7 +400,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 26, "metadata": { "tags": [] }, @@ -338,23 +417,31 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------+\n", - "|count(1)|\n", - "+--------+\n", - "|10000 |\n", - "+--------+\n", - "\n" - ] - }, { "data": { - "text/plain": [] + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
count(1)
10000
" + ], + "text/plain": [ + "+----------+\n", + "| count(1) |\n", + "+----------+\n", + "| 10000 |\n", + "+----------+" + ] }, - "execution_count": 34, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -374,7 +461,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 27, "metadata": { "tags": [] }, @@ -385,7 +472,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 28, "metadata": { "tags": [] }, @@ -402,23 +489,31 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------+\n", - "|count(1)|\n", - "+--------+\n", - "|9476 |\n", - "+--------+\n", - "\n" - ] - }, { "data": { - "text/plain": [] + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
count(1)
9476
" + ], + "text/plain": [ + "+----------+\n", + "| count(1) |\n", + "+----------+\n", + "| 9476 |\n", + "+----------+" + ] }, - "execution_count": 36, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -431,7 +526,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 29, "metadata": { "tags": [] }, @@ -442,7 +537,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 30, "metadata": { "tags": [] }, @@ -459,23 +554,31 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------+\n", - "|count(1)|\n", - "+--------+\n", - "|642 |\n", - "+--------+\n", - "\n" - ] - }, { "data": { - "text/plain": [] + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
count(1)
642
" + ], + "text/plain": [ + "+----------+\n", + "| count(1) |\n", + "+----------+\n", + "| 642 |\n", + "+----------+" + ] }, - "execution_count": 38, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -496,7 +599,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 31, "metadata": { "tags": [] }, @@ -537,7 +640,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 32, "metadata": { "tags": [] }, @@ -554,23 +657,35 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+------------------+------------------+------------------+\n", - "|min(trip_distance)|avg(trip_distance)|max(trip_distance)|\n", - "+------------------+------------------+------------------+\n", - "|0.0 |3.1091381872213963|18.46 |\n", - "+------------------+------------------+------------------+\n", - "\n" - ] - }, { "data": { - "text/plain": [] + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
min(trip_distance)avg(trip_distance)max(trip_distance)
0.03.109138187221396318.46
" + ], + "text/plain": [ + "+--------------------+--------------------+--------------------+\n", + "| min(trip_distance) | avg(trip_distance) | max(trip_distance) |\n", + "+--------------------+--------------------+--------------------+\n", + "| 0.0 | 3.1091381872213963 | 18.46 |\n", + "+--------------------+--------------------+--------------------+" + ] }, - "execution_count": 40, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -591,7 +706,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 33, "metadata": { "tags": [] }, @@ -616,6 +731,326 @@ "print(query)" ] }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Following statistics are not available in\n", + " SparkSession: STD, 25%, 50%, 75%
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
VendorIDtpep_pickup_datetimetpep_dropoff_datetimepassenger_counttrip_distanceRatecodeIDstore_and_fwd_flagPULocationIDDOLocationIDpayment_typefare_amountextramta_taxtip_amounttolls_amountimprovement_surchargetotal_amountcongestion_surchargeairport_fee
count1000010000100001000010000100001000010000100001000010000100001000010000100001000010000100000
unique287668745712436217323042288350418395930
topnan2021-01-01 00:41:192021-01-02 00:00:00nannannanNnannannannannannannannannannannanNone
freqnan47nannannan9808nannannannannannannannannannannan0
mean1.6901nannan1.50803.10021.0712nan158.5551154.72961.381911.88220.82590.48641.78460.22460.294516.96962.1063nan
std0.4625nannan1.13543.59701.0755nan70.928875.25040.555210.84201.11670.10412.43511.27300.057012.50230.9562nan
min1nannan0.00.01.0nan111-100.0-0.5-0.5-1.07-6.12-0.3-100.3-2.5nan
25%1.0000nannan1.00001.04001.0000nan100.000083.00001.00006.00000.00000.50000.00000.00000.300010.30002.5000nan
50%2.0000nannan1.00001.93001.0000nan152.0000151.00001.00008.50000.50000.50001.54000.00000.300013.55002.5000nan
75%2.0000nannan2.00003.60001.0000nan234.0000234.00002.000013.50002.50000.50002.65000.00000.300019.30002.5000nan
max2nannan6.045.9299.0nan2652654121.03.50.580.025.50.3137.762.5nan
" + ], + "text/plain": [ + "+--------+----------+----------------------+-----------------------+-----------------+---------------+------------+--------------------+--------------+--------------+--------------+-------------+--------+---------+------------+--------------+-----------------------+--------------+----------------------+-------------+\n", + "| | VendorID | tpep_pickup_datetime | tpep_dropoff_datetime | passenger_count | trip_distance | RatecodeID | store_and_fwd_flag | PULocationID | DOLocationID | payment_type | fare_amount | extra | mta_tax | tip_amount | tolls_amount | improvement_surcharge | total_amount | congestion_surcharge | airport_fee |\n", + "+--------+----------+----------------------+-----------------------+-----------------+---------------+------------+--------------------+--------------+--------------+--------------+-------------+--------+---------+------------+--------------+-----------------------+--------------+----------------------+-------------+\n", + "| count | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 10000 | 0 |\n", + "| unique | 2 | 8766 | 8745 | 7 | 1243 | 6 | 2 | 173 | 230 | 4 | 228 | 8 | 3 | 504 | 18 | 3 | 959 | 3 | 0 |\n", + "| top | nan | 2021-01-01 00:41:19 | 2021-01-02 00:00:00 | nan | nan | nan | N | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | None |\n", + "| freq | nan | 4 | 7 | nan | nan | nan | 9808 | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | 0 |\n", + "| mean | 1.6901 | nan | nan | 1.5080 | 3.1002 | 1.0712 | nan | 158.5551 | 154.7296 | 1.3819 | 11.8822 | 0.8259 | 0.4864 | 1.7846 | 0.2246 | 0.2945 | 16.9696 | 2.1063 | nan |\n", + "| std | 0.4625 | nan | nan | 1.1354 | 3.5970 | 1.0755 | nan | 70.9288 | 75.2504 | 0.5552 | 10.8420 | 1.1167 | 0.1041 | 2.4351 | 1.2730 | 0.0570 | 12.5023 | 0.9562 | nan |\n", + "| min | 1 | nan | nan | 0.0 | 0.0 | 1.0 | nan | 1 | 1 | 1 | -100.0 | -0.5 | -0.5 | -1.07 | -6.12 | -0.3 | -100.3 | -2.5 | nan |\n", + "| 25% | 1.0000 | nan | nan | 1.0000 | 1.0400 | 1.0000 | nan | 100.0000 | 83.0000 | 1.0000 | 6.0000 | 0.0000 | 0.5000 | 0.0000 | 0.0000 | 0.3000 | 10.3000 | 2.5000 | nan |\n", + "| 50% | 2.0000 | nan | nan | 1.0000 | 1.9300 | 1.0000 | nan | 152.0000 | 151.0000 | 1.0000 | 8.5000 | 0.5000 | 0.5000 | 1.5400 | 0.0000 | 0.3000 | 13.5500 | 2.5000 | nan |\n", + "| 75% | 2.0000 | nan | nan | 2.0000 | 3.6000 | 1.0000 | nan | 234.0000 | 234.0000 | 2.0000 | 13.5000 | 2.5000 | 0.5000 | 2.6500 | 0.0000 | 0.3000 | 19.3000 | 2.5000 | nan |\n", + "| max | 2 | nan | nan | 6.0 | 45.92 | 99.0 | nan | 265 | 265 | 4 | 121.0 | 3.5 | 0.5 | 80.0 | 25.5 | 0.3 | 137.76 | 2.5 | nan |\n", + "+--------+----------+----------------------+-----------------------+-----------------+---------------+------------+--------------------+--------------+--------------+--------------+-------------+--------+---------+------------+--------------+-----------------------+--------------+----------------------+-------------+" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%sqlcmd profile -t taxi" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -634,26 +1069,24 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 35, "metadata": { "tags": [] }, "outputs": [ { "data": { - "text/html": [ - "Running query in 'SparkSession'" - ], "text/plain": [ - "Running query in 'SparkSession'" + "" ] }, + "execution_count": 35, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAHHCAYAAABeLEexAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABJeUlEQVR4nO3dfXzO9f////sxsxlzHHO2zd6GiTCht3nLIiXL1DoRKhET5a22Cjn9KSelJj4lOqFT04mc9I5ETtacFSMmJ4mR1Ihtiu2wxTbb6/dHl72+jracrNkxXrfr5fK65Hg9H8fzeLyOl9q919lshmEYAgAAsDAPdzcAAADgbgQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQioAJo2LChBgwY4O42rnrTpk1To0aNVKlSJV1//fWX5TNsNpsmTpx4Wea+WLfccotuueUWt/YAXGkIREAZi4+Pl81m07Zt20ocv+WWW3Tdddf948/58ssv3f6D90qyevVqjRo1Sh06dNCcOXP04osvFqtZt26dbDbbRS3lZcCAAS6f6+vrq0aNGqlXr1763//+p8LCwjL5nE2bNmnixInKzMwsk/mAK42nuxsAIKWkpMjD49L+/+TLL7/UG2+8QSi6SGvWrJGHh4fee+89eXl5lVjTvHlzffjhhy7rxo4dK19fX40bN+6iPuf06dPy9Czb/7R6e3vr3XffNef/5Zdf9MUXX6hXr1665ZZb9Pnnn8tut5v1q1evvuTP2LRpkyZNmqQBAwbIz8+vrFoHrhgEIqAC8Pb2dncLlywnJ0fVqlVzdxsXLSMjQz4+Pn8bhiQpICBADz30kMu6KVOmqHbt2sXWn6uwsFB5eXmqUqWKqlSpUmY9F/H09Cz2+ZMnT9aUKVM0duxYPfroo1qwYIE5dr5tBFAyTpkBFcBfryHKz8/XpEmT1KRJE1WpUkW1atVSx44dlZCQIOnP0yhvvPGGJJV4GicnJ0dPP/20goOD5e3traZNm+r//u//ZBiGy+eePn1aTz75pGrXrq3q1avr7rvv1q+//lrsOpiJEyfKZrPphx9+UJ8+fVSjRg117NhRkrRr1y4NGDBAjRo1UpUqVRQYGKiBAwfq999/d/msojn279+vhx56SA6HQ3Xq1NGzzz4rwzB0+PBh3XPPPbLb7QoMDNTLL798Ud/d2bNn9fzzz+uaa66Rt7e3GjZsqP/v//v/lJuba9bYbDbNmTNHOTk55ncVHx9/UfOXxGazKTY2Vh9//LFatGghb29vrVy50hwr6bvbt2+f7r//ftntdtWqVUtPPfWUzpw5U+oeJGnMmDHq2rWrFi1apP3795vrS7qG6LXXXlOLFi1UtWpV1ahRQ23bttW8efPMHkeOHClJCgkJMb+jn3/+WZI0Z84c3XrrrfL395e3t7dCQ0M1a9asYv00bNhQd955p7755hu1a9dOVapUUaNGjfTBBx8Uq83MzNSwYcPUsGFDeXt7q169eurfv79+++03syY3N1cTJkxQ48aN5e3treDgYI0aNcpl3wJlhSNEwGWSlZXl8h/3Ivn5+Rd878SJExUXF6dHHnlE7dq1k9Pp1LZt27R9+3bddttt+u9//6ujR48qISGh2CkewzB09913a+3atRo0aJCuv/56rVq1SiNHjtSvv/6q6dOnm7UDBgzQwoUL1a9fP7Vv317r169XVFTU3/Z13333qUmTJnrxxRfNcJWQkKCffvpJDz/8sAIDA7Vnzx69/fbb2rNnjzZv3lzsepsHHnhAzZs315QpU7R8+XJNnjxZNWvW1FtvvaVbb71VL730kj7++GONGDFC//nPf9SpU6fzflePPPKI5s6dq169eunpp5/Wli1bFBcXp71792rx4sWSpA8//FBvv/22vv32W/PU04033njB/XA+a9as0cKFCxUbG6vatWurYcOG562///771bBhQ8XFxWnz5s2aOXOmTp48WWJYuBT9+vXT6tWrlZCQoGuvvbbEmnfeeUdPPvmkevXqZQaxXbt2acuWLerTp4969Oih/fv365NPPtH06dNVu3ZtSVKdOnUkSbNmzVKLFi109913y9PTU1988YUef/xxFRYWKiYmxuWzfvzxR/Xq1UuDBg1SdHS03n//fQ0YMEBhYWFq0aKFJCk7O1s33XST9u7dq4EDB6pNmzb67bfftHTpUh05ckS1a9dWYWGh7r77bn3zzTcaPHiwmjdvrt27d2v69Onav3+/lixZ8o++N6AYA0CZmjNnjiHpvEuLFi1c3tOgQQMjOjrafN26dWsjKirqvJ8TExNjlPSv8JIlSwxJxuTJk13W9+rVy7DZbMaPP/5oGIZhJCcnG5KMoUOHutQNGDDAkGRMmDDBXDdhwgRDkvHggw8W+7w//vij2LpPPvnEkGRs2LCh2ByDBw821509e9aoV6+eYbPZjClTppjrT548afj4+Lh8JyXZsWOHIcl45JFHXNaPGDHCkGSsWbPGXBcdHW1Uq1btvPOVpEWLFsbNN9/ssk6S4eHhYezZs6dY/d99d3fffbdL3eOPP25IMnbu3Hnez79Q3999950hyRg2bJi57uabb3bp+Z577in2d+6vpk2bZkgyDh06VGyspH0cGRlpNGrUyGVdgwYNiu33jIwMw9vb23j66afNdePHjzckGZ999lmxeQsLCw3DMIwPP/zQ8PDwML7++muX8dmzZxuSjI0bN553e4BLxSkz4DJ54403lJCQUGxp1arVBd/r5+enPXv26MCBA5f8uV9++aUqVaqkJ5980mX9008/LcMwtGLFCkkyT/E8/vjjLnVPPPHE3849ZMiQYut8fHzMP585c0a//fab2rdvL0navn17sfpHHnnE/HOlSpXUtm1bGYahQYMGmev9/PzUtGlT/fTTT3/bi/TntkrS8OHDXdY//fTTkqTly5ef9/3/xM0336zQ0NCLrv/rkZSi77loG0rL19dXknTq1Km/rfHz89ORI0e0devWUn3Gufu46MjnzTffrJ9++klZWVkutaGhobrpppvM13Xq1Cm2L//3v/+pdevWuvfee4t9VtERxUWLFql58+Zq1qyZfvvtN3O59dZbJUlr164t1bYAf4dTZsBl0q5dO7Vt27bY+ho1apR4Ku1czz33nO655x5de+21uu6669StWzf169fvosLUL7/8oqCgIFWvXt1lffPmzc3xon96eHgoJCTEpa5x48Z/O/dfayXpxIkTmjRpkubPn6+MjAyXsb/+sJSk+vXru7x2OByqUqWKeZrm3PV/vQ7pr4q24a89BwYGys/Pz9zWy6Gk7+J8mjRp4vL6mmuukYeHh3mdTmllZ2dLUrH9fa7Ro0frq6++Urt27dS4cWN17dpVffr0UYcOHS7qMzZu3KgJEyYoKSlJf/zxh8tYVlaWHA6H+fqv+1f68+/8yZMnzdcHDx5Uz549z/uZBw4c0N69e83Tdn/1179rwD9FIAIqoE6dOungwYP6/PPPtXr1ar377ruaPn26Zs+e7XKEpbyde6SgyP33369NmzZp5MiRuv766+Xr66vCwkJ169atxGfkVKpU6aLWSSp2EfjfKc/nAhUp6bu4FGXV8/fffy/p/EG2efPmSklJ0bJly7Ry5Ur973//05tvvqnx48dr0qRJ553/4MGD6tKli5o1a6ZXXnlFwcHB8vLy0pdffqnp06cX28f/dF8WKSwsVMuWLfXKK6+UOB4cHHxJ8wEXQiACKqiaNWvq4Ycf1sMPP6zs7Gx16tRJEydONAPR3/1AbdCggb766iudOnXK5ajBvn37zPGifxYWFurQoUMuRy9+/PHHi+7x5MmTSkxM1KRJkzR+/HhzfWlO9ZVG0TYcOHDAPAImSenp6crMzDS3tSI4cOCAy1GlH3/8UYWFhRe8GPtCPvzwQ9lsNt12223nratWrZoeeOABPfDAA8rLy1OPHj30wgsvaOzYsapSpcrf/n364osvlJubq6VLl7oc/fknp6yuueYaM8idr2bnzp3q0qWLWwIvrIdriIAK6K+ninx9fdW4cWOX242LngH01ycL33HHHSooKNDrr7/usn769Omy2Wy6/fbbJUmRkZGSpDfffNOl7rXXXrvoPouOBvz1//5fffXVi57jn7jjjjtK/Lyiowrnu2OuvBU9JqFI0fdctD9KY8qUKVq9erUeeOCBYqfkzvXXv09eXl4KDQ2VYRjmXY9/9/eppH2clZWlOXPmlLrvnj17aufOneZdgOcq+pz7779fv/76q955551iNadPn1ZOTk6pPx8oCUeIgAooNDRUt9xyi8LCwlSzZk1t27ZNn376qWJjY82asLAwSdKTTz6pyMhIVapUSb1799Zdd92lzp07a9y4cfr555/VunVrrV69Wp9//rmGDh2qa665xnx/z5499eqrr+r33383b7svep7Nxfxfud1uV6dOnTR16lTl5+frX//6l1avXq1Dhw5dhm+luNatWys6Olpvv/22MjMzdfPNN+vbb7/V3Llz1b17d3Xu3Llc+rgYhw4d0t13361u3bopKSlJH330kfr06aPWrVtf8L1nz57VRx99JOnPC9d/+eUXLV26VLt27VLnzp319ttvn/f9Xbt2VWBgoDp06KCAgADt3btXr7/+uqKiosyjiEV/n8aNG6fevXurcuXKuuuuu9S1a1d5eXnprrvu0n//+19lZ2frnXfekb+/v44dO1aq72LkyJH69NNPdd9992ngwIEKCwvTiRMntHTpUs2ePVutW7dWv379tHDhQg0ZMkRr165Vhw4d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eeYZxcTEyNvbW5I0ZMgQvf766xo1apQGDhyoNWvWaOHChVq+fHlpNh0AAFyFShWIDhw4cMEaLy8vRUdHn7fmyJEjevDBB/X777+rTp066tixozZv3qw6depIkqZPny4PDw/17NlTubm5ioyM1Jtvvmm+v1KlSlq2bJkee+wxhYeHq1q1aoqOjtZzzz1n1oSEhGj58uUaNmyYZsyYoXr16undd9/llnsAAGCyGYZhXOqb5syZI19fX913330u6xctWqQ//vjjgkHoSuN0OuVwOJSVlXVZridqOObKO1r185Qod7cAAMB5XcrP71JdQxQXF6fatWsXW+/v768XX3yxNFMCAAC4TakCUWpqqkJCQoqtb9CggflgRQAAgCtFqQKRv7+/du3aVWz9zp07VatWrX/cFAAAQHkqVSB68MEH9eSTT2rt2rUqKChQQUGB1qxZo6eeekq9e/cu6x4BAAAuq1LdZfb888/r559/VpcuXeTp+ecUhYWF6t+/P9cQAQCAK06pApGXl5cWLFig559/Xjt37pSPj49atmypBg0alHV/AAAAl12pAlGRa6+9Vtdee21Z9QIAAOAWpQpEBQUFio+PV2JiojIyMlRYWOgyvmbNmjJpDgAAoDyUKhA99dRTio+PV1RUlK677jrZbLay7gsAAKDclCoQzZ8/XwsXLtQdd9xR1v0AAACUu1Lddu/l5aXGjRuXdS8AAABuUapA9PTTT2vGjBkqxa9BAwAAqHBKdcrsm2++0dq1a7VixQq1aNFClStXdhn/7LPPyqQ5AACA8lCqQOTn56d77723rHsBAABwi1IFojlz5pR1HwAAAG5TqmuIJOns2bP66quv9NZbb+nUqVOSpKNHjyo7O7vMmgMAACgPpTpC9Msvv6hbt25KTU1Vbm6ubrvtNlWvXl0vvfSScnNzNXv27LLuEwAA4LIp1RGip556Sm3bttXJkyfl4+Njrr/33nuVmJhYZs0BAACUh1IdIfr666+1adMmeXl5uaxv2LChfv311zJpDAAAoLyU6ghRYWGhCgoKiq0/cuSIqlev/o+bAgAAKE+lCkRdu3bVq6++ar622WzKzs7WhAkT+HUeAADgilOqU2Yvv/yyIiMjFRoaqjNnzqhPnz46cOCAateurU8++aSsewQAALisShWI6tWrp507d2r+/PnatWuXsrOzNWjQIPXt29fl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3X777apfv75OnTqlefPmad26dVq1apUcDocGDRqk4cOHq2bNmrLb7XriiScUHh6u9u3bS5K6du2q0NBQ9evXT1OnTlVaWpqeeeYZxcTEmEd4hgwZotdff12jRo3SwIEDtWbNGi1cuFDLl195d3YBAIDLw62BKCMjQ/3799exY8fkcDjUqlUrrVq1Srfddpskafr06fLw8FDPnj2Vm5uryMhIvfnmm+b7K1WqpGXLlumxxx5TeHi4qlWrpujoaD333HNmTUhIiJYvX65hw4ZpxowZqlevnt59911FRkaW+/YCAICKqcI9h6gi4jlExfEcIgBARXdFPocIAADAXQhEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8twaiOLi4vSf//xH1atXl7+/v7p3766UlBSXmjNnzigmJka1atWSr6+vevbsqfT0dJea1NRURUVFqWrVqvL399fIkSN19uxZl5p169apTZs28vb2VuPGjRUfH3+5Nw8AAFwh3BqI1q9fr5iYGG3evFkJCQnKz89X165dlZOTY9YMGzZMX3zxhRYtWqT169fr6NGj6tGjhzleUFCgqKgo5eXladOmTZo7d67i4+M1fvx4s+bQoUOKiopS586dtWPHDg0dOlSPPPKIVq1aVa7bCwAAKiabYRiGu5socvz4cfn7+2v9+vXq1KmTsrKyVKdOHc2bN0+9evWSJO3bt0/NmzdXUlKS2rdvrxUrVujOO+/U0aNHFRAQIEmaPXu2Ro8erePHj8vLy0ujR4/W8uXL9f3335uf1bt3b2VmZmrlypUX7MvpdMrhcCgrK0t2u73Mt7vhmOVlPufl9vOUKHe3AADAeV3Kz+8KdQ1RVlaWJKlmzZqSpOTkZOXn5ysiIsKsadasmerXr6+kpCRJUlJSklq2bGmGIUmKjIyU0+nUnj17zJpz5yiqKZoDAABYm6e7GyhSWFiooUOHqkOHDrruuuskSWlpafLy8pKfn59LbUBAgNLS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", + "image/png": "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VtOmTTVkyBBt27ZNISEheuedd5Sdna2FCxeWehtt27ZVv3799PrrrysvL0+33367kpOTdfDgwSu+d9GiRXr99dfVu3dvNW7cWCdPntRbb70lu92ue+65R9Lvl4wiIyP1/vvvq2nTpqpdu7Zatmypli1bqkuXLpo5c6bOnTunG264QevWrVNGRsZVzYX0+z1VL7zwgl588UV17txZDz74oHx8fLRt2zaFhYVp+vTpstvtmj9/vgYMGKB27dqpb9++CgoKUmZmplatWqU77rhDr7766lX3UBoTJkzQe++9p549e2rMmDGqXbu2Fi1apIyMDH3wwQclbgivaOXxGb300ktKT09XcnKyatasqdatW2vy5MmaNGmSHnroIfPnAagy3PeAGwDD+P3x7tjYWKNmzZqGJPMR/OJH2i/2CPulHruPjY011q5da7Ru3drw8fExmjdvbixbtqzMPZ05c8YYM2aMUadOHaNGjRrGfffdZxw+fPiKj93v2LHD6Nevn9GwYUPDx8fHCA4ONu69915j+/btLtvfvHmzERUVZXh7e7ts86effjJ69+5tBAYGGgEBAcbDDz9sHDlypMR+L/VY+8XmxTAM45133jFuueUWw8fHx6hVq5bRtWtXIykpyaVmw4YNRkxMjBEQEGD4+voajRs3NgYNGlSi99K43GP3sbGxF33P999/bzz00ENGYGCg4evra9x2223GypUrS/QoqcRneqmflUv1URp/5jNKS0szPD09jdGjR7ts8/z588att95qhIWFGSdOnHDpEXA3m2GU4912ANwmPDxcLVu21MqVK93dCgBcc7iHCAAAWB73EAEWcqUvZ/Tz8+MJIACWRCACLKRevXqXHR84cKASExMrpxkAqEIIRMB1ojTfRJ2UlHTZ8bCwsHLqBgCuLdxUDQAALI+bqgEAgOVxyawUioqKdOTIEdWsWZM/TAgAwDXCMAydPHlSYWFhV/yCUwJRKRw5cqTEX5EGAADXhsOHD6t+/fqXrSEQlULNmjUl/T6hdrvdzd0AAIDScDqdatCggfl7/HIIRKVQfJnMbrcTiAAAuMaU5nYXbqoGAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACW5+nuBiCFT1jl7hbK7NCMWHe3AABAueEMEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDy3B6Kff/5Zjz32mOrUqSM/Pz+1atVK27dvN8cNw9DkyZNVr149+fn5KTo6WgcOHHDZxvHjx9W/f3/Z7XYFBgZqyJAhOnXqlEvNzp071blzZ/n6+qpBgwaaOXNmpRwfAACo+twaiE6cOKE77rhDXl5eWr16tfbu3atZs2apVq1aZs3MmTM1b948LViwQFu3blWNGjUUExOjs2fPmjX9+/fXnj17lJSUpJUrV2rTpk0aPny4Oe50OtWjRw81atRIaWlpevnll5WQkKA333yzUo8XAABUTTbDMAx37XzChAn68ssv9fnnn1903DAMhYWF6ZlnntGzzz4rScrLy1NISIgSExPVt29fffvtt4qMjNS2bdvUvn17SdKaNWt0zz336KefflJYWJjmz5+vF154QVlZWfL29jb3vWLFCu3bt++KfTqdTgUEBCgvL092u72cjv7/42+ZAQBQ/sry+9utZ4g+/vhjtW/fXg8//LCCg4N1yy236K233jLHMzIylJWVpejoaHNdQECAOnTooNTUVElSamqqAgMDzTAkSdHR0fLw8NDWrVvNmi5duphhSJJiYmK0f/9+nThxokRf+fn5cjqdLgsAALh+uTUQ/fDDD5o/f75uuukmrV27ViNHjtSYMWO0aNEiSVJWVpYkKSQkxOV9ISEh5lhWVpaCg4Ndxj09PVW7dm2Xmott48J9XGj69OkKCAgwlwYNGpTD0QIAgKrKrYGoqKhI7dq109///nfdcsstGj58uIYNG6YFCxa4sy1NnDhReXl55nL48GG39gMAACqWWwNRvXr1FBkZ6bKuRYsWyszMlCSFhoZKkrKzs11qsrOzzbHQ0FDl5OS4jJ8/f17Hjx93qbnYNi7cx4V8fHxkt9tdFgAAcP1yayC64447tH//fpd13333nRo1aiRJioiIUGhoqJKTk81xp9OprVu3yuFwSJIcDodyc3OVlpZm1qxfv15FRUXq0KGDWbNp0yadO3fOrElKSlKzZs1cnmgDAADW5NZANHbsWG3ZskV///vfdfDgQS1evFhvvvmm4uLiJEk2m03x8fF66aWX9PHHH2vXrl16/PHHFRYWpl69ekn6/YzS3XffrWHDhumrr77Sl19+qVGjRqlv374KCwuTJD366KPy9vbWkCFDtGfPHr3//vuaO3euxo0b565DBwAAVYinO3d+6623avny5Zo4caKmTZumiIgIzZkzR/379zdrxo8fr9OnT2v48OHKzc1Vp06dtGbNGvn6+po1//3vfzVq1Ch1795dHh4e6tOnj+bNm2eOBwQEaN26dYqLi1NUVJTq1q2ryZMnu3xXEQAAsC63fg/RtYLvISqJ7yECAFR118z3EAEAAFQFBCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5bg1ECQkJstlsLkvz5s3N8bNnzyouLk516tSRv7+/+vTpo+zsbJdtZGZmKjY2VtWrV1dwcLCee+45nT9/3qVm48aNateunXx8fNSkSRMlJiZWxuEBAIBrhNvPEN188806evSouXzxxRfm2NixY/XJJ59o2bJlSklJ0ZEjR/Tggw+a44WFhYqNjVVBQYE2b96sRYsWKTExUZMnTzZrMjIyFBsbq27duik9PV3x8fEaOnSo1q5dW6nHCQAAqi5Ptzfg6anQ0NAS6/Py8vSvf/1Lixcv1l133SVJWrhwoVq0aKEtW7aoY8eOWrdunfbu3avPPvtMISEhatu2rV588UU9//zzSkhIkLe3txYsWKCIiAjNmjVLktSiRQt98cUXmj17tmJiYir1WAEAQNXk9jNEBw4cUFhYmG688Ub1799fmZmZkqS0tDSdO3dO0dHRZm3z5s3VsGFDpaamSpJSU1PVqlUrhYSEmDUxMTFyOp3as2ePWXPhNoprirdxMfn5+XI6nS4LAAC4frk1EHXo0EGJiYlas2aN5s+fr4yMDHXu3FknT55UVlaWvL29FRgY6PKekJAQZWVlSZKysrJcwlDxePHY5WqcTqfOnDlz0b6mT5+ugIAAc2nQoEF5HC4AAKii3HrJrGfPnua/W7durQ4dOqhRo0ZaunSp/Pz83NbXxIkTNW7cOPO10+kkFAEAcB1z+yWzCwUGBqpp06Y6ePCgQkNDVVBQoNzcXJea7Oxs856j0NDQEk+dFb++Uo3dbr9k6PLx8ZHdbndZAADA9atKBaJTp07p+++/V7169RQVFSUvLy8lJyeb4/v371dmZqYcDockyeFwaNeuXcrJyTFrkpKSZLfbFRkZadZcuI3imuJtAAAAuDUQPfvss0pJSdGhQ4e0efNm9e7dW9WqVVO/fv0UEBCgIUOGaNy4cdqwYYPS0tL0xBNPyOFwqGPHjpKkHj16KDIyUgMGDNA333yjtWvXatKkSYqLi5OPj48kacSIEfrhhx80fvx47du3T6+//rqWLl2qsWPHuvPQAQBAFeLWe4h++ukn9evXT7/++quCgoLUqVMnbdmyRUFBQZKk2bNny8PDQ3369FF+fr5iYmL0+uuvm++vVq2aVq5cqZEjR8rhcKhGjRoaOHCgpk2bZtZERERo1apVGjt2rObOnav69evr7bff5pF7AABgshmGYbi7iarO6XQqICBAeXl5FXI/UfiEVeW+zYp2aEasu1sAAOCyyvL7u0rdQwQAAOAOBCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5BCIAAGB5VSYQzZgxQzabTfHx8ea6s2fPKi4uTnXq1JG/v7/69Omj7Oxsl/dlZmYqNjZW1atXV3BwsJ577jmdP3/epWbjxo1q166dfHx81KRJEyUmJlbCEQEAgGtFlQhE27Zt0xtvvKHWrVu7rB87dqw++eQTLVu2TCkpKTpy5IgefPBBc7ywsFCxsbEqKCjQ5s2btWjRIiUmJmry5MlmTUZGhmJjY9WtWzelp6crPj5eQ4cO1dq1ayvt+AAAQNXm9kB06tQp9e/fX2+99ZZq1aplrs/Ly9O//vUvvfLKK7rrrrsUFRWlhQsXavPmzdqyZYskad26ddq7d6/effddtW3bVj179tSLL76o1157TQUFBZKkBQsWKCIiQrNmzVKLFi00atQoPfTQQ5o9e7ZbjhcAAFQ9bg9EcXFxio2NVXR0tMv6tLQ0nTt3zmV98+bN1bBhQ6WmpkqSUlNT1apVK4WEhJg1MTExcjqd2rNnj1nzx23HxMSY27iY/Px8OZ1OlwUAAFy/PN258yVLlmjHjh3atm1bibGsrCx5e3srMDDQZX1ISIiysrLMmgvDUPF48djlapxOp86cOSM/P78S+54+fbqmTp161ccFAACuLW47Q3T48GE9/fTT+u9//ytfX193tXFREydOVF5enrkcPnzY3S0BAIAK5LZAlJaWppycHLVr106enp7y9PRUSkqK5s2bJ09PT4WEhKigoEC5ubku78vOzlZoaKgkKTQ0tMRTZ8Wvr1Rjt9svenZIknx8fGS3210WAABw/XJbIOrevbt27dql9PR0c2nfvr369+9v/tvLy0vJycnme/bv36/MzEw5HA5JksPh0K5du5STk2PWJCUlyW63KzIy0qy5cBvFNcXbAAAAcNs9RDVr1lTLli1d1tWoUUN16tQx1w8ZMkTjxo1T7dq1ZbfbNXr0aDkcDnXs2FGS1KNHD0VGRmrAgAGaOXOmsrKyNGnSJMXFxcnHx0eSNGLECL366qsaP368Bg8erPXr12vp0qVatWpV5R4wAACostx6U/WVzJ49Wx4eHurTp4/y8/MVExOj119/3RyvVq2aVq5cqZEjR8rhcKhGjRoaOHCgpk2bZtZERERo1apVGjt2rObOnav69evr7bffVkxMjDsOCQAAVEE2wzAMdzdR1TmdTgUEBCgvL69C7icKn3Dtna06NCPW3S0AAHBZZfn97fbvIQIAAHA3AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALA8AhEAALC8qwpEN954o3799dcS63Nzc3XjjTf+6aYAAAAq01UFokOHDqmwsLDE+vz8fP38889/uikAAIDK5FmW4o8//tj899q1axUQEGC+LiwsVHJyssLDw8utOQAAgMpQpkDUq1cvSZLNZtPAgQNdxry8vBQeHq5Zs2aVW3MAAACVoUyBqKioSJIUERGhbdu2qW7duhXSFAAAQGUqUyAqlpGRUd59AAAAuM1VBSJJSk5OVnJysnJycswzR8XeeeedP90YAABAZbmqQDR16lRNmzZN7du3V7169WSz2cq7LwAAgEpzVYFowYIFSkxM1IABA8q7HwAAgEp3Vd9DVFBQoNtvv728ewEAAHCLqwpEQ4cO1eLFi8u7FwAAALe4qktmZ8+e1ZtvvqnPPvtMrVu3lpeXl8v4K6+8Ui7NAQAAVIarCkQ7d+5U27ZtJUm7d+92GeMGawAAcK25qkC0YcOG8u4DAADAba7qHiIAAIDryVWdIerWrdtlL42tX7/+qhsCAACobFcViIrvHyp27tw5paena/fu3SX+6CsAAEBVd1WBaPbs2Rddn5CQoFOnTv2phgAAACpbud5D9Nhjj/F3zAAAwDWnXANRamqqfH19y3OTAAAAFe6qLpk9+OCDLq8Nw9DRo0e1fft2/e1vfyuXxgAAACrLVQWigIAAl9ceHh5q1qyZpk2bph49epRLYwAAAJXlqgLRwoULy7sPAAAAt7mqQFQsLS1N3377rSTp5ptv1i233FIuTQEAAFSmqwpEOTk56tu3rzZu3KjAwEBJUm5urrp166YlS5YoKCioPHsEAACoUFf1lNno0aN18uRJ7dmzR8ePH9fx48e1e/duOZ1OjRkzprx7BAAAqFBXdYZozZo1+uyzz9SiRQtzXWRkpF577TVuqgYAANecqzpDVFRUJC8vrxLrvby8VFRU9KebAgAAqExXFYjuuusuPf300zpy5Ii57ueff9bYsWPVvXv3cmsOAACgMlxVIHr11VfldDoVHh6uxo0bq3HjxoqIiJDT6dQ///nP8u4RAACgQl1VIGrQoIF27NihVatWKT4+XvHx8fr000+1Y8cO1a9fv9TbmT9/vlq3bi273S673S6Hw6HVq1eb42fPnlVcXJzq1Kkjf39/9enTR9nZ2S7byMzMVGxsrKpXr67g4GA999xzOn/+vEvNxo0b1a5dO/n4+KhJkyZKTEy8msMGAADXqTIFovXr1ysyMlJOp1M2m01/+ctfNHr0aI0ePVq33nqrbr75Zn3++eel3l79+vU1Y8YMpaWlafv27brrrrv0wAMPaM+ePZKksWPH6pNPPtGyZcuUkpKiI0eOuPzZkMLCQsXGxqqgoECbN2/WokWLlJiYqMmTJ5s1GRkZio2NVbdu3ZSenq74+HgNHTpUa9euLcuhAwCA65jNMAyjtMX333+/unXrprFjx150fN68edqwYYOWL19+1Q3Vrl1bL7/8sh566CEFBQVp8eLFeuihhyRJ+/btU4sWLZSamqqOHTtq9erVuvfee3XkyBGFhIRIkhYsWKDnn39ex44dk7e3t55//nmtWrVKu3fvNvfRt29f5ebmas2aNaXqyel0KiAgQHl5ebLb7Vd9bJcSPmFVuW+zoh2aEevuFgAAuKyy/P4u0xmib775Rnffffclx3v06KG0tLSybNJUWFioJUuW6PTp03I4HEpLS9O5c+cUHR1t1jRv3lwNGzZUamqqJCk1NVWtWrUyw5AkxcTEyOl0mmeZUlNTXbZRXFO8jYvJz8+X0+l0WQAAwPWrTIEoOzv7oo/bF/P09NSxY8fK1MCuXbvk7+8vHx8fjRgxQsuXL1dkZKSysrLk7e1tfhN2sZCQEGVlZUmSsrKyXMJQ8Xjx2OVqnE6nzpw5c9Gepk+froCAAHNp0KBBmY4JAABcW8oUiG644QaXS09/tHPnTtWrV69MDTRr1kzp6enaunWrRo4cqYEDB2rv3r1l2kZ5mzhxovLy8szl8OHDbu0HAABUrDIFonvuuUd/+9vfdPbs2RJjZ86c0ZQpU3TvvfeWqQFvb281adJEUVFRmj59utq0aaO5c+cqNDRUBQUFys3NdanPzs5WaGioJCk0NLTEU2fFr69UY7fb5efnd9GefHx8zCffihcAAHD9KlMgmjRpko4fP66mTZtq5syZ+uijj/TRRx/pH//4h5o1a6bjx4/rhRde+FMNFRUVKT8/X1FRUfLy8lJycrI5tn//fmVmZsrhcEiSHA6Hdu3apZycHLMmKSlJdrtdkZGRZs2F2yiuKd4GAABAmf6WWUhIiDZv3qyRI0dq4sSJKn5AzWazKSYmRq+99lqJ+3UuZ+LEierZs6caNmyokydPavHixdq4caPWrl2rgIAADRkyROPGjVPt2rVlt9s1evRoORwOdezYUdLvN3FHRkZqwIABmjlzprKysjRp0iTFxcXJx8dHkjRixAi9+uqrGj9+vAYPHqz169dr6dKlWrXq2nuyCwAAVIwy/3HXRo0a6dNPP9WJEyd08OBBGYahm266SbVq1SrzznNycvT444/r6NGjCggIUOvWrbV27Vr95S9/kSTNnj1bHh4e6tOnj/Lz8xUTE6PXX3/dfH+1atW0cuVKjRw5Ug6HQzVq1NDAgQM1bdo0syYiIkKrVq3S2LFjNXfuXNWvX19vv/22YmJiytwvAAC4PpXpe4isiu8huj7w3UkAYC0V9j1EAAAA1yMCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDwCEQAAsDy3BqLp06fr1ltvVc2aNRUcHKxevXpp//79LjVnz55VXFyc6tSpI39/f/Xp00fZ2dkuNZmZmYqNjVX16tUVHBys5557TufPn3ep2bhxo9q1aycfHx81adJEiYmJFX14AADgGuHWQJSSkqK4uDht2bJFSUlJOnfunHr06KHTp0+bNWPHjtUnn3yiZcuWKSUlRUeOHNGDDz5ojhcWFio2NlYFBQXavHmzFi1apMTERE2ePNmsycjIUGxsrLp166b09HTFx8dr6NChWrt2baUeLwAAqJpshmEY7m6i2LFjxxQcHKyUlBR16dJFeXl5CgoK0uLFi/XQQw9Jkvbt26cWLVooNTVVHTt21OrVq3XvvffqyJEjCgkJkSQtWLBAzz//vI4dOyZvb289//zzWrVqlXbv3m3uq2/fvsrNzdWaNWuu2JfT6VRAQIDy8vJkt9vL/bjDJ6wq922ipEMzYt3dAgCgEpXl93eVuocoLy9PklS7dm1JUlpams6dO6fo6Gizpnnz5mrYsKFSU1MlSampqWrVqpUZhiQpJiZGTqdTe/bsMWsu3EZxTfE2/ig/P19Op9NlAQAA168qE4iKiooUHx+vO+64Qy1btpQkZWVlydvbW4GBgS61ISEhysrKMmsuDEPF48Vjl6txOp06c+ZMiV6mT5+ugIAAc2nQoEG5HCMAAKiaqkwgiouL0+7du7VkyRJ3t6KJEycqLy/PXA4fPuzulgAAQAXydHcDkjRq1CitXLlSmzZtUv369c31oaGhKigoUG5urstZouzsbIWGhpo1X331lcv2ip9Cu7Dmj0+mZWdny263y8/Pr0Q/Pj4+8vHxKZdjAwAAVZ9bzxAZhqFRo0Zp+fLlWr9+vSIiIlzGo6Ki5OXlpeTkZHPd/v37lZmZKYfDIUlyOBzatWuXcnJyzJqkpCTZ7XZFRkaaNRduo7imeBsAAMDa3HqGKC4uTosXL9ZHH32kmjVrmvf8BAQEyM/PTwEBARoyZIjGjRun2rVry263a/To0XI4HOrYsaMkqUePHoqMjNSAAQM0c+ZMZWVladKkSYqLizPP8owYMUKvvvqqxo8fr8GDB2v9+vVaunSpVq3i6S4AAODmM0Tz589XXl6e7rzzTtWrV89c3n//fbNm9uzZuvfee9WnTx916dJFoaGh+vDDD83xatWqaeXKlapWrZocDocee+wxPf7445o2bZpZExERoVWrVikpKUlt2rTRrFmz9PbbbysmJqZSjxcAAFRNVep7iKoqvofo+sD3EAGAtVyz30MEAADgDgQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeQQiAABgeW4NRJs2bdJ9992nsLAw2Ww2rVixwmXcMAxNnjxZ9erVk5+fn6Kjo3XgwAGXmuPHj6t///6y2+0KDAzUkCFDdOrUKZeanTt3qnPnzvL19VWDBg00c+bMij40AABwDXFrIDp9+rTatGmj11577aLjM2fO1Lx587RgwQJt3bpVNWrUUExMjM6ePWvW9O/fX3v27FFSUpJWrlypTZs2afjw4ea40+lUjx491KhRI6Wlpenll19WQkKC3nzzzQo/PgAAcG2wGYZhuLsJSbLZbFq+fLl69eol6fezQ2FhYXrmmWf07LPPSpLy8vIUEhKixMRE9e3bV99++60iIyO1bds2tW/fXpK0Zs0a3XPPPfrpp58UFham+fPn64UXXlBWVpa8vb0lSRMmTNCKFSu0b9++UvXmdDoVEBCgvLw82e32cj/28Amryn2bKOnQjFh3twAAqERl+f1dZe8hysjIUFZWlqKjo811AQEB6tChg1JTUyVJqampCgwMNMOQJEVHR8vDw0Nbt241a7p06WKGIUmKiYnR/v37deLEiYvuOz8/X06n02UBAADXryobiLKysiRJISEhLutDQkLMsaysLAUHB7uMe3p6qnbt2i41F9vGhfv4o+nTpysgIMBcGjRo8OcPCAAAVFlVNhC508SJE5WXl2cuhw8fdndLAACgAlXZQBQaGipJys7OdlmfnZ1tjoWGhionJ8dl/Pz58zp+/LhLzcW2ceE+/sjHx0d2u91lAQAA168qG4giIiIUGhqq5ORkc53T6dTWrVvlcDgkSQ6HQ7m5uUpLSzNr1q9fr6KiInXo0MGs2bRpk86dO2fWJCUlqVmzZqpVq1YlHQ0AAKjK3BqITp06pfT0dKWnp0v6/Ubq9PR0ZWZmymazKT4+Xi+99JI+/vhj7dq1S48//rjCwsLMJ9FatGihu+++W8OGDdNXX32lL7/8UqNGjVLfvn0VFhYmSXr00Ufl7e2tIUOGaM+ePXr//fc1d+5cjRs3zk1HDQAAqhpPd+58+/bt6tatm/m6OKQMHDhQiYmJGj9+vE6fPq3hw4crNzdXnTp10po1a+Tr62u+57///a9GjRql7t27y8PDQ3369NG8efPM8YCAAK1bt05xcXGKiopS3bp1NXnyZJfvKgIAANZWZb6HqCrje4iuD3wPEQBYy3XxPUQAAACVhUAEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsj0AEAAAsz9PdDQCVJXzCKne3UGaHZsS6uwUAsATOEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMvzdHcDAC4tfMIqd7dQZodmxLq7BQAoM84QAQAAyyMQAQAAy+OSGYByxWU+ANcizhABAADLs1Qgeu211xQeHi5fX1916NBBX331lbtbAgAAVYBlAtH777+vcePGacqUKdqxY4fatGmjmJgY5eTkuLs1AADgZpYJRK+88oqGDRumJ554QpGRkVqwYIGqV6+ud955x92tAQAAN7NEICooKFBaWpqio6PNdR4eHoqOjlZqaqobOwMAAFWBJZ4y++WXX1RYWKiQkBCX9SEhIdq3b1+J+vz8fOXn55uv8/LyJElOp7NC+ivK/61CtgugdBqOXebuFlCF7Z4a4+4WcJWKf28bhnHFWksEorKaPn26pk6dWmJ9gwYN3NANAMCdAua4uwP8WSdPnlRAQMBlaywRiOrWratq1aopOzvbZX12drZCQ0NL1E+cOFHjxo0zXxcVFen48eOqU6eObDZbhfdbHpxOpxo0aKDDhw/Lbre7u53rEnNcsZjfisccVyzmt2KVZn4Nw9DJkycVFhZ2xe1ZIhB5e3srKipKycnJ6tWrl6TfQ05ycrJGjRpVot7Hx0c+Pj4u6wIDAyuh0/Jnt9v5H2IFY44rFvNb8ZjjisX8Vqwrze+VzgwVs0QgkqRx48Zp4MCBat++vW677TbNmTNHp0+f1hNPPOHu1gAAgJtZJhA98sgjOnbsmCZPnqysrCy1bdtWa9asKXGjNQAAsB7LBCJJGjVq1EUvkV2PfHx8NGXKlBKX/lB+mOOKxfxWPOa4YjG/Fau859dmlOZZNAAAgOuYJb6YEQAA4HIIRAAAwPIIRAAAwPIIRAAAwPIIRNep1157TeHh4fL19VWHDh301Vdfubula9amTZt03333KSwsTDabTStWrHAZNwxDkydPVr169eTn56fo6GgdOHDAPc1eY6ZPn65bb71VNWvWVHBwsHr16qX9+/e71Jw9e1ZxcXGqU6eO/P391adPnxLfOo9Lmz9/vlq3bm1+eZ3D4dDq1avNcea3fM2YMUM2m03x8fHmOub4z0lISJDNZnNZmjdvbo6X1/wSiK5D77//vsaNG6cpU6Zox44datOmjWJiYpSTk+Pu1q5Jp0+fVps2bfTaa69ddHzmzJmaN2+eFixYoK1bt6pGjRqKiYnR2bNnK7nTa09KSori4uK0ZcsWJSUl6dy5c+rRo4dOnz5t1owdO1affPKJli1bppSUFB05ckQPPvigG7u+ttSvX18zZsxQWlqatm/frrvuuksPPPCA9uzZI4n5LU/btm3TG2+8odatW7usZ47/vJtvvllHjx41ly+++MIcK7f5NXDdue2224y4uDjzdWFhoREWFmZMnz7djV1dHyQZy5cvN18XFRUZoaGhxssvv2yuy83NNXx8fIz33nvPDR1e23JycgxJRkpKimEYv8+ll5eXsWzZMrPm22+/NSQZqamp7mrzmlerVi3j7bffZn7L0cmTJ42bbrrJSEpKMrp27Wo8/fTThmHwM1wepkyZYrRp0+aiY+U5v5whus4UFBQoLS1N0dHR5joPDw9FR0crNTXVjZ1dnzIyMpSVleUy3wEBAerQoQPzfRXy8vIkSbVr15YkpaWl6dy5cy7z27x5czVs2JD5vQqFhYVasmSJTp8+LYfDwfyWo7i4OMXGxrrMpcTPcHk5cOCAwsLCdOONN6p///7KzMyUVL7za6lvqraCX375RYWFhSX+JElISIj27dvnpq6uX1lZWZJ00fkuHkPpFBUVKT4+XnfccYdatmwp6ff59fb2LvHHlZnfstm1a5ccDofOnj0rf39/LV++XJGRkUpPT2d+y8GSJUu0Y8cObdu2rcQYP8N/XocOHZSYmKhmzZrp6NGjmjp1qjp37qzdu3eX6/wSiABUCXFxcdq9e7fLvQEoH82aNVN6erry8vL0v//9TwMHDlRKSoq727ouHD58WE8//bSSkpLk6+vr7nauSz179jT/3bp1a3Xo0EGNGjXS0qVL5efnV2774ZLZdaZu3bqqVq1aiTvss7OzFRoa6qaurl/Fc8p8/zmjRo3SypUrtWHDBtWvX99cHxoaqoKCAuXm5rrUM79l4+3trSZNmigqKkrTp09XmzZtNHfuXOa3HKSlpSknJ0ft2rWTp6enPD09lZKSonnz5snT01MhISHMcTkLDAxU06ZNdfDgwXL9GSYQXWe8vb0VFRWl5ORkc11RUZGSk5PlcDjc2Nn1KSIiQqGhoS7z7XQ6tXXrVua7FAzD0KhRo7R8+XKtX79eERERLuNRUVHy8vJymd/9+/crMzOT+f0TioqKlJ+fz/yWg+7du2vXrl1KT083l/bt26t///7mv5nj8nXq1Cl9//33qlevXvn+DP+JG79RRS1ZssTw8fExEhMTjb179xrDhw83AgMDjaysLHe3dk06efKk8fXXXxtff/21Icl45ZVXjK+//tr48ccfDcMwjBkzZhiBgYHGRx99ZOzcudN44IEHjIiICOPMmTNu7rzqGzlypBEQEGBs3LjROHr0qLn89ttvZs2IESOMhg0bGuvXrze2b99uOBwOw+FwuLHra8uECROMlJQUIyMjw9i5c6cxYcIEw2azGevWrTMMg/mtCBc+ZWYYzPGf9cwzzxgbN240MjIyjC+//NKIjo426tata+Tk5BiGUX7zSyC6Tv3zn/80GjZsaHh7exu33XabsWXLFne3dM3asGGDIanEMnDgQMMwfn/0/m9/+5sREhJi+Pj4GN27dzf279/v3qavERebV0nGwoULzZozZ84YTz31lFGrVi2jevXqRu/evY2jR4+6r+lrzODBg41GjRoZ3t7eRlBQkNG9e3czDBkG81sR/hiImOM/55FHHjHq1atneHt7GzfccIPxyCOPGAcPHjTHy2t+bYZhGOVwBgsAAOCaxT1EAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEANwqISFBbdu2rdB93HnnnYqPjzdfh4eHa86cORW6TwDXFgIRgArxxxByKc8++6zL3yGqDNu2bdPw4cNLVUt4AqzB090NALAmwzBUWFgof39/+fv7V+q+g4KCKnV/AKo+zhABKHeDBg1SSkqK5s6dK5vNJpvNpsTERNlsNq1evVpRUVHy8fHRF198UeKS2aBBg9SrVy9NnTpVQUFBstvtGjFihAoKCkq179OnT+vxxx+Xv7+/6tWrp1mzZpWoufCsj2EYSkhIUMOGDeXj46OwsDCNGTNG0u9nuX788UeNHTvWPA5J+vXXX9WvXz/dcMMNql69ulq1aqX33nvPZR933nmnxowZo/Hjx6t27doKDQ1VQkKCS01ubq6efPJJhYSEyNfXVy1bttTKlSvN8S+++EKdO3eWn5+fGjRooDFjxuj06dOlmgcAZUMgAlDu5s6dK4fDoWHDhuno0aM6evSoGjRoIEmaMGGCZsyYoW+//VatW7e+6PuTk5P17bffauPGjXrvvff04YcfaurUqaXa93PPPaeUlBR99NFHWrdunTZu3KgdO3Zcsv6DDz7Q7Nmz9cYbb+jAgQNasWKFWrVqJUn68MMPVb9+fU2bNs08Dkk6e/asoqKitGrVKu3evVvDhw/XgAED9NVXX7lse9GiRapRo4a2bt2qmTNnatq0aUpKSpIkFRUVqWfPnvryyy/17rvvau/evZoxY4aqVasmSfr+++919913q0+fPtq5c6fef/99ffHFFxo1alSp5gFAGZXXX6MFgAv98S9+b9iwwZBkrFixwqVuypQpRps2bczXAwcONGrXrm2cPn3aXDd//nzD39/fKCwsvOw+T548aXh7extLly411/3666+Gn5+fSy+NGjUyZs+ebRiGYcyaNcto2rSpUVBQcNFtXlh7ObGxscYzzzxjvu7atavRqVMnl5pbb73VeP755w3DMIy1a9caHh4exv79+y+6vSFDhhjDhw93Wff5558bHh4expkzZ67YD4Cy4QwRgErVvn37K9a0adNG1atXN187HA6dOnVKhw8fvuz7vv/+exUUFKhDhw7mutq1a6tZs2aXfM/DDz+sM2fO6MYbb9SwYcO0fPlynT9//rL7KSws1IsvvqhWrVqpdu3a8vf319q1a5WZmelS98czYPXq1VNOTo4kKT09XfXr11fTpk0vuo9vvvlGiYmJ5j1W/v7+iomJUVFRkTIyMi7bH4Cy46ZqAJWqRo0a7m7BRYMGDbR//3599tlnSkpK0lNPPaWXX35ZKSkp8vLyuuh7Xn75Zc2dO1dz5sxRq1atVKNGDcXHx5e4z+mP77fZbCoqKpIk+fn5XbavU6dO6cknnzTvZ7pQw4YNy3KIAEqBQASgQnh7e6uwsPCq3vvNN9/ozJkzZmjYsmWL/P39zfuQLqVx48by8vLS1q1bzdBw4sQJfffdd+ratesl3+fn56f77rtP9913n+Li4tS8eXPt2rVL7dq1u+hxfPnll3rggQf02GOPSfr9fqDvvvtOkZGRpT7G1q1b66efftJ333130bNE7dq10969e9WkSZNSbxPA1eOSGYAKER4erq1bt+rQoUP65ZdfzDMjpVFQUKAhQ4Zo7969+vTTTzVlyhSNGjVKHh6X/0+Wv7+/hgwZoueee07r16/X7t27NWjQoMu+LzExUf/617+0e/du/fDDD3r33Xfl5+enRo0amcexadMm/fzzz/rll18kSTfddJOSkpK0efNmffvtt3ryySeVnZ1d6uOTpK5du6pLly7q06ePkpKSlJGRodWrV2vNmjWSpOeff16bN2/WqFGjlJ6ergMHDuijjz7ipmqgghCIAFSIZ599VtWqVVNkZKSCgoJK3F9zOd27d9dNN92kLl266JFHHtH9999f4pH1S3n55ZfVuXNn3XfffYqOjlanTp0UFRV1yfrAwEC99dZbuuOOO9S6dWt99tln+uSTT1SnTh1J0rRp03To0CE1btzY/P6iSZMmqV27doqJidGdd96p0NBQ9erVq9THV+yDDz7Qrbfeqn79+ikyMlLjx483z0a1bt1aKSkp+u6779S5c2fdcsstmjx5ssLCwsq8HwBXZjMMw3B3EwBQbNCgQcrNzdWKFSvc3QoAC+EMEQAAsDwCEYBrRmZmpstj6H9cynJZDgAuxCUzANeM8+fP69ChQ5ccDw8Pl6cnD88CKDsCEQAAsDwumQEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMv7f+dZjQYV06HwAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -663,33 +1096,53 @@ } ], "source": [ - "result = %sql SELECT trip_distance FROM taxi\n", - "\n", - "import matplotlib.pyplot as plt\n", - "\n", - "data = result.toPandas()\n", - "\n", - "plt.hist(data[\"trip_distance\"])\n", - "plt.xlabel(\"Trip Distance\")\n", - "plt.ylabel(\"Frequency\")\n", - "plt.title(\"Histogram of Trip Distance\")\n", - "plt.show()" + "%sqlplot histogram --table taxi --column trip_distance --bins 10" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 36, "metadata": { "tags": [] }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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p379+mnEiBHq16+f4uLilJmZ6enSAHgAYQaAqWRmZmro0KFKTExUbm6uSktLlZubq8TERA0dOpRAAzRDFsMwDE8X0ZBKSkoUEhKi4uJi7s0EmJzValVcXJwSExO1dOlSeXn97/uYzWZTamqq8vLytGfPHk45ASZXn89vRmYAmEZOTo4KCgr01FNPOQQZSfLy8lJaWpry8/OVk5PjoQoBeAJhBoBpFBYWSpISEhLq3F7TXtMPQPNAmAFgGpGRkZKkvLy8OrfXtNf0A9A8EGYAmEZKSopiYmL04osvymazOWyz2WxKT09XbGysUlJSPFQhAE8gzAAwDW9vb82aNUvLly9Xamqqw9VMqampWr58uV577TUm/wLNDIvmATCVwYMHa/HixXrkkUfUq1cve3tsbKwWL16swYMHe7A6AJ7ApdkATIkVgIGmrT6f34zMADAlb29v9e3b19NlAGgEmDMDAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMjTADAABMrVGFmZdeekkWi0VTpkyxt508eVITJkxQaGioAgMDNWTIEB06dMhzRQIAgEal0YSZDRs26E9/+pOSkpIc2qdOnaply5bp448/1po1a3Tw4EENHjzYQ1UCAIDGplGEmRMnTuiuu+7SggUL1KZNG3t7cXGx3n77bc2ePVvXX3+9kpOTtXDhQq1du1br1q3zYMUAAKCxaBRhZsKECRo0aJD69+/v0L5p0yadOnXKoT0+Pl4dOnRQbm5unfuqrKxUSUmJww8AAGi6Wni6gA8//FCbN2/Whg0bam0rKiqSj4+PWrdu7dAeHh6uoqKiOveXnp6uGTNmNESpAACgEfLoyMz+/fv18MMP629/+5v8/Pzcss+0tDQVFxfbf/bv3++W/QIAgMbJo2Fm06ZNOnz4sLp3764WLVqoRYsWWrNmjV5//XW1aNFC4eHhqqqq0vHjxx2ed+jQIUVERNS5T19fXwUHBzv8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pvP4449rzpw5DmtctWjRQlOnTtUrr7ziwcoAuEt9Pr9dGpn5/PPPtXLlylrtK1eu1BdffOHKLgHAKY8//rheffVVhYaGasGCBSosLNSCBQsUGhqqV199VY8//rinSwRwgbk0MpOUlKSXXnpJN998s0P7ihUr9MQTT+j77793W4Hni5EZoOmoqqpSq1atFBoaqgMHDqhFi/9N+6uurlb79u119OhRlZWVycfHx4OVAjhfDT4ys2fPHnXt2rVWe3x8vH744QdXdgkA55SRkaHq6mo9//zzslgsys7O1gcffKDs7GxZLBbNnDlT1dXVysjI8HSpAC4gl65mCgkJ0Y8//qiYmBiH9h9++EGtWrVyR10AUEvNopwWi0VxcXEqKCiwb4uJidHTTz/t0A9A8+BSmLnttts0ZcoULVmyRJ06dZJ0Osg88sgjuvXWW91aIADUqPl7M3bsWA0aNEiPPfaY/P39VVFRoS+++EIPPPCAQz8AzYNLc2aKi4t10003aePGjWrfvr0k6cCBA0pJSVFmZqZat27t7jpdxpwZoOmoqKhQQECAWrRooaioKPtNbiWpQ4cOOnjwoKqrq1VeXi5/f38PVgrgfNXn89vl00xr167V6tWr9f3338vf319JSUnq3bu3SwUDgDPWr18v6fRk36KiIj3xxBMaM2aM3n77bYdLtdevX6++fft6sFIAF5JLYUY6fc76xhtv1I033ujOegDgjP79739LkmJjY7Vv3z69/PLLevnllyWdXmcmNjZW+fn59n4AmgeXw0xWVpaysrJ0+PBh2Ww2h23vvPPOeRcGAL925MgRSdJTTz2le++9VxkZGdq7d686deqk8ePHa9GiRRo3bpy9H4DmwaUwM2PGDM2cOVNXXnmlIiMjuVM2gAsiLCxMkpSZman77rtPU6ZMsW+z2WxaunSpQz8AzYNLYebNN9/UokWLdM8997i7HgA4o4svvljS6QU6U1NTlZaWpoSEBOXl5Sk9PV0rVqxw6AegeXApzFRVValXr17urgUAziolJUUxMTG66KKLtHXrVoe/Q7GxsUpOTtbRo0eVkpLiwSoBXGgurQB8//336/3333d3LQBwVt7e3po1a5Y2btyowsJCh20HDx7Uxo0b9dprr8nb29tDFQLwBJdGZk6ePKm33npL//jHP5SUlKSWLVs6bJ89e7ZbigOAM6msrDzr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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%sqlplot boxplot --table taxi --column trip_distance" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, "outputs": [ { "data": { "text/html": [ - "Running query in 'SparkSession'" + "Removing NULLs, if there exists any from payment_type" ], "text/plain": [ - "Running query in 'SparkSession'" + "Removing NULLs, if there exists any from payment_type" ] }, "metadata": {}, @@ -697,7 +1150,17 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjIAAAHHCAYAAACle7JuAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA6fklEQVR4nO3deVxWZf7/8fcNCrK7sTYqGBo2on7VSlFMciGyhZCa0iY1aybHNm21zWVKyyW1zZaZsmXUJr+ok2VpJsokNqVpuaMjaYGopSwuiNzn94c/7q93oIHecrjw9Xw8ziPOda77nA9g3O/7Otc5x2FZliUAAAADedldAAAAwNkiyAAAAGMRZAAAgLEIMgAAwFgEGQAAYCyCDAAAMBZBBgAAGIsgAwAAjEWQAQAAxiLIAPWcw+HQuHHj7C7Dzddff62EhAQFBATI4XBo/fr1Hj/G0KFDFR0d7fH91sTs2bPlcDiUm5trax1AfUaQAc5SxZvUqUtYWJiSkpK0ZMkSu8s7Z5s3b9a4ceM8/iZcVlamm266Sb/88oumT5+u9957T61atarULzo6utLPt6pl9uzZHq3vdDIzM92O6+vrq/DwcPXu3VsTJ07U/v37PXKcI0eOaNy4ccrMzPTI/oD6roHdBQCmmzBhgmJiYmRZlgoKCjR79mxdc801+uijj3TttdfaXd5Z27x5s8aPH6/evXt7dGRj586d+uGHH/Tmm2/qzjvvPG2/GTNmqKSkxLX+ySefaO7cuZo+fbqaN2/uak9ISKjy9W+++aacTqfH6q5w33336bLLLlN5ebn279+v1atXa+zYsXrhhRf0z3/+U1dddZWr7x//+Efdcsst8vX1rfb+jxw5ovHjx0uSevfu7enygXqHIAOco5SUFHXt2tW1Pnz4cIWHh2vu3LlGB5nzZd++fZKkxo0bn7Ffamqq2/revXs1d+5cpaamnjFYHT58WAEBAWrYsOE5Vlq1xMREpaenu7Vt2LBB/fv318CBA7V582ZFRkZKkry9veXt7X1e6gBwEqeWAA9r3Lix/Pz81KCB++eEw4cP68EHH1SLFi3k6+urSy65RFOnTlXFA+iPHj2quLg4xcXF6ejRo67X/fLLL4qMjFRCQoLKy8slnZz/ERgYqP/+979KTk5WQECAoqKiNGHCBFXngfbffvutUlJSFBwcrMDAQPXp00dr1qxxbZ89e7ZuuukmSVJSUpLrdMpvne744osvlJiYqICAADVu3Fg33HCDtmzZ4to+dOhQXXnllZKkm266SQ6H45xGHSp+Djt37tQ111yjoKAgDR482LXt1MCTm5srh8OhqVOnavr06WrVqpX8/Px05ZVXauPGjWddgyR17NhRM2bM0KFDh/Tyyy+72quaI/PNN98oOTlZzZs3l5+fn2JiYnTHHXe4agwNDZUkjR8/3vVzr5jj9N1332no0KFq3bq1GjVqpIiICN1xxx36+eef3eoZN26cHA6HduzYoaFDh6px48YKCQnRsGHDdOTIkUr1v//++7r88svl7++vJk2aqFevXlq6dKlbnyVLlrh+t0FBQRowYIA2bdp0Tj83wBMYkQHOUWFhoQ4cOCDLsrRv3z699NJLKikp0W233ebqY1mWrr/+eq1YsULDhw9Xp06d9Nlnn+nhhx/WTz/9pOnTp8vPz0/vvPOOevTooSeeeEIvvPCCJGnkyJEqLCzU7Nmz3T7dl5eX6+qrr1a3bt00efJkffrppxo7dqxOnDihCRMmnLbeTZs2KTExUcHBwXrkkUfUsGFDvf766+rdu7dWrlypK664Qr169dJ9992nF198UY8//rjatWsnSa7/VuXzzz9XSkqKWrdurXHjxuno0aN66aWX1KNHD61bt07R0dH685//rIsuukgTJ050naIJDw8/p5//iRMnlJycrJ49e2rq1Kny9/c/Y/93331XxcXFGjlypI4dO6aZM2fqqquu0vfff39OtaSnp2v48OFaunSpnn322Sr77Nu3T/3791doaKgee+wxNW7cWLm5ucrIyJAkhYaGatasWRoxYoRuvPFGpaWlSZI6dOggSVq2bJn++9//atiwYYqIiNCmTZv0xhtvaNOmTVqzZo0cDofb8W6++WbFxMRo0qRJWrdunf72t78pLCxMzz//vKvP+PHjNW7cOCUkJGjChAny8fHRV199pS+++EL9+/eXJL333nsaMmSIkpOT9fzzz+vIkSOaNWuWevbsqW+//db2SdW4wFkAzsrbb79tSaq0+Pr6WrNnz3bru3DhQkuS9cwzz7i1p6enWw6Hw9qxY4erbcyYMZaXl5e1atUq68MPP7QkWTNmzHB73ZAhQyxJ1r333utqczqd1oABAywfHx9r//79rnZJ1tixY13rqamplo+Pj7Vz505XW15enhUUFGT16tXL1VZx7BUrVlTr59GpUycrLCzM+vnnn11tGzZssLy8vKzbb7/d1bZixQpLkvXhhx9Wa78VpkyZYkmydu3a5Wqr+Dk89thjlfoPGTLEatWqlWt9165dliTLz8/P+vHHH13tX331lSXJGjVq1BmPX526O3bsaDVp0sS1XvFvpKLmBQsWWJKsr7/++rT72L9/f6XfWYUjR45Uaps7d64lyVq1apWrbezYsZYk64477nDre+ONN1rNmjVzrefk5FheXl7WjTfeaJWXl7v1dTqdlmVZVnFxsdW4cWPrrrvuctu+d+9eKyQkpFI7UNs4tQSco1deeUXLli3TsmXL9P777yspKUl33nmn61O2dHKiqre3t+677z631z744IOyLMvtKqdx48bp97//vYYMGaK//OUvuvLKKyu9rsI999zj+trhcOiee+7R8ePH9fnnn1fZv7y8XEuXLlVqaqpat27tao+MjNSgQYP073//W0VFRTX+GeTn52v9+vUaOnSomjZt6mrv0KGD+vXrp08++aTG+6yJESNGVLtvamqqLrroItf65ZdfriuuuMIjNQYGBqq4uPi02yvmBS1evFhlZWU13r+fn5/r62PHjunAgQPq1q2bJGndunWV+t99991u64mJifr5559dv+OFCxfK6XTq6aeflpeX+9tBxejOsmXLdOjQId166606cOCAa/H29tYVV1yhFStW1Pj7ADyJIAOco8svv1x9+/ZV3759NXjwYH388ce69NJLXaFCkn744QdFRUUpKCjI7bUVp2p++OEHV5uPj4/eeust7dq1S8XFxXr77bcrnTKQJC8vL7cwIklt27aVpNNeMr1//34dOXJEl1xySaVt7dq1k9Pp1J49e6r/zf9/FfWfbr8HDhzQ4cOHa7zf6mjQoIF+97vfVbt/mzZtKrW1bdvWI5eZl5SUVPodn+rKK6/UwIEDNX78eDVv3lw33HCD3n77bZWWllZr/7/88ovuv/9+hYeHy8/PT6GhoYqJiZF08hTnr7Vs2dJtvUmTJpKkgwcPSjp5BZmXl5cuvfTS0x4zJydHknTVVVcpNDTUbVm6dKlr8jZgF+bIAB7m5eWlpKQkzZw5Uzk5Ofr9739f43189tlnkk5+6s7JyXG9WaEyX1/fSqMJdigrK9P27dvVvn370/ZxOByaP3++1qxZo48++kifffaZ7rjjDk2bNk1r1qxRYGDgGY9x8803a/Xq1Xr44YfVqVMnBQYGyul06uqrr67yUvPTXTFlVWNCeIWK/b733nuKiIiotP3Xk9qB2sa/QOA8OHHihCS57oPSqlUrff755youLnb7xL5161bX9grfffedJkyYoGHDhmn9+vW688479f333yskJMTtGE6nU//9739dozCStH37dkk67eTL0NBQ+fv7a9u2bZW2bd26VV5eXmrRooUkVTkKdDoV9Z9uv82bN1dAQEC193c+VYwwnGr79u3nPGF1/vz5Onr0qJKTk3+zb7du3dStWzc9++yzmjNnjgYPHqx58+bpzjvvPO3P/eDBg1q+fLnGjx+vp59+2tVe1fdTXRdffLGcTqc2b96sTp06nbaPJIWFhalv375nfSzgfLH/YwxQz5SVlWnp0qXy8fFxnTq65pprVF5e7nZpriRNnz5dDodDKSkprtcOHTpUUVFRmjlzpmbPnq2CggKNGjWqymOduj/LsvTyyy+rYcOG6tOnT5X9vb291b9/fy1atMjtVEpBQYHmzJmjnj17Kjg4WJJcwePQoUO/+T1HRkaqU6dOeuedd9z6b9y4UUuXLtU111zzm/uoLQsXLtRPP/3kWv/Pf/6jr776yvU7OBsbNmzQAw88oCZNmmjkyJGn7Xfw4MFKoyEVAaLi9FLFVVe//rlXjK78+vUzZsw467pTU1Pl5eWlCRMmVBrRqThOcnKygoODNXHixCrn9XjqjsbA2WJEBjhHS5YscY2s7Nu3T3PmzFFOTo4ee+wxVyi47rrrlJSUpCeeeEK5ubnq2LGjli5dqkWLFumBBx5wfep95plntH79ei1fvlxBQUHq0KGDnn76aT355JNKT093CwSNGjXSp59+qiFDhuiKK67QkiVL9PHHH+vxxx933YukKs8884yWLVumnj176i9/+YsaNGig119/XaWlpZo8ebKrX6dOneTt7a3nn39ehYWF8vX11VVXXaWwsLAq9ztlyhSlpKSoe/fuGj58uOvy65CQkDr1rKfY2Fj17NlTI0aMUGlpqWbMmKFmzZrpkUceqdbrs7KydOzYMZWXl+vnn3/Wl19+qX/9618KCQnRggULqjz9UuGdd97Rq6++qhtvvFEXX3yxiouL9eabbyo4ONj1u/Xz89Oll16qDz74QG3btlXTpk3Vvn17tW/fXr169dLk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aty48W/ud+jQoTp06JAWLlx41rUBqNt41hIAl/z8fNfXH3zwgZ5++mlt27bN1RYYGOj62rIslZeXKzAw0K39XGzbtk3BwcEqKirSunXrNHnyZP39739XZmam4uPjJUkhISEeORaA+oFTSwBcIiIiXEtISIgcDodrfevWrQoKCtKSJUvUpUsX+fr66t///nelU0sVp37Gjx+v0NBQBQcH6+6779bx48d/8/hhYWGKiIhQ27Ztdcstt+jLL79UaGioRowYUWn/FebPn6/4+Hj5+fmpWbNm6tu3rw4fPqxx48bpnXfe0aJFi+RwOORwOJSZmSlJevTRR9W2bVv5+/urdevWeuqpp1RWVubaZ8X39N577yk6OlohISG65ZZbVFxc7OrjdDo1efJkxcbGytfXVy1bttSzzz7r2r5nzx7dfPPNaty4sZo2baobbrhBubm5Nf+lADgjggyAGnnsscf03HPPacuWLerQoUOVfZYvX64tW7YoMzNTc+fOVUZGhsaPH1/jY/n5+enuu+/Wl19+qX379lXanp+fr1tvvVV33HGH63hpaWmyLEsPPfSQbr75Zl199dXKz89Xfn6+EhISJElBQUGaPXu2Nm/erJkzZ+rNN9/U9OnT3fa9c+dOLVy4UIsXL9bixYu1cuVKPffcc67tY8aM0XPPPaennnpKmzdv1pw5cxQeHi5JKisrU3JysoKCgpSVlaUvv/xSgYGBuvrqq6sV6ABUH6eWANTIhAkT1K9fvzP28fHx0VtvvSV/f3/9/ve/14QJE/Twww/rr3/9q7y8avb5KS4uTpKUm5ursLAwt235+fk6ceKE0tLS1KpVK0lynYKSTgah0tJSRUREuL3uySefdH0dHR2thx56SPPmzdMjjzzianc6nZo9e7aCgoIkSX/84x+1fPlyPfvssyouLtbMmTP18ssva8iQIZKkiy++WD179pR08rSc0+nU3/72N9e8o7fffluNGzdWZmam+vfvX6OfAYDTI8gAqJGuXbv+Zp+OHTvK39/ftd69e3eVlJRoz549rsBRXRXXI1Q1Ebljx47q06eP4uPjlZycrP79+ys9PV1NmjQ54z4/+OADvfjii9q5c6dKSkp04sQJBQcHu/WJjo52hRhJioyMdI0KbdmyRaWlperTp0+V+9+wYYN27Njh9npJOnbsmHbu3Pnb3zSAauPUEoAaCQgIqNXjbdmyRdLJYPFr3t7eWrZsmZYsWaJLL71UL730ki655BLt2rXrtPvLzs7W4MGDdc0112jx4sX69ttv9cQTT1Q65dOwYUO3dYfDIafTKenkSM+ZlJSUqEuXLlq/fr3bsn37dg0aNKg63zaAaiLIAPC4DRs26OjRo671NWvWKDAwUC1atKjRfo4ePao33nhDvXr1UmhoaJV9HA6HevToofHjx+vbb7+Vj4+PFixYIOnkKa7y8nK3/qtXr1arVq30xBNPqGvXrmrTpo1++OGHGtXVpk0b+fn5afny5VVu79y5s3JychQWFqbY2Fi3hauuAM8iyADwuOPHj2v48OHavHmzPvnkE40dO1b33HPPb86P2bdvn/bu3aucnBzNmzdPPXr00IEDBzRr1qwq+3/11VeaOHGivvnmG+3evVsZGRnav3+/2rVrJ+nkKM53332nbdu26cCBAyorK1ObNm20e/duzZs3Tzt37tSLL77oCj7V1ahRIz366KN65JFH9O6772rnzp1as2aN/v73v0uSBg8erObNm+uGG25QVlaWdu3apczMTN1333368ccfa3QsAGfGHBkAHtenTx+1adNGvXr1UmlpqW699dZKN9KryiWXXCKHw6HAwEC1bt1a/fv31+jRoytN1q0QHBysVatWacaMGSoqKlKrVq00bdo0paSkSJLuuusuZWZmqmvXriopKdGKFSt0/fXXa9SoUbrnnntUWlqqAQMG6KmnnqpWfad66qmn1KBBAz399NPKy8tTZGSk7r77bkmSv7+/Vq1apUcffVRpaWkqLi7WRRddpD59+lSaiwPg3HBnXwAexd10AdQmTi0BAABjEWQAAICxOLUEAACMxYgMAAAwFkEGAAAYiyADAACMRZABAADGIsgAAABjEWQAAICxCDIAAMBYBBkAAGAsggwAADDW/wPY4fOD7IJuHgAAAABJRU5ErkJggg==", + "text/plain": [ + "" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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t6/VqFEja9KkSdahQ4esv//971aDBg2sV155xbIsy3r99det9957zzp69KiVkpJiORwOa/Dgwa7z/+d//seKjIx0m/Pxxx+37rjjDtdjSdZPfvITa/Xq1VZGRoY1dOhQKywszBowYIC1adMm68CBA1afPn2sO++803XOjh07LLvdbiUmJlpHjx61tmzZYoWFhVlz5sxxm7d169bWypUrrcOHD1uPP/641ahRI+u7776zLl68aP3jH/+wJFkZGRnWyZMnrby8vGv+LPLy8iyHw2E9+uij1smTJ62TJ09aFy9eLHOPzZo1s1599VUrIyPDmjlzpuXt7W0dOHDAsizLKioqsiIiIqxHHnnE+vLLL60DBw5YDzzwgNW+fXursLCwrMsF4CoIRABuWN++fa2IiAirpKTEte/JJ5+0IiIirjp+z549liTrzJkzlmVZ1jfffGN5e3tbu3fvtizrP3/4mzdvbiUmJrrOkWTNnDnT9TglJcWSZL3++uuufW+99Zbl7+/vejxw4EDrz3/+s9tz/+1vf7NatWr1o/OePXvWkmS9//77lmVZ1ocffmhJsr7//vty/Twu/Xbu8vQ4fvx4t/N69+5tTZgwwVV7+/bt3X7OhYWFVv369a3NmzeXuT4AV+IlMwCVok+fPrLZbK7HDodDhw8fVnFxsVJTU/WrX/1Kbdq0UePGjdW3b19JUlZWliQpJCREMTExeuONNyRJ69evV2FhoX7961+7PUfnzp1d/w4KCpIkderUyW1fQUGBnE6nJOmLL77QvHnz1KhRI9f26KOP6uTJk/rhhx+uOm/Dhg1lt9tdL+lVlrL26HA4rnh88OBBVz9HjhxR48aNXf00bdpUBQUFV7xUCKB86nm6AAB1W0FBgaKjoxUdHa0333xTLVq0UFZWlqKjo1VUVOQaN3bsWI0aNUoJCQlavny57rvvPjVo0MBtLh8fH9e/S8PX1faVlJRIks6ePau5c+dq2LBhV9Tl7+9/1XlL5ymdozKVpcdrOXv2rLp3764333zzimMtWrSozFIB4xCIAFSK3bt3uz3etWuXbr75Zh06dEjfffedFixYoNDQUEnSZ599dsX5Q4YMUcOGDbV06VJt2rRJO3bsuOGaunXrpoyMDLVr167Cc/j6+kqSiouLy3XO1caXpcddu3bpwQcfdHt86623SvpPP6tXr1bLli1lt9vL2wqAa+AlMwCVIisrS1OmTFFGRobeeustvfDCC5o0aZLatGkjX19fvfDCC/rqq6+0bt06PfXUU1ec7+3trYceekgzZszQzTfffMVLRxUxa9Ys/fWvf9XcuXO1f/9+HTx4UKtWrdLMmTPLPEfbtm1ls9m0YcMGnTp1SmfPnr3uOWFhYdq9e7eOHTumb7/91nW1qSw9rlmzRm+88Yb+9a9/afbs2fr0008VFxcnSRo5cqSaN2+uu+++Wx999JEyMzO1fft2Pf744/r666/L3BOAKxGIAFSKBx98UOfPn1evXr0UGxurSZMmady4cWrRooUSExO1Zs0aRUZGasGCBVe8/bzUmDFjVFRUpIcffrhSaoqOjtaGDRu0ZcsW9ezZU3369FFCQoLatm1b5jl+8pOfaO7cuZo+fbqCgoJc4eRannjiCXl7eysyMtL1EmGp6/U4d+5crVq1Sp07d9Zf//pXvfXWW4qMjJQkNWjQQDt27FCbNm00bNgwRUREaMyYMSooKOCKEXCDbJb1/z4oBAAqqF+/furatauee+65G5rno48+0sCBA3X8+HHXTdN1zbV6tNlsWrt2rYYOHeqZ4gCDcQ8RAI8rLCzUqVOnNGfOHP3617+uk2HIhB6B2oyXzAB43FtvvaW2bdsqLy9PCxcu9HQ515SVleX2Nv7Lt0tfHrtUbeoRMBEvmQFAOVy8ePGaX5URFhamevW4+A7UNgQiAABgPF4yAwAAxiMQAQAA4xGIAACA8QhEAADAeAQiAABgPAIRAAAwHoEIAAAYj0AEAACM939faPJBMq+8vAAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -707,17 +1170,88 @@ } ], "source": [ - "result = %sql SELECT trip_distance FROM taxi\n", - "\n", - "import matplotlib.pyplot as plt\n", - "\n", - "data = result.toPandas()\n", - "\n", - "plt.boxplot(data[\"trip_distance\"])\n", - "plt.xlabel(\"Trip Distance\")\n", - "plt.ylabel(\"Value\")\n", - "plt.title(\"Boxplot of Trip Distance\")\n", - "plt.show()" + "%sqlplot bar --table taxi --column payment_type" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "Removing NULLs, if there exists any from payment_type" + ], + "text/plain": [ + "Removing NULLs, if there exists any from payment_type" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%sqlplot pie --table taxi --column payment_type" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "from sql.ggplot import ggplot, aes, geom_boxplot, geom_histogram, facet_wrap" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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VtOmTTVkyBBt27ZNISEheuedd5Sdna2FCxeWehtt27ZVv3799PrrrysvL0+33367kpOTdfDgwSu+d9GiRXr99dfVu3dvNW7cWCdPntRbb70lu92ue+65R9Lvl4wiIyP1/vvvq2nTpqpdu7Zatmypli1bqkuXLpo5c6bOnTunG264QevWrVNGRsZVzYX0+z1VL7zwgl588UV17txZDz74oHx8fLRt2zaFhYVp+vTpstvtmj9/vgYMGKB27dqpb9++CgoKUmZmplatWqU77rhDr7766lX3UBoTJkzQe++9p549e2rMmDGqXbu2Fi1apIyMDH3wwQclbgivaOXxGb300ktKT09XcnKyatasqdatW2vy5MmaNGmSHnroIfPnAagy3PeAGwDD+P3x7tjYWKNmzZqGJPMR/OJH2i/2CPulHruPjY011q5da7Ru3drw8fExmjdvbixbtqzMPZ05c8YYM2aMUadOHaNGjRrGfffdZxw+fPiKj93v2LHD6Nevn9GwYUPDx8fHCA4ONu69915j+/btLtvfvHmzERUVZXh7e7ts86effjJ69+5tBAYGGgEBAcbDDz9sHDlypMR+L/VY+8XmxTAM45133jFuueUWw8fHx6hVq5bRtWtXIykpyaVmw4YNRkxMjBEQEGD4+voajRs3NgYNGlSi99K43GP3sbGxF33P999/bzz00ENGYGCg4evra9x2223GypUrS/QoqcRneqmflUv1URp/5jNKS0szPD09jdGjR7ts8/z588att95qhIWFGSdOnHDpEXA3m2GU4912ANwmPDxcLVu21MqVK93dCgBcc7iHCAAAWB73EAEWcqUvZ/Tz8+MJIACWRCACLKRevXqXHR84cKASExMrpxkAqEIIRMB1ojTfRJ2UlHTZ8bCwsHLqBgCuLdxUDQAALI+bqgEAgOVxyawUioqKdOTIEdWsWZM/TAgAwDXCMAydPHlSYWFhV/yCUwJRKRw5cqTEX5EGAADXhsOHD6t+/fqXrSEQlULNmjUl/T6hdrvdzd0AAIDScDqdatCggfl7/HIIRKVQfJnMbrcTiAAAuMaU5nYXbqoGAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACWRyACAACW5+nu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FAzD0KhRo7R8+XKtX79eERERLuNRUVHy8vJymd/9+/crMzOT+f0TioqKlJ+fz/yWg+7du2vXrl1KT083l/bt26t///7mv5nj8nXq1Cl9//33qlevXvn+DP+JG79RRS1ZssTw8fExEhMTjb179xrDhw83AgMDjaysLHe3dk06efKk8fXXXxtff/21Icl45ZVXjK+//tr48ccfDcMwjBkzZhiBgYHGRx99ZOzcudN44IEHjIiICOPMmTNu7rzqGzlypBEQEGBs3LjROHr0qLn89ttvZs2IESOMhg0bGuvXrze2b99uOBwOw+FwuLHra8uECROMlJQUIyMjw9i5c6cxYcIEw2azGevWrTMMg/mtCBc+ZWYYzPGf9cwzzxgbN240MjIyjC+//NKIjo426tata+Tk5BiGUX7zSyC6Tv3zn/80GjZsaHh7exu33XabsWXLFne3dM3asGGDIanEMnDgQMMwfn/0/m9/+5sREhJi+Pj4GN27dzf279/v3qavERebV0nGwoULzZozZ84YTz31lFGrVi2jevXqRu/evY2jR4+6r+lrzODBg41GjRoZ3t7eRlBQkNG9e3czDBkG81sR/hiImOM/55FHHjHq1atneHt7GzfccIPxyCOPGAcPHjTHy2t+bYZhGOVwBgsAAOCaxT1EAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEAADA8ghEANwqISFBbdu2rdB93HnnnYqPjzdfh4eHa86cORW6TwDXFgIRgArxxxByKc8++6zL3yGqDNu2bdPw4cNLVUt4AqzB090NALAmwzBUWFgof39/+fv7V+q+g4KCKnV/AKo+zhABKHeDBg1SSkqK5s6dK5vNJpvNpsTERNlsNq1evVpRUVHy8fHRF198UeKS2aBBg9SrVy9NnTpVQUFBstvtGjFihAoKCkq179OnT+vxxx+Xv7+/6tWrp1mzZpWoufCsj2EYSkhIUMOGDeXj46OwsDCNGTNG0u9nuX788UeNHTvWPA5J+vXXX9WvXz/dcMMNql69ulq1aqX33nvPZR933nmnxowZo/Hjx6t27doKDQ1VQkKCS01ubq6efPJJhYSEyNfXVy1bttTKlSvN8S+++EKdO3eWn5+fGjRooDFjxuj06dOlmgcAZUMgAlDu5s6dK4fDoWHDhuno0aM6evSoGjRoIEmaMGGCZsyYoW+//VatW7e+6PuTk5P17bffauPGjXrvvff04YcfaurUqaXa93PPPaeUlBR99NFHWrdunTZu3KgdO3Zcsv6DDz7Q7Nmz9cYbb+jAgQNasWKFWrVqJUn68MMPVb9+fU2bNs08Dkk6e/asoqKitGrVKu3evVvDhw/XgAED9NVXX7lse9GiRapRo4a2bt2qmTNnatq0aUpKSpIkFRUVqWfPnvryyy/17rvvau/evZoxY4aqVasmSfr+++919913q0+fPtq5c6fef/99ffHFFxo1alSp5gFAGZXXX6MFgAv98S9+b9iwwZBkrFixwqVuypQpRps2bczXAwcONGrXrm2cPn3aXDd//nzD39/fKCwsvOw+T548aXh7extLly411/3666+Gn5+fSy+NGjUyZs+ebRiGYcyaNcto2rSpUVBQcNFtXlh7ObGxscYzzzxjvu7atavRqVMnl5pbb73VeP755w3DMIy1a9caHh4exv79+y+6vSFDhhjDhw93Wff5558bHh4expkzZ67YD4Cy4QwRgErVvn37K9a0adNG1atXN187HA6dOnVKhw8fvuz7vv/+exUUFKhDhw7mutq1a6tZs2aXfM/DDz+sM2fO6MYbb9SwYcO0fPlynT9//rL7KSws1IsvvqhWrVqpdu3a8vf319q1a5WZmelS98czYPXq1VNOTo4kKT09XfXr11fTpk0vuo9vvvlGiYmJ5j1W/v7+iomJUVFRkTIyMi7bH4Cy46ZqAJWqRo0a7m7BRYMGDbR//3599tlnSkpK0lNPPaWXX35ZKSkp8vLyuuh7Xn75Zc2dO1dz5sxRq1atVKNGDcXHx5e4z+mP77fZbCoqKpIk+fn5XbavU6dO6cknnzTvZ7pQw4YNy3KIAEqBQASgQnh7e6uwsPCq3vvNN9/ozJkzZmjYsmWL/P39zfuQLqVx48by8vLS1q1bzdBw4sQJfffdd+ratesl3+fn56f77rtP9913n+Li4tS8eXPt2rVL7dq1u+hxfPnll3rggQf02GOPSfr9fqDvvvtOkZGRpT7G1q1b66efftJ333130bNE7dq10969e9WkSZNSbxPA1eOSGYAKER4erq1bt+rQoUP65ZdfzDMjpVFQUKAhQ4Zo7969+vTTTzVlyhSNGjVKHh6X/0+Wv7+/hgwZoueee07r16/X7t27NWjQoMu+LzExUf/617+0e/du/fDDD3r33Xfl5+enRo0amcexadMm/fzzz/rll18kSTfddJOSkpK0efNmffvtt3ryySeVnZ1d6uOTpK5du6pLly7q06ePkpKSlJGRodWrV2vNmjWSpOeff16bN2/WqFGjlJ6ergMHDuijjz7ipmqgghCIAFSIZ599VtWqVVNkZKSCgoJK3F9zOd27d9dNN92kLl266JFHHtH9999f4pH1S3n55ZfVuXNn3XfffYqOjlanTp0UFRV1yfrAwEC99dZbuuOOO9S6dWt99tln+uSTT1SnTh1J0rRp03To0CE1btzY/P6iSZMmqV27doqJidGdd96p0NBQ9erVq9THV+yDDz7Qrbfeqn79+ikyMlLjx483z0a1bt1aKSkp+u6779S5c2fdcsstmjx5ssLCwsq8HwBXZjMMw3B3EwBQbNCgQcrNzdWKFSvc3QoAC+EMEQAAsDwCEYBrRmZmpstj6H9cynJZDgAuxCUzANeM8+fP69ChQ5ccDw8Pl6cnD88CKDsCEQAAsDwumQEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMsjEAEAAMv7f+dZjQYV06HwAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "(ggplot(table=\"taxi\", mapping=aes(x=\"trip_distance\")) + geom_histogram(bins=10))" ] }, { @@ -732,7 +1266,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 41, "metadata": { "tags": [] }, @@ -741,9 +1275,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n", - "4a7133e0b70b wh1isper/sparglim-server \"tini -- sparglim-se…\" 4 minutes ago Up 4 minutes 0.0.0.0:4040->4040/tcp, 0.0.0.0:15002->15002/tcp spark\n", - "f019407c6426 docker.dev.slicelife.com/onelogin-aws-assume-role:stable \"onelogin-aws-assume…\" 2 weeks ago Up 2 weeks heuristic_tu\n" + "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n", + "12f699ee8e8e wh1isper/sparglim-server \"tini -- sparglim-se…\" About a minute ago Up About a minute 0.0.0.0:4040->4040/tcp, 0.0.0.0:15002->15002/tcp spark\n", + "f019407c6426 docker.dev.slicelife.com/onelogin-aws-assume-role:stable \"onelogin-aws-assume…\" 2 weeks ago Up 2 weeks heuristic_tu\n" ] } ], @@ -753,7 +1287,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 42, "metadata": { "tags": [] }, @@ -765,7 +1299,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 43, "metadata": { "tags": [] }, @@ -774,7 +1308,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Container id: 4a7133e0b70b\n" + "Container id: 12f699ee8e8e\n" ] } ], @@ -785,7 +1319,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 44, "metadata": { "tags": [] }, @@ -794,7 +1328,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "4a7133e0b70b\n" + "12f699ee8e8e\n" ] } ], @@ -804,7 +1338,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 45, "metadata": { "tags": [] }, @@ -813,7 +1347,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "4a7133e0b70b\n" + "12f699ee8e8e\n" ] } ], diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index bf7bc44e0..fb2a0159d 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -17,6 +17,8 @@ ProgrammingError, ) +from sql.run.sparkdataframe import handle_spark_dataframe + try: from pyspark.sql.connect.session import SparkSession as CSparkSession from pyspark.sql import SparkSession @@ -1099,7 +1101,7 @@ def dialect(self): def raw_execute(self, query, parameters=None): """Run the query without any pre-processing""" - return self._connection.sql(query) + return handle_spark_dataframe(self._connection.sql(query)) def _get_database_information(self): """ diff --git a/src/sql/magic.py b/src/sql/magic.py index d34d32e2c..32d231d6b 100644 --- a/src/sql/magic.py +++ b/src/sql/magic.py @@ -147,6 +147,14 @@ class SqlMagic(Magics, Configurable): config=True, help="Verbosity level. 0=minimal, 1=normal, 2=all", ) + lazy_spark = Bool( + default_value=False, + config=True, + help="Whether to evalute using ResultSet which will " + "cause the plan to execute or just return a Spark " + "DataFrame plan allowing lazy execution but still " + "validating schemas.", + ) named_parameters = Bool( default_value=False, config=True, diff --git a/src/sql/plot.py b/src/sql/plot.py index 10e7e7895..d61f3d2f6 100644 --- a/src/sql/plot.py +++ b/src/sql/plot.py @@ -271,7 +271,7 @@ def _min_max(conn, table, column, with_=None, use_backticks=False): template = Template(template_) query = template.render(table=table, column=column) - + y = conn.execute(query, with_) min_, max_ = conn.execute(query, with_).fetchone() return min_, max_ diff --git a/src/sql/run/run.py b/src/sql/run/run.py index f59ef96d2..3b3c2c719 100644 --- a/src/sql/run/run.py +++ b/src/sql/run/run.py @@ -3,6 +3,7 @@ from sql import exceptions, display from sql.run.resultset import ResultSet from sql.run.pgspecial import handle_postgres_special +from sql.run.sparkdataframe import handle_spark_dataframe # TODO: conn also has access to config, we should clean this up to provide a clean @@ -50,7 +51,9 @@ def run_statements(conn, sql, config, parameters=None): result = handle_postgres_special(conn, statement) if is_spark(conn.dialect): - return conn.raw_execute(statement, parameters=parameters) + result = conn.raw_execute(statement, parameters=parameters) + if config.lazy_spark: + return conn.raw_execute(statement, parameters=parameters).dataframe # regular query else: diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py new file mode 100644 index 000000000..4ac86b35e --- /dev/null +++ b/src/sql/run/sparkdataframe.py @@ -0,0 +1,42 @@ +try: + from pyspark.sql import DataFrame + from pyspark.sql.connect.dataframe import DataFrame as CDataFrame +except ModuleNotFoundError: + DataFrame = None + CDataFrame = None + +from sql import exceptions + + +def handle_spark_dataframe(dataframe, should_cache=False): + """Execute a ResultSet sqlaproxy using pysark module.""" + if not DataFrame and not CDataFrame: + raise exceptions.MissingPackageError("pysark not installed") + + return FakeResultProxy(dataframe, dataframe.columns, should_cache) + + +class FakeResultProxy(object): + """A fake class that pretends to behave like the ResultProxy from + SqlAlchemy. + """ + + dataframe = None + + def __init__(self, dataframe: DataFrame, headers, should_cache): + self.dataframe = dataframe + self.fetchall = dataframe.collect + self.rowcount = dataframe.count + self.keys = lambda: headers + self.returns_rows = True + if should_cache: + self.dataframe.cache() + + def fetchmany(self, size): + return self.dataframe.take(size) + + def fetchone(self): + return self.dataframe.head() + + def close(self): + self.dataframe.unpersist() From e6a25a780baed0ce617482882a958ab448ee800e Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 00:23:38 +0000 Subject: [PATCH 13/29] finishing integration tests --- CHANGELOG.md | 1 + doc/integrations/compatibility.md | 4 +- doc/integrations/spark.ipynb | 16 ++-- setup.py | 1 + src/sql/_testing.py | 4 + src/sql/connection/connection.py | 12 ++- src/sql/error_handler.py | 4 + src/sql/run/resultset.py | 11 ++- src/sql/run/sparkdataframe.py | 2 +- src/sql/stats.py | 1 - src/sql/util.py | 2 + src/tests/integration/conftest.py | 40 ++++++++- .../integration/test_generic_db_operations.py | 83 ++++++++++++++++++- src/tests/integration/test_stats.py | 22 ++++- 14 files changed, 182 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69dfdc271..0682b5139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG ## 0.10.6dev +* [Feature] Add Spark Connection as a dialect for Jupysql (#965) * [Fix] Fix error when `%sql` includes a query with negative numbers (#958) ## 0.10.5 (2023-12-11) diff --git a/doc/integrations/compatibility.md b/doc/integrations/compatibility.md index 1771823f9..67d1e9258 100644 --- a/doc/integrations/compatibility.md +++ b/doc/integrations/compatibility.md @@ -120,7 +120,7 @@ These table reflects the compatibility status of JupySQL `>=0.7` - Running queries with `%%sql` ✅ - CTEs with `%%sql --save NAME` ✅ -- Plotting with `%%sqlplot boxplot` ✅ +- Plotting with `%%sqlplot boxplot` ❓ - Plotting with `%%sqlplot bar` ✅ - Plotting with `%%sqlplot pie` ✅ - Plotting with `%%sqlplot histogram` ✅ @@ -130,4 +130,4 @@ These table reflects the compatibility status of JupySQL `>=0.7` - Listing columns with `%sqlcmd columns` ❌ - Parametrized SQL queries via `{{parameter}}` ✅ - Interactive SQL queries via `--interact` ✅ -- Persiting Dataframes via `--persist` ❌ \ No newline at end of file +- Persiting Dataframes via `--persist` ✅ \ No newline at end of file diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index 90e296902..fa9f14c46 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -708,6 +708,7 @@ "cell_type": "code", "execution_count": 33, "metadata": { + "scrolled": true, "tags": [] }, "outputs": [ @@ -731,6 +732,13 @@ "print(query)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Profiling" + ] + }, { "cell_type": "code", "execution_count": 34, @@ -1059,14 +1067,6 @@ "## Plotting" ] }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The %sqlplot magic command currently supported by the spark connection but you can still plot as like this:" - ] - }, { "cell_type": "code", "execution_count": 35, diff --git a/setup.py b/setup.py index 50c5a19ce..e1ceb01d9 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ "polars==0.17.2", # 04/18/23 this breaks our CI "pyarrow", "pyspark", + "grpcio-status", "invoke", "pkgmt", "twine", diff --git a/src/sql/_testing.py b/src/sql/_testing.py index 14a5e9675..041994e28 100644 --- a/src/sql/_testing.py +++ b/src/sql/_testing.py @@ -210,6 +210,10 @@ def get_tmp_dir(): "docker_ct": None, "query": {}, }, + "spark": { + "alias": "SparkSession", + "drivername": "SparkSession", + }, "clickhouse": { "drivername": "clickhouse+native", "username": "username", diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index fb2a0159d..8490fe42f 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1130,9 +1130,15 @@ def connection_sqlalchemy(self): ) def to_table(self, table_name, data_frame, if_exists, index, schema=None): - raise exceptions.NotImplementedError( - "--persist/--persist-replace is not available for Spark connections" - " (only available for SQLAlchemy connections)" + mode = ( + "overwrite" + if if_exists == "replace" + else "append" + if if_exists == "append" + else "error" + ) + self._connection.createDataFrame(data_frame).write.mode(mode).saveAsTable( + f"{schema}.{table_name}" if schema else table_name ) def close(self): diff --git a/src/sql/error_handler.py b/src/sql/error_handler.py index 0eac9292a..ef1def3f5 100644 --- a/src/sql/error_handler.py +++ b/src/sql/error_handler.py @@ -107,8 +107,12 @@ def handle_exception(error, query=None, short_error=True): if util.is_sqlalchemy_error(error) or util.is_non_sqlalchemy_error(error): detailed_message, error_type = _detailed_message_with_error_type(error, query) if short_error: + print("YEY") _raise_error(error, detailed_message, error_type) else: + print("NEH") _display_error_msg_with_trace(error, detailed_message) else: + print("sadge") + print(str(error)) raise error diff --git a/src/sql/run/resultset.py b/src/sql/run/resultset.py index 8451150aa..15a77c775 100644 --- a/src/sql/run/resultset.py +++ b/src/sql/run/resultset.py @@ -434,7 +434,11 @@ def fetchmany(self, size): raise RuntimeError(f"Error running the query: {str(e)}") from e self.mark_fetching_as_done() return - + # spark doesn't support curser + if hasattr(self._sqlaproxy, "dataframe"): + self._results = [] + self._pretty_table.clear() + print(self._conn) self._extend_results(returned) if len(returned) < size: @@ -458,6 +462,9 @@ def fetch_for_repr_if_needed(self): def fetchall(self): if not self._done_fetching(): + if hasattr(self._sqlaproxy, "dataframe"): + self._results = [] + self._pretty_table.clear() self._extend_results(self.sqlaproxy.fetchall()) self.mark_fetching_as_done() @@ -500,6 +507,8 @@ def _convert_to_data_frame( # maybe create accessors in the connection objects? if result_set._conn.is_dbapi_connection: native_connection = result_set.sqlaproxy + elif hasattr(result_set.sqlaproxy, "dataframe"): + return result_set.sqlaproxy.dataframe.toPandas() else: native_connection = result_set._conn._connection.connection diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py index 4ac86b35e..9f8d96bfa 100644 --- a/src/sql/run/sparkdataframe.py +++ b/src/sql/run/sparkdataframe.py @@ -23,7 +23,7 @@ class FakeResultProxy(object): dataframe = None - def __init__(self, dataframe: DataFrame, headers, should_cache): + def __init__(self, dataframe, headers, should_cache): self.dataframe = dataframe self.fetchall = dataframe.collect self.rowcount = dataframe.count diff --git a/src/sql/stats.py b/src/sql/stats.py index 0fc12e87f..b03252154 100644 --- a/src/sql/stats.py +++ b/src/sql/stats.py @@ -45,7 +45,6 @@ def _summary_stats_one_by_one(conn, table, column, with_=None): other = list(conn.execute(query, with_).fetchone()) keys = ["q1", "med", "q3", "mean", "N"] - return {k: float(v) for k, v in zip(keys, percentiles + other)} diff --git a/src/sql/util.py b/src/sql/util.py index b0d9c0c3b..0956ac68c 100644 --- a/src/sql/util.py +++ b/src/sql/util.py @@ -543,6 +543,8 @@ def is_non_sqlalchemy_error(error): "pyodbc.ProgrammingError", # Clickhouse errors "DB::Exception:", + # Pyspark + "UNRESOLVED_ROUTINE", ] return any(msg in str(error) for msg in specific_db_errors) diff --git a/src/tests/integration/conftest.py b/src/tests/integration/conftest.py index d92fe4d70..a07241971 100644 --- a/src/tests/integration/conftest.py +++ b/src/tests/integration/conftest.py @@ -290,10 +290,46 @@ def setup_duckDB_native(test_table_name_dict): @pytest.fixture(scope="session") -def setup_spark(): - spark = SparkSession.Builder.master("local").getOrCreate() +def setup_spark(test_table_name_dict): + import os + import shutil + + os.environ["PYSPARK_PYTHON"] = os.environ.get("CONDA_PYTHON_EXE") + os.environ["PYSPARK_DRIVER_PYTHON"] = os.environ.get("CONDA_PYTHON_EXE") + spark = SparkSession.builder.master("local[1]").enableHiveSupport().getOrCreate() + load_generic_testing_data_spark(spark, test_table_name_dict) yield spark spark.stop() + shutil.rmtree("metastore_db") + shutil.rmtree("spark-warehouse") + os.remove("derby.log") + + +def load_generic_testing_data_spark(spark: SparkSession, test_table_name_dict): + spark.createDataFrame( + pd.DataFrame( + {"taxi_driver_name": ["Eric Ken", "John Smith", "Kevin Kelly"] * 15} + ) + ).createOrReplaceTempView(test_table_name_dict["taxi"]) + spark.createDataFrame( + pd.DataFrame({"x": range(0, 5), "y": range(5, 10)}) + ).createOrReplaceTempView(test_table_name_dict["plot_something"]) + spark.createDataFrame( + pd.DataFrame({"numbers_elements": [1, 2, 3] * 20}) + ).createOrReplaceTempView(test_table_name_dict["numbers"]) + + +@pytest.fixture +def ip_with_spark(ip_empty, setup_spark): + configKey = "spark" + alias = "SparkSession" + + ip_empty.push({"conn": setup_spark}) + # Select database engine, use different sqlite database endpoint + ip_empty.run_cell("%sql " + "conn" + " --alias " + alias) + yield ip_empty + # Disconnect database + ip_empty.run_cell("%sql -x " + alias) def load_generic_testing_data_duckdb_native(ip, test_table_name_dict): diff --git a/src/tests/integration/test_generic_db_operations.py b/src/tests/integration/test_generic_db_operations.py index 47325ed16..2255c4485 100644 --- a/src/tests/integration/test_generic_db_operations.py +++ b/src/tests/integration/test_generic_db_operations.py @@ -21,6 +21,7 @@ "ip_with_Snowflake", "ip_with_oracle", "ip_with_clickhouse", + "ip_with_spark", ] @@ -54,6 +55,7 @@ def mock_log_api(monkeypatch): ("ip_with_clickhouse", "", "LIMIT 3"), ("ip_with_oracle", "", "FETCH FIRST 3 ROWS ONLY"), ("ip_with_MSSQL", "TOP 3", ""), + ("ip_with_spark", "", "LIMIT 3"), ], ) def test_run_query( @@ -93,6 +95,7 @@ def test_run_query( "ip_with_Snowflake", "ip_with_redshift", "ip_with_clickhouse", + "ip_with_spark", ], ) def test_handle_multiple_open_result_sets( @@ -151,6 +154,7 @@ def test_handle_multiple_open_result_sets( "No engine for table " ), ), + ("ip_with_spark", "--no-index"), ], ) def test_create_table_with_indexed_df( @@ -218,6 +222,7 @@ def get_connection_count(ip_with_dynamic_db): ("ip_with_MSSQL", 1), ("ip_with_Snowflake", 1), ("ip_with_clickhouse", 1), + ("ip_with_spark", 1), ], ) def test_active_connection_number(ip_with_dynamic_db, expected, request): @@ -273,6 +278,7 @@ def test_close_and_connect( ("ip_with_Snowflake", "snowflake", "snowflake"), ("ip_with_oracle", "oracle", "oracledb"), ("ip_with_clickhouse", "clickhouse", "native"), + ("ip_with_spark", "spark", "SparkSession"), ], ) def test_telemetry_execute_command_has_connection_info( @@ -337,6 +343,7 @@ def test_telemetry_execute_command_has_connection_info( ("ip_with_Snowflake"), ("ip_with_duckDB_native"), ("ip_with_redshift"), + ("ip_with_spark"), pytest.param( "ip_with_MSSQL", marks=pytest.mark.xfail(reason="sqlglot does not support SQL server"), @@ -419,6 +426,9 @@ def test_sqlplot_histogram(ip_with_dynamic_db, cell, request, test_table_name_di reason="Plotting from snippet not working in clickhouse" ), ), + pytest.param( + "ip_with_spark", marks=pytest.mark.xfail(reason=BOX_PLOT_FAIL_REASON) + ), ], ) def test_sqlplot_boxplot(ip_with_dynamic_db, cell, request, test_table_name_dict): @@ -442,6 +452,7 @@ def test_sqlplot_boxplot(ip_with_dynamic_db, cell, request, test_table_name_dict "ip_with_duckDB", "ip_with_redshift", "ip_with_MSSQL", + "ip_with_spark", ], ) def test_sqlplot_bar(ip_with_dynamic_db, request, test_table_name_dict): @@ -464,7 +475,13 @@ def test_sqlplot_bar(ip_with_dynamic_db, request, test_table_name_dict): @pytest.mark.parametrize( "ip_with_dynamic_db", - ["ip_with_postgreSQL", "ip_with_duckDB", "ip_with_redshift", "ip_with_MSSQL"], + [ + "ip_with_postgreSQL", + "ip_with_duckDB", + "ip_with_redshift", + "ip_with_MSSQL", + "ip_with_spark", + ], ) def test_sqlplot_pie(ip_with_dynamic_db, request, test_table_name_dict): plt.cla() @@ -517,6 +534,10 @@ def test_sqlplot_pie(ip_with_dynamic_db, request, test_table_name_dict): reason="Plotting from snippet not working in clickhouse" ), ), + pytest.param( + "ip_with_spark", + marks=pytest.mark.xfail(reason="Schema not implemented"), + ), ], ) def test_sqlplot_using_schema(ip_with_dynamic_db, request): @@ -569,6 +590,10 @@ def test_sqlplot_using_schema(ip_with_dynamic_db, request): ("ip_with_Snowflake"), ("ip_with_oracle"), ("ip_with_clickhouse"), + pytest.param( + "ip_with_spark", + marks=pytest.mark.xfail(reason="not supported yet for sparkconnections"), + ), ], ) def test_sqlcmd_test(ip_with_dynamic_db, request, test_table_name_dict): @@ -604,6 +629,7 @@ def test_sqlcmd_test(ip_with_dynamic_db, request, test_table_name_dict): ), ("ip_with_oracle"), ("ip_with_clickhouse"), + ("ip_with_spark"), ], ) def test_profile_data_mismatch(ip_with_dynamic_db, request, capsys): @@ -786,6 +812,25 @@ def test_profile_data_mismatch(ip_with_dynamic_db, request, capsys): }, "Following statistics are not available in", ), + ( + "ip_with_spark", + "taxi", + ["taxi_driver_name"], + { + "count": [45], + "mean": [math.nan], + "min": ["Eric Ken"], + "max": ["Kevin Kelly"], + "unique": [3], + "freq": [15], + "top": ["Eric Ken"], + "std": [math.nan], + "25%": [math.nan], + "50%": [math.nan], + "75%": [math.nan], + }, + None, + ), ], ) def test_sqlcmd_profile( @@ -847,6 +892,10 @@ def test_sqlcmd_profile( ("ip_with_MSSQL"), ("ip_with_Snowflake"), ("ip_with_clickhouse"), + pytest.param( + "ip_with_spark", + marks=pytest.mark.xfail(reason="Not Implemented"), + ), ], ) def test_sqlcmd_columns(ip_with_dynamic_db, table, request, test_table_name_dict): @@ -873,6 +922,10 @@ def test_sqlcmd_columns(ip_with_dynamic_db, table, request, test_table_name_dict ("ip_with_MSSQL"), ("ip_with_Snowflake"), ("ip_with_clickhouse"), + pytest.param( + "ip_with_spark", + marks=pytest.mark.xfail(reason="Not Implemented"), + ), ], ) def test_sqlcmd_tables(ip_with_dynamic_db, request): @@ -927,6 +980,7 @@ def test_sql_query(ip_with_dynamic_db, cell, request, test_table_name_dict): "ip_with_Snowflake", "ip_with_oracle", "ip_with_clickhouse", + "ip_with_spark", ], ) def test_sql_query_cte(ip_with_dynamic_db, request, test_table_name_dict, cell): @@ -957,6 +1011,10 @@ def test_sql_query_cte(ip_with_dynamic_db, request, test_table_name_dict, cell): "ip_with_clickhouse", marks=pytest.mark.xfail(reason="Not yet implemented"), ), + pytest.param( + "ip_with_spark", + marks=pytest.mark.xfail(reason="Not yet implemented"), + ), ], ) def test_sql_error_suggests_using_cte(ip_with_dynamic_db, request): @@ -987,6 +1045,7 @@ def test_sql_error_suggests_using_cte(ip_with_dynamic_db, request): "ip_with_MSSQL", "ip_with_oracle", "ip_with_clickhouse", + "ip_with_spark", ], ) def test_results_sets_are_closed(ip_with_dynamic_db, request, test_table_name_dict): @@ -1024,6 +1083,7 @@ def test_results_sets_are_closed(ip_with_dynamic_db, request, test_table_name_di "ip_with_MSSQL", "ip_with_oracle", "ip_with_clickhouse", + "ip_with_spark", ], ) @pytest.mark.parametrize( @@ -1150,6 +1210,7 @@ def test_autocommit_retrieve_existing_resultssets_duckdb_from( CREATE_TABLE, marks=pytest.mark.xfail(reason="Not working yet"), ), + ("ip_with_spark", CREATE_TABLE), ], ) def test_autocommit_create_table_single_cell( @@ -1222,6 +1283,7 @@ def test_autocommit_create_table_single_cell( CREATE_TABLE, marks=pytest.mark.xfail(reason="Not working yet"), ), + ("ip_with_spark", CREATE_TABLE), ], ) def test_autocommit_create_table_multiple_cells( @@ -1408,6 +1470,20 @@ def test_autocommit_create_table_multiple_cells( ["Table with name mysnip does not exist!"], "RuntimeError", ), + ( + "ip_with_spark", + "mysnippet", + [ + "Cannot resolve function `not_a_function` on search path", + ], + "RuntimeError", + ), + ( + "ip_with_spark", + "mysnip", + ["Cannot resolve function `not_a_function` on search path"], + "RuntimeError", + ), ], ids=[ "no-typo-postgreSQL", @@ -1428,6 +1504,8 @@ def test_autocommit_create_table_multiple_cells( "with-typo-redshift", "no-typo-duckDB-native", "with-typo-duckDB-native", + "no-typo-spark", + "with-typo-spark", ], ) def test_query_snippet_invalid_function_error_message( @@ -1456,7 +1534,7 @@ def test_query_snippet_invalid_function_error_message( # Save result and test error message result_error = excinfo.value.error_type result_msg = str(excinfo.value) - + print(result_msg) assert error_type == result_error assert all(msg in result_msg for msg in error_msgs) @@ -1502,6 +1580,7 @@ def test_query_snippet_invalid_function_error_message( "No engine for table " ), ), + ("ip_with_spark", "--no-index"), ], ) def test_persist_in_schema(ip_with_dynamic_db, args, request, test_table_name_dict): diff --git a/src/tests/integration/test_stats.py b/src/tests/integration/test_stats.py index d8f93439d..01593f8ce 100644 --- a/src/tests/integration/test_stats.py +++ b/src/tests/integration/test_stats.py @@ -1,7 +1,7 @@ import pytest from sql.stats import _summary_stats -from sql.connection import SQLAlchemyConnection +from sql.connection import SQLAlchemyConnection, SparkConnectConnection @pytest.mark.parametrize( @@ -26,3 +26,23 @@ def test_summary_stats(fixture_name, request, test_table_name_dict): "mean": 2.0, "N": 5.0, } + + +@pytest.mark.parametrize( + "fixture_name", + [ + "setup_spark", + ], +) +def test_summary_stats(fixture_name, request, test_table_name_dict): + conn = SparkConnectConnection(request.getfixturevalue(fixture_name)) + table = test_table_name_dict["plot_something"] + column = "x" + + assert _summary_stats(conn, table, column) == { + "q1": 1.0, + "med": 2.0, + "q3": 3.0, + "mean": 2.0, + "N": 5.0, + } From a9716008146949d894d8978929386a5eaaf71674 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 00:32:44 +0000 Subject: [PATCH 14/29] pass config and alias correctly --- src/sql/connection/connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 8490fe42f..c68c5acea 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -267,7 +267,9 @@ def set( elif is_pep249_compliant(descriptor): cls.current = DBAPIConnection(descriptor, config=config, alias=alias) elif is_spark(descriptor): - cls.current = SparkConnectConnection(descriptor) + cls.current = SparkConnectConnection( + descriptor, config=config, alias=alias + ) else: existing = rough_dict_get(cls.connections, descriptor) if existing and existing.alias == alias: From a64b2a314626f0f307195c1d93a3793602188993 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 01:42:21 +0000 Subject: [PATCH 15/29] fixed issue with backticks and also implemented fake cursor --- src/sql/connection/connection.py | 2 +- src/sql/error_handler.py | 1 + src/sql/plot.py | 9 ++++++++- src/sql/run/sparkdataframe.py | 7 +++++++ src/sql/util.py | 1 + .../integration/test_generic_db_operations.py | 17 ++++------------- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index c68c5acea..cfdb85c8d 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1099,7 +1099,7 @@ def __init__(self, payload, connection, alias=None, config=None): @property def dialect(self): """Returns a string with the SQL dialect name""" - return "spark" + return "spark2" def raw_execute(self, query, parameters=None): """Run the query without any pre-processing""" diff --git a/src/sql/error_handler.py b/src/sql/error_handler.py index ef1def3f5..7792ecbd9 100644 --- a/src/sql/error_handler.py +++ b/src/sql/error_handler.py @@ -50,6 +50,7 @@ def _detailed_message_with_error_type(error, query): "error in your sql syntax", "incorrect syntax", "invalid sql", + "syntax_error", ] not_found_substrings = [ r"(\btable with name\b).+(\bdoes not exist\b)", diff --git a/src/sql/plot.py b/src/sql/plot.py index d61f3d2f6..b6e8f9ca4 100644 --- a/src/sql/plot.py +++ b/src/sql/plot.py @@ -268,7 +268,7 @@ def _min_max(conn, table, column, with_=None, use_backticks=False): """ if use_backticks: template_ = template_.replace('"', "`") - + table = table.replace('"', "`") template = Template(template_) query = template.render(table=table, column=column) y = conn.execute(query, with_) @@ -628,6 +628,7 @@ def _histogram( if use_backticks: template_ = template_.replace('"', "`") + table = table.replace('"', "`") template = Template(template_) @@ -663,6 +664,7 @@ def _histogram( if use_backticks: template_ = template_.replace('"', "`") + table = table.replace('"', "`") template = Template(template_) @@ -681,6 +683,7 @@ def _histogram( if use_backticks: template_ = template_.replace('"', "`") + table = table.replace('"', "`") template = Template(template_) @@ -835,6 +838,7 @@ def _bar(table, column, with_=None, conn=None): if use_backticks: template_ = template_.replace('"', "`") + table = table.replace('"', "`") template = Template(template_) query = template.render(table=table, x_=x_, height_=height_) @@ -854,6 +858,7 @@ def _bar(table, column, with_=None, conn=None): if use_backticks: template_ = template_.replace('"', "`") + table = table.replace('"', "`") template = Template(template_) query = template.render(table=table, column=column) @@ -1022,6 +1027,7 @@ def _pie(table, column, with_=None, conn=None): """ if use_backticks: template_ = template_.replace('"', "`") + table = table.replace('"', "`") template = Template(template_) query = template.render(table=table, labels_=labels_, size_=size_) @@ -1037,6 +1043,7 @@ def _pie(table, column, with_=None, conn=None): """ if use_backticks: template_ = template_.replace('"', "`") + table = table.replace('"', "`") template = Template(template_) query = template.render(table=table, column=column) diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py index 9f8d96bfa..8d4df5696 100644 --- a/src/sql/run/sparkdataframe.py +++ b/src/sql/run/sparkdataframe.py @@ -28,6 +28,7 @@ def __init__(self, dataframe, headers, should_cache): self.fetchall = dataframe.collect self.rowcount = dataframe.count self.keys = lambda: headers + self.cursor = FakeCursor(headers) self.returns_rows = True if should_cache: self.dataframe.cache() @@ -40,3 +41,9 @@ def fetchone(self): def close(self): self.dataframe.unpersist() + +class FakeCursor(object): + description = None + + def __init__(self,headers) -> None: + self.description = headers \ No newline at end of file diff --git a/src/sql/util.py b/src/sql/util.py index 0956ac68c..4f7fd6d4f 100644 --- a/src/sql/util.py +++ b/src/sql/util.py @@ -545,6 +545,7 @@ def is_non_sqlalchemy_error(error): "DB::Exception:", # Pyspark "UNRESOLVED_ROUTINE", + "PARSE_SYNTAX_ERROR", ] return any(msg in str(error) for msg in specific_db_errors) diff --git a/src/tests/integration/test_generic_db_operations.py b/src/tests/integration/test_generic_db_operations.py index 2255c4485..3bebaecd1 100644 --- a/src/tests/integration/test_generic_db_operations.py +++ b/src/tests/integration/test_generic_db_operations.py @@ -278,7 +278,7 @@ def test_close_and_connect( ("ip_with_Snowflake", "snowflake", "snowflake"), ("ip_with_oracle", "oracle", "oracledb"), ("ip_with_clickhouse", "clickhouse", "native"), - ("ip_with_spark", "spark", "SparkSession"), + ("ip_with_spark", "spark2", "SparkSession"), ], ) def test_telemetry_execute_command_has_connection_info( @@ -534,10 +534,7 @@ def test_sqlplot_pie(ip_with_dynamic_db, request, test_table_name_dict): reason="Plotting from snippet not working in clickhouse" ), ), - pytest.param( - "ip_with_spark", - marks=pytest.mark.xfail(reason="Schema not implemented"), - ), + "ip_with_spark" ], ) def test_sqlplot_using_schema(ip_with_dynamic_db, request): @@ -590,10 +587,7 @@ def test_sqlplot_using_schema(ip_with_dynamic_db, request): ("ip_with_Snowflake"), ("ip_with_oracle"), ("ip_with_clickhouse"), - pytest.param( - "ip_with_spark", - marks=pytest.mark.xfail(reason="not supported yet for sparkconnections"), - ), + ("ip_with_spark"), ], ) def test_sqlcmd_test(ip_with_dynamic_db, request, test_table_name_dict): @@ -1011,10 +1005,7 @@ def test_sql_query_cte(ip_with_dynamic_db, request, test_table_name_dict, cell): "ip_with_clickhouse", marks=pytest.mark.xfail(reason="Not yet implemented"), ), - pytest.param( - "ip_with_spark", - marks=pytest.mark.xfail(reason="Not yet implemented"), - ), + "ip_with_spark", ], ) def test_sql_error_suggests_using_cte(ip_with_dynamic_db, request): From 0a9b4de3b019cc55f884823b542fd7a9c831e052 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 09:17:35 +0000 Subject: [PATCH 16/29] change configuration name --- doc/integrations/spark.ipynb | 4 ++-- src/sql/magic.py | 8 ++++---- src/sql/run/run.py | 2 +- src/sql/run/sparkdataframe.py | 7 ++++--- src/tests/integration/test_generic_db_operations.py | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index fa9f14c46..99e121c18 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -276,7 +276,7 @@ "metadata": {}, "outputs": [], "source": [ - "%config SqlMagic.lazy_spark = True" + "%config SqlMagic.lazy_execution = True" ] }, { @@ -327,7 +327,7 @@ "metadata": {}, "outputs": [], "source": [ - "%config SqlMagic.lazy_spark = False" + "%config SqlMagic.lazy_execution = False" ] }, { diff --git a/src/sql/magic.py b/src/sql/magic.py index 32d231d6b..eddb344c6 100644 --- a/src/sql/magic.py +++ b/src/sql/magic.py @@ -147,13 +147,13 @@ class SqlMagic(Magics, Configurable): config=True, help="Verbosity level. 0=minimal, 1=normal, 2=all", ) - lazy_spark = Bool( + lazy_execution = Bool( default_value=False, config=True, help="Whether to evalute using ResultSet which will " - "cause the plan to execute or just return a Spark " - "DataFrame plan allowing lazy execution but still " - "validating schemas.", + "cause the plan to execute or just return a lazily " + "executed plan allowing validating schemas, " + "without expensive compute.", ) named_parameters = Bool( default_value=False, diff --git a/src/sql/run/run.py b/src/sql/run/run.py index 3b3c2c719..f9b1fd964 100644 --- a/src/sql/run/run.py +++ b/src/sql/run/run.py @@ -52,7 +52,7 @@ def run_statements(conn, sql, config, parameters=None): if is_spark(conn.dialect): result = conn.raw_execute(statement, parameters=parameters) - if config.lazy_spark: + if config.lazy_execution: return conn.raw_execute(statement, parameters=parameters).dataframe # regular query diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py index 8d4df5696..77ef31854 100644 --- a/src/sql/run/sparkdataframe.py +++ b/src/sql/run/sparkdataframe.py @@ -42,8 +42,9 @@ def fetchone(self): def close(self): self.dataframe.unpersist() + class FakeCursor(object): - description = None + description = None - def __init__(self,headers) -> None: - self.description = headers \ No newline at end of file + def __init__(self, headers) -> None: + self.description = headers diff --git a/src/tests/integration/test_generic_db_operations.py b/src/tests/integration/test_generic_db_operations.py index 3bebaecd1..95722758a 100644 --- a/src/tests/integration/test_generic_db_operations.py +++ b/src/tests/integration/test_generic_db_operations.py @@ -534,7 +534,7 @@ def test_sqlplot_pie(ip_with_dynamic_db, request, test_table_name_dict): reason="Plotting from snippet not working in clickhouse" ), ), - "ip_with_spark" + "ip_with_spark", ], ) def test_sqlplot_using_schema(ip_with_dynamic_db, request): From 189e4a0f6e1c9f06733344d9ad526cadff16d5f9 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 10:06:34 +0000 Subject: [PATCH 17/29] fix env variable error integration tests CI --- doc/api/configuration.md | 16 ++++++++++++++++ doc/integrations/spark.ipynb | 2 +- src/tests/integration/conftest.py | 5 +++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/doc/api/configuration.md b/doc/api/configuration.md index e2bb114a5..b7945683e 100644 --- a/doc/api/configuration.md +++ b/doc/api/configuration.md @@ -234,6 +234,22 @@ value enables the ones from previous values plus new ones: - `2`: All feedback - Footer to distinguish pandas/polars data frames from JupySQL's result sets +## `lazy_execution` + +Default: `False` + +Return lazy relation to dataset rather than executing through JupySql. + +```{code-cell} ipython3 +%config SqlMagic.lazy_execution = True +df = %sql SELECT * FROM languages +``` + +```{code-cell} ipython3 +%config SqlMagic.lazy_execution = False +res = %sql SELECT * FROM languages +``` + ## `named_parameters` ```{versionadded} 0.9 diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index 99e121c18..01940eb0f 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -1221,7 +1221,7 @@ "metadata": {}, "outputs": [], "source": [ - "from sql.ggplot import ggplot, aes, geom_boxplot, geom_histogram, facet_wrap" + "from sql.ggplot import ggplot, aes, geom_histogram" ] }, { diff --git a/src/tests/integration/conftest.py b/src/tests/integration/conftest.py index a07241971..0eb81f2b3 100644 --- a/src/tests/integration/conftest.py +++ b/src/tests/integration/conftest.py @@ -293,9 +293,10 @@ def setup_duckDB_native(test_table_name_dict): def setup_spark(test_table_name_dict): import os import shutil + import sys - os.environ["PYSPARK_PYTHON"] = os.environ.get("CONDA_PYTHON_EXE") - os.environ["PYSPARK_DRIVER_PYTHON"] = os.environ.get("CONDA_PYTHON_EXE") + os.environ["PYSPARK_PYTHON"] = sys.executable + os.environ["PYSPARK_DRIVER_PYTHON"] = sys.executable spark = SparkSession.builder.master("local[1]").enableHiveSupport().getOrCreate() load_generic_testing_data_spark(spark, test_table_name_dict) yield spark From dc5e208c51b7496f429c2ca1b8612add66bf0dd8 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 10:18:17 +0000 Subject: [PATCH 18/29] fixing lint errors --- src/sql/connection/__init__.py | 2 ++ src/sql/connection/connection.py | 3 ++- src/sql/plot.py | 1 - src/sql/run/run.py | 1 - src/tests/integration/conftest.py | 5 ++--- src/tests/integration/test_stats.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sql/connection/__init__.py b/src/sql/connection/__init__.py index 259b549d4..4d9dfb10a 100644 --- a/src/sql/connection/__init__.py +++ b/src/sql/connection/__init__.py @@ -16,7 +16,9 @@ "ConnectionManager", "SQLAlchemyConnection", "DBAPIConnection", + "SparkConnectConnection", "is_pep249_compliant", + "is_spark", "PLOOMBER_DOCS_LINK_STR", "default_alias_for_engine", "ResultSetCollection", diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index cfdb85c8d..9f4495408 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1144,7 +1144,8 @@ def to_table(self, table_name, data_frame, if_exists, index, schema=None): ) def close(self): - """Override of the abstract close as SparkSession is usually shared with pyspark""" + """Override of the abstract close as SparkSession is usually + shared with pyspark""" pass diff --git a/src/sql/plot.py b/src/sql/plot.py index b6e8f9ca4..d3cecb759 100644 --- a/src/sql/plot.py +++ b/src/sql/plot.py @@ -271,7 +271,6 @@ def _min_max(conn, table, column, with_=None, use_backticks=False): table = table.replace('"', "`") template = Template(template_) query = template.render(table=table, column=column) - y = conn.execute(query, with_) min_, max_ = conn.execute(query, with_).fetchone() return min_, max_ diff --git a/src/sql/run/run.py b/src/sql/run/run.py index f9b1fd964..7dcd5049e 100644 --- a/src/sql/run/run.py +++ b/src/sql/run/run.py @@ -3,7 +3,6 @@ from sql import exceptions, display from sql.run.resultset import ResultSet from sql.run.pgspecial import handle_postgres_special -from sql.run.sparkdataframe import handle_spark_dataframe # TODO: conn also has access to config, we should clean this up to provide a clean diff --git a/src/tests/integration/conftest.py b/src/tests/integration/conftest.py index 0eb81f2b3..cad899583 100644 --- a/src/tests/integration/conftest.py +++ b/src/tests/integration/conftest.py @@ -301,8 +301,8 @@ def setup_spark(test_table_name_dict): load_generic_testing_data_spark(spark, test_table_name_dict) yield spark spark.stop() - shutil.rmtree("metastore_db") - shutil.rmtree("spark-warehouse") + shutil.rmtree("metastore_db", ignore_errors=True) + shutil.rmtree("spark-warehouse", ignore_errors=True) os.remove("derby.log") @@ -322,7 +322,6 @@ def load_generic_testing_data_spark(spark: SparkSession, test_table_name_dict): @pytest.fixture def ip_with_spark(ip_empty, setup_spark): - configKey = "spark" alias = "SparkSession" ip_empty.push({"conn": setup_spark}) diff --git a/src/tests/integration/test_stats.py b/src/tests/integration/test_stats.py index 01593f8ce..fe33b18f5 100644 --- a/src/tests/integration/test_stats.py +++ b/src/tests/integration/test_stats.py @@ -34,7 +34,7 @@ def test_summary_stats(fixture_name, request, test_table_name_dict): "setup_spark", ], ) -def test_summary_stats(fixture_name, request, test_table_name_dict): +def test_summary_stats_spark(fixture_name, request, test_table_name_dict): conn = SparkConnectConnection(request.getfixturevalue(fixture_name)) table = test_table_name_dict["plot_something"] column = "x" From a4ef89b98c09c58e46ad9e9e3e3fda8c12513879 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 10:29:28 +0000 Subject: [PATCH 19/29] change log formating --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0682b5139..a2113753f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # CHANGELOG ## 0.10.6dev -* [Feature] Add Spark Connection as a dialect for Jupysql (#965) +* [Feature] Add Spark Connection as a dialect for Jupysql [#965](https://github.com/ploomber/jupysql/issues/965) +(by [@gilandose](https://github.com/gilandose)) * [Fix] Fix error when `%sql` includes a query with negative numbers (#958) ## 0.10.5 (2023-12-11) From 45e7b6c363c051b2b9c8ed9a5ae3309a9b304e66 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Wed, 20 Dec 2023 11:01:41 +0000 Subject: [PATCH 20/29] metadata ipynb --- doc/integrations/spark.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/integrations/spark.ipynb b/doc/integrations/spark.ipynb index 01940eb0f..4f150500d 100644 --- a/doc/integrations/spark.ipynb +++ b/doc/integrations/spark.ipynb @@ -1376,8 +1376,8 @@ }, "myst": { "html_meta": { - "description lang=en": "Query a PostgreSQL database from Jupyter via JupySQL", - "keywords": "jupyter, sql, jupysql, postgres", + "description lang=en": "Query using Spark SQL from Jupyter via JupySQL", + "keywords": "jupyter, sql, jupysql, spark", "property=og:locale": "en_US" } }, From 96173de1ae3f3535c328ab262861679192f6f127 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Thu, 21 Dec 2023 17:13:34 +0000 Subject: [PATCH 21/29] addressing comments --- doc/integrations/compatibility.md | 2 +- src/sql/error_handler.py | 3 --- src/sql/magic.py | 2 +- src/sql/run/resultset.py | 2 +- src/sql/run/run.py | 7 ++----- src/sql/run/sparkdataframe.py | 10 ++++++---- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/doc/integrations/compatibility.md b/doc/integrations/compatibility.md index 67d1e9258..d59760a98 100644 --- a/doc/integrations/compatibility.md +++ b/doc/integrations/compatibility.md @@ -130,4 +130,4 @@ These table reflects the compatibility status of JupySQL `>=0.7` - Listing columns with `%sqlcmd columns` ❌ - Parametrized SQL queries via `{{parameter}}` ✅ - Interactive SQL queries via `--interact` ✅ -- Persiting Dataframes via `--persist` ✅ \ No newline at end of file +- Persisting Dataframes via `--persist` ✅ \ No newline at end of file diff --git a/src/sql/error_handler.py b/src/sql/error_handler.py index 7792ecbd9..cedc7ade7 100644 --- a/src/sql/error_handler.py +++ b/src/sql/error_handler.py @@ -108,12 +108,9 @@ def handle_exception(error, query=None, short_error=True): if util.is_sqlalchemy_error(error) or util.is_non_sqlalchemy_error(error): detailed_message, error_type = _detailed_message_with_error_type(error, query) if short_error: - print("YEY") _raise_error(error, detailed_message, error_type) else: - print("NEH") _display_error_msg_with_trace(error, detailed_message) else: - print("sadge") print(str(error)) raise error diff --git a/src/sql/magic.py b/src/sql/magic.py index eddb344c6..ac49bbeba 100644 --- a/src/sql/magic.py +++ b/src/sql/magic.py @@ -150,7 +150,7 @@ class SqlMagic(Magics, Configurable): lazy_execution = Bool( default_value=False, config=True, - help="Whether to evalute using ResultSet which will " + help="Whether to evaluate using ResultSet which will " "cause the plan to execute or just return a lazily " "executed plan allowing validating schemas, " "without expensive compute.", diff --git a/src/sql/run/resultset.py b/src/sql/run/resultset.py index 15a77c775..e1e55deeb 100644 --- a/src/sql/run/resultset.py +++ b/src/sql/run/resultset.py @@ -434,7 +434,7 @@ def fetchmany(self, size): raise RuntimeError(f"Error running the query: {str(e)}") from e self.mark_fetching_as_done() return - # spark doesn't support curser + # spark doesn't support cursor if hasattr(self._sqlaproxy, "dataframe"): self._results = [] self._pretty_table.clear() diff --git a/src/sql/run/run.py b/src/sql/run/run.py index 7dcd5049e..a1e34aa7d 100644 --- a/src/sql/run/run.py +++ b/src/sql/run/run.py @@ -49,14 +49,11 @@ def run_statements(conn, sql, config, parameters=None): if first_word.startswith("\\") and is_postgres_or_redshift(conn.dialect): result = handle_postgres_special(conn, statement) - if is_spark(conn.dialect): - result = conn.raw_execute(statement, parameters=parameters) - if config.lazy_execution: - return conn.raw_execute(statement, parameters=parameters).dataframe - # regular query else: result = conn.raw_execute(statement, parameters=parameters) + if is_spark(conn.dialect) and config.lazy_execution: + return result.dataframe if ( config.feedback >= 1 diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py index 77ef31854..ba8765141 100644 --- a/src/sql/run/sparkdataframe.py +++ b/src/sql/run/sparkdataframe.py @@ -13,10 +13,10 @@ def handle_spark_dataframe(dataframe, should_cache=False): if not DataFrame and not CDataFrame: raise exceptions.MissingPackageError("pysark not installed") - return FakeResultProxy(dataframe, dataframe.columns, should_cache) + return SparkResultProxy(dataframe, dataframe.columns, should_cache) -class FakeResultProxy(object): +class SparkResultProxy(object): """A fake class that pretends to behave like the ResultProxy from SqlAlchemy. """ @@ -28,7 +28,7 @@ def __init__(self, dataframe, headers, should_cache): self.fetchall = dataframe.collect self.rowcount = dataframe.count self.keys = lambda: headers - self.cursor = FakeCursor(headers) + self.cursor = SparkCuror(headers) self.returns_rows = True if should_cache: self.dataframe.cache() @@ -43,7 +43,9 @@ def close(self): self.dataframe.unpersist() -class FakeCursor(object): +class SparkCuror(object): + """Clas to extend to give SqlAlchemy Cursor like behaviour""" + description = None def __init__(self, headers) -> None: From df5925e43845e9139218006a1637edbd13f0790c Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Thu, 21 Dec 2023 17:21:32 +0000 Subject: [PATCH 22/29] update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2113753f..6574bcd82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # CHANGELOG -## 0.10.6dev +## 0.10.7dev + * [Feature] Add Spark Connection as a dialect for Jupysql [#965](https://github.com/ploomber/jupysql/issues/965) (by [@gilandose](https://github.com/gilandose)) + +## 0.10.6 (2023-12-21) + * [Fix] Fix error when `%sql` includes a query with negative numbers (#958) ## 0.10.5 (2023-12-11) From 27dca2b9018e84c42d12ff7c3a1090133cc09540 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Thu, 21 Dec 2023 17:25:49 +0000 Subject: [PATCH 23/29] changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6574bcd82..f533cb816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,12 @@ ## 0.10.7dev -* [Feature] Add Spark Connection as a dialect for Jupysql [#965](https://github.com/ploomber/jupysql/issues/965) +* [Feature] Add Spark Connection as a dialect for Jupysql ([#965](https://github.com/ploomber/jupysql/issues/965)) (by [@gilandose](https://github.com/gilandose)) ## 0.10.6 (2023-12-21) -* [Fix] Fix error when `%sql` includes a query with negative numbers (#958) +* [Fix] Fix error when `%sql` includes a query with negative numbers ([#958](https://github.com/ploomber/jupysql/issues/958)) ## 0.10.5 (2023-12-11) From 743dee85f77a799742381809473eba98f7a09444 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Thu, 21 Dec 2023 20:39:12 +0000 Subject: [PATCH 24/29] fix row count --- src/sql/run/sparkdataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py index ba8765141..5dc497744 100644 --- a/src/sql/run/sparkdataframe.py +++ b/src/sql/run/sparkdataframe.py @@ -26,7 +26,7 @@ class SparkResultProxy(object): def __init__(self, dataframe, headers, should_cache): self.dataframe = dataframe self.fetchall = dataframe.collect - self.rowcount = dataframe.count + self.rowcount = dataframe.count() self.keys = lambda: headers self.cursor = SparkCuror(headers) self.returns_rows = True From ac20efd7a893b8a6c97d83cebe6fd3d6f2ec6a92 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Thu, 21 Dec 2023 20:43:21 +0000 Subject: [PATCH 25/29] spelling --- src/sql/run/sparkdataframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py index 5dc497744..d8f2297ba 100644 --- a/src/sql/run/sparkdataframe.py +++ b/src/sql/run/sparkdataframe.py @@ -44,7 +44,7 @@ def close(self): class SparkCuror(object): - """Clas to extend to give SqlAlchemy Cursor like behaviour""" + """Class to extend to give SqlAlchemy Cursor like behaviour""" description = None From 7bf2b5b2b9a4e4427764ab3a4d4a3dae839cb6e8 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Thu, 21 Dec 2023 20:43:44 +0000 Subject: [PATCH 26/29] spelling --- src/sql/run/sparkdataframe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sql/run/sparkdataframe.py b/src/sql/run/sparkdataframe.py index d8f2297ba..81644b1e2 100644 --- a/src/sql/run/sparkdataframe.py +++ b/src/sql/run/sparkdataframe.py @@ -28,7 +28,7 @@ def __init__(self, dataframe, headers, should_cache): self.fetchall = dataframe.collect self.rowcount = dataframe.count() self.keys = lambda: headers - self.cursor = SparkCuror(headers) + self.cursor = SparkCursor(headers) self.returns_rows = True if should_cache: self.dataframe.cache() @@ -43,7 +43,7 @@ def close(self): self.dataframe.unpersist() -class SparkCuror(object): +class SparkCursor(object): """Class to extend to give SqlAlchemy Cursor like behaviour""" description = None From 36aaa1003f50c75188b6f0cf169d3a7157d53645 Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Thu, 21 Dec 2023 22:15:16 +0000 Subject: [PATCH 27/29] remove pypark dev dependency --- setup.py | 4 ++-- src/sql/connection/connection.py | 29 ++++++++++++++-------- src/tests/test_connection.py | 41 +++++++++++++++++++++++--------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index 7207b88d8..51dfc5cbc 100644 --- a/setup.py +++ b/setup.py @@ -35,8 +35,6 @@ "pandas", # previously pinned to 2.0.3 "polars==0.17.2", # 04/18/23 this breaks our CI "pyarrow", - "pyspark", - "grpcio-status", "invoke", "pkgmt", "twine", @@ -73,6 +71,8 @@ "redshift-connector", "sqlalchemy-redshift", "clickhouse-sqlalchemy", + "pyspark", + "grpcio-status", ] setup( diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 9f4495408..8f7d4758e 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -19,12 +19,6 @@ from sql.run.sparkdataframe import handle_spark_dataframe -try: - from pyspark.sql.connect.session import SparkSession as CSparkSession - from pyspark.sql import SparkSession -except ModuleNotFoundError: - CSparkSession = None - SparkSession = None from IPython.core.error import UsageError import sqlglot import sqlparse @@ -1243,10 +1237,25 @@ def is_pep249_compliant(conn): return True -def is_spark(ins): - return (CSparkSession is not None and isinstance(ins, CSparkSession)) or ( - SparkSession is not None and isinstance(ins, SparkSession) - ) +def is_spark(conn): + """Check if it is a SparkSession by checking for available methods""" + + sparksession_methods = [ + "table", + "read", + "readStream", + "createDataFrame", + "sql", + "stop", + "catalog", + "version", + ] + for method_name in sparksession_methods: + # Checking whether the connection object has the method + if not hasattr(conn, method_name): + return False + + return True def default_alias_for_engine(engine): diff --git a/src/tests/test_connection.py b/src/tests/test_connection.py index bdcb0fa86..4da0c60f5 100644 --- a/src/tests/test_connection.py +++ b/src/tests/test_connection.py @@ -13,7 +13,6 @@ from sqlalchemy.engine import Engine from sqlalchemy import exc -import pyspark from sql.connection import connection as connection_module import sql.connection @@ -44,9 +43,33 @@ def mock_database(monkeypatch, cleanup): monkeypatch.setattr(sqlalchemy, "create_engine", Mock()) -@pytest.fixture -def mock_spark(monkeypatch, cleanup): - monkeypatch.setitem(sys.modules, "pyspark.sql.SparkSession", Mock()) +def mock_sparksession(): + mock = Mock( + spec=[ + "table", + "read", + "readStream", + "createDataFrame", + "sql", + "stop", + "catalog", + "version", + ] + ) + return mock + + +def mock_not_sparksession(): + mock = Mock( + spec=[ + "read", + "readStream", + "createDataFrame", + "sql", + "verison", + ] + ) + return mock @pytest.fixture @@ -471,8 +494,8 @@ def test_is_pep249_compliant(conn, expected): [sqlite3.connect(""), False], [duckdb.connect(""), False], [create_engine("sqlite://"), False], - [Mock(spec=pyspark.sql.SparkSession), True], - [Mock(spec=pyspark.sql.connect.session.SparkSession), True], + [mock_sparksession(), True], + [mock_not_sparksession(), False], [None, False], [object(), False], ["not_a_valid_connection", False], @@ -619,11 +642,7 @@ def test_set_dbapi(monkeypatch, callable_, key): @pytest.mark.parametrize( "spark, key", [ - [Mock(name="SparkSession", spec=pyspark.sql.SparkSession), "Mock"], - [ - Mock(name="SparkSession", spec=pyspark.sql.connect.session.SparkSession), - "Mock", - ], + [mock_sparksession(), "Mock"], ], ) def test_set_spark(monkeypatch, spark, key): From db024fb2149b3b050f6ea0507c4cc36a3da77cfc Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Sat, 23 Dec 2023 20:05:40 +0000 Subject: [PATCH 28/29] review comments --- CHANGELOG.md | 3 +-- doc/api/configuration.md | 4 ++++ noxfile.py | 2 ++ setup.py | 1 + src/sql/error_handler.py | 1 - src/sql/magic.py | 3 ++- src/sql/run/resultset.py | 1 - src/tests/test_connection.py | 3 +-- 8 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f533cb816..a15174f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,7 @@ ## 0.10.7dev -* [Feature] Add Spark Connection as a dialect for Jupysql ([#965](https://github.com/ploomber/jupysql/issues/965)) -(by [@gilandose](https://github.com/gilandose)) +* [Feature] Add Spark Connection as a dialect for Jupysql ([#965](https://github.com/ploomber/jupysql/issues/965)) (by [@gilandose](https://github.com/gilandose)) ## 0.10.6 (2023-12-21) diff --git a/doc/api/configuration.md b/doc/api/configuration.md index b7945683e..254ea712a 100644 --- a/doc/api/configuration.md +++ b/doc/api/configuration.md @@ -236,6 +236,10 @@ value enables the ones from previous values plus new ones: ## `lazy_execution` +```{versionadded} 0.10.7 +This option only works when connecting to Spark +``` + Default: `False` Return lazy relation to dataset rather than executing through JupySql. diff --git a/noxfile.py b/noxfile.py index 92c2b8fb6..5a62c0f26 100644 --- a/noxfile.py +++ b/noxfile.py @@ -35,6 +35,8 @@ "pyodbc==4.0.34", "sqlalchemy-pytds", "python-tds", + "pyspark>=3.4.1", + "grpcio-status", ] diff --git a/setup.py b/setup.py index 51dfc5cbc..1c03419f8 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ "redshift-connector", "sqlalchemy-redshift", "clickhouse-sqlalchemy", + # following two dependencies required for spark "pyspark", "grpcio-status", ] diff --git a/src/sql/error_handler.py b/src/sql/error_handler.py index cedc7ade7..cd1dfb3cd 100644 --- a/src/sql/error_handler.py +++ b/src/sql/error_handler.py @@ -112,5 +112,4 @@ def handle_exception(error, query=None, short_error=True): else: _display_error_msg_with_trace(error, detailed_message) else: - print(str(error)) raise error diff --git a/src/sql/magic.py b/src/sql/magic.py index ac49bbeba..17a2a2a49 100644 --- a/src/sql/magic.py +++ b/src/sql/magic.py @@ -153,7 +153,8 @@ class SqlMagic(Magics, Configurable): help="Whether to evaluate using ResultSet which will " "cause the plan to execute or just return a lazily " "executed plan allowing validating schemas, " - "without expensive compute.", + "without expensive compute." + "Currently only supported for Spark Connection.", ) named_parameters = Bool( default_value=False, diff --git a/src/sql/run/resultset.py b/src/sql/run/resultset.py index e1e55deeb..4b977a8e2 100644 --- a/src/sql/run/resultset.py +++ b/src/sql/run/resultset.py @@ -438,7 +438,6 @@ def fetchmany(self, size): if hasattr(self._sqlaproxy, "dataframe"): self._results = [] self._pretty_table.clear() - print(self._conn) self._extend_results(returned) if len(returned) < size: diff --git a/src/tests/test_connection.py b/src/tests/test_connection.py index 4da0c60f5..8369ff690 100644 --- a/src/tests/test_connection.py +++ b/src/tests/test_connection.py @@ -48,7 +48,6 @@ def mock_sparksession(): spec=[ "table", "read", - "readStream", "createDataFrame", "sql", "stop", @@ -66,7 +65,7 @@ def mock_not_sparksession(): "readStream", "createDataFrame", "sql", - "verison", + "version", ] ) return mock From cfb5431a909f13d8c946c8ef6161aaf35036ac6a Mon Sep 17 00:00:00 2001 From: richard gilmore Date: Sun, 24 Dec 2023 00:40:56 +0000 Subject: [PATCH 29/29] missed readStream in connection.py --- src/sql/connection/connection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sql/connection/connection.py b/src/sql/connection/connection.py index 8f7d4758e..fc4552aaa 100644 --- a/src/sql/connection/connection.py +++ b/src/sql/connection/connection.py @@ -1243,7 +1243,6 @@ def is_spark(conn): sparksession_methods = [ "table", "read", - "readStream", "createDataFrame", "sql", "stop",