perspective-python 4.5.2__cp311-abi3-pyemscripten_2025_0_wasm32.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 +403 -0
  2. perspective/extension/finos-perspective-nbextension.json +5 -0
  3. perspective/handlers/__init__.py +11 -0
  4. perspective/handlers/aiohttp.py +69 -0
  5. perspective/handlers/starlette.py +63 -0
  6. perspective/handlers/tornado.py +192 -0
  7. perspective/perspective.abi3.so +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 +263 -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_join.py +115 -0
  33. perspective/tests/table/test_leaks.py +105 -0
  34. perspective/tests/table/test_ports.py +178 -0
  35. perspective/tests/table/test_remove.py +102 -0
  36. perspective/tests/table/test_table.py +641 -0
  37. perspective/tests/table/test_table_arrow.py +500 -0
  38. perspective/tests/table/test_table_datetime.py +2409 -0
  39. perspective/tests/table/test_table_infer.py +201 -0
  40. perspective/tests/table/test_table_limit.py +45 -0
  41. perspective/tests/table/test_table_numpy.py +1022 -0
  42. perspective/tests/table/test_table_page_to_disk.py +304 -0
  43. perspective/tests/table/test_table_pandas.py +1018 -0
  44. perspective/tests/table/test_table_polars.py +251 -0
  45. perspective/tests/table/test_table_view_table.py +130 -0
  46. perspective/tests/table/test_to_arrow.py +417 -0
  47. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  48. perspective/tests/table/test_to_format.py +1024 -0
  49. perspective/tests/table/test_to_polars.py +26 -0
  50. perspective/tests/table/test_update.py +536 -0
  51. perspective/tests/table/test_update_arrow.py +980 -0
  52. perspective/tests/table/test_update_pandas.py +211 -0
  53. perspective/tests/table/test_view.py +2444 -0
  54. perspective/tests/table/test_view_expression.py +1940 -0
  55. perspective/tests/test_dependencies.py +53 -0
  56. perspective/tests/viewer/__init__.py +11 -0
  57. perspective/tests/viewer/test_viewer.py +246 -0
  58. perspective/tests/virtual_servers/__init__.py +12 -0
  59. perspective/tests/virtual_servers/test_coerce_types.py +209 -0
  60. perspective/tests/virtual_servers/test_duckdb.py +908 -0
  61. perspective/tests/virtual_servers/test_polars.py +1032 -0
  62. perspective/tests/widget/__init__.py +11 -0
  63. perspective/tests/widget/test_widget.py +278 -0
  64. perspective/tests/widget/test_widget_pandas.py +453 -0
  65. perspective/virtual_servers/__init__.py +142 -0
  66. perspective/virtual_servers/clickhouse.py +240 -0
  67. perspective/virtual_servers/duckdb.py +236 -0
  68. perspective/virtual_servers/polars.py +710 -0
  69. perspective/widget/__init__.py +349 -0
  70. perspective/widget/viewer/__init__.py +15 -0
  71. perspective/widget/viewer/validate.py +22 -0
  72. perspective/widget/viewer/viewer.py +343 -0
  73. perspective/widget/viewer/viewer_traitlets.py +101 -0
  74. perspective_python-4.5.2.dist-info/METADATA +29 -0
  75. perspective_python-4.5.2.dist-info/RECORD +79 -0
  76. perspective_python-4.5.2.dist-info/WHEEL +4 -0
  77. perspective_python-4.5.2.dist-info/licenses/LICENSE.md +193 -0
  78. perspective_python-4.5.2.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +19255 -0
  79. perspective_python-4.5.2.dist-info/sboms/perspective-python.cyclonedx.json +5546 -0
