pyspiral 0.8.9__cp311-abi3-macosx_11_0_arm64.whl → 0.9.9__cp311-abi3-macosx_11_0_arm64.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.
- {pyspiral-0.8.9.dist-info → pyspiral-0.9.9.dist-info}/METADATA +4 -2
- {pyspiral-0.8.9.dist-info → pyspiral-0.9.9.dist-info}/RECORD +39 -34
- spiral/__init__.py +3 -2
- spiral/_lib.abi3.so +0 -0
- spiral/api/__init__.py +7 -0
- spiral/api/client.py +86 -8
- spiral/api/projects.py +4 -2
- spiral/api/tables.py +77 -0
- spiral/arrow_.py +4 -155
- spiral/cli/app.py +10 -4
- spiral/cli/chooser.py +30 -0
- spiral/cli/fs.py +3 -2
- spiral/cli/iceberg.py +1 -1
- spiral/cli/key_spaces.py +4 -4
- spiral/cli/orgs.py +1 -1
- spiral/cli/projects.py +2 -2
- spiral/cli/tables.py +47 -20
- spiral/cli/telemetry.py +13 -6
- spiral/cli/text.py +4 -4
- spiral/cli/transactions.py +84 -0
- spiral/cli/{types.py → types_.py} +6 -6
- spiral/cli/workloads.py +4 -4
- spiral/client.py +70 -8
- spiral/core/client/__init__.pyi +25 -16
- spiral/core/table/__init__.pyi +24 -22
- spiral/debug/manifests.py +21 -9
- spiral/debug/scan.py +4 -6
- spiral/demo.py +145 -38
- spiral/enrichment.py +18 -23
- spiral/expressions/__init__.py +3 -75
- spiral/expressions/base.py +5 -10
- spiral/huggingface.py +456 -0
- spiral/input.py +131 -0
- spiral/ray_.py +75 -0
- spiral/scan.py +218 -64
- spiral/table.py +5 -4
- spiral/transaction.py +95 -15
- spiral/iterable_dataset.py +0 -106
- {pyspiral-0.8.9.dist-info → pyspiral-0.9.9.dist-info}/WHEEL +0 -0
- {pyspiral-0.8.9.dist-info → pyspiral-0.9.9.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyspiral
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.9
|
|
4
4
|
Classifier: Intended Audience :: Science/Research
|
|
5
5
|
Classifier: Operating System :: OS Independent
|
|
6
6
|
Classifier: Programming Language :: Python
|
|
@@ -31,17 +31,19 @@ Requires-Dist: xxhash>=3.4.1
|
|
|
31
31
|
Requires-Dist: polars>=1.31.0 ; extra == 'polars'
|
|
32
32
|
Requires-Dist: duckdb>=1.3.2 ; extra == 'duckdb'
|
|
33
33
|
Requires-Dist: pyiceberg[s3fs]>=0.9.1 ; extra == 'iceberg'
|
|
34
|
-
Requires-Dist: datasets>=4.
|
|
34
|
+
Requires-Dist: datasets>=4.3.0 ; extra == 'huggingface'
|
|
35
35
|
Requires-Dist: mosaicml-streaming>=0.13.0 ; extra == 'streaming'
|
|
36
36
|
Requires-Dist: vortex-data>=0.52.1 ; extra == 'streaming'
|
|
37
37
|
Requires-Dist: dask>=2025.10.0 ; extra == 'dask'
|
|
38
38
|
Requires-Dist: distributed>=2025.10.0 ; extra == 'dask'
|
|
39
|
+
Requires-Dist: ray[data]>=2.0.0 ; python_full_version < '3.14' and extra == 'ray'
|
|
39
40
|
Provides-Extra: polars
|
|
40
41
|
Provides-Extra: duckdb
|
|
41
42
|
Provides-Extra: iceberg
|
|
42
43
|
Provides-Extra: huggingface
|
|
43
44
|
Provides-Extra: streaming
|
|
44
45
|
Provides-Extra: dask
|
|
46
|
+
Provides-Extra: ray
|
|
45
47
|
Summary: Python client for Spiral.
|
|
46
48
|
Home-Page: https://spiraldb.com
|
|
47
49
|
Author-email: SpiralDB <hello@spiraldb.com>
|
|
@@ -1,45 +1,48 @@
|
|
|
1
|
-
pyspiral-0.
|
|
2
|
-
pyspiral-0.
|
|
3
|
-
pyspiral-0.
|
|
4
|
-
spiral/__init__.py,sha256=
|
|
5
|
-
spiral/_lib.abi3.so,sha256=
|
|
1
|
+
pyspiral-0.9.9.dist-info/METADATA,sha256=M360x1KvrpXFeGHPIqmUwuUt6UllvRwff7sp4laWZ7k,2055
|
|
2
|
+
pyspiral-0.9.9.dist-info/WHEEL,sha256=wLM-4-OuCEmDufFmMnL4mT6DsF8lSbFPkcIGjsPxDb0,104
|
|
3
|
+
pyspiral-0.9.9.dist-info/entry_points.txt,sha256=R96Y3FpYX6XbQu9qMPfUTgiCcf4qM9OBQQZTDdBkZwA,74
|
|
4
|
+
spiral/__init__.py,sha256=8g-RFlbqvKOMgQtkXrtfXFjoqkDbym5n6RZSXsrmgcA,1492
|
|
5
|
+
spiral/_lib.abi3.so,sha256=r5jlbRgZES9Ct4VzoiJZkI0O7gUjB5gNoTTnmz8Pb5o,90734912
|
|
6
6
|
spiral/adbc.py,sha256=Mc2wdC_fqvE4jqlgHqCI7M9Y-jRH4SaAjxJMibmIvbc,14854
|
|
7
|
-
spiral/api/__init__.py,sha256=
|
|
7
|
+
spiral/api/__init__.py,sha256=JRLzPC3BYjkEBdcqAYBYv43C2V4Z51tClug6ztyEqBw,2319
|
|
8
8
|
spiral/api/admin.py,sha256=A1iVR1XYJSObZivPAD5UzmPuMgupXc9kaHNYYa_kwfs,585
|
|
9
|
-
spiral/api/client.py,sha256=
|
|
9
|
+
spiral/api/client.py,sha256=mrnFdmwDB6FYuNKl7QIJqyBVx-Y3ENYgNPXEQ6nLJuI,7932
|
|
10
10
|
spiral/api/filesystems.py,sha256=8g_YdFjFFZLybQqV1xSH91SxVvOer4O6sGECexWXRnw,4548
|
|
11
11
|
spiral/api/key_space_indexes.py,sha256=-38rZXTdkL4mLhp9h3CtqyIyutzzq88tV6bhK05MqYE,640
|
|
12
12
|
spiral/api/organizations.py,sha256=eXAzxrKPmd3IVFfEaEbqbhqG0AjBM4IDz3O-ZoxJI5w,1928
|
|
13
|
-
spiral/api/projects.py,sha256=
|
|
13
|
+
spiral/api/projects.py,sha256=G9SXAYg6PH_dTbogB_roZQVUi3rXaFPI9_ytFg25i98,6430
|
|
14
|
+
spiral/api/tables.py,sha256=AH-Kdp2Jy6lReSoK7GcY9L8WsWMWAACaBjYcK3kbNlM,2070
|
|
14
15
|
spiral/api/telemetry.py,sha256=tfdA3E_EWJwFVxkQfkm8tiYGRubnx2LuE5nbfsk1oG4,474
|
|
15
16
|
spiral/api/text_indexes.py,sha256=_zVlGBytl-9-Unbu2POfZgLh40H1YRcagFtplgIG428,1828
|
|
16
17
|
spiral/api/types.py,sha256=HpHsoBuf7IdlXb7Dw-BkBkEvxBVIhkI8JviqhuoP9pY,696
|
|
17
18
|
spiral/api/workers.py,sha256=0wZNUHMioDT53P1OBJfpjyDfIodHwwT6858z2IlRIM4,636
|
|
18
19
|
spiral/api/workloads.py,sha256=GBZ4tLa_-NtZvV-P5GTJgPSxBQ_YiLyWaOpr9ELojOo,1764
|
|
19
|
-
spiral/arrow_.py,sha256=
|
|
20
|
+
spiral/arrow_.py,sha256=pV36BbkPL2Gvq5z_O7w2iFySkZTecrTlWYcCkplhVto,1828
|
|
20
21
|
spiral/cli/__init__.py,sha256=GdTQZVArIw19zSKi92ZtwD8pXQExuubnaN854XLTSzY,2505
|
|
21
22
|
spiral/cli/__main__.py,sha256=kNaKM2xgJo7GRogf83nYldLM-RGUR6vymdGwZxywQu0,71
|
|
22
23
|
spiral/cli/admin.py,sha256=sC_XUZvi7t91qHMR5vea_KD3lXUcygil1MUw7zVFmpE,945
|
|
23
|
-
spiral/cli/app.py,sha256=
|
|
24
|
+
spiral/cli/app.py,sha256=tr8vR1g_9aJBnn2Dea_a-E8Wpn73brbDHrm_zRg-wVA,3048
|
|
25
|
+
spiral/cli/chooser.py,sha256=JmlETVEfHd9JOkL4ILPly1TgyESVa8vZqNDgFMcnQm8,1091
|
|
24
26
|
spiral/cli/console.py,sha256=6JHbAQV6MFWz3P-VzqPOjhHpkIQagsCdzTMvmuDKMkU,2580
|
|
25
|
-
spiral/cli/fs.py,sha256=
|
|
26
|
-
spiral/cli/iceberg.py,sha256=
|
|
27
|
-
spiral/cli/key_spaces.py,sha256=
|
|
27
|
+
spiral/cli/fs.py,sha256=WcUeyh9sFoWJ2xn25-xKzLEi2u9KLdmoVXwyKynxrK8,4194
|
|
28
|
+
spiral/cli/iceberg.py,sha256=r5qJTy2YACGQALPwU5VQXsQvkY5Qv07qXgpA4qvmVGU,3250
|
|
29
|
+
spiral/cli/key_spaces.py,sha256=0Mv7jwY8coF_wBW8klLJHPpiXqLwFNfVzT06zokzqfU,3526
|
|
28
30
|
spiral/cli/login.py,sha256=2l2i38XNHGKtV4DP6PZPN4LHxceCn3AdHDE5nM2iK5M,760
|
|
29
|
-
spiral/cli/orgs.py,sha256=
|
|
31
|
+
spiral/cli/orgs.py,sha256=L68jO-SEHlocy-tMQYmQ7LAW0WuY0r7q5yr3QBUm0CM,2538
|
|
30
32
|
spiral/cli/printer.py,sha256=HcvSUpaMItzmhBUfIHROK1Z3SL8J8wDopS3Qo8H00uw,1781
|
|
31
|
-
spiral/cli/projects.py,sha256=
|
|
33
|
+
spiral/cli/projects.py,sha256=maH8uGJT_TopIEoPADiz5Qe4SziH0jYJW9bJK1QtWL4,5831
|
|
32
34
|
spiral/cli/state.py,sha256=3sKQuFtV2vCn3E1Dv7Sw9-IK5jiXCVBEQ9Ze17NZXDs,129
|
|
33
|
-
spiral/cli/tables.py,sha256=
|
|
34
|
-
spiral/cli/telemetry.py,sha256=
|
|
35
|
-
spiral/cli/text.py,sha256=
|
|
36
|
-
spiral/cli/
|
|
37
|
-
spiral/cli/
|
|
38
|
-
spiral/
|
|
35
|
+
spiral/cli/tables.py,sha256=r4cogd-4Az-KYoIAcrJ4Fxi1lPj5AEBB7_qdCdpmhgQ,8967
|
|
36
|
+
spiral/cli/telemetry.py,sha256=bhbFyMtQ2Wc_-Rl1u4VlQcr8Yt3hNlW-Gi2NbBHC09c,794
|
|
37
|
+
spiral/cli/text.py,sha256=u2XR9SOs0vIz5NjV5P5Lj3PC1XOlsD8RFQrGwc0AMUY,4033
|
|
38
|
+
spiral/cli/transactions.py,sha256=t3yB2pN3ishYQjlSkP_hVkENgBnqi0QvdBMyVS8pKXU,2748
|
|
39
|
+
spiral/cli/types_.py,sha256=8FEaKDNVtzrQswuX7KHKL1znHeJK7AKrDTRTPer5PWM,1335
|
|
40
|
+
spiral/cli/workloads.py,sha256=NOoYZQ2OewtIAgmWXhPpwGwio233VHhdDfdDgqFMVc8,2999
|
|
41
|
+
spiral/client.py,sha256=bpzXee4HqNaP6Q646LWa4wesQgA7tbqeA3rUS7rVXFU,12099
|
|
39
42
|
spiral/core/__init__.pyi,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
43
|
spiral/core/_tools/__init__.pyi,sha256=b2KLfTOQ67pjfbYt07o0IGiTu5o2bZw69lllV8v0Dps,143
|
|
41
44
|
spiral/core/authn/__init__.pyi,sha256=deZvPlCyiPC6PpXxpEZVglxL5mUJ1Qqg20ieEQgU6ik,582
|
|
42
|
-
spiral/core/client/__init__.pyi,sha256=
|
|
45
|
+
spiral/core/client/__init__.pyi,sha256=a5jsYTjBQVkH3WtxMHNHFK_U8z5yx-mh-FeSa5N0bYY,7655
|
|
43
46
|
spiral/core/config/__init__.pyi,sha256=1BaB7fTGly_fW-qTSQtxbGrYErzkqxuJokDFPupP7d0,955
|
|
44
47
|
spiral/core/expr/__init__.pyi,sha256=3HSKjkotiEkxBvGBALXEBIie0JiyI9bCpehwA3nMQkU,571
|
|
45
48
|
spiral/core/expr/images/__init__.pyi,sha256=wnE_wZXq7a4iqTg3SVm-ssxGw1WQZyk5dGOPaP4Btko,73
|
|
@@ -52,7 +55,7 @@ spiral/core/expr/struct_/__init__.pyi,sha256=MXckd98eV_x3X0RhEWvlkA3DcDXRtLs5pNn
|
|
|
52
55
|
spiral/core/expr/text/__init__.pyi,sha256=ed83n1xcsGY7_QDhMmJGnSQ20UrJFXcdv1AveSEcS1c,175
|
|
53
56
|
spiral/core/expr/udf/__init__.pyi,sha256=zsZs081KVhY3-1JidqTkWMW81Qd_ScoTGZvasIhIK-4,358
|
|
54
57
|
spiral/core/expr/video/__init__.pyi,sha256=nQJEcSsigZuRpMjkI_O4EEtMK_n2zRvorcL_KEeD5vU,95
|
|
55
|
-
spiral/core/table/__init__.pyi,sha256=
|
|
58
|
+
spiral/core/table/__init__.pyi,sha256=hdAu59xHgMWsA1I2Vmz7sRq4DJhh8OVhnHPDgGoz6CQ,4802
|
|
56
59
|
spiral/core/table/manifests/__init__.pyi,sha256=eVfDpmhYSjafIvvALqAkZe5baN3Y1HpKpxYEbjwd4gQ,1043
|
|
57
60
|
spiral/core/table/metastore/__init__.pyi,sha256=rc3u9MwEKRvL2kxOc8lBorddFRnM8o_o1frqtae86a4,1697
|
|
58
61
|
spiral/core/table/spec/__init__.pyi,sha256=839FNXvUS1PD-jYx2pXIIlTEjKszZUGfVD7WKSpflIQ,5659
|
|
@@ -60,13 +63,13 @@ spiral/dataloader.py,sha256=FAaV_B3HTH_gz2FQDDG_5gLjQYR3jScgyHaOa8fSoQk,10766
|
|
|
60
63
|
spiral/dataset.py,sha256=xse0evrNDKPXNrqaS5ZklyvPsrTPaFov5A2uwwMd9sU,8429
|
|
61
64
|
spiral/datetime_.py,sha256=elXaUWtZuuLVcu9E0aXnvYRPB9XWqZbLDToozQYQYjU,950
|
|
62
65
|
spiral/debug/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
|
-
spiral/debug/manifests.py,sha256=
|
|
66
|
+
spiral/debug/manifests.py,sha256=DvhDUkW9Ca8YfsPchArnajzEdedd1jg0Zx-xSRmnrmw,4282
|
|
64
67
|
spiral/debug/metrics.py,sha256=_B1LoHejOQk7sfKX1dhVmHrcB3HNZzhr2M4iQfsyOUQ,2058
|
|
65
|
-
spiral/debug/scan.py,sha256=
|
|
66
|
-
spiral/demo.py,sha256=
|
|
67
|
-
spiral/enrichment.py,sha256=
|
|
68
|
-
spiral/expressions/__init__.py,sha256=
|
|
69
|
-
spiral/expressions/base.py,sha256=
|
|
68
|
+
spiral/debug/scan.py,sha256=PYU4FJiE5aZ0Rc7LkTK62UEjlLwdAm-gNe869KHpNTs,9475
|
|
69
|
+
spiral/demo.py,sha256=28jH4Y0VmBifFCEEPxsUVfzqBqWW-pTUI1__N8nwwPA,6793
|
|
70
|
+
spiral/enrichment.py,sha256=wp9EKN0baQwqwV7I2aL87BgTt5zSY0mvn8M2I6mxUe8,10771
|
|
71
|
+
spiral/expressions/__init__.py,sha256=bb54V06ybqrf9P7E8gyKktLrs01m94-NktZpN6mnfRA,5042
|
|
72
|
+
spiral/expressions/base.py,sha256=J9apXEX47ufrofHo37pFr-6Qj2t0mxCsVtabXFsCrD4,6080
|
|
70
73
|
spiral/expressions/file.py,sha256=7D9jIENJcoT0KFharBLkzK9dZgO4DYn5K_KCt0twefg,518
|
|
71
74
|
spiral/expressions/http.py,sha256=OOHh0WBxg3vwza_m74-rkoQWSclRMI60aPAbQ6yKZi0,486
|
|
72
75
|
spiral/expressions/list_.py,sha256=-OHzTkTYvTY_Q2IuATfK5QNx7KEyic3DzZLEYn8otIk,2050
|
|
@@ -78,8 +81,9 @@ spiral/expressions/text.py,sha256=-02gBWYoyNQ3qQ1--9HTa8IryUDojYQVIp8C7rgnOWQ,18
|
|
|
78
81
|
spiral/expressions/tiff.py,sha256=B1N6ck1-CcIPSU9_Vnol7fXNnTbhV1CnMxvtAG5wmx0,7979
|
|
79
82
|
spiral/expressions/udf.py,sha256=XhePtyzrMgX0SQ5mOmf2XrdkhN7BSyyZpLZtF862B1U,2046
|
|
80
83
|
spiral/grpc_.py,sha256=f3czdP1Mxme42Y5--a5ogYq1TTiWn-J_MlGjwJ2mWwM,1015
|
|
84
|
+
spiral/huggingface.py,sha256=eJPX0npe4IBfZ8bwXPc374W22kN5iK00xOzTIklxPwA,15809
|
|
81
85
|
spiral/iceberg.py,sha256=02OkA348eFxkEbgreeTuVlzavVvZmM4hldrZI76PZ9I,914
|
|
82
|
-
spiral/
|
|
86
|
+
spiral/input.py,sha256=yCqojJDwdkXg92tfLcgfR0ltSrDwCwxS_GjoP9r9HGU,4195
|
|
83
87
|
spiral/key_space_index.py,sha256=NAB_nONEjpMYbse8suz42w7Qb5OPHuKN9h9CT2NJe08,1460
|
|
84
88
|
spiral/project.py,sha256=bUqfROIouk_2WSZXc8DPbFwJuzPac8ucxOM7qHpw6gE,8796
|
|
85
89
|
spiral/protogen/_/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -99,7 +103,8 @@ spiral/protogen/_/substrait/extensions/__init__.py,sha256=nhnEnho70GAT8WPj2xtwJU
|
|
|
99
103
|
spiral/protogen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
100
104
|
spiral/protogen/util.py,sha256=smnvVo6nYH3FfDm9jqhNLaXz4bbTBaQezHQDCTvZyiQ,1486
|
|
101
105
|
spiral/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
102
|
-
spiral/
|
|
106
|
+
spiral/ray_.py,sha256=xktNdvcZfqqU7CizsD1caefAuGmlfE9z9dCKp_Zj-J4,2387
|
|
107
|
+
spiral/scan.py,sha256=mOw1FbNPnEj1WadReLqRRSHbccdsrzD-f7tBK4dnbyQ,19478
|
|
103
108
|
spiral/server.py,sha256=Q1FcOAV0EsDespdBJI25R5K2mihP_i1xNRe99SYUhsY,1401
|
|
104
109
|
spiral/settings.py,sha256=zeiEhWC2H94r3o5-jsOqSIHu-hthRYO48ShWnXvGrQ8,895
|
|
105
110
|
spiral/snapshot.py,sha256=nf0ywmFy1Z2v6NCDBKBzfwmht5nGqWv2V_BifP_Q6Ag,1995
|
|
@@ -107,8 +112,8 @@ spiral/streaming_/__init__.py,sha256=s7MlW2ERsuZmZGExLFL6RcZon2e0tNBocBg5ANgki7k
|
|
|
107
112
|
spiral/streaming_/reader.py,sha256=tl_lC9xgh1-QFhsZn4xQT7It3PVTzHCEUT2BG2dWBRQ,4166
|
|
108
113
|
spiral/streaming_/stream.py,sha256=efqhExky4YgI1f3Me5ctfayFbTExoyS3TRMkrPIjvv0,5918
|
|
109
114
|
spiral/substrait_.py,sha256=AKeOD4KIXvz2J4TYxnIneOiHddtBIyOhuNxVO_uH0eg,12592
|
|
110
|
-
spiral/table.py,sha256=
|
|
115
|
+
spiral/table.py,sha256=qnppfbien_Ytogr9eQuK9pOtgRnV6iWgHw_ynvZtOno,8038
|
|
111
116
|
spiral/text_index.py,sha256=FQ9rgIEGLSJryS9lFdMhKtPFey18BXoWbPXyvZPJJ04,442
|
|
112
|
-
spiral/transaction.py,sha256=
|
|
117
|
+
spiral/transaction.py,sha256=EEz_1VnFojr0D553TF8_YntZE0UWlWvxq6C31P6ab_M,7866
|
|
113
118
|
spiral/types_.py,sha256=W_jyO7F6rpPiH69jhgSgV7OxQZbOlb1Ho3InpKUP6Eo,155
|
|
114
|
-
pyspiral-0.
|
|
119
|
+
pyspiral-0.9.9.dist-info/RECORD,,
|
spiral/__init__.py
CHANGED
|
@@ -22,7 +22,7 @@ from spiral.scan import Scan # noqa: E402
|
|
|
22
22
|
from spiral.snapshot import Snapshot # noqa: E402
|
|
23
23
|
from spiral.table import Table # noqa: E402
|
|
24
24
|
from spiral.text_index import TextIndex # noqa: E402
|
|
25
|
-
from spiral.transaction import Transaction # noqa: E402
|
|
25
|
+
from spiral.transaction import Transaction, TransactionOps # noqa: E402
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
28
|
"Spiral",
|
|
@@ -30,6 +30,7 @@ __all__ = [
|
|
|
30
30
|
"Table",
|
|
31
31
|
"Snapshot",
|
|
32
32
|
"Transaction",
|
|
33
|
+
"TransactionOps",
|
|
33
34
|
"Enrichment",
|
|
34
35
|
"Scan",
|
|
35
36
|
"Shard",
|
|
@@ -41,7 +42,7 @@ __all__ = [
|
|
|
41
42
|
"Iceberg",
|
|
42
43
|
]
|
|
43
44
|
|
|
44
|
-
__version__ = importlib.metadata.version("pyspiral")
|
|
45
|
+
__version__: str = importlib.metadata.version("pyspiral")
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def _warn_msg():
|
spiral/_lib.abi3.so
CHANGED
|
Binary file
|
spiral/api/__init__.py
CHANGED
|
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from .key_space_indexes import KeySpaceIndexesService
|
|
14
14
|
from .organizations import OrganizationsService
|
|
15
15
|
from .projects import ProjectsService
|
|
16
|
+
from .tables import TablesService
|
|
16
17
|
from .telemetry import TelemetryService
|
|
17
18
|
from .text_indexes import TextIndexesService
|
|
18
19
|
from .workloads import WorkloadsService
|
|
@@ -59,6 +60,12 @@ class SpiralAPI:
|
|
|
59
60
|
|
|
60
61
|
return WorkloadsService(self.client)
|
|
61
62
|
|
|
63
|
+
@property
|
|
64
|
+
def tables(self) -> "TablesService":
|
|
65
|
+
from .tables import TablesService
|
|
66
|
+
|
|
67
|
+
return TablesService(self.client)
|
|
68
|
+
|
|
62
69
|
@property
|
|
63
70
|
def text_indexes(self) -> "TextIndexesService":
|
|
64
71
|
from .text_indexes import TextIndexesService
|
spiral/api/client.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import time
|
|
4
5
|
from collections.abc import Iterable, Iterator, Mapping
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from email.utils import parsedate_to_datetime
|
|
5
8
|
from typing import Any, Generic, TypeVar
|
|
6
9
|
|
|
7
10
|
import httpx
|
|
@@ -35,7 +38,7 @@ class Paged(Iterable[E], Generic[E]):
|
|
|
35
38
|
client: _Client,
|
|
36
39
|
path: str,
|
|
37
40
|
page_token: str | None,
|
|
38
|
-
page_size: int,
|
|
41
|
+
page_size: int | None,
|
|
39
42
|
response_cls: type[PagedResponse[E]],
|
|
40
43
|
params: Mapping[str, str] | None = None,
|
|
41
44
|
):
|
|
@@ -48,9 +51,8 @@ class Paged(Iterable[E], Generic[E]):
|
|
|
48
51
|
self._params = params or {}
|
|
49
52
|
if page_token is not None:
|
|
50
53
|
self._params["page_token"] = str(page_token)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# self._params["page_size"] = str(page_size)
|
|
54
|
+
if page_size is not None:
|
|
55
|
+
self._params["page_size"] = str(page_size)
|
|
54
56
|
|
|
55
57
|
self._response: PagedResponse[E] = client.get(path, response_cls, params=self._params)
|
|
56
58
|
|
|
@@ -58,6 +60,13 @@ class Paged(Iterable[E], Generic[E]):
|
|
|
58
60
|
def page(self) -> PagedResponse[E]:
|
|
59
61
|
return self._response
|
|
60
62
|
|
|
63
|
+
def _fetch_next_page(self):
|
|
64
|
+
assert self._response.next_page_token
|
|
65
|
+
|
|
66
|
+
params = self._params.copy()
|
|
67
|
+
params["page_token"] = self._response.next_page_token
|
|
68
|
+
self._response = self._client.get(self._path, self._response_cls, params=params)
|
|
69
|
+
|
|
61
70
|
def __iter__(self) -> Iterator[E]:
|
|
62
71
|
while True:
|
|
63
72
|
yield from self._response.items
|
|
@@ -65,9 +74,7 @@ class Paged(Iterable[E], Generic[E]):
|
|
|
65
74
|
if self._response.next_page_token is None:
|
|
66
75
|
break
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
params["page_token"] = self._response.next_page_token
|
|
70
|
-
self._response = self._client.get(self._path, self._response_cls, params=params)
|
|
77
|
+
self._fetch_next_page()
|
|
71
78
|
|
|
72
79
|
|
|
73
80
|
class ServiceBase:
|
|
@@ -90,6 +97,73 @@ class _Client:
|
|
|
90
97
|
self.http = http
|
|
91
98
|
self.authn = authn
|
|
92
99
|
|
|
100
|
+
def _handle_deprecation(self, response: httpx.Response, path: str) -> None:
|
|
101
|
+
"""Handle deprecation headers from API responses.
|
|
102
|
+
|
|
103
|
+
- Logs warnings if the endpoint is deprecated
|
|
104
|
+
- Sleeps progressively longer as sunset date approaches
|
|
105
|
+
- Logs errors if past the sunset date
|
|
106
|
+
"""
|
|
107
|
+
deprecation_header = response.headers.get("Deprecation")
|
|
108
|
+
sunset_header = response.headers.get("Sunset")
|
|
109
|
+
|
|
110
|
+
if not deprecation_header:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
deprecation_date = parsedate_to_datetime(deprecation_header)
|
|
115
|
+
sunset_date = parsedate_to_datetime(sunset_header) if sunset_header else None
|
|
116
|
+
except (ValueError, TypeError):
|
|
117
|
+
log.warning("Failed to parse deprecation headers for path %s", path)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
sunset_str = sunset_date.isoformat() if sunset_date else "unknown"
|
|
121
|
+
log.warning(
|
|
122
|
+
"SpiralDB is using a deprecated API endpoint, please migrate to a supported version "
|
|
123
|
+
"(path=%s, deprecation_date=%s, sunset_date=%s)",
|
|
124
|
+
path,
|
|
125
|
+
deprecation_date.isoformat(),
|
|
126
|
+
sunset_str,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if sunset_date:
|
|
130
|
+
now = datetime.now(UTC)
|
|
131
|
+
|
|
132
|
+
if now > sunset_date:
|
|
133
|
+
# Past sunset date - log error and use maximum sleep
|
|
134
|
+
days_past_sunset = (now - sunset_date).days
|
|
135
|
+
log.error(
|
|
136
|
+
"SpiralDB API endpoint has been sunset, please migrate to a supported version "
|
|
137
|
+
"(path=%s, sunset_date=%s, days_past_sunset=%d)",
|
|
138
|
+
path,
|
|
139
|
+
sunset_date.isoformat(),
|
|
140
|
+
days_past_sunset,
|
|
141
|
+
)
|
|
142
|
+
sleep_ms = 5000 # Max sleep after sunset
|
|
143
|
+
else:
|
|
144
|
+
# Before sunset - calculate progressive sleep
|
|
145
|
+
time_until_sunset = (sunset_date - now).total_seconds()
|
|
146
|
+
time_since_deprecation = (now - deprecation_date).total_seconds()
|
|
147
|
+
total_deprecation_window = max((sunset_date - deprecation_date).total_seconds(), 1.0)
|
|
148
|
+
|
|
149
|
+
# Calculate progress: 0.0 (just deprecated) to 1.0 (at sunset)
|
|
150
|
+
progress = max(0.0, min(1.0, time_since_deprecation / total_deprecation_window))
|
|
151
|
+
|
|
152
|
+
# Exponential backoff: 0ms → 5000ms as we approach sunset
|
|
153
|
+
sleep_ms = int((progress**2) * 5000.0)
|
|
154
|
+
|
|
155
|
+
if sleep_ms > 0:
|
|
156
|
+
days_until_sunset = int(time_until_sunset / 86400) + 1
|
|
157
|
+
log.warning(
|
|
158
|
+
"Sleeping due to deprecated endpoint usage (path=%s, sleep_ms=%d, days_until_sunset=%d)",
|
|
159
|
+
path,
|
|
160
|
+
sleep_ms,
|
|
161
|
+
days_until_sunset,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if sleep_ms > 0:
|
|
165
|
+
time.sleep(sleep_ms / 1000.0)
|
|
166
|
+
|
|
93
167
|
def get(
|
|
94
168
|
self, path: str, response_cls: type[ResponseT], *, params: Mapping[str, str | list[str]] | None = None
|
|
95
169
|
) -> ResponseT:
|
|
@@ -142,6 +216,9 @@ class _Client:
|
|
|
142
216
|
**req_data,
|
|
143
217
|
)
|
|
144
218
|
|
|
219
|
+
# Handle deprecation headers before processing response
|
|
220
|
+
self._handle_deprecation(resp, path)
|
|
221
|
+
|
|
145
222
|
try:
|
|
146
223
|
resp.raise_for_status()
|
|
147
224
|
except HTTPStatusError as e:
|
|
@@ -159,7 +236,8 @@ class _Client:
|
|
|
159
236
|
response_cls: type[PagedResponse[E]],
|
|
160
237
|
*,
|
|
161
238
|
page_token: str | None = None,
|
|
162
|
-
page_size: int =
|
|
239
|
+
page_size: int | None = None,
|
|
163
240
|
params: Mapping[str, str] | None = None,
|
|
164
241
|
) -> Paged[E]:
|
|
242
|
+
# TODO(DK): When paging is uniformly supported, set a default page size *here* rather than in the callers.
|
|
165
243
|
return Paged(self, path, page_token, page_size, response_cls, params)
|
spiral/api/projects.py
CHANGED
|
@@ -163,7 +163,7 @@ class ProjectsService(ServiceBase):
|
|
|
163
163
|
return self.client.paged("/v1/projects", PagedResponse[Project])
|
|
164
164
|
|
|
165
165
|
def list_tables(
|
|
166
|
-
self, project_id: ProjectId, dataset: str | None = None, table: str | None = None
|
|
166
|
+
self, project_id: ProjectId, dataset: str | None = None, table: str | None = None, page_size: int | None = None
|
|
167
167
|
) -> Paged[TableResource]:
|
|
168
168
|
"""List tables in a project."""
|
|
169
169
|
params = {}
|
|
@@ -171,7 +171,9 @@ class ProjectsService(ServiceBase):
|
|
|
171
171
|
params["dataset"] = dataset
|
|
172
172
|
if table:
|
|
173
173
|
params["table"] = table
|
|
174
|
-
return self.client.paged(
|
|
174
|
+
return self.client.paged(
|
|
175
|
+
f"/v1/projects/{project_id}/tables", PagedResponse[TableResource], params=params, page_size=page_size
|
|
176
|
+
)
|
|
175
177
|
|
|
176
178
|
def list_text_indexes(self, project_id: ProjectId, name: str | None = None) -> Paged[TextIndexResource]:
|
|
177
179
|
"""List text indexes in a project."""
|
spiral/api/tables.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from ..types_ import Timestamp
|
|
6
|
+
from .client import _Client
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Transaction(BaseModel):
|
|
10
|
+
"""Represents a committed transaction in SpiralDB."""
|
|
11
|
+
|
|
12
|
+
txn_idx: int
|
|
13
|
+
committed_at: Timestamp
|
|
14
|
+
# TODO(marko): Define a proper Operation model
|
|
15
|
+
operations: list[dict[str, Any]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TransactionsListResponse(BaseModel):
|
|
19
|
+
"""Response for listing transactions."""
|
|
20
|
+
|
|
21
|
+
items: list[Transaction]
|
|
22
|
+
next_page_token: Timestamp | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TablesService:
|
|
26
|
+
"""Service for managing table transactions."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: _Client):
|
|
29
|
+
self.client = client
|
|
30
|
+
|
|
31
|
+
def list_transactions(
|
|
32
|
+
self,
|
|
33
|
+
table_id: str,
|
|
34
|
+
*,
|
|
35
|
+
since: Timestamp | None = None,
|
|
36
|
+
) -> list[Transaction]:
|
|
37
|
+
"""List transactions for a table.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
table_id: The ID of the table
|
|
41
|
+
since: Only return transactions committed after this timestamp (microseconds since epoch)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of transactions
|
|
45
|
+
"""
|
|
46
|
+
params = {"ordering": "asc"}
|
|
47
|
+
if since is not None:
|
|
48
|
+
params["page_token"] = str(since)
|
|
49
|
+
|
|
50
|
+
all_transactions = []
|
|
51
|
+
|
|
52
|
+
while True:
|
|
53
|
+
response = self.client.get(
|
|
54
|
+
f"/v1/tables/{table_id}/transactions-list",
|
|
55
|
+
TransactionsListResponse,
|
|
56
|
+
params=params,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Parse transactions from the API response
|
|
60
|
+
all_transactions.extend(response.items)
|
|
61
|
+
|
|
62
|
+
# Check for next page
|
|
63
|
+
if response.next_page_token is None:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
params["page_token"] = str(response.next_page_token)
|
|
67
|
+
|
|
68
|
+
return all_transactions
|
|
69
|
+
|
|
70
|
+
def revert_transaction(self, table_id: str, txn_idx: int) -> None:
|
|
71
|
+
"""Revert a transaction by marking it as reverted.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
table_id: The ID of the table
|
|
75
|
+
txn_idx: The index of the transaction to revert
|
|
76
|
+
"""
|
|
77
|
+
self.client.delete(f"/v1/tables/{table_id}/transactions/{txn_idx}", type[None])
|
spiral/arrow_.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from collections import defaultdict
|
|
2
|
-
from collections.abc import Callable, Iterable
|
|
3
1
|
from functools import reduce
|
|
4
2
|
from typing import TypeVar
|
|
5
3
|
|
|
@@ -9,108 +7,6 @@ from pyarrow import compute as pc
|
|
|
9
7
|
T = TypeVar("T")
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
def zip_tables(tables: Iterable[pa.Table]) -> pa.Table:
|
|
13
|
-
data = []
|
|
14
|
-
names = []
|
|
15
|
-
for table in tables:
|
|
16
|
-
data.extend(table.columns)
|
|
17
|
-
names.extend(table.column_names)
|
|
18
|
-
return pa.Table.from_arrays(data, names=names)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def merge_arrays(*arrays: pa.StructArray) -> pa.StructArray:
|
|
22
|
-
"""Recursively merge arrays into nested struct arrays."""
|
|
23
|
-
if len(arrays) == 1:
|
|
24
|
-
return arrays[0]
|
|
25
|
-
|
|
26
|
-
nstructs = sum(pa.types.is_struct(a.type) for a in arrays)
|
|
27
|
-
if nstructs == 0:
|
|
28
|
-
# Then we have conflicting arrays and we choose the last.
|
|
29
|
-
return arrays[-1]
|
|
30
|
-
|
|
31
|
-
if nstructs != len(arrays):
|
|
32
|
-
raise ValueError("Cannot merge structs with non-structs.")
|
|
33
|
-
|
|
34
|
-
data = defaultdict(list)
|
|
35
|
-
for array in arrays:
|
|
36
|
-
if isinstance(array, pa.ChunkedArray):
|
|
37
|
-
array = array.combine_chunks()
|
|
38
|
-
for field in array.type:
|
|
39
|
-
data[field.name].append(array.field(field.name))
|
|
40
|
-
|
|
41
|
-
return pa.StructArray.from_arrays([merge_arrays(*v) for v in data.values()], names=list(data.keys()))
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def merge_scalars(*scalars: pa.StructScalar) -> pa.StructScalar:
|
|
45
|
-
"""Recursively merge scalars into nested struct scalars."""
|
|
46
|
-
if len(scalars) == 1:
|
|
47
|
-
return scalars[0]
|
|
48
|
-
|
|
49
|
-
nstructs = sum(pa.types.is_struct(a.type) for a in scalars)
|
|
50
|
-
if nstructs == 0:
|
|
51
|
-
# Then we have conflicting scalars and we choose the last.
|
|
52
|
-
return scalars[-1]
|
|
53
|
-
|
|
54
|
-
if nstructs != len(scalars):
|
|
55
|
-
raise ValueError("Cannot merge scalars with non-scalars.")
|
|
56
|
-
|
|
57
|
-
data = defaultdict(list)
|
|
58
|
-
for scalar in scalars:
|
|
59
|
-
for field in scalar.type:
|
|
60
|
-
data[field.name].append(scalar[field.name])
|
|
61
|
-
|
|
62
|
-
return pa.scalar({k: merge_scalars(*v) for k, v in data.items()})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def null_table(schema: pa.Schema, length: int = 0) -> pa.Table:
|
|
66
|
-
# We add an extra nulls column to ensure the length is correctly applied.
|
|
67
|
-
return pa.table(
|
|
68
|
-
[pa.nulls(length, type=field.type) for field in schema] + [pa.nulls(length)],
|
|
69
|
-
schema=pa.schema(list(schema) + [pa.field("__", type=pa.null())]),
|
|
70
|
-
).drop(["__"])
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def coalesce_all(table: pa.Table) -> pa.Table:
|
|
74
|
-
"""Coalesce all columns that share the same name."""
|
|
75
|
-
columns: dict[str, list[pa.Array]] = defaultdict(list)
|
|
76
|
-
for i, col in enumerate(table.column_names):
|
|
77
|
-
columns[col].append(table[i])
|
|
78
|
-
|
|
79
|
-
data = []
|
|
80
|
-
names = []
|
|
81
|
-
for col, arrays in columns.items():
|
|
82
|
-
names.append(col)
|
|
83
|
-
if len(arrays) == 1:
|
|
84
|
-
data.append(arrays[0])
|
|
85
|
-
else:
|
|
86
|
-
data.append(pc.coalesce(*arrays))
|
|
87
|
-
|
|
88
|
-
return pa.Table.from_arrays(data, names=names)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def nest_structs(array: pa.StructArray | pa.StructScalar | dict) -> dict:
|
|
92
|
-
"""Turn a struct-like value with dot-separated column names into a nested dictionary."""
|
|
93
|
-
data = {}
|
|
94
|
-
|
|
95
|
-
if isinstance(array, pa.StructArray | pa.StructScalar):
|
|
96
|
-
array = {f.name: field(array, f.name) for f in array.type}
|
|
97
|
-
|
|
98
|
-
for name in array.keys():
|
|
99
|
-
if "." not in name:
|
|
100
|
-
data[name] = array[name]
|
|
101
|
-
continue
|
|
102
|
-
|
|
103
|
-
parts = name.split(".")
|
|
104
|
-
child_data = data
|
|
105
|
-
for part in parts[:-1]:
|
|
106
|
-
if part not in child_data:
|
|
107
|
-
child_data[part] = {}
|
|
108
|
-
child_data = child_data[part]
|
|
109
|
-
child_data[parts[-1]] = array[name]
|
|
110
|
-
|
|
111
|
-
return data
|
|
112
|
-
|
|
113
|
-
|
|
114
10
|
def flatten_struct_table(table: pa.Table, separator=".") -> pa.Table:
|
|
115
11
|
"""Turn a nested struct table into a flat table with dot-separated names."""
|
|
116
12
|
data = []
|
|
@@ -121,7 +17,7 @@ def flatten_struct_table(table: pa.Table, separator=".") -> pa.Table:
|
|
|
121
17
|
if isinstance(array, pa.ChunkedArray):
|
|
122
18
|
array = array.combine_chunks()
|
|
123
19
|
for f in array.type:
|
|
124
|
-
_unfold(field(
|
|
20
|
+
_unfold(array.field(f.name), f"{prefix}{separator}{f.name}")
|
|
125
21
|
else:
|
|
126
22
|
data.append(array)
|
|
127
23
|
names.append(prefix)
|
|
@@ -133,6 +29,7 @@ def flatten_struct_table(table: pa.Table, separator=".") -> pa.Table:
|
|
|
133
29
|
|
|
134
30
|
|
|
135
31
|
def struct_array(fields: list[tuple[str, bool, pa.Array]], /, mask: list[bool] | None = None) -> pa.StructArray:
|
|
32
|
+
"""Helper to create struct arrays from field definitions."""
|
|
136
33
|
return pa.StructArray.from_arrays(
|
|
137
34
|
arrays=[x[2] for x in fields],
|
|
138
35
|
fields=[pa.field(x[0], type=x[2].type, nullable=x[1]) for x in fields],
|
|
@@ -144,59 +41,11 @@ def table(fields: list[tuple[str, bool, pa.Array]], /) -> pa.Table:
|
|
|
144
41
|
return pa.Table.from_struct_array(struct_array(fields))
|
|
145
42
|
|
|
146
43
|
|
|
147
|
-
def
|
|
148
|
-
return pa.Table.from_struct_array(dict_to_struct_array(data))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def dict_to_struct_array(data: dict | pa.StructArray, propagate_nulls: bool = False) -> pa.StructArray:
|
|
44
|
+
def dict_to_struct_array(data: dict[str, dict | pa.Array], propagate_nulls: bool = False) -> pa.StructArray:
|
|
152
45
|
"""Convert a nested dictionary of arrays to a table with nested structs."""
|
|
153
|
-
if isinstance(
|
|
154
|
-
return data
|
|
155
|
-
arrays = [dict_to_struct_array(value) for value in data.values()]
|
|
46
|
+
arrays = [value if not isinstance(value, dict) else dict_to_struct_array(value) for value in data.values()]
|
|
156
47
|
return pa.StructArray.from_arrays(
|
|
157
48
|
arrays,
|
|
158
49
|
names=list(data.keys()),
|
|
159
50
|
mask=reduce(pc.and_, [pc.is_null(array) for array in arrays]) if propagate_nulls else None,
|
|
160
51
|
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def struct_array_to_dict(array: pa.StructArray, array_fn: Callable[[pa.Array], T] = lambda a: a) -> dict | T:
|
|
164
|
-
"""Convert a struct array to a nested dictionary."""
|
|
165
|
-
if not pa.types.is_struct(array.type):
|
|
166
|
-
return array_fn(array)
|
|
167
|
-
if isinstance(array, pa.ChunkedArray):
|
|
168
|
-
array = array.combine_chunks()
|
|
169
|
-
return {field.name: struct_array_to_dict(array.field(i), array_fn=array_fn) for i, field in enumerate(array.type)}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def table_to_struct_array(table: pa.Table) -> pa.StructArray:
|
|
173
|
-
if not table.num_rows:
|
|
174
|
-
return pa.array([], type=pa.struct(table.schema))
|
|
175
|
-
array = table.to_struct_array()
|
|
176
|
-
if isinstance(array, pa.ChunkedArray):
|
|
177
|
-
array = array.combine_chunks()
|
|
178
|
-
return array
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def table_from_struct_array(array: pa.StructArray | pa.ChunkedArray):
|
|
182
|
-
if len(array) == 0:
|
|
183
|
-
return null_table(pa.schema(array.type))
|
|
184
|
-
return pa.Table.from_struct_array(array)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def field(value: pa.StructArray | pa.StructScalar, name: str) -> pa.Array | pa.Scalar:
|
|
188
|
-
"""Get a field from a struct-like value."""
|
|
189
|
-
if isinstance(value, pa.StructScalar):
|
|
190
|
-
return value[name]
|
|
191
|
-
return value.field(name)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def concat_tables(tables: list[pa.Table]) -> pa.Table:
|
|
195
|
-
"""
|
|
196
|
-
Concatenate pyarrow.Table objects, filling "missing" data with appropriate null arrays
|
|
197
|
-
and casting arrays to the most common denominator type that fits all fields.
|
|
198
|
-
"""
|
|
199
|
-
if len(tables) == 1:
|
|
200
|
-
return tables[0]
|
|
201
|
-
else:
|
|
202
|
-
return pa.concat_tables(tables, promote_options="permissive")
|