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