@@ -0,0 +1,240 @@
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
+ import logging
17
+
18
+ from perspective.virtual_servers import VirtualServerHandler
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ NUMBER_AGGS = [
23
+ "sum",
24
+ "count",
25
+ "any_value",
26
+ "arbitrary",
27
+ "array_agg",
28
+ "avg",
29
+ "bit_and",
30
+ "bit_or",
31
+ "bit_xor",
32
+ "bitstring_agg",
33
+ "bool_and",
34
+ "bool_or",
35
+ "countif",
36
+ "favg",
37
+ "fsum",
38
+ "geomean",
39
+ "kahan_sum",
40
+ "last",
41
+ "max",
42
+ "min",
43
+ "product",
44
+ "string_agg",
45
+ "sumkahan",
46
+ ]
47
+
48
+ STRING_AGGS = [
49
+ "count",
50
+ "any_value",
51
+ "arbitrary",
52
+ "first",
53
+ "countif",
54
+ "last",
55
+ "string_agg",
56
+ ]
57
+
58
+ FILTER_OPS = [
59
+ "==",
60
+ "!=",
61
+ "LIKE",
62
+ "IS DISTINCT FROM",
63
+ "IS NOT DISTINCT FROM",
64
+ ">=",
65
+ "<=",
66
+ ">",
67
+ "<",
68
+ ]
69
+
70
+
71
+ class ClickhouseVirtualSession:
72
+ def __init__(self, callback, db):
73
+ self.session = perspective.VirtualServer(ClickhouseVirtualServerHandler(db))
74
+ self.callback = callback
75
+
76
+ def handle_request(self, msg):
77
+ self.callback(self.session.handle_request(msg))
78
+
79
+
80
+ class ClickhouseVirtualServer:
81
+ def __init__(self, db):
82
+ self.db = db
83
+
84
+ def new_session(self, callback):
85
+ return ClickhouseVirtualSession(callback, self.db)
86
+
87
+
88
+ class ClickhouseVirtualServerHandler(VirtualServerHandler):
89
+ """
90
+ An implementation of a `perspective.VirtualServerHandler` for ClickHouse.
91
+ """
92
+
93
+ def __init__(self, db):
94
+ self.db = db
95
+ self.sql_builder = perspective.GenericSQLVirtualServerModel(
96
+ {"create_entity": "VIEW", "grouping_fn": "GROUPING"}
97
+ )
98
+
99
+ def get_features(self):
100
+ return {
101
+ "group_by": True,
102
+ "split_by": False,
103
+ "sort": True,
104
+ "expressions": True,
105
+ "group_rollup_mode": ["rollup", "flat", "total"],
106
+ "filter_ops": {
107
+ "integer": FILTER_OPS,
108
+ "float": FILTER_OPS,
109
+ "string": FILTER_OPS,
110
+ "boolean": FILTER_OPS,
111
+ "date": FILTER_OPS,
112
+ "datetime": FILTER_OPS,
113
+ },
114
+ "aggregates": {
115
+ "integer": NUMBER_AGGS,
116
+ "float": NUMBER_AGGS,
117
+ "string": STRING_AGGS,
118
+ "boolean": STRING_AGGS,
119
+ "date": STRING_AGGS,
120
+ "datetime": STRING_AGGS,
121
+ },
122
+ }
123
+
124
+ def get_hosted_tables(self):
125
+ query = "SHOW TABLES"
126
+ results = run_query(self.db, query)
127
+ return [result[0] for result in results]
128
+
129
+ def table_schema(self, table_name, config=None):
130
+ query = self.sql_builder.table_schema(table_name)
131
+ results = run_query(self.db, query)
132
+ schema = {}
133
+ for result in results:
134
+ col_name = result[0]
135
+ if not col_name.startswith("__"):
136
+ schema[col_name] = clickhouse_type_to_psp(result[1])
137
+
138
+ return schema
139
+
140
+ def view_column_size(self, view_name, config):
141
+ query = f"SELECT COUNT() FROM system.columns WHERE table = '{view_name}'"
142
+ results = run_query(self.db, query)
143
+ gs = len(config["group_by"])
144
+ return results[0][0] - (
145
+ 0 if gs == 0 else gs + (1 if len(config["split_by"]) == 0 else 0)
146
+ )
147
+
148
+ def table_size(self, table_name):
149
+ query = self.sql_builder.table_size(table_name)
150
+ results = run_query(self.db, query)
151
+ return results[0][0]
152
+
153
+ def table_make_view(self, table_name, view_name, config):
154
+ query = self.sql_builder.table_make_view(table_name, view_name, config)
155
+ run_query(self.db, query, execute=True)
156
+
157
+ def table_validate_expression(self, view_name, expression):
158
+ query = self.sql_builder.table_validate_expression(view_name, expression)
159
+ results = run_query(self.db, query)
160
+ return clickhouse_type_to_psp(results[0][1])
161
+
162
+ def view_delete(self, view_name):
163
+ query = self.sql_builder.view_delete(view_name)
164
+ run_query(self.db, query, execute=True)
165
+
166
+ def view_get_min_max(self, view_name, column_name, config):
167
+ query = self.sql_builder.view_get_min_max(view_name, column_name, config)
168
+ results = run_query(self.db, query)
169
+ row = results[0]
170
+ return (row[0], row[1])
171
+
172
+ def view_get_data(self, view_name, config, schema, viewport, data):
173
+ group_by = config["group_by"]
174
+ query = self.sql_builder.view_get_data(view_name, config, viewport, schema)
175
+ results, columns, dtypes = run_query(self.db, query, columns=True)
176
+ for cidx, col in enumerate(columns):
177
+ dtype = clickhouse_type_to_psp(str(dtypes[cidx]))
178
+ for ridx, row in enumerate(results):
179
+ grouping_id = row[0] if len(group_by) > 0 else None
180
+
181
+ value = row[cidx]
182
+ if dtype == "string" and not isinstance(value, str):
183
+ value = str(value)
184
+
185
+ data.set_col(dtype, col, ridx, value, grouping_id)
186
+
187
+
188
+ ################################################################################
189
+ #
190
+ # ClickHouse Utils
191
+
192
+
193
+ def clickhouse_type_to_psp(name):
194
+ """Convert a ClickHouse `dtype` to a Perspective `ColumnType`."""
195
+ if name.startswith("Nullable(") and name.endswith(")"):
196
+ name = name[9:-1]
197
+
198
+ if name.startswith("Array"):
199
+ return "string"
200
+
201
+ if name in ("Int64", "UInt64", "Float64"):
202
+ return "float"
203
+
204
+ if name == "String":
205
+ return "string"
206
+
207
+ if name == "DateTime":
208
+ return "datetime"
209
+
210
+ if name == "Date":
211
+ return "date"
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.command(query)
224
+ else:
225
+ req = db.query(query)
226
+ result = req.result_rows
227
+ except Exception 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 (
235
+ result,
236
+ req.column_names,
237
+ [(x.name if hasattr(x, "name") else str(x)) for x in req.column_types],
238
+ )
239
+ else:
240
+ 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 io
14
+ import duckdb
15
+ import perspective
16
+ import pyarrow.ipc as ipc
17
+
18
+ from datetime import datetime
19
+ import logging
20
+
21
+ from perspective.virtual_servers import VirtualServerHandler
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ NUMBER_AGGS = [
26
+ "sum",
27
+ "count",
28
+ "any_value",
29
+ "arbitrary",
30
+ # "arg_max",
31
+ # "arg_max_null",
32
+ # "arg_min",
33
+ # "arg_min_null",
34
+ "array_agg",
35
+ "avg",
36
+ "bit_and",
37
+ "bit_or",
38
+ "bit_xor",
39
+ "bitstring_agg",
40
+ "bool_and",
41
+ "bool_or",
42
+ "countif",
43
+ "favg",
44
+ "fsum",
45
+ "geomean",
46
+ # "histogram",
47
+ # "histogram_values",
48
+ "kahan_sum",
49
+ "last",
50
+ # "list"
51
+ "max",
52
+ # "max_by"
53
+ "min",
54
+ # "min_by"
55
+ "product",
56
+ "string_agg",
57
+ "sumkahan",
58
+ # "weighted_avg",
59
+ ]
60
+
61
+ STRING_AGGS = [
62
+ "count",
63
+ "any_value",
64
+ "arbitrary",
65
+ "first",
66
+ "countif",
67
+ "last",
68
+ "string_agg",
69
+ ]
70
+
71
+ FILTER_OPS = [
72
+ "==",
73
+ "!=",
74
+ "LIKE",
75
+ "IS DISTINCT FROM",
76
+ "IS NOT DISTINCT FROM",
77
+ ">=",
78
+ "<=",
79
+ ">",
80
+ "<",
81
+ ]
82
+
83
+
84
+ class DuckDBVirtualSession:
85
+ def __init__(self, callback, db):
86
+ self.session = perspective.VirtualServer(DuckDBVirtualServerHandler(db))
87
+ self.callback = callback
88
+
89
+ def handle_request(self, msg):
90
+ self.callback(self.session.handle_request(msg))
91
+
92
+
93
+ class DuckDBVirtualServer:
94
+ def __init__(self, db):
95
+ self.db = db
96
+
97
+ def new_session(self, callback):
98
+ return DuckDBVirtualSession(callback, self.db)
99
+
100
+
101
+ class DuckDBVirtualServerHandler(VirtualServerHandler):
102
+ """
103
+ An implementation of a `perspective.VirtualServerHandler` for DuckDB.
104
+ """
105
+
106
+ def __init__(self, db):
107
+ self.db = db
108
+ self.sql_builder = perspective.GenericSQLVirtualServerModel({})
109
+
110
+ def get_features(self):
111
+ return {
112
+ "group_by": True,
113
+ "split_by": True,
114
+ "sort": True,
115
+ "expressions": True,
116
+ "group_rollup_mode": ["rollup", "flat", "total"],
117
+ "filter_ops": {
118
+ "integer": FILTER_OPS,
119
+ "float": FILTER_OPS,
120
+ "string": FILTER_OPS,
121
+ "boolean": FILTER_OPS,
122
+ "date": FILTER_OPS,
123
+ "datetime": FILTER_OPS,
124
+ },
125
+ "aggregates": {
126
+ "integer": NUMBER_AGGS,
127
+ "float": NUMBER_AGGS,
128
+ "string": STRING_AGGS,
129
+ "boolean": STRING_AGGS,
130
+ "date": STRING_AGGS,
131
+ "datetime": STRING_AGGS,
132
+ },
133
+ }
134
+
135
+ def get_hosted_tables(self):
136
+ query = self.sql_builder.get_hosted_tables()
137
+ results = run_query(self.db, query)
138
+ return [f"{result[0]}.{result[2]}" for result in results]
139
+
140
+ def table_schema(self, table_name, config=None):
141
+ query = self.sql_builder.table_schema(table_name)
142
+ results = run_query(self.db, query)
143
+ schema = {}
144
+ for result in results:
145
+ col_name = result[0]
146
+ if not col_name.startswith("__"):
147
+ schema[col_name] = duckdb_type_to_psp(result[1])
148
+
149
+ return schema
150
+
151
+ def view_column_size(self, table_name, config):
152
+ query = self.sql_builder.view_column_size(table_name)
153
+ results = run_query(self.db, query)
154
+ gs = len(config["group_by"])
155
+ return results[0][0] - (
156
+ 0 if gs == 0 else gs + (1 if len(config["split_by"]) == 0 else 0)
157
+ )
158
+
159
+ def table_size(self, table_name):
160
+ query = self.sql_builder.table_size(table_name)
161
+ results = run_query(self.db, query)
162
+ return results[0][0]
163
+
164
+ def table_make_view(self, table_name, view_name, config):
165
+ query = self.sql_builder.table_make_view(table_name, view_name, config)
166
+ run_query(self.db, query, execute=True)
167
+
168
+ def table_validate_expression(self, view_name, expression):
169
+ query = self.sql_builder.table_validate_expression(view_name, expression)
170
+ results = run_query(self.db, query)
171
+ return duckdb_type_to_psp(results[0][1])
172
+
173
+ def view_delete(self, view_name):
174
+ query = self.sql_builder.view_delete(view_name)
175
+ run_query(self.db, query, execute=True)
176
+
177
+ def view_get_min_max(self, view_name, column_name, config):
178
+ query = self.sql_builder.view_get_min_max(view_name, column_name, config)
179
+ results = run_query(self.db, query)
180
+ row = results[0]
181
+ return (row[0], row[1])
182
+
183
+ def view_get_data(self, view_name, config, schema, viewport, data):
184
+ query = self.sql_builder.view_get_data(view_name, config, viewport, schema)
185
+ result = self.db.sql(query)
186
+ arrow_table = result.fetch_arrow_table()
187
+ buf = io.BytesIO()
188
+ with ipc.new_stream(buf, arrow_table.schema) as writer:
189
+ writer.write_table(arrow_table)
190
+ data.from_arrow_ipc(buf.getvalue())
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