perspective-python 4.2.0__cp311-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. perspective/__init__.py +396 -0
  2. perspective/extension/finos-perspective-nbextension.json +5 -0
  3. perspective/handlers/__init__.py +11 -0
  4. perspective/handlers/aiohttp.py +61 -0
  5. perspective/handlers/starlette.py +55 -0
  6. perspective/handlers/tornado.py +184 -0
  7. perspective/perspective.pyd +0 -0
  8. perspective/templates/exported_widget.html.template +35 -0
  9. perspective/tests/__init__.py +11 -0
  10. perspective/tests/async/test_async_client.py +83 -0
  11. perspective/tests/async/test_websocket_client.py +124 -0
  12. perspective/tests/conftest.py +272 -0
  13. perspective/tests/core/__init__.py +11 -0
  14. perspective/tests/core/test_async.py +351 -0
  15. perspective/tests/multi_threaded/__init__.py +11 -0
  16. perspective/tests/multi_threaded/test_multi_threaded.py +201 -0
  17. perspective/tests/server/__init__.py +11 -0
  18. perspective/tests/server/test_server.py +1016 -0
  19. perspective/tests/server/test_session.py +110 -0
  20. perspective/tests/table/__init__.py +11 -0
  21. perspective/tests/table/arrow/date32.arrow +0 -0
  22. perspective/tests/table/arrow/date64.arrow +0 -0
  23. perspective/tests/table/arrow/dict.arrow +0 -0
  24. perspective/tests/table/arrow/dict_update.arrow +0 -0
  25. perspective/tests/table/arrow/int_float_str.arrow +0 -0
  26. perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
  27. perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
  28. perspective/tests/table/object_sequence.py +402 -0
  29. perspective/tests/table/test_column_paths.py +89 -0
  30. perspective/tests/table/test_delete.py +124 -0
  31. perspective/tests/table/test_exception.py +65 -0
  32. perspective/tests/table/test_leaks.py +54 -0
  33. perspective/tests/table/test_ports.py +178 -0
  34. perspective/tests/table/test_remove.py +102 -0
  35. perspective/tests/table/test_table.py +641 -0
  36. perspective/tests/table/test_table_arrow.py +503 -0
  37. perspective/tests/table/test_table_datetime.py +2409 -0
  38. perspective/tests/table/test_table_infer.py +201 -0
  39. perspective/tests/table/test_table_limit.py +45 -0
  40. perspective/tests/table/test_table_numpy.py +1022 -0
  41. perspective/tests/table/test_table_pandas.py +1018 -0
  42. perspective/tests/table/test_table_polars.py +251 -0
  43. perspective/tests/table/test_table_view_table.py +130 -0
  44. perspective/tests/table/test_to_arrow.py +417 -0
  45. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  46. perspective/tests/table/test_to_format.py +1024 -0
  47. perspective/tests/table/test_to_polars.py +26 -0
  48. perspective/tests/table/test_update.py +545 -0
  49. perspective/tests/table/test_update_arrow.py +980 -0
  50. perspective/tests/table/test_update_pandas.py +211 -0
  51. perspective/tests/table/test_view.py +2261 -0
  52. perspective/tests/table/test_view_expression.py +1940 -0
  53. perspective/tests/test_dependencies.py +53 -0
  54. perspective/tests/viewer/__init__.py +11 -0
  55. perspective/tests/viewer/test_viewer.py +246 -0
  56. perspective/tests/widget/__init__.py +11 -0
  57. perspective/tests/widget/test_widget.py +278 -0
  58. perspective/tests/widget/test_widget_pandas.py +453 -0
  59. perspective/virtual_servers/__init__.py +134 -0
  60. perspective/virtual_servers/clickhouse.py +245 -0
  61. perspective/virtual_servers/duckdb.py +236 -0
  62. perspective/widget/__init__.py +349 -0
  63. perspective/widget/viewer/__init__.py +15 -0
  64. perspective/widget/viewer/validate.py +22 -0
  65. perspective/widget/viewer/viewer.py +343 -0
  66. perspective/widget/viewer/viewer_traitlets.py +101 -0
  67. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/install.json +5 -0
  68. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/package.json +71 -0
  69. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js +2 -0
  70. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js.LICENSE.txt +25 -0
  71. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/523.c030af5d3c4f67ff83f6.js +1 -0
  72. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/remoteEntry.95a8ea1b44d96032833f.js +1 -0
  73. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/style.js +4 -0
  74. perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/third-party-licenses.json +16 -0
  75. perspective_python-4.2.0.dist-info/METADATA +27 -0
  76. perspective_python-4.2.0.dist-info/RECORD +79 -0
  77. perspective_python-4.2.0.dist-info/WHEEL +4 -0
  78. perspective_python-4.2.0.dist-info/licenses/LICENSE.md +193 -0
  79. perspective_python-4.2.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +17395 -0
