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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyspiral
3
- Version: 0.8.9
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.0.0 ; extra == 'huggingface'
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.8.9.dist-info/METADATA,sha256=Ti8e0-xOdp5VUMbJ8X30eLyH-tkaPi-9MdG0DDWLZuI,1953
2
- pyspiral-0.8.9.dist-info/WHEEL,sha256=wLM-4-OuCEmDufFmMnL4mT6DsF8lSbFPkcIGjsPxDb0,104
3
- pyspiral-0.8.9.dist-info/entry_points.txt,sha256=R96Y3FpYX6XbQu9qMPfUTgiCcf4qM9OBQQZTDdBkZwA,74
4
- spiral/__init__.py,sha256=PwaYBWFBtB7cYi7peMmhk_Lm5XzjRoLwOtLbUhc1ZDo,1449
5
- spiral/_lib.abi3.so,sha256=EA4kgWe6pzgxACe8OELMyZfscv2wOE8ADrPk0CikrZ0,80996768
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=XlDdWLyEfnK3FRyYaA02JN91890QYpcPnyvilz9XcTk,2140
7
+ spiral/api/__init__.py,sha256=JRLzPC3BYjkEBdcqAYBYv43C2V4Z51tClug6ztyEqBw,2319
8
8
  spiral/api/admin.py,sha256=A1iVR1XYJSObZivPAD5UzmPuMgupXc9kaHNYYa_kwfs,585
9
- spiral/api/client.py,sha256=v1_FD46Hy4XMAUGfoNBcbnqwRX10WfkTpPQfVsLkozM,4665
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=j2sYhhh_Q3Pk9MBukb8pQgKN2JP00RKyDYdAcgLbmtw,6357
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=fUpXmjUjG-rGfqMhKR332QzC7zrfIU2yjLaWKYzefwU,6778
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=nNDoMxnGCJIiaD8PMDqSL2rK_QErmtZGOGHT4V29CRA,2825
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=Swawf9oPQjL0NOE-sB96xtK7G6hhfTu8q9XxSwflSQw,4173
26
- spiral/cli/iceberg.py,sha256=wdMyl0j821MLnXNZ6Kwm65ogh98C-pjMJm3Y6YqlnTI,3249
27
- spiral/cli/key_spaces.py,sha256=84MibTdjI5bFK7lhL0w1WOlw-uBZtFnPPlTQuI2PPGw,3524
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=QHvpUQrKqaNC99efa8v0l-bg38aJCE6m7N9LU6VmMlc,2537
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=2d-um9qlX9TK7ehvDUq5hUTsqvj-4S9DoL_h5U111-Y,5802
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=JnTJbHzUC48SOeb8sgzj8dfpR67y4eTTdg7T5fEZzAs,7914
34
- spiral/cli/telemetry.py,sha256=9kp7lmimShsGoLRUic5aOEQ4hti-pPFMBFc4cdlPDmk,587
35
- spiral/cli/text.py,sha256=DlWGe4JrkdERAiqyITNpk91Wqb63Re99rNYlIFsIamc,4031
36
- spiral/cli/types.py,sha256=0Zau84chh3XGIdFYqzC3LvgRtY8Ljw7WnKpzvjQ45SA,1360
37
- spiral/cli/workloads.py,sha256=r6f5y_MTPW460wpd50hC4BkFrg7WM4NOqUkgEI4Vg-U,2997
38
- spiral/client.py,sha256=UGFYteCv02_8vt46N3dC_nGNxHeJ6feUhKPLvuidg-4,9918
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=CxoshdsdmaEff7eoBf1eWMNMqNw6PfQKgy0QVzjSwKg,7395
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=_BvwxwaTxILYTh2O5nDGpx23gqv3rxh-awNbDWsPKU0,4600
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=IJjF5mqpTUeTLpSQ10PBn0DnkkNvPl0c1ghN-7s3PMI,3999
66
+ spiral/debug/manifests.py,sha256=DvhDUkW9Ca8YfsPchArnajzEdedd1jg0Zx-xSRmnrmw,4282
64
67
  spiral/debug/metrics.py,sha256=_B1LoHejOQk7sfKX1dhVmHrcB3HNZzhr2M4iQfsyOUQ,2058
65
- spiral/debug/scan.py,sha256=bYLY4nZNdo0cv7Ldcy3hkXix3aFClHOCG6QbOtn8954,9549
66
- spiral/demo.py,sha256=YXjKAlxetzpZ9g3rEP0E9fa1bngWtaVicrECZa7IdTg,3173
67
- spiral/enrichment.py,sha256=DpnCtKcdqwvogCu3ReR1iDktSQqA_GOXXLTzvHKb64w,10713
68
- spiral/expressions/__init__.py,sha256=ZsD8g7vB0G7xy19GUiH4m79kw7KEkTQRwJl5Gn1cgtw,8049
69
- spiral/expressions/base.py,sha256=ooTtXy5QkCmPNMYa7lJuFAguFpBrd59UWIxOGxhQ5h0,6261
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/iterable_dataset.py,sha256=Eekg9ad8tcwXcloHWReBbvCSr5ZappRHn2ldKTvwqS0,4622
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/scan.py,sha256=PNjoY_GpjbdAQxXFXLNsB-P8Q1d_GLgQ_1whXvgpC_k,14268
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=vRgNRvKld1kdCTpHgHNcb1VcRXyWZ-2O-3iRUGFIGkU,8031
115
+ spiral/table.py,sha256=qnppfbien_Ytogr9eQuK9pOtgRnV6iWgHw_ynvZtOno,8038
111
116
  spiral/text_index.py,sha256=FQ9rgIEGLSJryS9lFdMhKtPFey18BXoWbPXyvZPJJ04,442
112
- spiral/transaction.py,sha256=rifPjzGsLl2hdoQmFMSGI049EWr7yNh1PnTvvohZU6Y,5418
117
+ spiral/transaction.py,sha256=EEz_1VnFojr0D553TF8_YntZE0UWlWvxq6C31P6ab_M,7866
113
118
  spiral/types_.py,sha256=W_jyO7F6rpPiH69jhgSgV7OxQZbOlb1Ho3InpKUP6Eo,155
114
- pyspiral-0.8.9.dist-info/RECORD,,
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
- # TODO(marko): Support paging.
52
- # if page_size is not None:
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
- params = self._params.copy()
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 = 50,
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(f"/v1/projects/{project_id}/tables", PagedResponse[TableResource], params=params)
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(array, f.name), f"{prefix}{separator}{f.name}")
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 dict_to_table(data) -> pa.Table:
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(data, pa.Array):
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")