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,464 @@
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
+ from decimal import Decimal
15
+
16
+ import pyarrow as pa
17
+ import pyarrow.ipc as ipc
18
+ import pytest
19
+
20
+ import perspective
21
+ from perspective import Client
22
+ from perspective.virtual_servers import VirtualServerHandler
23
+
24
+ FEATURES = {
25
+ "group_by": True,
26
+ "split_by": True,
27
+ "sort": True,
28
+ "group_rollup_mode": ["rollup", "flat", "total"],
29
+ "filter_ops": {
30
+ "integer": ["=="],
31
+ "float": ["=="],
32
+ "string": ["=="],
33
+ "boolean": ["=="],
34
+ "date": ["=="],
35
+ "datetime": ["=="],
36
+ },
37
+ "aggregates": {
38
+ "integer": ["sum"],
39
+ "float": ["sum"],
40
+ "string": ["count"],
41
+ },
42
+ }
43
+
44
+
45
+ class ArrowFixtureHandler(VirtualServerHandler):
46
+ def __init__(self, arrow_table, schema, ipc_bytes=None):
47
+ self.arrow_table = arrow_table
48
+ self.schema = schema
49
+ # `write_table` *drops* zero-row batches, so the only way to serve
50
+ # a stream that carries one is to hand over the bytes.
51
+ self.ipc_bytes = ipc_bytes
52
+
53
+ def get_features(self):
54
+ return FEATURES
55
+
56
+ def get_hosted_tables(self):
57
+ return ["fixture"]
58
+
59
+ def table_schema(self, table_name):
60
+ return self.schema
61
+
62
+ def table_size(self, table_name):
63
+ return self.arrow_table.num_rows
64
+
65
+ def table_make_view(self, table_name, view_name, config):
66
+ pass
67
+
68
+ def view_delete(self, view_name):
69
+ pass
70
+
71
+ def view_get_data(self, view_name, config, schema, viewport, data):
72
+ if self.ipc_bytes is not None:
73
+ data.from_arrow_ipc(self.ipc_bytes)
74
+ return
75
+
76
+ buf = io.BytesIO()
77
+ with ipc.new_stream(buf, self.arrow_table.schema) as writer:
78
+ writer.write_table(self.arrow_table)
79
+
80
+ data.from_arrow_ipc(buf.getvalue())
81
+
82
+
83
+ class SetColFixtureHandler(VirtualServerHandler):
84
+ def __init__(self, row_path, schema):
85
+ self.row_path = row_path
86
+ self.schema = schema
87
+
88
+ def get_features(self):
89
+ return FEATURES
90
+
91
+ def get_hosted_tables(self):
92
+ return ["fixture"]
93
+
94
+ def table_schema(self, table_name):
95
+ return self.schema
96
+
97
+ def table_size(self, table_name):
98
+ return len(self.row_path)
99
+
100
+ def table_make_view(self, table_name, view_name, config):
101
+ pass
102
+
103
+ def view_delete(self, view_name):
104
+ pass
105
+
106
+ def view_get_data(self, view_name, config, schema, viewport, data):
107
+ for row_idx, value in enumerate(self.row_path):
108
+ data.set_col("string", "__ROW_PATH_0__", row_idx, value, 0)
109
+
110
+
111
+ def make_client(handler):
112
+ session = perspective.VirtualServer(handler)
113
+
114
+ def handle_request(msg):
115
+ handle_response(session.handle_request(msg))
116
+
117
+ def handle_response(msg):
118
+ client.handle_response(msg)
119
+
120
+ client = Client(handle_request)
121
+ return client
122
+
123
+
124
+ def arrow_client(arrow_table, schema=None, ipc_bytes=None):
125
+ if schema is None:
126
+ schema = {
127
+ name: "string"
128
+ for name in arrow_table.column_names
129
+ if not name.startswith("__")
130
+ }
131
+
132
+ return make_client(ArrowFixtureHandler(arrow_table, schema, ipc_bytes))
133
+
134
+
135
+ def round_trip(arrow_table, schema=None, **config):
136
+ table = arrow_client(arrow_table, schema).open_table("fixture")
137
+ view = table.view(**config)
138
+ result = view.to_columns()
139
+ view.delete()
140
+ return result
141
+
142
+
143
+ def dictionary(values, indices, index_type, value_type=pa.utf8()):
144
+ return pa.DictionaryArray.from_arrays(
145
+ pa.array(indices, type=index_type),
146
+ pa.array(values, type=value_type),
147
+ )
148
+
149
+
150
+ class TestCoerceSmallIntegers:
151
+ def test_coerce_int8(self):
152
+ table = pa.table({"col": pa.array([-1, 127, None], type=pa.int8())})
153
+ result = round_trip(table, {"col": "integer"})
154
+ assert result["col"] == [-1, 127, None]
155
+
156
+ def test_coerce_int16(self):
157
+ table = pa.table({"col": pa.array([-300, 32000, None], type=pa.int16())})
158
+ result = round_trip(table, {"col": "integer"})
159
+ assert result["col"] == [-300, 32000, None]
160
+
161
+
162
+ class TestCoerceUnsignedIntegers:
163
+ def test_coerce_uint8(self):
164
+ table = pa.table({"col": pa.array([0, 255, None], type=pa.uint8())})
165
+ result = round_trip(table, {"col": "integer"})
166
+ assert result["col"] == [0, 255, None]
167
+
168
+ def test_coerce_uint16(self):
169
+ table = pa.table({"col": pa.array([0, 65535, None], type=pa.uint16())})
170
+ result = round_trip(table, {"col": "integer"})
171
+ assert result["col"] == [0, 65535, None]
172
+
173
+ def test_coerce_uint32(self):
174
+ table = pa.table({"col": pa.array([0, 4_294_967_295, None], type=pa.uint32())})
175
+ result = round_trip(table, {"col": "float"})
176
+ assert result["col"] == [0.0, 4_294_967_295.0, None]
177
+
178
+ def test_coerce_uint64(self):
179
+ table = pa.table({"col": pa.array([0, 1 << 53, None], type=pa.uint64())})
180
+ result = round_trip(table, {"col": "float"})
181
+ assert result["col"] == [0.0, 9_007_199_254_740_992.0, None]
182
+
183
+
184
+ class TestCoerceFloats:
185
+ def test_coerce_float32(self):
186
+ table = pa.table({"col": pa.array([3.14, -0.0, None], type=pa.float32())})
187
+ result = round_trip(table, {"col": "float"})
188
+ assert result["col"][0] == pytest.approx(3.14, abs=0.001)
189
+ assert result["col"][1] == 0.0
190
+ assert result["col"][2] is None
191
+
192
+ def test_coerce_float16(self):
193
+ table = pa.table({"col": pa.array([1.5, -2.0, None], type=pa.float16())})
194
+ result = round_trip(table, {"col": "float"})
195
+ assert result["col"] == [1.5, -2.0, None]
196
+
197
+
198
+ class TestCoerceDecimal:
199
+ def test_coerce_decimal128(self):
200
+ table = pa.table(
201
+ {
202
+ "col": pa.array(
203
+ [Decimal("1.234"), Decimal("-5.678"), None],
204
+ type=pa.decimal128(18, 3),
205
+ )
206
+ }
207
+ )
208
+ result = round_trip(table, {"col": "float"})
209
+ assert result["col"][0] == pytest.approx(1.234)
210
+ assert result["col"][1] == pytest.approx(-5.678)
211
+ assert result["col"][2] is None
212
+
213
+
214
+ class TestCoerceDates:
215
+ def test_coerce_date64(self):
216
+ day = 19738
217
+ table = pa.table({"col": pa.array([day * 86_400_000, None], type=pa.date64())})
218
+ result = round_trip(table, {"col": "date"})
219
+ assert result["col"] == [day * 86_400_000, None]
220
+
221
+ def test_coerce_date32(self):
222
+ day = 19738
223
+ table = pa.table({"col": pa.array([day, None], type=pa.date32())})
224
+ result = round_trip(table, {"col": "date"})
225
+ assert result["col"] == [day * 86_400_000, None]
226
+
227
+
228
+ class TestCoerceTimes:
229
+ def test_coerce_time32_seconds(self):
230
+ table = pa.table({"col": pa.array([3661, None], type=pa.time32("s"))})
231
+ result = round_trip(table, {"col": "datetime"})
232
+ assert result["col"] == [3_661_000, None]
233
+
234
+ def test_coerce_time32_millis(self):
235
+ table = pa.table({"col": pa.array([3_661_000, None], type=pa.time32("ms"))})
236
+ result = round_trip(table, {"col": "datetime"})
237
+ assert result["col"] == [3_661_000, None]
238
+
239
+ def test_coerce_time64_micros(self):
240
+ table = pa.table({"col": pa.array([3_661_000_000, None], type=pa.time64("us"))})
241
+ result = round_trip(table, {"col": "datetime"})
242
+ assert result["col"] == [3_661_000, None]
243
+
244
+ def test_coerce_time64_nanos(self):
245
+ table = pa.table(
246
+ {"col": pa.array([3_661_000_000_000, None], type=pa.time64("ns"))}
247
+ )
248
+ result = round_trip(table, {"col": "datetime"})
249
+ assert result["col"] == [3_661_000, None]
250
+
251
+ @pytest.mark.parametrize(
252
+ "unit,value",
253
+ [
254
+ ("s", 1_700_000_000),
255
+ ("ms", 1_700_000_000_000),
256
+ ("us", 1_700_000_000_000_000),
257
+ ("ns", 1_700_000_000_000_000_000),
258
+ ],
259
+ )
260
+ def test_coerce_timestamp_with_timezone(self, unit, value):
261
+ table = pa.table(
262
+ {"col": pa.array([value, None], type=pa.timestamp(unit, tz="UTC"))}
263
+ )
264
+ result = round_trip(table, {"col": "datetime"})
265
+ assert result["col"] == [1_700_000_000_000, None]
266
+
267
+ def test_coerce_timestamp_seconds(self):
268
+ table = pa.table(
269
+ {"col": pa.array([1_700_000_000, None], type=pa.timestamp("s"))}
270
+ )
271
+ result = round_trip(table, {"col": "datetime"})
272
+ assert result["col"] == [1_700_000_000_000, None]
273
+
274
+
275
+ class TestCoerceStrings:
276
+ def test_coerce_utf8(self):
277
+ table = pa.table({"col": pa.array(["a", None, "c"], type=pa.utf8())})
278
+ result = round_trip(table)
279
+ assert result["col"] == ["a", None, "c"]
280
+
281
+ def test_coerce_large_utf8(self):
282
+ table = pa.table({"col": pa.array(["a", None, "c"], type=pa.large_utf8())})
283
+ result = round_trip(table)
284
+ assert result["col"] == ["a", None, "c"]
285
+
286
+
287
+ class TestFallback:
288
+ def test_fallback_fixed_size_binary(self):
289
+ # No canonical mapping - lossy but total, and warned about.
290
+ table = pa.table(
291
+ {"col": pa.array([b"ab", b"cd", None], type=pa.binary(2))}
292
+ )
293
+ result = round_trip(table)
294
+ assert isinstance(result["col"][0], str)
295
+ assert isinstance(result["col"][1], str)
296
+ assert result["col"][0] != result["col"][1]
297
+ assert result["col"][2] is None
298
+
299
+
300
+ class TestDictionary:
301
+ @pytest.mark.parametrize(
302
+ "index_type",
303
+ [
304
+ pa.int8(),
305
+ pa.int16(),
306
+ pa.int32(),
307
+ pa.int64(),
308
+ pa.uint8(),
309
+ pa.uint16(),
310
+ pa.uint32(),
311
+ pa.uint64(),
312
+ ],
313
+ )
314
+ @pytest.mark.parametrize("value_type", [pa.utf8(), pa.large_utf8()])
315
+ def test_dictionary_keys_and_values(self, index_type, value_type):
316
+ col = dictionary(
317
+ ["alpha", "beta"], [0, 1, 0, None], index_type, value_type
318
+ )
319
+ result = round_trip(pa.table({"col": col}))
320
+ assert result["col"] == ["alpha", "beta", "alpha", None]
321
+
322
+ def test_dictionary_null_value_slot(self):
323
+ col = pa.DictionaryArray.from_arrays(
324
+ pa.array([0, 1], type=pa.int8()),
325
+ pa.array(["alpha", None], type=pa.utf8()),
326
+ )
327
+ result = round_trip(pa.table({"col": col}))
328
+ assert result["col"] == ["alpha", None]
329
+
330
+ def test_dictionary_of_integers(self):
331
+ col = pa.DictionaryArray.from_arrays(
332
+ pa.array([0, 1, 0], type=pa.uint8()),
333
+ pa.array([-7, 42], type=pa.int16()),
334
+ )
335
+ result = round_trip(pa.table({"col": col}), {"col": "integer"})
336
+ assert result["col"] == [-7, 42, -7]
337
+
338
+ def test_dictionary_group_by(self):
339
+ table = pa.table(
340
+ {
341
+ "__GROUPING_ID__": pa.array([1, 0, 0], type=pa.uint64()),
342
+ "__ROW_PATH_0__": dictionary(
343
+ ["alpha", "beta"], [None, 0, 1], pa.uint8()
344
+ ),
345
+ "Sales": pa.array([3.0, 1.0, 2.0], type=pa.float64()),
346
+ }
347
+ )
348
+
349
+ result = round_trip(
350
+ table,
351
+ {"Region": "string", "Sales": "float"},
352
+ group_by=["Region"],
353
+ columns=["Sales"],
354
+ )
355
+
356
+ assert result["__ROW_PATH__"] == [[], ["alpha"], ["beta"]]
357
+ assert result["Sales"] == [3.0, 1.0, 2.0]
358
+
359
+
360
+ class TestGroupingId:
361
+ def test_unsigned_grouping_id(self):
362
+ table = pa.table(
363
+ {
364
+ "__GROUPING_ID__": pa.array([1, 0, 0], type=pa.uint64()),
365
+ "__ROW_PATH_0__": pa.array([None, "alpha", "beta"], type=pa.utf8()),
366
+ "Sales": pa.array([3.0, 1.0, 2.0], type=pa.float64()),
367
+ }
368
+ )
369
+
370
+ result = round_trip(
371
+ table,
372
+ {"Region": "string", "Sales": "float"},
373
+ group_by=["Region"],
374
+ columns=["Sales"],
375
+ )
376
+
377
+ assert result["__ROW_PATH__"] == [[], ["alpha"], ["beta"]]
378
+
379
+ def test_decimal_row_path(self):
380
+ table = pa.table(
381
+ {
382
+ "__GROUPING_ID__": pa.array([1, 0], type=pa.int64()),
383
+ "__ROW_PATH_0__": pa.array(
384
+ [None, Decimal("1.500")], type=pa.decimal128(18, 3)
385
+ ),
386
+ "Sales": pa.array([3.0, 3.0], type=pa.float64()),
387
+ }
388
+ )
389
+
390
+ result = round_trip(
391
+ table,
392
+ {"Price": "float", "Sales": "float"},
393
+ group_by=["Price"],
394
+ columns=["Sales"],
395
+ )
396
+
397
+ assert result["__ROW_PATH__"] == [[], [1.5]]
398
+
399
+
400
+ class TestEmpty:
401
+ def test_zero_row_batch(self):
402
+ # One batch, no rows - written explicitly, since `write_table`
403
+ # would drop it.
404
+ schema = pa.schema([("col", pa.int32())])
405
+ buf = io.BytesIO()
406
+ writer = ipc.RecordBatchStreamWriter(buf, schema)
407
+ writer.write_batch(pa.record_batch([[]], schema=schema))
408
+ writer.close()
409
+
410
+ table = pa.Table.from_batches([], schema)
411
+ client = arrow_client(table, {"col": "integer"}, ipc_bytes=buf.getvalue())
412
+ view = client.open_table("fixture").view()
413
+ assert view.to_columns()["col"] == []
414
+ view.delete()
415
+
416
+ def test_no_batches_at_all(self):
417
+ schema = pa.schema([("col", pa.int32())])
418
+ table = pa.Table.from_batches([], schema)
419
+ result = round_trip(table, {"col": "integer"})
420
+ assert result["col"] == []
421
+
422
+ def test_row_path_only_batch(self):
423
+ table = pa.table(
424
+ {
425
+ "__GROUPING_ID__": pa.array([1, 0, 0], type=pa.int64()),
426
+ "__ROW_PATH_0__": pa.array([None, "alpha", "beta"], type=pa.utf8()),
427
+ }
428
+ )
429
+
430
+ client = arrow_client(table, {"Region": "string"})
431
+ view = client.open_table("fixture").view(group_by=["Region"], columns=[])
432
+ assert view.to_json() == [
433
+ {"__ROW_PATH__": []},
434
+ {"__ROW_PATH__": ["alpha"]},
435
+ {"__ROW_PATH__": ["beta"]},
436
+ ]
437
+
438
+ view.delete()
439
+
440
+ def test_metadata_only_batch_preserves_row_count(self):
441
+ table = pa.table({"__GROUPING_ID__": pa.array([0, 0, 0], type=pa.int64())})
442
+ client = arrow_client(table, {"Region": "string"})
443
+ view = client.open_table("fixture").view(columns=[])
444
+ assert view.to_json() == [{}, {}, {}]
445
+ view.delete()
446
+
447
+ def test_set_col_row_path_only(self):
448
+ handler = SetColFixtureHandler(["alpha", "beta"], {"Region": "string"})
449
+ view = make_client(handler).open_table("fixture").view(
450
+ group_by=["Region"], columns=[]
451
+ )
452
+
453
+ assert view.to_json() == [
454
+ {"__ROW_PATH__": ["alpha"]},
455
+ {"__ROW_PATH__": ["beta"]},
456
+ ]
457
+
458
+ view.delete()
459
+
460
+
461
+ class TestNotConstructible:
462
+ def test_virtual_data_slice_is_not_constructible(self):
463
+ with pytest.raises(TypeError):
464
+ perspective.VirtualDataSlice()