@@ -0,0 +1,245 @@
1
+ # ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ # ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ # ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ # ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ # ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ # ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ # ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ # ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ # ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import perspective
14
+
15
+ from datetime import datetime
16
+ from loguru import logger
17
+
18
+ from perspective.virtual_servers import VirtualSessionModel
19
+
20
+
21
+ NUMBER_AGGS = [
22
+ "sum",
23
+ "count",
24
+ "any_value",
25
+ "arbitrary",
26
+ "array_agg",
27
+ "avg",
28
+ "bit_and",
29
+ "bit_or",
30
+ "bit_xor",
31
+ "bitstring_agg",
32
+ "bool_and",
33
+ "bool_or",
34
+ "countif",
35
+ "favg",
36
+ "fsum",
37
+ "geomean",
38
+ "kahan_sum",
39
+ "last",
40
+ "max",
41
+ "min",
42
+ "product",
43
+ "string_agg",
44
+ "sumkahan",
45
+ ]
46
+
47
+ STRING_AGGS = [
48
+ "count",
49
+ "any_value",
50
+ "arbitrary",
51
+ "first",
52
+ "countif",
53
+ "last",
54
+ "string_agg",
55
+ ]
56
+
57
+ FILTER_OPS = [
58
+ "==",
59
+ "!=",
60
+ "LIKE",
61
+ "IS DISTINCT FROM",
62
+ "IS NOT DISTINCT FROM",
63
+ ">=",
64
+ "<=",
65
+ ">",
66
+ "<",
67
+ ]
68
+
69
+
70
+ class ClickhouseVirtualSession:
71
+ def __init__(self, callback, db):
72
+ self.session = perspective.VirtualServer(ClickhouseVirtualSessionModel(db))
73
+ self.callback = callback
74
+
75
+ def handle_request(self, msg):
76
+ self.callback(self.session.handle_request(msg))
77
+
78
+
79
+ class ClickhouseVirtualServer:
80
+ def __init__(self, db):
81
+ self.db = db
82
+
83
+ def new_session(self, callback):
84
+ return ClickhouseVirtualSession(callback, self.db)
85
+
86
+
87
+ class ClickhouseVirtualSessionModel(VirtualSessionModel):
88
+ """
89
+ An implementation of a `perspective.VirtualSessionModel` for ClickHouse.
90
+ """
91
+
92
+ def __init__(self, db):
93
+ self.db = db
94
+ self.sql_builder = perspective.GenericSQLVirtualServerModel(
95
+ {"create_entity": "VIEW", "grouping_fn": "GROUPING"}
96
+ )
97
+
98
+ def get_features(self):
99
+ return {
100
+ "group_by": True,
101
+ "split_by": False,
102
+ "sort": True,
103
+ "expressions": True,
104
+ "filter_ops": {
105
+ "integer": FILTER_OPS,
106
+ "float": FILTER_OPS,
107
+ "string": FILTER_OPS,
108
+ "boolean": FILTER_OPS,
109
+ "date": FILTER_OPS,
110
+ "datetime": FILTER_OPS,
111
+ },
112
+ "aggregates": {
113
+ "integer": NUMBER_AGGS,
114
+ "float": NUMBER_AGGS,
115
+ "string": STRING_AGGS,
116
+ "boolean": STRING_AGGS,
117
+ "date": STRING_AGGS,
118
+ "datetime": STRING_AGGS,
119
+ },
120
+ }
121
+
122
+ def get_hosted_tables(self):
123
+ query = "SHOW TABLES"
124
+ results = run_query(self.db, query)
125
+ return [result[0] for result in results]
126
+
127
+ def table_schema(self, table_name, config=None):
128
+ query = self.sql_builder.table_schema(table_name)
129
+ results = run_query(self.db, query)
130
+ schema = {}
131
+ for result in results:
132
+ col_name = result[0]
133
+ if not col_name.startswith("__"):
134
+ schema[col_name] = clickhouse_type_to_psp(result[1])
135
+
136
+ return schema
137
+
138
+ def view_column_size(self, view_name, config):
139
+ query = f"SELECT COUNT() FROM system.columns WHERE table = '{view_name}'"
140
+ results = run_query(self.db, query)
141
+ gs = len(config["group_by"])
142
+ return results[0][0] - (
143
+ 0 if gs == 0 else gs + (1 if len(config["split_by"]) == 0 else 0)
144
+ )
145
+
146
+ def table_size(self, table_name):
147
+ query = self.sql_builder.table_size(table_name)
148
+ results = run_query(self.db, query)
149
+ return results[0][0]
150
+
151
+ def table_make_view(self, table_name, view_name, config):
152
+ query = self.sql_builder.table_make_view(table_name, view_name, config)
153
+ run_query(self.db, query, execute=True)
154
+
155
+ def table_validate_expression(self, view_name, expression):
156
+ query = self.sql_builder.table_validate_expression(view_name, expression)
157
+ results = run_query(self.db, query)
158
+ return clickhouse_type_to_psp(results[0][1])
159
+
160
+ def view_delete(self, view_name):
161
+ query = self.sql_builder.view_delete(view_name)
162
+ run_query(self.db, query, execute=True)
163
+
164
+ def view_get_data(self, view_name, config, schema, viewport, data):
165
+ group_by = config["group_by"]
166
+ split_by = config["split_by"]
167
+ query = self.sql_builder.view_get_data(view_name, config, viewport, schema)
168
+ results, columns, dtypes = run_query(self.db, query, columns=True)
169
+ for cidx, col in enumerate(columns):
170
+ if cidx == 0 and len(group_by) > 0 and len(split_by) == 0:
171
+ continue
172
+
173
+ if len(split_by) > 0 and not col.startswith("__ROW_PATH_"):
174
+ col = col.replace("_", "|")
175
+
176
+ # print(
177
+ # dtypes[cidx], type(dtypes[cidx]), dir(dtypes[cidx]), dtypes[cidx].name
178
+ # )
179
+
180
+ dtype = clickhouse_type_to_psp(str(dtypes[cidx]))
181
+ for ridx, row in enumerate(results):
182
+ grouping_id = (
183
+ row[0] if len(group_by) > 0 and len(split_by) == 0 else None
184
+ )
185
+
186
+ value = row[cidx]
187
+ if dtype == "string" and not isinstance(value, str):
188
+ value = str(value)
189
+
190
+ data.set_col(dtype, col, ridx, value, grouping_id)
191
+
192
+
193
+ ################################################################################
194
+ #
195
+ # ClickHouse Utils
196
+
197
+
198
+ def clickhouse_type_to_psp(name):
199
+ """Convert a ClickHouse `dtype` to a Perspective `ColumnType`."""
200
+ if name.startswith("Nullable(") and name.endswith(")"):
201
+ name = name[9:-1]
202
+
203
+ if name.startswith("Array"):
204
+ return "string"
205
+
206
+ if name in ("Int64", "UInt64", "Float64"):
207
+ return "float"
208
+
209
+ if name == "String":
210
+ return "string"
211
+
212
+ if name == "DateTime":
213
+ return "datetime"
214
+
215
+ if name == "Date":
216
+ return "date"
217
+
218
+ msg = f"Unknown type '{name}'"
219
+ raise ValueError(msg)
220
+
221
+
222
+ def run_query(db, query, execute=False, columns=False):
223
+ query = " ".join(query.split())
224
+ start = datetime.now()
225
+ result = None
226
+ try:
227
+ if execute:
228
+ db.command(query)
229
+ else:
230
+ req = db.query(query)
231
+ result = req.result_rows
232
+ except Exception as e:
233
+ logger.error(e)
234
+ logger.error(f"{query}")
235
+ raise e
236
+ else:
237
+ logger.debug(f"{datetime.now() - start} {query}")
238
+ if columns:
239
+ return (
240
+ result,
241
+ req.column_names,
242
+ [(x.name if hasattr(x, "name") else str(x)) for x in req.column_types],
243
+ )
244
+ else:
245
+ return result
@@ -0,0 +1,236 @@
1
+ # ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ # ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ # ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ # ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ # ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ # ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ # ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ # ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ # ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import duckdb
14
+ import perspective
15
+
16
+ from datetime import datetime
17
+ from loguru import logger
18
+
19
+ from perspective.virtual_servers import VirtualSessionModel
20
+
21
+
22
+ NUMBER_AGGS = [
23
+ "sum",
24
+ "count",
25
+ "any_value",
26
+ "arbitrary",
27
+ # "arg_max",
28
+ # "arg_max_null",
29
+ # "arg_min",
30
+ # "arg_min_null",
31
+ "array_agg",
32
+ "avg",
33
+ "bit_and",
34
+ "bit_or",
35
+ "bit_xor",
36
+ "bitstring_agg",
37
+ "bool_and",
38
+ "bool_or",
39
+ "countif",
40
+ "favg",
41
+ "fsum",
42
+ "geomean",
43
+ # "histogram",
44
+ # "histogram_values",
45
+ "kahan_sum",
46
+ "last",
47
+ # "list"
48
+ "max",
49
+ # "max_by"
50
+ "min",
51
+ # "min_by"
52
+ "product",
53
+ "string_agg",
54
+ "sumkahan",
55
+ # "weighted_avg",
56
+ ]
57
+
58
+ STRING_AGGS = [
59
+ "count",
60
+ "any_value",
61
+ "arbitrary",
62
+ "first",
63
+ "countif",
64
+ "last",
65
+ "string_agg",
66
+ ]
67
+
68
+ FILTER_OPS = [
69
+ "==",
70
+ "!=",
71
+ "LIKE",
72
+ "IS DISTINCT FROM",
73
+ "IS NOT DISTINCT FROM",
74
+ ">=",
75
+ "<=",
76
+ ">",
77
+ "<",
78
+ ]
79
+
80
+
81
+ class DuckDBVirtualSession:
82
+ def __init__(self, callback, db):
83
+ self.session = perspective.VirtualServer(DuckDBVirtualSessionModel(db))
84
+ self.callback = callback
85
+
86
+ def handle_request(self, msg):
87
+ self.callback(self.session.handle_request(msg))
88
+
89
+
90
+ class DuckDBVirtualServer:
91
+ def __init__(self, db):
92
+ self.db = db
93
+
94
+ def new_session(self, callback):
95
+ return DuckDBVirtualSession(callback, self.db)
96
+
97
+
98
+ class DuckDBVirtualSessionModel(VirtualSessionModel):
99
+ """
100
+ An implementation of a `perspective.VirtualSessionModel` for DuckDB.
101
+ """
102
+
103
+ def __init__(self, db):
104
+ self.db = db
105
+ self.sql_builder = perspective.GenericSQLVirtualServerModel()
106
+
107
+ def get_features(self):
108
+ return {
109
+ "group_by": True,
110
+ "split_by": True,
111
+ "sort": True,
112
+ "expressions": True,
113
+ "filter_ops": {
114
+ "integer": FILTER_OPS,
115
+ "float": FILTER_OPS,
116
+ "string": FILTER_OPS,
117
+ "boolean": FILTER_OPS,
118
+ "date": FILTER_OPS,
119
+ "datetime": FILTER_OPS,
120
+ },
121
+ "aggregates": {
122
+ "integer": NUMBER_AGGS,
123
+ "float": NUMBER_AGGS,
124
+ "string": STRING_AGGS,
125
+ "boolean": STRING_AGGS,
126
+ "date": STRING_AGGS,
127
+ "datetime": STRING_AGGS,
128
+ },
129
+ }
130
+
131
+ def get_hosted_tables(self):
132
+ query = self.sql_builder.get_hosted_tables()
133
+ results = run_query(self.db, query)
134
+ return [result[2] for result in results]
135
+
136
+ def table_schema(self, table_name, config=None):
137
+ query = self.sql_builder.table_schema(table_name)
138
+ results = run_query(self.db, query)
139
+ schema = {}
140
+ for result in results:
141
+ col_name = result[0]
142
+ if not col_name.startswith("__"):
143
+ schema[col_name] = duckdb_type_to_psp(result[1])
144
+
145
+ return schema
146
+
147
+ def view_column_size(self, table_name, config):
148
+ query = self.sql_builder.view_column_size(table_name)
149
+ results = run_query(self.db, query)
150
+ gs = len(config["group_by"])
151
+ return results[0][0] - (
152
+ 0 if gs == 0 else gs + (1 if len(config["split_by"]) == 0 else 0)
153
+ )
154
+
155
+ def table_size(self, table_name):
156
+ query = self.sql_builder.table_size(table_name)
157
+ results = run_query(self.db, query)
158
+ return results[0][0]
159
+
160
+ def table_make_view(self, table_name, view_name, config):
161
+ query = self.sql_builder.table_make_view(table_name, view_name, config)
162
+ run_query(self.db, query, execute=True)
163
+
164
+ def table_validate_expression(self, view_name, expression):
165
+ query = self.sql_builder.table_validate_expression(view_name, expression)
166
+ results = run_query(self.db, query)
167
+ return duckdb_type_to_psp(results[0][1])
168
+
169
+ def view_delete(self, view_name):
170
+ query = self.sql_builder.view_delete(view_name)
171
+ run_query(self.db, query, execute=True)
172
+
173
+ def view_get_data(self, view_name, config, schema, viewport, data):
174
+ group_by = config["group_by"]
175
+ split_by = config["split_by"]
176
+ query = self.sql_builder.view_get_data(view_name, config, viewport, schema)
177
+ results, columns, dtypes = run_query(self.db, query, columns=True)
178
+ for cidx, col in enumerate(columns):
179
+ if cidx == 0 and len(group_by) > 0 and len(split_by) == 0:
180
+ continue
181
+
182
+ if len(split_by) > 0 and not col.startswith("__ROW_PATH_"):
183
+ col = col.replace("_", "|")
184
+
185
+ dtype = duckdb_type_to_psp(str(dtypes[cidx]))
186
+ for ridx, row in enumerate(results):
187
+ grouping_id = (
188
+ row[0] if len(group_by) > 0 and len(split_by) == 0 else None
189
+ )
190
+ data.set_col(dtype, col, ridx, row[cidx], grouping_id)
191
+
192
+
193
+ ################################################################################
194
+ #
195
+ # DuckDB Utils
196
+
197
+
198
+ def duckdb_type_to_psp(name):
199
+ """Convert a DuckDB `dtype` to a Perspective `ColumnType`."""
200
+ if name == "VARCHAR":
201
+ return "string"
202
+ if name in ("DOUBLE", "BIGINT", "HUGEINT"):
203
+ return "float"
204
+ if name == "INTEGER":
205
+ return "integer"
206
+ if name == "DATE":
207
+ return "date"
208
+ if name == "BOOLEAN":
209
+ return "boolean"
210
+ if name == "TIMESTAMP":
211
+ return "datetime"
212
+
213
+ msg = f"Unknown type '{name}'"
214
+ raise ValueError(msg)
215
+
216
+
217
+ def run_query(db, query, execute=False, columns=False):
218
+ query = " ".join(query.split())
219
+ start = datetime.now()
220
+ result = None
221
+ try:
222
+ if execute:
223
+ db.execute(query)
224
+ else:
225
+ req = db.sql(query)
226
+ result = req.fetchall()
227
+ except (duckdb.ParserException, duckdb.BinderException) as e:
228
+ logger.error(e)
229
+ logger.error(f"{query}")
230
+ raise e
231
+ else:
232
+ logger.debug(f"{datetime.now() - start} {query}")
233
+ if columns:
234
+ return (result, req.columns, req.dtypes)
235
+ else:
236
+ return result