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,1032 @@
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 os
14
+ import tempfile
15
+ import pytest
16
+ import polars as pl
17
+
18
+ from perspective import Client
19
+ from perspective.virtual_servers.polars import PolarsVirtualServer
20
+ import urllib
21
+
22
+
23
+ def approx_json(expected):
24
+ """Wrap float values in pytest.approx for approximate comparison."""
25
+ return [
26
+ {k: pytest.approx(v) if isinstance(v, float) else v for k, v in row.items()}
27
+ for row in expected
28
+ ]
29
+
30
+
31
+ _SUPERSTORE_LOCAL = os.path.join(
32
+ os.path.dirname(__file__),
33
+ "..",
34
+ "..",
35
+ "..",
36
+ "node_modules",
37
+ "superstore-arrow",
38
+ "superstore.parquet",
39
+ )
40
+
41
+ _SUPERSTORE_URL = (
42
+ "https://cdn.jsdelivr.net/npm/superstore-arrow@3.2.0/superstore.parquet"
43
+ )
44
+
45
+
46
+ def _get_superstore_parquet():
47
+ if os.path.exists(_SUPERSTORE_LOCAL):
48
+ return _SUPERSTORE_LOCAL
49
+ path = os.path.join(tempfile.gettempdir(), "superstore.parquet")
50
+ if not os.path.exists(path):
51
+ urllib.request.urlretrieve(_SUPERSTORE_URL, path)
52
+ return path
53
+
54
+
55
+ SUPERSTORE_PARQUET = _get_superstore_parquet()
56
+
57
+
58
+ @pytest.fixture(scope="module")
59
+ def client():
60
+ df = pl.read_parquet(SUPERSTORE_PARQUET)
61
+ server = PolarsVirtualServer({"superstore": df})
62
+
63
+ def handle_request(msg):
64
+ session.handle_request(msg)
65
+
66
+ def handle_response(msg):
67
+ c.handle_response(msg)
68
+
69
+ session = server.new_session(handle_response)
70
+ c = Client(handle_request)
71
+ return c
72
+
73
+
74
+ class TestPolarsClient:
75
+ def test_get_hosted_table_names(self, client):
76
+ tables = client.get_hosted_table_names()
77
+ assert tables == ["superstore"]
78
+
79
+
80
+ class TestPolarsTable:
81
+ def test_schema(self, client):
82
+ table = client.open_table("superstore")
83
+ schema = table.schema()
84
+ assert schema == {
85
+ "Product Name": "string",
86
+ "Ship Date": "date",
87
+ "City": "string",
88
+ "Row ID": "integer",
89
+ "Customer Name": "string",
90
+ "Quantity": "integer",
91
+ "Discount": "float",
92
+ "Sub-Category": "string",
93
+ "Segment": "string",
94
+ "Category": "string",
95
+ "Order Date": "date",
96
+ "Order ID": "string",
97
+ "Sales": "float",
98
+ "State": "string",
99
+ "Postal Code": "float",
100
+ "Country": "string",
101
+ "Customer ID": "string",
102
+ "Ship Mode": "string",
103
+ "Region": "string",
104
+ "Profit": "float",
105
+ "Product ID": "string",
106
+ }
107
+
108
+ def test_columns(self, client):
109
+ table = client.open_table("superstore")
110
+ columns = table.columns()
111
+ assert columns == [
112
+ "Row ID",
113
+ "Order ID",
114
+ "Order Date",
115
+ "Ship Date",
116
+ "Ship Mode",
117
+ "Customer ID",
118
+ "Customer Name",
119
+ "Segment",
120
+ "Country",
121
+ "City",
122
+ "State",
123
+ "Postal Code",
124
+ "Region",
125
+ "Product ID",
126
+ "Category",
127
+ "Sub-Category",
128
+ "Product Name",
129
+ "Sales",
130
+ "Quantity",
131
+ "Discount",
132
+ "Profit",
133
+ ]
134
+
135
+ def test_size(self, client):
136
+ table = client.open_table("superstore")
137
+ size = table.size()
138
+ assert size == 9994
139
+
140
+
141
+ class TestPolarsView:
142
+ def test_num_rows(self, client):
143
+ table = client.open_table("superstore")
144
+ view = table.view(columns=["Sales", "Profit"])
145
+ num_rows = view.num_rows()
146
+ assert num_rows == 9994
147
+ view.delete()
148
+
149
+ def test_num_columns(self, client):
150
+ table = client.open_table("superstore")
151
+ view = table.view(columns=["Sales", "Profit", "State"])
152
+ num_columns = view.num_columns()
153
+ assert num_columns == 3
154
+ view.delete()
155
+
156
+ def test_schema(self, client):
157
+ table = client.open_table("superstore")
158
+ view = table.view(columns=["Sales", "Profit", "State"])
159
+ schema = view.schema()
160
+ assert schema == {
161
+ "Sales": "float",
162
+ "Profit": "float",
163
+ "State": "string",
164
+ }
165
+ view.delete()
166
+
167
+ def test_to_json(self, client):
168
+ table = client.open_table("superstore")
169
+ view = table.view(columns=["Sales", "Quantity"])
170
+ json = view.to_json(start_row=0, end_row=5)
171
+ assert json == [
172
+ {"Sales": 261.96, "Quantity": 2},
173
+ {"Sales": 731.94, "Quantity": 3},
174
+ {"Sales": 14.62, "Quantity": 2},
175
+ {"Sales": 957.5775, "Quantity": 5},
176
+ {"Sales": 22.368, "Quantity": 2},
177
+ ]
178
+ view.delete()
179
+
180
+ def test_to_columns(self, client):
181
+ table = client.open_table("superstore")
182
+ view = table.view(columns=["Sales", "Quantity"])
183
+ columns = view.to_columns(start_row=0, end_row=5)
184
+ assert columns == {
185
+ "Sales": [261.96, 731.94, 14.62, 957.5775, 22.368],
186
+ "Quantity": [2, 3, 2, 5, 2],
187
+ }
188
+ view.delete()
189
+
190
+ def test_column_paths(self, client):
191
+ table = client.open_table("superstore")
192
+ view = table.view(columns=["Sales", "Profit", "State"])
193
+ paths = view.column_paths()
194
+ assert paths == ["Sales", "Profit", "State"]
195
+ view.delete()
196
+
197
+
198
+ class TestPolarsGroupBy:
199
+ def test_single_group_by(self, client):
200
+ table = client.open_table("superstore")
201
+ view = table.view(
202
+ columns=["Sales"],
203
+ group_by=["Region"],
204
+ aggregates={"Sales": "sum"},
205
+ )
206
+ num_rows = view.num_rows()
207
+ assert num_rows == 5
208
+ json = view.to_json()
209
+ assert json == approx_json(
210
+ [
211
+ {"__ROW_PATH__": [], "Sales": 2297200.860299955},
212
+ {"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
213
+ {"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
214
+ {"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
215
+ {"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
216
+ ]
217
+ )
218
+ view.delete()
219
+
220
+ def test_multi_level_group_by(self, client):
221
+ table = client.open_table("superstore")
222
+ view = table.view(
223
+ columns=["Sales"],
224
+ group_by=["Region", "Category"],
225
+ aggregates={"Sales": "sum"},
226
+ )
227
+ json = view.to_json()
228
+ assert json == approx_json(
229
+ [
230
+ {"__ROW_PATH__": [], "Sales": 2297200.860299955},
231
+ {"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
232
+ {"__ROW_PATH__": ["Central", "Furniture"], "Sales": 163797.16380000004},
233
+ {
234
+ "__ROW_PATH__": ["Central", "Office Supplies"],
235
+ "Sales": 167026.41500000027,
236
+ },
237
+ {"__ROW_PATH__": ["Central", "Technology"], "Sales": 170416.3119999999},
238
+ {"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
239
+ {"__ROW_PATH__": ["East", "Furniture"], "Sales": 208291.20400000009},
240
+ {
241
+ "__ROW_PATH__": ["East", "Office Supplies"],
242
+ "Sales": 205516.0549999999,
243
+ },
244
+ {"__ROW_PATH__": ["East", "Technology"], "Sales": 264973.9810000003},
245
+ {"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
246
+ {"__ROW_PATH__": ["South", "Furniture"], "Sales": 117298.6840000001},
247
+ {
248
+ "__ROW_PATH__": ["South", "Office Supplies"],
249
+ "Sales": 125651.31299999992,
250
+ },
251
+ {"__ROW_PATH__": ["South", "Technology"], "Sales": 148771.9079999999},
252
+ {"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
253
+ {"__ROW_PATH__": ["West", "Furniture"], "Sales": 252612.7435000003},
254
+ {
255
+ "__ROW_PATH__": ["West", "Office Supplies"],
256
+ "Sales": 220853.24900000007,
257
+ },
258
+ {"__ROW_PATH__": ["West", "Technology"], "Sales": 251991.83199999997},
259
+ ]
260
+ )
261
+ view.delete()
262
+
263
+ def test_group_by_with_count_aggregate(self, client):
264
+ table = client.open_table("superstore")
265
+ view = table.view(
266
+ columns=["Sales"],
267
+ group_by=["Region"],
268
+ aggregates={"Sales": "count"},
269
+ )
270
+ json = view.to_json()
271
+ assert json == [
272
+ {"__ROW_PATH__": [], "Sales": 9994},
273
+ {"__ROW_PATH__": ["Central"], "Sales": 2323},
274
+ {"__ROW_PATH__": ["East"], "Sales": 2848},
275
+ {"__ROW_PATH__": ["South"], "Sales": 1620},
276
+ {"__ROW_PATH__": ["West"], "Sales": 3203},
277
+ ]
278
+ view.delete()
279
+
280
+ def test_group_by_with_avg_aggregate(self, client):
281
+ table = client.open_table("superstore")
282
+ view = table.view(
283
+ columns=["Sales"],
284
+ group_by=["Category"],
285
+ aggregates={"Sales": "avg"},
286
+ )
287
+ json = view.to_json()
288
+ assert json == approx_json(
289
+ [
290
+ {"__ROW_PATH__": [], "Sales": 229.8580008304938},
291
+ {"__ROW_PATH__": ["Furniture"], "Sales": 349.83488698727007},
292
+ {"__ROW_PATH__": ["Office Supplies"], "Sales": 119.32410089611732},
293
+ {"__ROW_PATH__": ["Technology"], "Sales": 452.70927612344155},
294
+ ]
295
+ )
296
+ view.delete()
297
+
298
+ def test_group_by_with_min_aggregate(self, client):
299
+ table = client.open_table("superstore")
300
+ view = table.view(
301
+ columns=["Quantity"],
302
+ group_by=["Region"],
303
+ aggregates={"Quantity": "min"},
304
+ )
305
+ json = view.to_json()
306
+ assert json == [
307
+ {"__ROW_PATH__": [], "Quantity": 1},
308
+ {"__ROW_PATH__": ["Central"], "Quantity": 1},
309
+ {"__ROW_PATH__": ["East"], "Quantity": 1},
310
+ {"__ROW_PATH__": ["South"], "Quantity": 1},
311
+ {"__ROW_PATH__": ["West"], "Quantity": 1},
312
+ ]
313
+ view.delete()
314
+
315
+ def test_group_by_with_max_aggregate(self, client):
316
+ table = client.open_table("superstore")
317
+ view = table.view(
318
+ columns=["Quantity"],
319
+ group_by=["Region"],
320
+ aggregates={"Quantity": "max"},
321
+ )
322
+ json = view.to_json()
323
+ assert json == [
324
+ {"__ROW_PATH__": [], "Quantity": 14},
325
+ {"__ROW_PATH__": ["Central"], "Quantity": 14},
326
+ {"__ROW_PATH__": ["East"], "Quantity": 14},
327
+ {"__ROW_PATH__": ["South"], "Quantity": 14},
328
+ {"__ROW_PATH__": ["West"], "Quantity": 14},
329
+ ]
330
+ view.delete()
331
+
332
+
333
+ class TestPolarsFilter:
334
+ def test_filter_with_equals(self, client):
335
+ table = client.open_table("superstore")
336
+ view = table.view(
337
+ columns=["Sales", "Region"],
338
+ filter=[["Region", "==", "West"]],
339
+ )
340
+ json = view.to_json(start_row=0, end_row=5)
341
+ assert json == [
342
+ {"Sales": 14.62, "Region": "West"},
343
+ {"Sales": 48.86, "Region": "West"},
344
+ {"Sales": 7.28, "Region": "West"},
345
+ {"Sales": 907.152, "Region": "West"},
346
+ {"Sales": 18.504, "Region": "West"},
347
+ ]
348
+ view.delete()
349
+
350
+ def test_filter_with_not_equals(self, client):
351
+ table = client.open_table("superstore")
352
+ view = table.view(
353
+ columns=["Sales", "Region"],
354
+ filter=[["Region", "!=", "West"]],
355
+ )
356
+ json = view.to_json(start_row=0, end_row=5)
357
+ assert json == [
358
+ {"Sales": 261.96, "Region": "South"},
359
+ {"Sales": 731.94, "Region": "South"},
360
+ {"Sales": 957.5775, "Region": "South"},
361
+ {"Sales": 22.368, "Region": "South"},
362
+ {"Sales": 15.552, "Region": "South"},
363
+ ]
364
+ view.delete()
365
+
366
+ def test_filter_with_greater_than(self, client):
367
+ table = client.open_table("superstore")
368
+ view = table.view(
369
+ columns=["Sales", "Quantity"],
370
+ filter=[["Quantity", ">", 5]],
371
+ )
372
+ json = view.to_json(start_row=0, end_row=5)
373
+ assert json == [
374
+ {"Sales": 48.86, "Quantity": 7},
375
+ {"Sales": 907.152, "Quantity": 6},
376
+ {"Sales": 1706.184, "Quantity": 9},
377
+ {"Sales": 665.88, "Quantity": 6},
378
+ {"Sales": 19.46, "Quantity": 7},
379
+ ]
380
+ view.delete()
381
+
382
+ def test_filter_with_less_than(self, client):
383
+ table = client.open_table("superstore")
384
+ view = table.view(
385
+ columns=["Sales", "Quantity"],
386
+ filter=[["Quantity", "<", 3]],
387
+ )
388
+ json = view.to_json(start_row=0, end_row=5)
389
+ assert json == [
390
+ {"Sales": 261.96, "Quantity": 2},
391
+ {"Sales": 14.62, "Quantity": 2},
392
+ {"Sales": 22.368, "Quantity": 2},
393
+ {"Sales": 55.5, "Quantity": 2},
394
+ {"Sales": 8.56, "Quantity": 2},
395
+ ]
396
+ view.delete()
397
+
398
+ def test_filter_with_greater_than_or_equal(self, client):
399
+ table = client.open_table("superstore")
400
+ view = table.view(
401
+ columns=["Sales", "Quantity"],
402
+ filter=[["Quantity", ">=", 10]],
403
+ )
404
+ json = view.to_json(start_row=0, end_row=5)
405
+ assert json == [
406
+ {"Sales": 40.096, "Quantity": 14},
407
+ {"Sales": 43.12, "Quantity": 14},
408
+ {"Sales": 384.45, "Quantity": 11},
409
+ {"Sales": 3347.37, "Quantity": 13},
410
+ {"Sales": 100.24, "Quantity": 10},
411
+ ]
412
+ view.delete()
413
+
414
+ def test_filter_with_less_than_or_equal(self, client):
415
+ table = client.open_table("superstore")
416
+ view = table.view(
417
+ columns=["Sales", "Quantity"],
418
+ filter=[["Quantity", "<=", 2]],
419
+ )
420
+ json = view.to_json(start_row=0, end_row=5)
421
+ assert json == [
422
+ {"Sales": 261.96, "Quantity": 2},
423
+ {"Sales": 14.62, "Quantity": 2},
424
+ {"Sales": 22.368, "Quantity": 2},
425
+ {"Sales": 55.5, "Quantity": 2},
426
+ {"Sales": 8.56, "Quantity": 2},
427
+ ]
428
+ view.delete()
429
+
430
+ def test_multiple_filters(self, client):
431
+ table = client.open_table("superstore")
432
+ view = table.view(
433
+ columns=["Sales", "Region", "Quantity"],
434
+ filter=[
435
+ ["Region", "==", "West"],
436
+ ["Quantity", ">", 3],
437
+ ],
438
+ )
439
+ json = view.to_json(start_row=0, end_row=5)
440
+ assert json == [
441
+ {"Sales": 48.86, "Region": "West", "Quantity": 7},
442
+ {"Sales": 7.28, "Region": "West", "Quantity": 4},
443
+ {"Sales": 907.152, "Region": "West", "Quantity": 6},
444
+ {"Sales": 114.9, "Region": "West", "Quantity": 5},
445
+ {"Sales": 1706.184, "Region": "West", "Quantity": 9},
446
+ ]
447
+ view.delete()
448
+
449
+ def test_filter_with_group_by(self, client):
450
+ table = client.open_table("superstore")
451
+ view = table.view(
452
+ columns=["Sales"],
453
+ group_by=["Category"],
454
+ filter=[["Region", "==", "West"]],
455
+ aggregates={"Sales": "sum"},
456
+ )
457
+ num_rows = view.num_rows()
458
+ assert num_rows == 4
459
+ json = view.to_json()
460
+ assert json == approx_json(
461
+ [
462
+ {"__ROW_PATH__": [], "Sales": 725457.8245000006},
463
+ {"__ROW_PATH__": ["Furniture"], "Sales": 252612.7435000003},
464
+ {"__ROW_PATH__": ["Office Supplies"], "Sales": 220853.24900000007},
465
+ {"__ROW_PATH__": ["Technology"], "Sales": 251991.83199999997},
466
+ ]
467
+ )
468
+ view.delete()
469
+
470
+
471
+ class TestPolarsSplitBy:
472
+ def test_single_split_by(self, client):
473
+ table = client.open_table("superstore")
474
+ view = table.view(
475
+ columns=["Sales"],
476
+ split_by=["Region"],
477
+ group_by=["Category"],
478
+ aggregates={"Sales": "sum"},
479
+ )
480
+
481
+ column_paths = view.column_paths()
482
+ assert column_paths == [
483
+ "Central_Sales",
484
+ "East_Sales",
485
+ "South_Sales",
486
+ "West_Sales",
487
+ ]
488
+
489
+ json = view.to_json()
490
+ assert json == approx_json(
491
+ [
492
+ {
493
+ "__ROW_PATH__": [],
494
+ "Central|Sales": 501239.8908000005,
495
+ "East|Sales": 678781.2399999979,
496
+ "South|Sales": 391721.9050000003,
497
+ "West|Sales": 725457.8245000006,
498
+ },
499
+ {
500
+ "__ROW_PATH__": ["Furniture"],
501
+ "Central|Sales": 163797.16380000004,
502
+ "East|Sales": 208291.20400000009,
503
+ "South|Sales": 117298.6840000001,
504
+ "West|Sales": 252612.7435000003,
505
+ },
506
+ {
507
+ "__ROW_PATH__": ["Office Supplies"],
508
+ "Central|Sales": 167026.41500000027,
509
+ "East|Sales": 205516.0549999999,
510
+ "South|Sales": 125651.31299999992,
511
+ "West|Sales": 220853.24900000007,
512
+ },
513
+ {
514
+ "__ROW_PATH__": ["Technology"],
515
+ "Central|Sales": 170416.3119999999,
516
+ "East|Sales": 264973.9810000003,
517
+ "South|Sales": 148771.9079999999,
518
+ "West|Sales": 251991.83199999997,
519
+ },
520
+ ]
521
+ )
522
+ view.delete()
523
+
524
+ def test_split_by_without_group_by(self, client):
525
+ table = client.open_table("superstore")
526
+ view = table.view(
527
+ columns=["Sales"],
528
+ split_by=["Category"],
529
+ )
530
+ paths = view.column_paths()
531
+ assert any("Furniture" in c for c in paths)
532
+ assert any("Office Supplies" in c for c in paths)
533
+ assert any("Technology" in c for c in paths)
534
+ view.delete()
535
+
536
+
537
+ class TestPolarsSort:
538
+ def test_sort_ascending(self, client):
539
+ table = client.open_table("superstore")
540
+ view = table.view(
541
+ columns=["Sales", "Quantity"],
542
+ sort=[["Sales", "asc"]],
543
+ )
544
+ json = view.to_json(start_row=0, end_row=5)
545
+ assert json == [
546
+ {"Sales": 0.444, "Quantity": 1},
547
+ {"Sales": 0.556, "Quantity": 1},
548
+ {"Sales": 0.836, "Quantity": 1},
549
+ {"Sales": 0.852, "Quantity": 1},
550
+ {"Sales": 0.876, "Quantity": 1},
551
+ ]
552
+ view.delete()
553
+
554
+ def test_sort_descending(self, client):
555
+ table = client.open_table("superstore")
556
+ view = table.view(
557
+ columns=["Sales", "Quantity"],
558
+ sort=[["Sales", "desc"]],
559
+ )
560
+ json = view.to_json(start_row=0, end_row=5)
561
+ assert json == [
562
+ {"Sales": 22638.48, "Quantity": 6},
563
+ {"Sales": 17499.95, "Quantity": 5},
564
+ {"Sales": 13999.96, "Quantity": 4},
565
+ {"Sales": 11199.968, "Quantity": 4},
566
+ {"Sales": 10499.97, "Quantity": 3},
567
+ ]
568
+ view.delete()
569
+
570
+ def test_sort_with_group_by(self, client):
571
+ table = client.open_table("superstore")
572
+ view = table.view(
573
+ columns=["Sales"],
574
+ group_by=["Region"],
575
+ sort=[["Sales", "desc"]],
576
+ aggregates={"Sales": "sum"},
577
+ )
578
+ json = view.to_json()
579
+ assert json == approx_json(
580
+ [
581
+ {"__ROW_PATH__": [], "Sales": 2297200.860299955},
582
+ {"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
583
+ {"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
584
+ {"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
585
+ {"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
586
+ ]
587
+ )
588
+ view.delete()
589
+
590
+ def test_multi_column_sort(self, client):
591
+ table = client.open_table("superstore")
592
+ view = table.view(
593
+ columns=["Region", "Sales", "Quantity"],
594
+ sort=[
595
+ ["Region", "asc"],
596
+ ["Sales", "desc"],
597
+ ],
598
+ )
599
+ json = view.to_json(start_row=0, end_row=5)
600
+ assert json == [
601
+ {"Region": "Central", "Sales": 17499.95, "Quantity": 5},
602
+ {"Region": "Central", "Sales": 9892.74, "Quantity": 13},
603
+ {"Region": "Central", "Sales": 9449.95, "Quantity": 5},
604
+ {"Region": "Central", "Sales": 8159.952, "Quantity": 8},
605
+ {"Region": "Central", "Sales": 5443.96, "Quantity": 4},
606
+ ]
607
+ view.delete()
608
+
609
+
610
+ class TestPolarsViewport:
611
+ def test_start_row_and_end_row(self, client):
612
+ table = client.open_table("superstore")
613
+ view = table.view(columns=["Sales", "Profit"])
614
+ json = view.to_json(start_row=10, end_row=15)
615
+ assert json == [
616
+ {"Sales": 1706.184, "Profit": 85.3092},
617
+ {"Sales": 911.424, "Profit": 68.3568},
618
+ {"Sales": 15.552, "Profit": 5.4432},
619
+ {"Sales": 407.976, "Profit": 132.5922},
620
+ {"Sales": 68.81, "Profit": -123.858},
621
+ ]
622
+ view.delete()
623
+
624
+ def test_start_col_and_end_col(self, client):
625
+ table = client.open_table("superstore")
626
+ view = table.view(
627
+ columns=["Sales", "Profit", "Quantity", "Discount"],
628
+ )
629
+ json = view.to_json(start_row=0, end_row=5, start_col=1, end_col=3)
630
+ assert json == [
631
+ {"Profit": 41.9136, "Quantity": 2},
632
+ {"Profit": 219.582, "Quantity": 3},
633
+ {"Profit": 6.8714, "Quantity": 2},
634
+ {"Profit": -383.031, "Quantity": 5},
635
+ {"Profit": 2.5164, "Quantity": 2},
636
+ ]
637
+ view.delete()
638
+
639
+
640
+ class TestPolarsDataTypes:
641
+ def test_integer_columns(self, client):
642
+ table = client.open_table("superstore")
643
+ view = table.view(columns=["Quantity"])
644
+ json = view.to_json(start_row=0, end_row=5)
645
+ assert json == [
646
+ {"Quantity": 2},
647
+ {"Quantity": 3},
648
+ {"Quantity": 2},
649
+ {"Quantity": 5},
650
+ {"Quantity": 2},
651
+ ]
652
+ view.delete()
653
+
654
+ def test_float_columns(self, client):
655
+ table = client.open_table("superstore")
656
+ view = table.view(columns=["Sales", "Profit"])
657
+ json = view.to_json(start_row=0, end_row=5)
658
+ assert json == [
659
+ {"Sales": 261.96, "Profit": 41.9136},
660
+ {"Sales": 731.94, "Profit": 219.582},
661
+ {"Sales": 14.62, "Profit": 6.8714},
662
+ {"Sales": 957.5775, "Profit": -383.031},
663
+ {"Sales": 22.368, "Profit": 2.5164},
664
+ ]
665
+ view.delete()
666
+
667
+ def test_string_columns(self, client):
668
+ table = client.open_table("superstore")
669
+ view = table.view(columns=["Region", "State", "City"])
670
+ json = view.to_json(start_row=0, end_row=5)
671
+ assert json == [
672
+ {"Region": "South", "State": "Kentucky", "City": "Henderson"},
673
+ {"Region": "South", "State": "Kentucky", "City": "Henderson"},
674
+ {"Region": "West", "State": "California", "City": "Los Angeles"},
675
+ {"Region": "South", "State": "Florida", "City": "Fort Lauderdale"},
676
+ {"Region": "South", "State": "Florida", "City": "Fort Lauderdale"},
677
+ ]
678
+ view.delete()
679
+
680
+ def test_date_columns(self, client):
681
+ table = client.open_table("superstore")
682
+ view = table.view(columns=["Order Date"])
683
+ json = view.to_json(start_row=0, end_row=5)
684
+ assert json == [
685
+ {"Order Date": 1478563200000},
686
+ {"Order Date": 1478563200000},
687
+ {"Order Date": 1465689600000},
688
+ {"Order Date": 1444521600000},
689
+ {"Order Date": 1444521600000},
690
+ ]
691
+ view.delete()
692
+
693
+
694
+ class TestPolarsGroupRollupMode:
695
+ def test_flat_mode_with_group_by(self, client):
696
+ table = client.open_table("superstore")
697
+ view = table.view(
698
+ columns=["Sales"],
699
+ group_by=["Region"],
700
+ aggregates={"Sales": "sum"},
701
+ group_rollup_mode="flat",
702
+ )
703
+ num_rows = view.num_rows()
704
+ assert num_rows == 4
705
+ json = view.to_json()
706
+ assert json == approx_json(
707
+ [
708
+ {"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
709
+ {"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
710
+ {"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
711
+ {"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
712
+ ]
713
+ )
714
+ view.delete()
715
+
716
+ def test_flat_mode_with_multi_level_group_by(self, client):
717
+ table = client.open_table("superstore")
718
+ view = table.view(
719
+ columns=["Sales"],
720
+ group_by=["Region", "Category"],
721
+ aggregates={"Sales": "sum"},
722
+ group_rollup_mode="flat",
723
+ )
724
+ num_rows = view.num_rows()
725
+ assert num_rows == 12
726
+ json = view.to_json()
727
+ assert json == approx_json(
728
+ [
729
+ {"__ROW_PATH__": ["Central", "Furniture"], "Sales": 163797.16380000004},
730
+ {
731
+ "__ROW_PATH__": ["Central", "Office Supplies"],
732
+ "Sales": 167026.41500000027,
733
+ },
734
+ {"__ROW_PATH__": ["Central", "Technology"], "Sales": 170416.3119999999},
735
+ {"__ROW_PATH__": ["East", "Furniture"], "Sales": 208291.20400000009},
736
+ {
737
+ "__ROW_PATH__": ["East", "Office Supplies"],
738
+ "Sales": 205516.0549999999,
739
+ },
740
+ {"__ROW_PATH__": ["East", "Technology"], "Sales": 264973.9810000003},
741
+ {"__ROW_PATH__": ["South", "Furniture"], "Sales": 117298.6840000001},
742
+ {
743
+ "__ROW_PATH__": ["South", "Office Supplies"],
744
+ "Sales": 125651.31299999992,
745
+ },
746
+ {"__ROW_PATH__": ["South", "Technology"], "Sales": 148771.9079999999},
747
+ {"__ROW_PATH__": ["West", "Furniture"], "Sales": 252612.7435000003},
748
+ {
749
+ "__ROW_PATH__": ["West", "Office Supplies"],
750
+ "Sales": 220853.24900000007,
751
+ },
752
+ {"__ROW_PATH__": ["West", "Technology"], "Sales": 251991.83199999997},
753
+ ]
754
+ )
755
+ view.delete()
756
+
757
+ def test_flat_mode_with_group_by_and_split_by(self, client):
758
+ table = client.open_table("superstore")
759
+ view = table.view(
760
+ columns=["Sales"],
761
+ group_by=["Category"],
762
+ split_by=["Region"],
763
+ aggregates={"Sales": "sum"},
764
+ group_rollup_mode="flat",
765
+ )
766
+ num_rows = view.num_rows()
767
+ assert num_rows == 3
768
+ json = view.to_json()
769
+ assert json == approx_json(
770
+ [
771
+ {
772
+ "__ROW_PATH__": ["Furniture"],
773
+ "Central|Sales": 163797.16380000004,
774
+ "East|Sales": 208291.20400000009,
775
+ "South|Sales": 117298.6840000001,
776
+ "West|Sales": 252612.7435000003,
777
+ },
778
+ {
779
+ "__ROW_PATH__": ["Office Supplies"],
780
+ "Central|Sales": 167026.41500000027,
781
+ "East|Sales": 205516.0549999999,
782
+ "South|Sales": 125651.31299999992,
783
+ "West|Sales": 220853.24900000007,
784
+ },
785
+ {
786
+ "__ROW_PATH__": ["Technology"],
787
+ "Central|Sales": 170416.3119999999,
788
+ "East|Sales": 264973.9810000003,
789
+ "South|Sales": 148771.9079999999,
790
+ "West|Sales": 251991.83199999997,
791
+ },
792
+ ]
793
+ )
794
+ view.delete()
795
+
796
+ def test_flat_mode_with_group_by_and_split_by_and_sort(self, client):
797
+ table = client.open_table("superstore")
798
+ view = table.view(
799
+ columns=["Sales"],
800
+ group_by=["Category"],
801
+ split_by=["Region"],
802
+ sort=[["Sales", "desc"]],
803
+ aggregates={"Sales": "sum"},
804
+ group_rollup_mode="flat",
805
+ )
806
+ num_rows = view.num_rows()
807
+ assert num_rows == 3
808
+ json = view.to_json()
809
+ assert json == approx_json(
810
+ [
811
+ {
812
+ "__ROW_PATH__": ["Technology"],
813
+ "Central|Sales": 170416.3119999999,
814
+ "East|Sales": 264973.9810000003,
815
+ "South|Sales": 148771.9079999999,
816
+ "West|Sales": 251991.83199999997,
817
+ },
818
+ {
819
+ "__ROW_PATH__": ["Furniture"],
820
+ "Central|Sales": 163797.16380000004,
821
+ "East|Sales": 208291.20400000009,
822
+ "South|Sales": 117298.6840000001,
823
+ "West|Sales": 252612.7435000003,
824
+ },
825
+ {
826
+ "__ROW_PATH__": ["Office Supplies"],
827
+ "Central|Sales": 167026.41500000027,
828
+ "East|Sales": 205516.0549999999,
829
+ "South|Sales": 125651.31299999992,
830
+ "West|Sales": 220853.24900000007,
831
+ },
832
+ ]
833
+ )
834
+ view.delete()
835
+
836
+ def test_flat_mode_with_group_by_and_sort(self, client):
837
+ table = client.open_table("superstore")
838
+ view = table.view(
839
+ columns=["Sales"],
840
+ group_by=["Region"],
841
+ sort=[["Sales", "desc"]],
842
+ aggregates={"Sales": "sum"},
843
+ group_rollup_mode="flat",
844
+ )
845
+ json = view.to_json()
846
+ assert json == approx_json(
847
+ [
848
+ {"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
849
+ {"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
850
+ {"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
851
+ {"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
852
+ ]
853
+ )
854
+ view.delete()
855
+
856
+ def test_total_mode(self, client):
857
+ table = client.open_table("superstore")
858
+ view = table.view(
859
+ columns=["Sales"],
860
+ aggregates={"Sales": "sum"},
861
+ group_rollup_mode="total",
862
+ )
863
+ num_rows = view.num_rows()
864
+ assert num_rows == 1
865
+ json = view.to_json()
866
+ assert json == approx_json([{"Sales": 2297200.860299955}])
867
+ view.delete()
868
+
869
+ def test_total_mode_with_multiple_columns(self, client):
870
+ table = client.open_table("superstore")
871
+ view = table.view(
872
+ columns=["Sales", "Quantity"],
873
+ aggregates={"Sales": "sum", "Quantity": "sum"},
874
+ group_rollup_mode="total",
875
+ )
876
+ json = view.to_json()
877
+ assert json == approx_json(
878
+ [{"Sales": 2297200.860299955, "Quantity": 37873}]
879
+ )
880
+ view.delete()
881
+
882
+ def test_total_mode_with_split_by(self, client):
883
+ table = client.open_table("superstore")
884
+ view = table.view(
885
+ columns=["Sales"],
886
+ split_by=["Region"],
887
+ aggregates={"Sales": "sum"},
888
+ group_rollup_mode="total",
889
+ )
890
+ num_rows = view.num_rows()
891
+ assert num_rows == 1
892
+ json = view.to_json()
893
+ assert json == approx_json(
894
+ [
895
+ {
896
+ "Central|Sales": 501239.8908000005,
897
+ "East|Sales": 678781.2399999979,
898
+ "South|Sales": 391721.9050000003,
899
+ "West|Sales": 725457.8245000006,
900
+ }
901
+ ]
902
+ )
903
+ view.delete()
904
+
905
+
906
+ class TestPolarsCombinedOperations:
907
+ def test_group_by_filter_sort(self, client):
908
+ table = client.open_table("superstore")
909
+ view = table.view(
910
+ columns=["Sales"],
911
+ group_by=["Category"],
912
+ filter=[["Region", "==", "West"]],
913
+ sort=[["Sales", "desc"]],
914
+ aggregates={"Sales": "sum"},
915
+ )
916
+ json = view.to_json()
917
+ assert json == approx_json(
918
+ [
919
+ {"__ROW_PATH__": [], "Sales": 725457.8245000006},
920
+ {"__ROW_PATH__": ["Furniture"], "Sales": 252612.7435000003},
921
+ {"__ROW_PATH__": ["Technology"], "Sales": 251991.83199999997},
922
+ {"__ROW_PATH__": ["Office Supplies"], "Sales": 220853.24900000007},
923
+ ]
924
+ )
925
+ view.delete()
926
+
927
+ def test_split_by_group_by_filter(self, client):
928
+ table = client.open_table("superstore")
929
+ view = table.view(
930
+ columns=["Sales"],
931
+ group_by=["Category"],
932
+ split_by=["Region"],
933
+ filter=[["Quantity", ">", 3]],
934
+ aggregates={"Sales": "sum"},
935
+ )
936
+
937
+ paths = view.column_paths()
938
+ assert paths == [
939
+ "Central_Sales",
940
+ "East_Sales",
941
+ "South_Sales",
942
+ "West_Sales",
943
+ ]
944
+
945
+ num_rows = view.num_rows()
946
+ assert num_rows == 4
947
+
948
+ json = view.to_json()
949
+ assert json == approx_json(
950
+ [
951
+ {
952
+ "__ROW_PATH__": [],
953
+ "Central|Sales": 332883.0567999998,
954
+ "East|Sales": 455143.735,
955
+ "South|Sales": 274208.7699999999,
956
+ "West|Sales": 470561.28350000136,
957
+ },
958
+ {
959
+ "__ROW_PATH__": ["Furniture"],
960
+ "Central|Sales": 111457.73279999988,
961
+ "East|Sales": 140376.95899999997,
962
+ "South|Sales": 80859.618,
963
+ "West|Sales": 165219.5734999998,
964
+ },
965
+ {
966
+ "__ROW_PATH__": ["Office Supplies"],
967
+ "Central|Sales": 103937.78599999992,
968
+ "East|Sales": 135823.893,
969
+ "South|Sales": 84393.3579999999,
970
+ "West|Sales": 140206.93099999975,
971
+ },
972
+ {
973
+ "__ROW_PATH__": ["Technology"],
974
+ "Central|Sales": 117487.53800000002,
975
+ "East|Sales": 178942.883,
976
+ "South|Sales": 108955.79400000005,
977
+ "West|Sales": 165134.77900000007,
978
+ },
979
+ ]
980
+ )
981
+ view.delete()
982
+
983
+ def test_split_by_only(self, client):
984
+ table = client.open_table("superstore")
985
+ view = table.view(
986
+ columns=["Sales"],
987
+ split_by=["Region"],
988
+ filter=[["Quantity", ">", 3]],
989
+ )
990
+
991
+ paths = view.column_paths()
992
+ assert paths == [
993
+ "Central_Sales",
994
+ "East_Sales",
995
+ "South_Sales",
996
+ "West_Sales",
997
+ ]
998
+
999
+ num_rows = view.num_rows()
1000
+ assert num_rows == 4284
1001
+
1002
+ json = view.to_json(start_row=0, end_row=1)
1003
+ assert json == [
1004
+ {
1005
+ "Central|Sales": None,
1006
+ "East|Sales": None,
1007
+ "South|Sales": 957.5775,
1008
+ "West|Sales": None,
1009
+ },
1010
+ ]
1011
+ view.delete()
1012
+
1013
+ def test_expressions_group_by_sort(self, client):
1014
+ table = client.open_table("superstore")
1015
+ view = table.view(
1016
+ columns=["profitmargin"],
1017
+ group_by=["Region"],
1018
+ expressions={"profitmargin": '"Profit" / "Sales" * 100'},
1019
+ sort=[["profitmargin", "desc"]],
1020
+ aggregates={"profitmargin": "avg"},
1021
+ )
1022
+ json = view.to_json()
1023
+ assert json == approx_json(
1024
+ [
1025
+ {"__ROW_PATH__": [], "profitmargin": 12.031392972104467},
1026
+ {"__ROW_PATH__": ["West"], "profitmargin": 21.948661793784012},
1027
+ {"__ROW_PATH__": ["East"], "profitmargin": 16.722695960406636},
1028
+ {"__ROW_PATH__": ["South"], "profitmargin": 16.35190329218107},
1029
+ {"__ROW_PATH__": ["Central"], "profitmargin": -10.407293926323575},
1030
+ ]
1031
+ )
1032
+ view.delete()