perspective-python 5.6.0__cp311-abi3-pyemscripten_2026_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 (90) 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 +39 -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/column_paths.py +33 -0
  13. perspective/tests/conftest.py +276 -0
  14. perspective/tests/core/__init__.py +11 -0
  15. perspective/tests/core/test_async.py +351 -0
  16. perspective/tests/multi_threaded/__init__.py +11 -0
  17. perspective/tests/multi_threaded/test_derived_threaded.py +103 -0
  18. perspective/tests/multi_threaded/test_multi_threaded.py +293 -0
  19. perspective/tests/server/__init__.py +11 -0
  20. perspective/tests/server/test_server.py +1016 -0
  21. perspective/tests/server/test_session.py +110 -0
  22. perspective/tests/table/__init__.py +11 -0
  23. perspective/tests/table/arrow/date32.arrow +0 -0
  24. perspective/tests/table/arrow/date64.arrow +0 -0
  25. perspective/tests/table/arrow/dict.arrow +0 -0
  26. perspective/tests/table/arrow/dict_update.arrow +0 -0
  27. perspective/tests/table/arrow/int_float_str.arrow +0 -0
  28. perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
  29. perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
  30. perspective/tests/table/object_sequence.py +402 -0
  31. perspective/tests/table/test_column_paths.py +88 -0
  32. perspective/tests/table/test_delete.py +124 -0
  33. perspective/tests/table/test_derived.py +145 -0
  34. perspective/tests/table/test_exception.py +73 -0
  35. perspective/tests/table/test_join.py +115 -0
  36. perspective/tests/table/test_leaks.py +105 -0
  37. perspective/tests/table/test_on_remove.py +140 -0
  38. perspective/tests/table/test_ports.py +178 -0
  39. perspective/tests/table/test_remove.py +102 -0
  40. perspective/tests/table/test_table.py +641 -0
  41. perspective/tests/table/test_table_arrow.py +500 -0
  42. perspective/tests/table/test_table_arrow_nested.py +535 -0
  43. perspective/tests/table/test_table_datetime.py +2544 -0
  44. perspective/tests/table/test_table_infer.py +201 -0
  45. perspective/tests/table/test_table_limit.py +45 -0
  46. perspective/tests/table/test_table_numpy.py +1022 -0
  47. perspective/tests/table/test_table_page_to_disk.py +304 -0
  48. perspective/tests/table/test_table_pandas.py +1018 -0
  49. perspective/tests/table/test_table_polars.py +251 -0
  50. perspective/tests/table/test_table_view_table.py +130 -0
  51. perspective/tests/table/test_to_arrow.py +416 -0
  52. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  53. perspective/tests/table/test_to_arrow_zstd.py +41 -0
  54. perspective/tests/table/test_to_format.py +1030 -0
  55. perspective/tests/table/test_to_polars.py +26 -0
  56. perspective/tests/table/test_update.py +561 -0
  57. perspective/tests/table/test_update_arrow.py +980 -0
  58. perspective/tests/table/test_update_pandas.py +211 -0
  59. perspective/tests/table/test_view.py +2503 -0
  60. perspective/tests/table/test_view_expression.py +2256 -0
  61. perspective/tests/test_dependencies.py +53 -0
  62. perspective/tests/viewer/__init__.py +11 -0
  63. perspective/tests/viewer/test_viewer.py +246 -0
  64. perspective/tests/virtual_servers/__init__.py +12 -0
  65. perspective/tests/virtual_servers/test_coerce_types.py +464 -0
  66. perspective/tests/virtual_servers/test_duckdb.py +1280 -0
  67. perspective/tests/virtual_servers/test_polars.py +1075 -0
  68. perspective/tests/virtual_servers/test_postgres.py +964 -0
  69. perspective/tests/widget/__init__.py +11 -0
  70. perspective/tests/widget/test_anywidget_smoke.py +109 -0
  71. perspective/tests/widget/test_widget.py +278 -0
  72. perspective/tests/widget/test_widget_html_export.py +113 -0
  73. perspective/tests/widget/test_widget_pandas.py +453 -0
  74. perspective/virtual_servers/__init__.py +206 -0
  75. perspective/virtual_servers/clickhouse.py +296 -0
  76. perspective/virtual_servers/duckdb.py +367 -0
  77. perspective/virtual_servers/polars.py +750 -0
  78. perspective/virtual_servers/postgres.py +448 -0
  79. perspective/widget/__init__.py +351 -0
  80. perspective/widget/static/perspective-anywidget.css +1 -0
  81. perspective/widget/static/perspective-anywidget.js +31 -0
  82. perspective/widget/viewer/__init__.py +15 -0
  83. perspective/widget/viewer/viewer.py +355 -0
  84. perspective/widget/viewer/viewer_traitlets.py +98 -0
  85. perspective_python-5.6.0.dist-info/METADATA +127 -0
  86. perspective_python-5.6.0.dist-info/RECORD +90 -0
  87. perspective_python-5.6.0.dist-info/WHEEL +4 -0
  88. perspective_python-5.6.0.dist-info/licenses/LICENSE.md +193 -0
  89. perspective_python-5.6.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +16075 -0
  90. perspective_python-5.6.0.dist-info/sboms/perspective-python.cyclonedx.json +5334 -0
