From 87cba01f19406980462ee7c6112abf424a162ea1 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:44:10 -0400 Subject: [PATCH] fix(urls): standardize pass-through of parsed query parameters (#9482) This PR cleans up our URL handling a bit. 1. We were doing a bunch of unnecessary url parse -> url unparse, only to immediately again parse the URL as the first call in every `_from_url` implementation. 1. Each backend was separately handling converting query parameters to single scalars where possible, due to the unparsing 1. Query parameters were sometimes passed into `kwargs` and sometimes parsed again to then be used as kwargs later So, I did the following: 1. Pass the `ParseResult` into the `_from_url` implementation. 1. Convert query parameters into single values where possible _before_ calling `backend._from_url` 1. Pass all query parameters as `**kwargs` into the `_from_url` call. Fixes #9456. --- ibis/backends/__init__.py | 42 +++++-------------- ibis/backends/bigquery/__init__.py | 10 ++--- .../bigquery/tests/system/test_client.py | 8 ++++ ibis/backends/clickhouse/__init__.py | 16 ++----- ibis/backends/clickhouse/tests/test_client.py | 15 ++++--- ibis/backends/druid/__init__.py | 20 +++------ ibis/backends/exasol/__init__.py | 18 +++----- ibis/backends/impala/__init__.py | 16 +------ ibis/backends/mysql/__init__.py | 16 ++----- ibis/backends/oracle/__init__.py | 8 ++-- ibis/backends/postgres/__init__.py | 15 ++----- ibis/backends/postgres/tests/test_client.py | 8 ++-- ibis/backends/pyspark/__init__.py | 19 ++------- ibis/backends/snowflake/__init__.py | 19 ++------- ibis/backends/trino/__init__.py | 6 +-- 15 files changed, 71 insertions(+), 165 deletions(-) diff --git a/ibis/backends/__init__.py b/ibis/backends/__init__.py index 710bae5df873..b91b97ae6367 100644 --- a/ibis/backends/__init__.py +++ b/ibis/backends/__init__.py @@ -9,7 +9,6 @@ import urllib.parse from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar -from urllib.parse import parse_qs, urlparse import ibis import ibis.common.exceptions as exc @@ -21,6 +20,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping, MutableMapping + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -1352,6 +1352,11 @@ def connect(resource: Path | str, **kwargs: Any) -> BaseBackend: orig_kwargs = kwargs.copy() kwargs = dict(urllib.parse.parse_qsl(parsed.query)) + # convert single parameter lists value to single values + for name, value in kwargs.items(): + if len(value) == 1: + kwargs[name] = value[0] + if scheme == "file": path = parsed.netloc + parsed.path # Merge explicit kwargs with query string, explicit kwargs @@ -1369,35 +1374,21 @@ def connect(resource: Path | str, **kwargs: Any) -> BaseBackend: else: raise ValueError(f"Don't know how to connect to {resource!r}") - if kwargs: - # If there are kwargs (either explicit or from the query string), - # re-add them to the parsed URL - query = urllib.parse.urlencode(kwargs) - parsed = parsed._replace(query=query) - - if scheme in ("postgres", "postgresql"): - # Treat `postgres://` and `postgresql://` the same - scheme = "postgres" - - # Convert all arguments back to a single URL string - url = parsed.geturl() - if "://" not in url: - # urllib may roundtrip `duckdb://` to `duckdb:`. Here we re-add the - # missing `//`. - url = url.replace(":", "://", 1) + # Treat `postgres://` and `postgresql://` the same + scheme = scheme.replace("postgresql", "postgres") try: backend = getattr(ibis, scheme) except AttributeError: raise ValueError(f"Don't know how to connect to {resource!r}") from None - return backend._from_url(url, **orig_kwargs) + return backend._from_url(parsed, **kwargs) class UrlFromPath: __slots__ = () - def _from_url(self, url: str, **kwargs) -> BaseBackend: + def _from_url(self, url: ParseResult, **kwargs: Any) -> BaseBackend: """Connect to a backend using a URL `url`. Parameters @@ -1413,7 +1404,6 @@ def _from_url(self, url: str, **kwargs) -> BaseBackend: A backend instance """ - url = urlparse(url) netloc = url.netloc parts = list(filter(None, (netloc, url.path[bool(netloc) :]))) database = Path(*parts) if parts and parts != [":memory:"] else ":memory:" @@ -1424,16 +1414,6 @@ def _from_url(self, url: str, **kwargs) -> BaseBackend: elif isinstance(database, Path): database = database.absolute() - query_params = parse_qs(url.query) - - for name, value in query_params.items(): - if len(value) > 1: - kwargs[name] = value - elif len(value) == 1: - kwargs[name] = value[0] - else: - raise exc.IbisError(f"Invalid URL parameter: {name}") - self._convert_kwargs(kwargs) return self.connect(database=database, **kwargs) @@ -1443,7 +1423,7 @@ class NoUrl: name: str - def _from_url(self, url: str, **kwargs) -> BaseBackend: + def _from_url(self, url: ParseResult, **kwargs) -> BaseBackend: """Connect to the backend with empty url. Parameters diff --git a/ibis/backends/bigquery/__init__.py b/ibis/backends/bigquery/__init__.py index 2e09a04185fb..1861d8da25f3 100644 --- a/ibis/backends/bigquery/__init__.py +++ b/ibis/backends/bigquery/__init__.py @@ -8,7 +8,6 @@ import os import re from typing import TYPE_CHECKING, Any, Optional -from urllib.parse import parse_qs, urlparse import google.api_core.exceptions import google.auth.credentials @@ -41,6 +40,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Mapping from pathlib import Path + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -332,12 +332,10 @@ def read_json( ) return self._read_file(path, table_name=table_name, job_config=job_config) - def _from_url(self, url: str, **kwargs): - result = urlparse(url) - params = parse_qs(result.query) + def _from_url(self, url: ParseResult, **kwargs): return self.connect( - project_id=result.netloc or params.get("project_id", [""])[0], - dataset_id=result.path[1:] or params.get("dataset_id", [""])[0], + project_id=url.netloc or kwargs.get("project_id", [""])[0], + dataset_id=url.path[1:] or kwargs.get("dataset_id", [""])[0], **kwargs, ) diff --git a/ibis/backends/bigquery/tests/system/test_client.py b/ibis/backends/bigquery/tests/system/test_client.py index dfb36bf69e61..af3cdbe940d9 100644 --- a/ibis/backends/bigquery/tests/system/test_client.py +++ b/ibis/backends/bigquery/tests/system/test_client.py @@ -3,6 +3,7 @@ import collections import datetime import decimal +from urllib.parse import urlparse import pandas as pd import pandas.testing as tm @@ -428,3 +429,10 @@ def test_table_suffix(): expr = t.filter(t._TABLE_SUFFIX == "1929", t.max != 9999.9).head(1) result = expr.execute() assert not result.empty + + +def test_parameters_in_url_connect(mocker): + spy = mocker.spy(ibis.bigquery, "_from_url") + parsed = urlparse("bigquery://ibis-gbq?location=us-east1") + ibis.connect("bigquery://ibis-gbq?location=us-east1") + spy.assert_called_once_with(parsed, location="us-east1") diff --git a/ibis/backends/clickhouse/__init__.py b/ibis/backends/clickhouse/__init__.py index 8800fe7559b1..e5b773afea0a 100644 --- a/ibis/backends/clickhouse/__init__.py +++ b/ibis/backends/clickhouse/__init__.py @@ -6,7 +6,7 @@ from contextlib import closing from functools import partial from typing import TYPE_CHECKING, Any, Literal -from urllib.parse import parse_qs, unquote_plus, urlparse +from urllib.parse import unquote_plus import clickhouse_connect as cc import pyarrow as pa @@ -32,6 +32,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping from pathlib import Path + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -60,7 +61,7 @@ class Options(ibis.config.Config): bool_type: Literal["Bool", "UInt8", "Int8"] = "Bool" - def _from_url(self, url: str, **kwargs) -> BaseBackend: + def _from_url(self, url: ParseResult, **kwargs) -> BaseBackend: """Connect to a backend using a URL `url`. Parameters @@ -76,25 +77,16 @@ def _from_url(self, url: str, **kwargs) -> BaseBackend: A backend instance """ - url = urlparse(url) database = url.path[1:] - query_params = parse_qs(url.query) connect_args = { "user": url.username, "password": unquote_plus(url.password or ""), "host": url.hostname, "database": database or "", + **kwargs, } - for name, value in query_params.items(): - if len(value) > 1: - connect_args[name] = value - elif len(value) == 1: - connect_args[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") - kwargs.update(connect_args) self._convert_kwargs(kwargs) diff --git a/ibis/backends/clickhouse/tests/test_client.py b/ibis/backends/clickhouse/tests/test_client.py index 427487938704..d9ccf75d1625 100644 --- a/ibis/backends/clickhouse/tests/test_client.py +++ b/ibis/backends/clickhouse/tests/test_client.py @@ -352,11 +352,10 @@ def test_create_table_no_syntax_error(con): def test_password_with_bracket(): password = f'{os.environ.get("IBIS_TEST_CLICKHOUSE_PASSWORD", "")}[' quoted_pass = quote_plus(password) - with pytest.raises(cc.driver.exceptions.DatabaseError) as e: - ibis.clickhouse.connect( - host=os.environ.get("IBIS_TEST_CLICKHOUSE_HOST", "localhost"), - user=os.environ.get("IBIS_TEST_CLICKHOUSE_USER", "default"), - port=int(os.environ.get("IBIS_TEST_CLICKHOUSE_PORT", 8123)), - password=quoted_pass, - ) - assert "password is incorrect" in str(e.value) + host = os.environ.get("IBIS_TEST_CLICKHOUSE_HOST", "localhost") + user = os.environ.get("IBIS_TEST_CLICKHOUSE_USER", "default") + port = int(os.environ.get("IBIS_TEST_CLICKHOUSE_PORT", 8123)) + with pytest.raises( + cc.driver.exceptions.DatabaseError, match="password is incorrect" + ): + ibis.clickhouse.connect(host=host, user=user, port=port, password=quoted_pass) diff --git a/ibis/backends/druid/__init__.py b/ibis/backends/druid/__init__.py index 3b5f80ec1eba..9dd44684e443 100644 --- a/ibis/backends/druid/__init__.py +++ b/ibis/backends/druid/__init__.py @@ -5,12 +5,11 @@ import contextlib import json from typing import TYPE_CHECKING, Any -from urllib.parse import parse_qs, unquote_plus, urlparse +from urllib.parse import unquote_plus import pydruid.db import sqlglot as sg -import ibis.common.exceptions as com import ibis.expr.datatypes as dt import ibis.expr.schema as sch from ibis.backends.druid.compiler import DruidCompiler @@ -20,6 +19,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Mapping + from urllib.parse import ParseResult import pandas as pd import pyarrow as pa @@ -40,7 +40,7 @@ def version(self) -> str: [(version,)] = result.fetchall() return version - def _from_url(self, url: str, **kwargs): + def _from_url(self, url: ParseResult, **kwargs): """Connect to a backend using a URL `url`. Parameters @@ -56,9 +56,6 @@ def _from_url(self, url: str, **kwargs): A backend instance """ - - url = urlparse(url) - query_params = parse_qs(url.query) kwargs = { "user": url.username, "password": unquote_plus(url.password) @@ -67,15 +64,8 @@ def _from_url(self, url: str, **kwargs): "host": url.hostname, "path": url.path, "port": url.port, - } | kwargs - - for name, value in query_params.items(): - if len(value) > 1: - kwargs[name] = value - elif len(value) == 1: - kwargs[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") + **kwargs, + } self._convert_kwargs(kwargs) diff --git a/ibis/backends/exasol/__init__.py b/ibis/backends/exasol/__init__.py index ded786cbabf2..0e781f898ef6 100644 --- a/ibis/backends/exasol/__init__.py +++ b/ibis/backends/exasol/__init__.py @@ -5,7 +5,7 @@ import datetime import re from typing import TYPE_CHECKING, Any -from urllib.parse import parse_qs, unquote_plus, urlparse +from urllib.parse import unquote_plus import pyexasol import sqlglot as sg @@ -25,6 +25,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Mapping + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -99,10 +100,8 @@ def do_connect( with self.begin() as con: con.execute(f"ALTER SESSION SET TIME_ZONE = {timezone!r}") - def _from_url(self, url: str, **kwargs) -> BaseBackend: + def _from_url(self, url: ParseResult, **kwargs) -> BaseBackend: """Construct an ibis backend from a URL.""" - url = urlparse(url) - query_params = parse_qs(url.query) kwargs = { "user": url.username, "password": unquote_plus(url.password) @@ -111,15 +110,8 @@ def _from_url(self, url: str, **kwargs) -> BaseBackend: "schema": url.path[1:] or None, "host": url.hostname, "port": url.port, - } | kwargs - - for name, value in query_params.items(): - if len(value) > 1: - kwargs[name] = value - elif len(value) == 1: - kwargs[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") + **kwargs, + } self._convert_kwargs(kwargs) diff --git a/ibis/backends/impala/__init__.py b/ibis/backends/impala/__init__.py index 65d89abc149a..8f492c2c8f3d 100644 --- a/ibis/backends/impala/__init__.py +++ b/ibis/backends/impala/__init__.py @@ -7,7 +7,6 @@ import os from functools import cached_property from typing import TYPE_CHECKING, Any, Literal -from urllib.parse import parse_qs, urlparse import impala.dbapi as impyla import sqlglot as sg @@ -44,6 +43,7 @@ if TYPE_CHECKING: from collections.abc import Mapping from pathlib import Path + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -67,7 +67,7 @@ class Backend(SQLBackend): supports_in_memory_tables = True - def _from_url(self, url: str, **kwargs: Any) -> Backend: + def _from_url(self, url: ParseResult, **kwargs: Any) -> Backend: """Connect to a backend using a URL `url`. Parameters @@ -83,8 +83,6 @@ def _from_url(self, url: str, **kwargs: Any) -> Backend: A backend instance """ - url = urlparse(url) - for name in ("username", "hostname", "port", "password"): if value := ( getattr(url, name, None) @@ -99,16 +97,6 @@ def _from_url(self, url: str, **kwargs: Any) -> Backend: if database: kwargs["database"] = database - query_params = parse_qs(url.query) - - for name, value in query_params.items(): - if len(value) > 1: - kwargs[name] = value - elif len(value) == 1: - kwargs[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") - self._convert_kwargs(kwargs) return self.connect(**kwargs) diff --git a/ibis/backends/mysql/__init__.py b/ibis/backends/mysql/__init__.py index 4968f2689426..f255dd46e9b9 100644 --- a/ibis/backends/mysql/__init__.py +++ b/ibis/backends/mysql/__init__.py @@ -8,7 +8,7 @@ from functools import cached_property from operator import itemgetter from typing import TYPE_CHECKING, Any -from urllib.parse import parse_qs, unquote_plus, urlparse +from urllib.parse import unquote_plus import numpy as np import pymysql @@ -29,6 +29,7 @@ if TYPE_CHECKING: from collections.abc import Mapping + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -40,7 +41,7 @@ class Backend(SQLBackend, CanCreateDatabase): compiler = MySQLCompiler() supports_create_or_replace = False - def _from_url(self, url: str, **kwargs): + def _from_url(self, url: ParseResult, **kwargs): """Connect to a backend using a URL `url`. Parameters @@ -56,10 +57,7 @@ def _from_url(self, url: str, **kwargs): A backend instance """ - - url = urlparse(url) database, *_ = url.path[1:].split("/", 1) - query_params = parse_qs(url.query) connect_args = { "user": url.username, "password": unquote_plus(url.password or ""), @@ -68,14 +66,6 @@ def _from_url(self, url: str, **kwargs): "port": url.port or None, } - for name, value in query_params.items(): - if len(value) > 1: - connect_args[name] = value - elif len(value) == 1: - connect_args[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") - kwargs.update(connect_args) self._convert_kwargs(kwargs) diff --git a/ibis/backends/oracle/__init__.py b/ibis/backends/oracle/__init__.py index 6ceeedaa2d77..ce4f34765e1c 100644 --- a/ibis/backends/oracle/__init__.py +++ b/ibis/backends/oracle/__init__.py @@ -9,7 +9,7 @@ from functools import cached_property from operator import itemgetter from typing import TYPE_CHECKING, Any -from urllib.parse import unquote_plus, urlparse +from urllib.parse import unquote_plus import numpy as np import oracledb @@ -29,6 +29,8 @@ from ibis.backends.sql.compiler import C if TYPE_CHECKING: + from urllib.parse import ParseResult + import pandas as pd import polars as pl import pyarrow as pa @@ -160,12 +162,12 @@ def do_connect( # Set to ensure decimals come back as decimals oracledb.defaults.fetch_decimals = True - def _from_url(self, url: str, **kwargs): - url = urlparse(url) + def _from_url(self, url: ParseResult, **kwargs): self.do_connect( user=url.username, password=unquote_plus(url.password) if url.password is not None else None, database=url.path.removeprefix("/"), + **kwargs, ) return self diff --git a/ibis/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py index 01c44742ecbe..d12e0a362d55 100644 --- a/ibis/backends/postgres/__init__.py +++ b/ibis/backends/postgres/__init__.py @@ -9,7 +9,7 @@ from itertools import takewhile from operator import itemgetter from typing import TYPE_CHECKING, Any -from urllib.parse import parse_qs, unquote_plus, urlparse +from urllib.parse import unquote_plus import numpy as np import pandas as pd @@ -33,6 +33,7 @@ if TYPE_CHECKING: from collections.abc import Callable + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -50,7 +51,7 @@ class Backend(SQLBackend, CanListCatalog, CanCreateDatabase, CanCreateSchema): compiler = PostgresCompiler() supports_python_udfs = True - def _from_url(self, url: str, **kwargs): + def _from_url(self, url: ParseResult, **kwargs): """Connect to a backend using a URL `url`. Parameters @@ -66,9 +67,7 @@ def _from_url(self, url: str, **kwargs): A backend instance """ - url = urlparse(url) database, *schema = url.path[1:].split("/", 1) - query_params = parse_qs(url.query) connect_args = { "user": url.username, "password": unquote_plus(url.password or ""), @@ -78,14 +77,6 @@ def _from_url(self, url: str, **kwargs): "port": url.port, } - for name, value in query_params.items(): - if len(value) > 1: - connect_args[name] = value - elif len(value) == 1: - connect_args[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") - kwargs.update(connect_args) self._convert_kwargs(kwargs) diff --git a/ibis/backends/postgres/tests/test_client.py b/ibis/backends/postgres/tests/test_client.py index da562d6517a0..ee94731cecf5 100644 --- a/ibis/backends/postgres/tests/test_client.py +++ b/ibis/backends/postgres/tests/test_client.py @@ -385,8 +385,8 @@ def test_password_with_bracket(): password = f"{IBIS_POSTGRES_PASS}[" quoted_pass = quote_plus(password) url = f"postgres://{IBIS_POSTGRES_USER}:{quoted_pass}@{IBIS_POSTGRES_HOST}:{IBIS_POSTGRES_PORT}/{POSTGRES_TEST_DB}" - with pytest.raises(PsycoPg2OperationalError) as e: + with pytest.raises( + PsycoPg2OperationalError, + match=f'password authentication failed for user "{IBIS_POSTGRES_USER}"', + ): ibis.connect(url) - assert f'password authentication failed for user "{IBIS_POSTGRES_USER}"' in str( - e.value - ) diff --git a/ibis/backends/pyspark/__init__.py b/ibis/backends/pyspark/__init__.py index 9d27da440bc9..85507d48a24b 100644 --- a/ibis/backends/pyspark/__init__.py +++ b/ibis/backends/pyspark/__init__.py @@ -31,6 +31,7 @@ if TYPE_CHECKING: from collections.abc import Mapping, Sequence + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -112,23 +113,9 @@ class Options(ibis.config.Config): treat_nan_as_null: bool = False - def _from_url(self, url: str, **kwargs) -> Backend: + def _from_url(self, url: ParseResult, **kwargs) -> Backend: """Construct a PySpark backend from a URL `url`.""" - from urllib.parse import parse_qs, urlparse - - url = urlparse(url) - query_params = parse_qs(url.query) - params = query_params.copy() - - for name, value in query_params.items(): - if len(value) > 1: - params[name] = value - elif len(value) == 1: - params[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") - - conf = SparkConf().setAll(params.items()) + conf = SparkConf().setAll(kwargs.items()) if database := url.path[1:]: conf = conf.set("spark.sql.warehouse.dir", str(Path(database).absolute())) diff --git a/ibis/backends/snowflake/__init__.py b/ibis/backends/snowflake/__init__.py index b78ca89b963a..d3594e38950b 100644 --- a/ibis/backends/snowflake/__init__.py +++ b/ibis/backends/snowflake/__init__.py @@ -16,7 +16,7 @@ from operator import itemgetter from pathlib import Path from typing import TYPE_CHECKING, Any -from urllib.parse import parse_qs, unquote_plus, urlparse +from urllib.parse import unquote_plus from urllib.request import urlretrieve import pyarrow as pa @@ -40,6 +40,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -92,7 +93,7 @@ def _convert_kwargs(self, kwargs): with contextlib.suppress(KeyError): kwargs["account"] = kwargs.pop("host") - def _from_url(self, url: str, **kwargs): + def _from_url(self, url: ParseResult, **kwargs): """Connect to a backend using a URL `url`. Parameters @@ -108,12 +109,9 @@ def _from_url(self, url: str, **kwargs): A backend instance """ - - url = urlparse(url) if url.path: database, schema = url.path[1:].split("/", 1) - query_params = parse_qs(url.query) - (warehouse,) = query_params.pop("warehouse", (None,)) + warehouse = kwargs.pop("warehouse", None) connect_args = { "user": url.username, "password": unquote_plus(url.password or ""), @@ -124,15 +122,6 @@ def _from_url(self, url: str, **kwargs): } else: connect_args = {} - query_params = {} - - for name, value in query_params.items(): - if len(value) > 1: - connect_args[name] = value - elif len(value) == 1: - connect_args[name] = value[0] - else: - raise com.IbisError(f"Invalid URL parameter: {name}") session_parameters = kwargs.setdefault("session_parameters", {}) diff --git a/ibis/backends/trino/__init__.py b/ibis/backends/trino/__init__.py index cf8e69b0be4c..1201d2a34775 100644 --- a/ibis/backends/trino/__init__.py +++ b/ibis/backends/trino/__init__.py @@ -7,7 +7,7 @@ from functools import cached_property from operator import itemgetter from typing import TYPE_CHECKING, Any -from urllib.parse import unquote_plus, urlparse +from urllib.parse import unquote_plus import sqlglot as sg import sqlglot.expressions as sge @@ -25,6 +25,7 @@ if TYPE_CHECKING: from collections.abc import Iterator, Mapping + from urllib.parse import ParseResult import pandas as pd import polars as pl @@ -39,8 +40,7 @@ class Backend(SQLBackend, CanListCatalog, CanCreateDatabase, CanCreateSchema): supports_create_or_replace = False supports_temporary_tables = False - def _from_url(self, url: str, **kwargs): - url = urlparse(url) + def _from_url(self, url: ParseResult, **kwargs): catalog, db = url.path.strip("/").split("/") self.do_connect( user=url.username or None,