From 9af49163ab431c9a58c658c4e1d21e035185b6f2 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Fri, 5 Aug 2022 14:19:40 +0200 Subject: [PATCH 1/2] Add a generic data type converter to the `Cursor` object This will allow converting fetched data from CrateDB data types to Python data types in different ways. Its usage is completely optional. When not used, the feature will not incur any overhead. There is also a `DefaultTypeConverter`, which aims to become a sane default choice when looking at this kind of convenience. It will enable the user to work with native Python data types from the start, for all database types where this makes sense, without needing to establish the required set of default value converters on their own. If the `DefaultTypeConverter` does not fit the user's aims, it is easy to define custom type converters, possibly reusing specific ones from the library. --- CHANGES.txt | 2 + docs/query.rst | 48 +++++++++ pyproject.toml | 5 + src/crate/client/connection.py | 15 ++- src/crate/client/converter.py | 135 ++++++++++++++++++++++++ src/crate/client/cursor.py | 39 ++++++- src/crate/client/doctests/cursor.txt | 75 +++++++++++++- src/crate/client/doctests/http.txt | 3 +- src/crate/client/http.py | 4 +- src/crate/client/test_cursor.py | 148 +++++++++++++++++++++++++++ src/crate/client/test_http.py | 2 +- src/crate/client/test_util.py | 44 ++++++++ src/crate/client/tests.py | 29 +----- 13 files changed, 513 insertions(+), 36 deletions(-) create mode 100644 pyproject.toml create mode 100644 src/crate/client/converter.py create mode 100644 src/crate/client/test_util.py diff --git a/CHANGES.txt b/CHANGES.txt index b22f2a60..5e99d18c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,8 @@ Changes for crate Unreleased ========== +- Added a generic data type converter to the ``Cursor`` object, for converting + fetched data from CrateDB data types to Python data types. 2022/10/10 0.27.2 diff --git a/docs/query.rst b/docs/query.rst index 10c6b42a..b80bcb9c 100644 --- a/docs/query.rst +++ b/docs/query.rst @@ -199,7 +199,55 @@ You can turn this into something more manageable with a `list comprehension`_:: >>> [column[0] for column in cursor.description] ['date', 'datetime_tz', 'datetime_notz', ..., 'nullable_datetime', 'position'] + +Data type conversion +==================== + +The cursor object can optionally convert database types to native Python data +types. There is a default implementation for the CrateDB data types ``IP`` and +``TIMESTAMP`` on behalf of the ``DefaultTypeConverter``. + +:: + + >>> from crate.client.converter import DefaultTypeConverter + >>> from crate.client.cursor import Cursor + >>> cursor = connection.cursor(converter=DefaultTypeConverter()) + + >>> cursor.execute("SELECT datetime_tz, datetime_notz FROM locations ORDER BY name") + + >>> cursor.fetchone() + [datetime.datetime(2022, 7, 18, 18, 10, 36, 758000), datetime.datetime(2022, 7, 18, 18, 10, 36, 758000)] + + +Custom data type conversion +=========================== + +By providing a custom converter instance, you can define your own data type +conversions. For investigating the list of available data types, please either +inspect the ``DataType`` enum, or the documentation about the list of available +`CrateDB data type identifiers for the HTTP interface`_. + +This example creates and applies a simple custom converter for converging +CrateDB's ``BOOLEAN`` type to Python's ``str`` type. It is using a simple +converter function defined as ``lambda``, which assigns ``yes`` for boolean +``True``, and ``no`` otherwise. + +:: + + >>> from crate.client.converter import Converter, DataType + + >>> converter = Converter() + >>> converter.set(DataType.BOOLEAN, lambda value: value is True and "yes" or "no") + >>> cursor = connection.cursor(converter=converter) + + >>> cursor.execute("SELECT flag FROM locations ORDER BY name") + + >>> cursor.fetchone() + ['no'] + + .. _Bulk inserts: https://crate.io/docs/crate/reference/en/latest/interfaces/http.html#bulk-operations +.. _CrateDB data type identifiers for the HTTP interface: https://crate.io/docs/crate/reference/en/latest/interfaces/http.html#column-types .. _Database API: http://www.python.org/dev/peps/pep-0249/ .. _database cursor: https://en.wikipedia.org/wiki/Cursor_(databases) .. _DB API 2.0: http://www.python.org/dev/peps/pep-0249/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2f6fe486 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.mypy] + +# Needed until `mypy-0.990` for `ConverterDefinition` in `converter.py`. +# https://github.com/python/mypy/issues/731#issuecomment-1260976955 +enable_recursive_aliases = true diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index 58059063..84e4ec8e 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -46,6 +46,7 @@ def __init__(self, socket_tcp_keepidle=None, socket_tcp_keepintvl=None, socket_tcp_keepcnt=None, + converter=None, ): """ :param servers: @@ -99,7 +100,13 @@ def __init__(self, Set the ``TCP_KEEPCNT`` socket option, which overrides ``net.ipv4.tcp_keepalive_probes`` kernel setting if ``socket_keepalive`` is ``True``. + :param converter: + (optional, defaults to ``None``) + A `Converter` object to propagate to newly created `Cursor` objects. """ + + self._converter = converter + if client: self.client = client else: @@ -123,12 +130,16 @@ def __init__(self, self.lowest_server_version = self._lowest_server_version() self._closed = False - def cursor(self): + def cursor(self, **kwargs) -> Cursor: """ Return a new Cursor Object using the connection. """ + converter = kwargs.pop("converter", self._converter) if not self._closed: - return Cursor(self) + return Cursor( + connection=self, + converter=converter, + ) else: raise ProgrammingError("Connection closed") diff --git a/src/crate/client/converter.py b/src/crate/client/converter.py new file mode 100644 index 00000000..d0dad3b0 --- /dev/null +++ b/src/crate/client/converter.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8; -*- +# +# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. Crate licenses +# this file to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# However, if you have executed another commercial license agreement +# with Crate these terms will supersede the license and you may use the +# software solely pursuant to the terms of the relevant commercial agreement. +""" +Machinery for converting CrateDB database types to native Python data types. + +https://crate.io/docs/crate/reference/en/latest/interfaces/http.html#column-types +""" +import ipaddress +from copy import deepcopy +from datetime import datetime +from enum import Enum +from typing import Any, Callable, Dict, List, Optional, Union + +ConverterFunction = Callable[[Optional[Any]], Optional[Any]] +ColTypesDefinition = Union[int, List[Union[int, "ColTypesDefinition"]]] + + +def _to_ipaddress(value: Optional[str]) -> Optional[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]: + """ + https://docs.python.org/3/library/ipaddress.html + """ + if value is None: + return None + return ipaddress.ip_address(value) + + +def _to_datetime(value: Optional[float]) -> Optional[datetime]: + """ + https://docs.python.org/3/library/datetime.html + """ + if value is None: + return None + return datetime.utcfromtimestamp(value / 1e3) + + +def _to_default(value: Optional[Any]) -> Optional[Any]: + return value + + +# Symbolic aliases for the numeric data type identifiers defined by the CrateDB HTTP interface. +# https://crate.io/docs/crate/reference/en/latest/interfaces/http.html#column-types +class DataType(Enum): + NULL = 0 + NOT_SUPPORTED = 1 + CHAR = 2 + BOOLEAN = 3 + TEXT = 4 + IP = 5 + DOUBLE = 6 + REAL = 7 + SMALLINT = 8 + INTEGER = 9 + BIGINT = 10 + TIMESTAMP_WITH_TZ = 11 + OBJECT = 12 + GEOPOINT = 13 + GEOSHAPE = 14 + TIMESTAMP_WITHOUT_TZ = 15 + UNCHECKED_OBJECT = 16 + REGPROC = 19 + TIME = 20 + OIDVECTOR = 21 + NUMERIC = 22 + REGCLASS = 23 + DATE = 24 + BIT = 25 + JSON = 26 + CHARACTER = 27 + ARRAY = 100 + + +ConverterMapping = Dict[DataType, ConverterFunction] + + +# Map data type identifier to converter function. +_DEFAULT_CONVERTERS: ConverterMapping = { + DataType.IP: _to_ipaddress, + DataType.TIMESTAMP_WITH_TZ: _to_datetime, + DataType.TIMESTAMP_WITHOUT_TZ: _to_datetime, +} + + +class Converter: + def __init__( + self, + mappings: Optional[ConverterMapping] = None, + default: ConverterFunction = _to_default, + ) -> None: + self._mappings = mappings or {} + self._default = default + + def get(self, type_: ColTypesDefinition) -> ConverterFunction: + if isinstance(type_, int): + return self._mappings.get(DataType(type_), self._default) + type_, inner_type = type_ + if DataType(type_) is not DataType.ARRAY: + raise ValueError(f"Data type {type_} is not implemented as collection type") + + inner_convert = self.get(inner_type) + + def convert(value: Any) -> Optional[List[Any]]: + if value is None: + return None + return [inner_convert(x) for x in value] + + return convert + + +class DefaultTypeConverter(Converter): + def __init__(self, more_mappings: Optional[ConverterMapping] = None) -> None: + mappings: ConverterMapping = {} + mappings.update(deepcopy(_DEFAULT_CONVERTERS)) + if more_mappings: + mappings.update(deepcopy(more_mappings)) + super().__init__( + mappings=mappings, default=_to_default + ) diff --git a/src/crate/client/cursor.py b/src/crate/client/cursor.py index 59e936d7..25b88667 100644 --- a/src/crate/client/cursor.py +++ b/src/crate/client/cursor.py @@ -19,9 +19,11 @@ # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. -from .exceptions import ProgrammingError import warnings +from .converter import Converter +from .exceptions import ProgrammingError + class Cursor(object): """ @@ -30,9 +32,10 @@ class Cursor(object): """ lastrowid = None # currently not supported - def __init__(self, connection): + def __init__(self, connection, converter: Converter): self.arraysize = 1 self.connection = connection + self._converter = converter self._closed = False self._result = None self.rows = None @@ -50,7 +53,10 @@ def execute(self, sql, parameters=None, bulk_parameters=None): self._result = self.connection.client.sql(sql, parameters, bulk_parameters) if "rows" in self._result: - self.rows = iter(self._result["rows"]) + if self._converter is None: + self.rows = iter(self._result["rows"]) + else: + self.rows = iter(self._convert_rows()) def executemany(self, sql, seq_of_parameters): """ @@ -73,9 +79,13 @@ def executemany(self, sql, seq_of_parameters): "duration": sum(durations) if durations else -1, "rows": [], "cols": self._result.get("cols", []), + "col_types": self._result.get("col_types", []), "results": self._result.get("results") } - self.rows = iter(self._result["rows"]) + if self._converter is None: + self.rows = iter(self._result["rows"]) + else: + self.rows = iter(self._convert_rows()) return self._result["results"] def fetchone(self): @@ -210,3 +220,24 @@ def duration(self): "duration" not in self._result: return -1 return self._result.get("duration", 0) + + def _convert_rows(self): + """ + Iterate rows, apply type converters, and generate converted rows. + """ + assert "col_types" in self._result and self._result["col_types"], \ + "Unable to apply type conversion without `col_types` information" + + # Resolve `col_types` definition to converter functions. Running the lookup + # redundantly on each row loop iteration would be a huge performance hog. + types = self._result["col_types"] + converters = [ + self._converter.get(type) for type in types + ] + + # Process result rows with conversion. + for row in self._result["rows"]: + yield [ + convert(value) + for convert, value in zip(converters, row) + ] diff --git a/src/crate/client/doctests/cursor.txt b/src/crate/client/doctests/cursor.txt index 970465da..f1f2ee6b 100644 --- a/src/crate/client/doctests/cursor.txt +++ b/src/crate/client/doctests/cursor.txt @@ -2,9 +2,18 @@ Cursor ====== + +Setup +===== + +This section sets up a cursor object, inspects some of its attributes, and sets +up the response for subsequent cursor operations. + :: >>> from crate.client import connect + >>> from crate.client.converter import DefaultTypeConverter + >>> from crate.client.cursor import Cursor >>> connection = connect(client=connection_client_mocked) >>> cursor = connection.cursor() @@ -81,7 +90,7 @@ If the specified number of rows not being available, fewer rows may returned:: >>> cursor.execute('') -If no number of rows are specified it defaults to the current cursor.arraysize:: +If no number of rows are specified it defaults to the current ``cursor.arraysize``:: >>> cursor.arraysize 1 @@ -179,7 +188,7 @@ The attribute is -1 in case the cursor has been closed:: >>> cursor.rowcount -1 -If the last respsonse doesn't contain the rowcount attribute -1 is returned:: +If the last response does not contain the rowcount attribute, ``-1`` is returned:: >>> cursor = connection.cursor() >>> connection.client.set_next_response({ @@ -296,6 +305,68 @@ closed connection an ``ProgrammingError`` exception will be raised:: ... crate.client.exceptions.ProgrammingError: Cursor closed + +Python data type conversion +=========================== + +The cursor object can optionally convert database types to native Python data +types. Currently, this is implemented for the CrateDB data types ``IP`` and +``TIMESTAMP`` on behalf of the ``DefaultTypeConverter``. + +:: + + >>> cursor = connection.cursor(converter=DefaultTypeConverter()) + + >>> connection.client.set_next_response({ + ... "col_types": [4, 5, 11], + ... "rows":[ [ "foo", "10.10.10.1", 1658167836758 ] ], + ... "cols":[ "name", "address", "timestamp" ], + ... "rowcount":1, + ... "duration":123 + ... }) + + >>> cursor.execute('') + + >>> cursor.fetchone() + ['foo', IPv4Address('10.10.10.1'), datetime.datetime(2022, 7, 18, 18, 10, 36, 758000)] + + +Custom data type conversion +=========================== + +By providing a custom converter instance, you can define your own data type +conversions. For investigating the list of available data types, please either +inspect the ``DataType`` enum, or the documentation about the list of available +`CrateDB data type identifiers for the HTTP interface`_. + +To create a simple converter for converging CrateDB's ``BIT`` type to Python's +``int`` type:: + + >>> from crate.client.converter import Converter, DataType + + >>> converter = Converter({DataType.BIT: lambda value: int(value[2:-1], 2)}) + >>> cursor = connection.cursor(converter=converter) + +Proof that the converter works correctly, ``B\'0110\'`` should be converted to +``6``. CrateDB's ``BIT`` data type has the numeric identifier ``25``:: + + >>> connection.client.set_next_response({ + ... "col_types": [25], + ... "rows":[ [ "B'0110'" ] ], + ... "cols":[ "value" ], + ... "rowcount":1, + ... "duration":123 + ... }) + + >>> cursor.execute('') + + >>> cursor.fetchone() + [6] + + .. Hidden: close connection >>> connection.close() + + +.. _CrateDB data type identifiers for the HTTP interface: https://crate.io/docs/crate/reference/en/latest/interfaces/http.html#column-types diff --git a/src/crate/client/doctests/http.txt b/src/crate/client/doctests/http.txt index fa9407c3..0c411f55 100644 --- a/src/crate/client/doctests/http.txt +++ b/src/crate/client/doctests/http.txt @@ -69,7 +69,8 @@ Issue a select statement against our with test data pre-filled crate instance:: >>> http_client = HttpClient(crate_host) >>> result = http_client.sql('select name from locations order by name') >>> pprint(result) - {'cols': ['name'], + {'col_types': [4], + 'cols': ['name'], 'duration': ..., 'rowcount': 13, 'rows': [['Aldebaran'], diff --git a/src/crate/client/http.py b/src/crate/client/http.py index 44643a36..e932f732 100644 --- a/src/crate/client/http.py +++ b/src/crate/client/http.py @@ -315,7 +315,7 @@ class Client(object): Crate connection client using CrateDB's HTTP API. """ - SQL_PATH = '/_sql' + SQL_PATH = '/_sql?types=true' """Crate URI path for issuing SQL statements.""" retry_interval = 30 @@ -385,7 +385,7 @@ def __init__(self, self.path = self.SQL_PATH if error_trace: - self.path += '?error_trace=true' + self.path += '&error_trace=true' def close(self): for server in self.server_pool.values(): diff --git a/src/crate/client/test_cursor.py b/src/crate/client/test_cursor.py index a926ae4c..8a1f3e7f 100644 --- a/src/crate/client/test_cursor.py +++ b/src/crate/client/test_cursor.py @@ -19,11 +19,15 @@ # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. +from datetime import datetime +from ipaddress import IPv4Address from unittest import TestCase from unittest.mock import MagicMock from crate.client import connect +from crate.client.converter import DataType, DefaultTypeConverter from crate.client.http import Client +from crate.client.test_util import ClientMocked class CursorTest(TestCase): @@ -45,3 +49,147 @@ def test_execute_with_bulk_args(self): c.execute(statement, bulk_parameters=[[1]]) client.sql.assert_called_once_with(statement, None, [[1]]) conn.close() + + def test_execute_with_converter(self): + client = ClientMocked() + conn = connect(client=client) + + # Use the set of data type converters from `DefaultTypeConverter` + # and add another custom converter. + converter = DefaultTypeConverter( + {DataType.BIT: lambda value: value is not None and int(value[2:-1], 2) or None}) + + # Create a `Cursor` object with converter. + c = conn.cursor(converter=converter) + + # Make up a response using CrateDB data types `TEXT`, `IP`, + # `TIMESTAMP`, `BIT`. + conn.client.set_next_response({ + "col_types": [4, 5, 11, 25], + "cols": ["name", "address", "timestamp", "bitmask"], + "rows": [ + ["foo", "10.10.10.1", 1658167836758, "B'0110'"], + [None, None, None, None], + ], + "rowcount": 1, + "duration": 123 + }) + + c.execute("") + result = c.fetchall() + self.assertEqual(result, [ + ['foo', IPv4Address('10.10.10.1'), datetime(2022, 7, 18, 18, 10, 36, 758000), 6], + [None, None, None, None], + ]) + + conn.close() + + def test_execute_with_converter_and_invalid_data_type(self): + client = ClientMocked() + conn = connect(client=client) + converter = DefaultTypeConverter() + + # Create a `Cursor` object with converter. + c = conn.cursor(converter=converter) + + # Make up a response using CrateDB data types `TEXT`, `IP`, + # `TIMESTAMP`, `BIT`. + conn.client.set_next_response({ + "col_types": [999], + "cols": ["foo"], + "rows": [ + ["n/a"], + ], + "rowcount": 1, + "duration": 123 + }) + + c.execute("") + with self.assertRaises(ValueError) as ex: + c.fetchone() + assert ex.exception.args == ("999 is not a valid DataType",) + + def test_execute_array_with_converter(self): + client = ClientMocked() + conn = connect(client=client) + converter = DefaultTypeConverter() + cursor = conn.cursor(converter=converter) + + conn.client.set_next_response({ + "col_types": [4, [100, 5]], + "cols": ["name", "address"], + "rows": [["foo", ["10.10.10.1", "10.10.10.2"]]], + "rowcount": 1, + "duration": 123 + }) + + cursor.execute("") + result = cursor.fetchone() + self.assertEqual(result, [ + 'foo', + [IPv4Address('10.10.10.1'), IPv4Address('10.10.10.2')], + ]) + + def test_execute_array_with_converter_and_invalid_collection_type(self): + client = ClientMocked() + conn = connect(client=client) + converter = DefaultTypeConverter() + cursor = conn.cursor(converter=converter) + + # Converting collections only works for `ARRAY`s. (ID=100). + # When using `DOUBLE` (ID=6), it should croak. + conn.client.set_next_response({ + "col_types": [4, [6, 5]], + "cols": ["name", "address"], + "rows": [["foo", ["10.10.10.1", "10.10.10.2"]]], + "rowcount": 1, + "duration": 123 + }) + + cursor.execute("") + + with self.assertRaises(ValueError) as ex: + cursor.fetchone() + assert ex.exception.args == ("Data type 6 is not implemented as collection type",) + + def test_execute_nested_array_with_converter(self): + client = ClientMocked() + conn = connect(client=client) + converter = DefaultTypeConverter() + cursor = conn.cursor(converter=converter) + + conn.client.set_next_response({ + "col_types": [4, [100, [100, 5]]], + "cols": ["name", "address_buckets"], + "rows": [["foo", [["10.10.10.1", "10.10.10.2"], ["10.10.10.3"], [], None]]], + "rowcount": 1, + "duration": 123 + }) + + cursor.execute("") + result = cursor.fetchone() + self.assertEqual(result, [ + 'foo', + [[IPv4Address('10.10.10.1'), IPv4Address('10.10.10.2')], [IPv4Address('10.10.10.3')], [], None], + ]) + + def test_executemany_with_converter(self): + client = ClientMocked() + conn = connect(client=client) + converter = DefaultTypeConverter() + cursor = conn.cursor(converter=converter) + + conn.client.set_next_response({ + "col_types": [4, 5], + "cols": ["name", "address"], + "rows": [["foo", "10.10.10.1"]], + "rowcount": 1, + "duration": 123 + }) + + cursor.executemany("", []) + result = cursor.fetchall() + + # ``executemany()`` is not intended to be used with statements returning result + # sets. The result will always be empty. + self.assertEqual(result, []) diff --git a/src/crate/client/test_http.py b/src/crate/client/test_http.py index 4a073099..42e77e43 100644 --- a/src/crate/client/test_http.py +++ b/src/crate/client/test_http.py @@ -436,7 +436,7 @@ def test_params(self): def test_no_params(self): client = Client() - self.assertEqual(client.path, "/_sql") + self.assertEqual(client.path, "/_sql?types=true") client.close() diff --git a/src/crate/client/test_util.py b/src/crate/client/test_util.py new file mode 100644 index 00000000..90379a79 --- /dev/null +++ b/src/crate/client/test_util.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8; -*- +# +# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. Crate licenses +# this file to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# However, if you have executed another commercial license agreement +# with Crate these terms will supersede the license and you may use the +# software solely pursuant to the terms of the relevant commercial agreement. + + +class ClientMocked(object): + + active_servers = ["http://localhost:4200"] + + def __init__(self): + self.response = {} + self._server_infos = ("http://localhost:4200", "my server", "2.0.0") + + def sql(self, stmt=None, parameters=None, bulk_parameters=None): + return self.response + + def server_infos(self, server): + return self._server_infos + + def set_next_response(self, response): + self.response = response + + def set_next_server_infos(self, server, server_name, version): + self._server_infos = (server, server_name, version) + + def close(self): + pass diff --git a/src/crate/client/tests.py b/src/crate/client/tests.py index e0abafd2..9abe1881 100644 --- a/src/crate/client/tests.py +++ b/src/crate/client/tests.py @@ -40,6 +40,7 @@ from crate.testing.tests import crate_path, docs_path from crate.client import connect from crate.client.sqlalchemy.dialect import CrateDialect +from crate.client.test_util import ClientMocked from . import http from .test_cursor import CursorTest @@ -69,30 +70,6 @@ def cprint(s): print(s) -class ClientMocked(object): - - active_servers = ["http://localhost:4200"] - - def __init__(self): - self.response = {} - self._server_infos = ("http://localhost:4200", "my server", "2.0.0") - - def sql(self, stmt=None, parameters=None, bulk_parameters=None): - return self.response - - def server_infos(self, server): - return self._server_infos - - def set_next_response(self, response): - self.response = response - - def set_next_server_infos(self, server, server_name, version): - self._server_infos = (server, server_name, version) - - def close(self): - pass - - def setUpMocked(test): test.globs['connection_client_mocked'] = ClientMocked() @@ -335,6 +312,10 @@ def _try_execute(cursor, stmt): try: cursor.execute(stmt) except Exception: + # FIXME: Why does this croak on statements like ``DROP TABLE cities``? + # Note: When needing to debug the test environment, you may want to + # enable this logger statement. + # log.exception("Executing SQL statement failed") pass From 3f79183847f06ab4d276a61adbed3f876bff1dcd Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Tue, 18 Oct 2022 15:37:52 +0200 Subject: [PATCH 2/2] Documentation: Satisfy link checker --- docs/sqlalchemy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sqlalchemy.rst b/docs/sqlalchemy.rst index 9ee38cf0..f53ea5ed 100644 --- a/docs/sqlalchemy.rst +++ b/docs/sqlalchemy.rst @@ -629,7 +629,7 @@ column on the ``Character`` class. .. _operator: https://docs.python.org/2/library/operator.html .. _any: http://docs.sqlalchemy.org/en/latest/core/type_basics.html#sqlalchemy.types.ARRAY.Comparator.any .. _tuple: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range -.. _count result rows: http://docs.sqlalchemy.org/en/latest/orm/tutorial.html#counting +.. _count result rows: http://docs.sqlalchemy.org/en/14/orm/tutorial.html#counting .. _MATCH predicate: https://crate.io/docs/crate/reference/en/latest/general/dql/fulltext.html#match-predicate .. _arguments reference: https://crate.io/docs/crate/reference/en/latest/general/dql/fulltext.html#arguments .. _boost values: https://crate.io/docs/crate/reference/en/latest/general/dql/fulltext.html#arguments