@@ -0,0 +1,453 @@
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 pandas as pd
14
+ import numpy as np
15
+ import pytest
16
+ from perspective.widget import PerspectiveWidget
17
+ import perspective as psp
18
+
19
+ client = psp.Server().new_local_client()
20
+ Table = client.table
21
+
22
+
23
+ pytest.skip(allow_module_level=True)
24
+
25
+
26
+ class TestWidgetPandas:
27
+ def test_widget_load_table_df(self, superstore):
28
+ table = Table(superstore)
29
+ widget = PerspectiveWidget(table)
30
+ assert widget.table.schema() == {
31
+ "index": "integer",
32
+ "Country": "string",
33
+ "Region": "string",
34
+ "Category": "string",
35
+ "City": "string",
36
+ "Customer ID": "string",
37
+ "Discount": "float",
38
+ "Order Date": "date",
39
+ "Order ID": "string",
40
+ "Postal Code": "string",
41
+ "Product ID": "string",
42
+ "Profit": "float",
43
+ "Quantity": "integer",
44
+ "Row ID": "integer",
45
+ "Sales": "integer",
46
+ "Segment": "string",
47
+ "Ship Date": "date",
48
+ "Ship Mode": "string",
49
+ "State": "string",
50
+ "Sub-Category": "string",
51
+ }
52
+
53
+ assert sorted(widget.columns) == sorted(
54
+ [
55
+ "index",
56
+ "Category",
57
+ "City",
58
+ "Country",
59
+ "Customer ID",
60
+ "Discount",
61
+ "Order Date",
62
+ "Order ID",
63
+ "Postal Code",
64
+ "Product ID",
65
+ "Profit",
66
+ "Quantity",
67
+ "Region",
68
+ "Row ID",
69
+ "Sales",
70
+ "Segment",
71
+ "Ship Date",
72
+ "Ship Mode",
73
+ "State",
74
+ "Sub-Category",
75
+ ]
76
+ )
77
+ view = widget.table.view()
78
+ assert view.num_rows() == len(superstore)
79
+ assert view.num_columns() == len(superstore.columns) + 1 # index
80
+
81
+ def test_widget_load_data_df(self, superstore):
82
+ widget = PerspectiveWidget(superstore)
83
+ assert sorted(widget.columns) == sorted(
84
+ [
85
+ "index",
86
+ "Category",
87
+ "City",
88
+ "Country",
89
+ "Customer ID",
90
+ "Discount",
91
+ "Order Date",
92
+ "Order ID",
93
+ "Postal Code",
94
+ "Product ID",
95
+ "Profit",
96
+ "Quantity",
97
+ "Region",
98
+ "Row ID",
99
+ "Sales",
100
+ "Segment",
101
+ "Ship Date",
102
+ "Ship Mode",
103
+ "State",
104
+ "Sub-Category",
105
+ ]
106
+ )
107
+ view = widget.table.view()
108
+ assert view.num_rows() == len(superstore)
109
+ assert view.num_columns() == 20
110
+
111
+ def test_widget_load_series(self, superstore):
112
+ series = pd.Series(superstore["Profit"].values, name="profit")
113
+ widget = PerspectiveWidget(series)
114
+ assert widget.table.schema() == {"index": "integer", "profit": "float"}
115
+
116
+ assert sorted(widget.columns) == sorted(["index", "profit"])
117
+ view = widget.table.view()
118
+ assert view.num_rows() == len(superstore)
119
+ assert view.num_columns() == 2
120
+
121
+ def test_widget_load_pivot_table(self, superstore):
122
+ pivot_table = pd.pivot_table(
123
+ superstore,
124
+ values="Discount",
125
+ index=["Country", "Region"],
126
+ columns=["Category", "Segment"],
127
+ )
128
+ widget = PerspectiveWidget(pivot_table)
129
+ assert widget.group_by == ["Country", "Region"]
130
+ assert widget.split_by == ["Category", "Segment"]
131
+ assert widget.columns == ["value"]
132
+ # table should host flattened data
133
+ view = widget.table.view()
134
+ assert view.num_rows() == 60
135
+ assert view.num_columns() == 6
136
+
137
+ def test_widget_load_pivot_table_with_user_pivots(self, superstore):
138
+ pivot_table = pd.pivot_table(
139
+ superstore,
140
+ values="Discount",
141
+ index=["Country", "Region"],
142
+ columns="Category",
143
+ )
144
+ widget = PerspectiveWidget(pivot_table, group_by=["Category", "Segment"])
145
+ assert widget.group_by == ["Category", "Segment"]
146
+ assert widget.split_by == []
147
+ assert widget.columns == [
148
+ "index",
149
+ "Country",
150
+ "Region",
151
+ "Financials",
152
+ "Industrials",
153
+ "Technology",
154
+ ]
155
+ # table should host flattened data
156
+ view = widget.table.view()
157
+ assert view.num_rows() == 5
158
+ assert view.num_columns() == 6
159
+
160
+ def test_widget_load_group_by(self, superstore):
161
+ df_pivoted = superstore.set_index(["Country", "Region"])
162
+ widget = PerspectiveWidget(df_pivoted)
163
+ assert widget.group_by == ["Country", "Region"]
164
+ assert widget.split_by == []
165
+ assert sorted(widget.columns) == sorted(
166
+ [
167
+ "index",
168
+ "Category",
169
+ "Country",
170
+ "City",
171
+ "Customer ID",
172
+ "Discount",
173
+ "Order Date",
174
+ "Order ID",
175
+ "Postal Code",
176
+ "Product ID",
177
+ "Profit",
178
+ "Quantity",
179
+ "Region",
180
+ "Row ID",
181
+ "Sales",
182
+ "Segment",
183
+ "Ship Date",
184
+ "Ship Mode",
185
+ "State",
186
+ "Sub-Category",
187
+ ]
188
+ )
189
+ assert widget.table.size() == 100
190
+ view = widget.table.view()
191
+ assert view.num_rows() == len(superstore)
192
+ assert view.num_columns() == len(superstore.columns) + 1 # index
193
+
194
+ def test_widget_load_group_by_with_user_pivots(self, superstore):
195
+ df_pivoted = superstore.set_index(["Country", "Region"])
196
+ widget = PerspectiveWidget(df_pivoted, group_by=["Category", "Segment"])
197
+ assert widget.group_by == ["Category", "Segment"]
198
+ assert widget.split_by == []
199
+ assert sorted(widget.columns) == sorted(
200
+ [
201
+ "index",
202
+ "Category",
203
+ "Country",
204
+ "City",
205
+ "Customer ID",
206
+ "Discount",
207
+ "Order Date",
208
+ "Order ID",
209
+ "Postal Code",
210
+ "Product ID",
211
+ "Profit",
212
+ "Quantity",
213
+ "Region",
214
+ "Row ID",
215
+ "Sales",
216
+ "Segment",
217
+ "Ship Date",
218
+ "Ship Mode",
219
+ "State",
220
+ "Sub-Category",
221
+ ]
222
+ )
223
+ assert widget.table.size() == 100
224
+ view = widget.table.view()
225
+ assert view.num_rows() == len(superstore)
226
+ assert view.num_columns() == len(superstore.columns) + 1 # index
227
+
228
+ def test_widget_load_split_by(self, superstore):
229
+ arrays = [
230
+ np.array(
231
+ [
232
+ "bar",
233
+ "bar",
234
+ "bar",
235
+ "bar",
236
+ "baz",
237
+ "baz",
238
+ "baz",
239
+ "baz",
240
+ "foo",
241
+ "foo",
242
+ "foo",
243
+ "foo",
244
+ "qux",
245
+ "qux",
246
+ "qux",
247
+ "qux",
248
+ ]
249
+ ),
250
+ np.array(
251
+ [
252
+ "one",
253
+ "one",
254
+ "two",
255
+ "two",
256
+ "one",
257
+ "one",
258
+ "two",
259
+ "two",
260
+ "one",
261
+ "one",
262
+ "two",
263
+ "two",
264
+ "one",
265
+ "one",
266
+ "two",
267
+ "two",
268
+ ]
269
+ ),
270
+ np.array(
271
+ [
272
+ "X",
273
+ "Y",
274
+ "X",
275
+ "Y",
276
+ "X",
277
+ "Y",
278
+ "X",
279
+ "Y",
280
+ "X",
281
+ "Y",
282
+ "X",
283
+ "Y",
284
+ "X",
285
+ "Y",
286
+ "X",
287
+ "Y",
288
+ ]
289
+ ),
290
+ ]
291
+ tuples = list(zip(*arrays))
292
+ index = pd.MultiIndex.from_tuples(tuples, names=["first", "second", "third"])
293
+ df_both = pd.DataFrame(
294
+ np.random.randn(3, 16), index=["A", "B", "C"], columns=index
295
+ )
296
+ widget = PerspectiveWidget(df_both)
297
+ assert widget.columns == ["value"]
298
+ assert widget.split_by == ["first", "second", "third"]
299
+ assert widget.group_by == ["index"]
300
+
301
+ def test_widget_load_split_by_preserve_user_settings(self, superstore):
302
+ arrays = [
303
+ np.array(
304
+ [
305
+ "bar",
306
+ "bar",
307
+ "bar",
308
+ "bar",
309
+ "baz",
310
+ "baz",
311
+ "baz",
312
+ "baz",
313
+ "foo",
314
+ "foo",
315
+ "foo",
316
+ "foo",
317
+ "qux",
318
+ "qux",
319
+ "qux",
320
+ "qux",
321
+ ]
322
+ ),
323
+ np.array(
324
+ [
325
+ "one",
326
+ "one",
327
+ "two",
328
+ "two",
329
+ "one",
330
+ "one",
331
+ "two",
332
+ "two",
333
+ "one",
334
+ "one",
335
+ "two",
336
+ "two",
337
+ "one",
338
+ "one",
339
+ "two",
340
+ "two",
341
+ ]
342
+ ),
343
+ np.array(
344
+ [
345
+ "X",
346
+ "Y",
347
+ "X",
348
+ "Y",
349
+ "X",
350
+ "Y",
351
+ "X",
352
+ "Y",
353
+ "X",
354
+ "Y",
355
+ "X",
356
+ "Y",
357
+ "X",
358
+ "Y",
359
+ "X",
360
+ "Y",
361
+ ]
362
+ ),
363
+ ]
364
+ tuples = list(zip(*arrays))
365
+ index = pd.MultiIndex.from_tuples(tuples, names=["first", "second", "third"])
366
+ df_both = pd.DataFrame(
367
+ np.random.randn(3, 16), index=["A", "B", "C"], columns=index
368
+ )
369
+ widget = PerspectiveWidget(df_both, columns=["first", "third"])
370
+ assert widget.columns == ["first", "third"]
371
+ assert widget.split_by == ["first", "second", "third"]
372
+ assert widget.group_by == ["index"]
373
+
374
+ def test_pivottable_values_index(self, superstore):
375
+ arrays = {
376
+ "A": [
377
+ "bar",
378
+ "bar",
379
+ "bar",
380
+ "bar",
381
+ "baz",
382
+ "baz",
383
+ "baz",
384
+ "baz",
385
+ "foo",
386
+ "foo",
387
+ "foo",
388
+ "foo",
389
+ "qux",
390
+ "qux",
391
+ "qux",
392
+ "qux",
393
+ ],
394
+ "B": [
395
+ "one",
396
+ "one",
397
+ "two",
398
+ "two",
399
+ "one",
400
+ "one",
401
+ "two",
402
+ "two",
403
+ "one",
404
+ "one",
405
+ "two",
406
+ "two",
407
+ "one",
408
+ "one",
409
+ "two",
410
+ "two",
411
+ ],
412
+ "C": [
413
+ "X",
414
+ "Y",
415
+ "X",
416
+ "Y",
417
+ "X",
418
+ "Y",
419
+ "X",
420
+ "Y",
421
+ "X",
422
+ "Y",
423
+ "X",
424
+ "Y",
425
+ "X",
426
+ "Y",
427
+ "X",
428
+ "Y",
429
+ ],
430
+ "D": np.arange(16),
431
+ }
432
+
433
+ df = pd.DataFrame(arrays)
434
+ df_pivot = df.pivot_table(
435
+ values=["D"], index=["A"], columns=["B", "C"], aggfunc={"D": "count"}
436
+ )
437
+ widget = PerspectiveWidget(df_pivot)
438
+ assert widget.columns == ["value"]
439
+ assert widget.split_by == ["B", "C"]
440
+ assert widget.group_by == ["A"]
441
+
442
+ def test_pivottable_multi_values(self, superstore):
443
+ pt = pd.pivot_table(
444
+ superstore,
445
+ values=["Discount", "Sales"],
446
+ index=["Country", "Region"],
447
+ aggfunc={"Discount": "count", "Sales": "sum"},
448
+ columns=["State", "Quantity"],
449
+ )
450
+ widget = PerspectiveWidget(pt)
451
+ assert widget.columns == ["Discount", "Sales"]
452
+ assert widget.split_by == ["State", "Quantity"]
453
+ assert widget.group_by == ["Country", "Region"]
@@ -0,0 +1,206 @@
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
+
14
+ class VirtualServerHandler:
15
+ """
16
+ An interface for implementing a Perspective `VirtualServer`. It operates
17
+ thusly:
18
+
19
+ - A table is selected by name (validated via `get_hosted_tables`).
20
+
21
+ - The UI will ask the model to create a temporary table with the results
22
+ of querying this table with a specific query `config`, a simple struct
23
+ which reflects the UI configurable fields (see `get_features`).
24
+
25
+ - The UI will query slices of the temporary table as it needs them to
26
+ render. This may be a rectangular slice, a whole column or the entire
27
+ set, and it is returned from teh model via a custom push-only
28
+ struct `PerspectiveColumn` for now, though in the future we will support
29
+ e.g. Polars and other arrow-native formats directly.
30
+
31
+ - The UI will delete its own temporary tables via `view_delete` but it is
32
+ ok for them to die intermittently, the UI will recover automatically.
33
+ """
34
+
35
+ def get_features(self):
36
+ """
37
+ [OPTIONAL] Toggle UI features through data model support. For example,
38
+ setting `"group_by": False` would hide the "Group By" UI control, as
39
+ well as prevent this field from appearing in `config` dicts later
40
+ provided to `table_make_view`.
41
+
42
+ This API defaults to just "columns", e.g. a simple flat datagrid in
43
+ which you can just scroll, select and format columns.
44
+
45
+ # Example
46
+
47
+ ```python
48
+ return {
49
+ "group_by": True,
50
+ "split_by": True,
51
+ "sort": True,
52
+ "expressions": True,
53
+ "filter_ops": {
54
+ "integer": ["==", "<"],
55
+ },
56
+ "aggregates": {
57
+ "string": ["count"],
58
+ "float": ["count", "sum"],
59
+ },
60
+ }
61
+ ```
62
+ """
63
+
64
+ pass
65
+
66
+ def get_hosted_tables(self) -> list[str]:
67
+ """
68
+ List of `Table` names available to query from.
69
+ """
70
+
71
+ pass
72
+
73
+ def table_schema(self, table_name):
74
+ """
75
+ Get the _Perspective Schema_ for a `Table`, a mapping of column name to
76
+ Perspective column types, a simplified set of six visually-relevant
77
+ types mapped from DuckDB's much richer type system. Optionally,
78
+ a model may also implement `view_schema` which describes temporary
79
+ tables, but for DuckDB this method is identical.
80
+ """
81
+
82
+ pass
83
+
84
+ def table_size(self, table_name):
85
+ """
86
+ Get a table's row count. Optionally, a model may also implement the
87
+ `view_size` method to get the row count for temporary tables, but for
88
+ DuckDB this method is identical.
89
+ """
90
+
91
+ pass
92
+
93
+ def view_schema(self, view_name, config):
94
+ return self.table_schema(view_name)
95
+
96
+ def view_size(self, view_name):
97
+ return self.table_size(view_name)
98
+
99
+ def table_make_view(self, table_name, view_name, config):
100
+ """
101
+ Create a temporary table `view_name` from the results of querying
102
+ `table_name` with a query configuration `config`.
103
+ """
104
+
105
+ pass
106
+
107
+ def table_describe(self, table_name, config):
108
+ """
109
+ Validate a complete query configuration `config` against `table_name`
110
+ and report the schema `table_make_view` would create, without creating
111
+ it.
112
+ """
113
+
114
+ pass
115
+
116
+ def view_delete(self, view_name):
117
+ """
118
+ Delete a temporary table. The UI will do this automatically, and it
119
+ can recover.
120
+ """
121
+
122
+ pass
123
+
124
+ def view_get_min_max(self, view_name, column_name, config):
125
+ """
126
+ [OPTIONAL] Get the min and max values of a column in a view.
127
+ Returns a tuple of (min, max) as native Python values.
128
+ """
129
+
130
+ pass
131
+
132
+ def view_get_data(self, view_name, config, viewport, data):
133
+ """
134
+ Serialize a rectangular slice `viewport` from temporary table
135
+ `view_name`, into the `PerspectiveColumn` serialization API injected
136
+ via `data`. The push-only `PerspectiveColumn` type can handle casting
137
+ Python types as input, but once a type is pushed to a column name it
138
+ must not be changed.
139
+ """
140
+
141
+ pass
142
+
143
+
144
+
145
+ def describe_via_make_view(handler, table_name, config):
146
+ """A `table_describe` that builds a temporary table, reads its schema and
147
+ deletes it."""
148
+ view_name = f"__psp_describe_{id(config)}__"
149
+ try:
150
+ handler.table_make_view(table_name, view_name, config)
151
+ except Exception as e:
152
+ return {"config_error": str(e)}
153
+
154
+ try:
155
+ view_schema = handler.view_schema(view_name, config)
156
+ finally:
157
+ handler.view_delete(view_name)
158
+
159
+ expression_schema = {
160
+ name: view_schema[name]
161
+ for name in config.get("expressions", {})
162
+ if name in view_schema
163
+ }
164
+
165
+ return {"expression_schema": expression_schema, "view_schema": view_schema}
166
+
167
+
168
+ def sql_table_describe(sql_builder, table_name, config, describe_query, schema=None):
169
+ """The `table_describe` flow shared by the SQL models."""
170
+ expression_schema = {}
171
+ query = sql_builder.expressions_describe(table_name, config)
172
+ if query is not None:
173
+ try:
174
+ expression_schema = describe_query(query)
175
+ except Exception as error:
176
+ expression_errors = {}
177
+ for name, expression in config.get("expressions", {}).items():
178
+ try:
179
+ single = describe_query(
180
+ sql_builder.expression_describe(table_name, expression)
181
+ )
182
+ expression_schema[name] = next(iter(single.values()))
183
+ except Exception as e:
184
+ expression_errors[name] = {
185
+ "error_message": str(e),
186
+ "line": 0,
187
+ "column": 0,
188
+ }
189
+
190
+ if not expression_errors:
191
+ return {"config_error": str(error)}
192
+
193
+ return {
194
+ "expression_schema": expression_schema,
195
+ "expression_errors": expression_errors,
196
+ }
197
+
198
+ view_schema = {}
199
+ query = sql_builder.table_describe(table_name, config, schema)
200
+ if query is not None:
201
+ try:
202
+ view_schema = describe_query(query)
203
+ except Exception as error:
204
+ return {"config_error": str(error)}
205
+
206
+ return {"expression_schema": expression_schema, "view_schema": view_schema}