pyspiral 0.4.0__pp310-pypy310_pp73-macosx_10_12_x86_64.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 (98) hide show
  1. pyspiral-0.4.0.dist-info/METADATA +46 -0
  2. pyspiral-0.4.0.dist-info/RECORD +98 -0
  3. pyspiral-0.4.0.dist-info/WHEEL +4 -0
  4. pyspiral-0.4.0.dist-info/entry_points.txt +2 -0
  5. spiral/__init__.py +10 -0
  6. spiral/_lib.pypy310-pp73-darwin.so +0 -0
  7. spiral/adbc.py +393 -0
  8. spiral/api/__init__.py +64 -0
  9. spiral/api/admin.py +15 -0
  10. spiral/api/client.py +160 -0
  11. spiral/api/filesystems.py +153 -0
  12. spiral/api/organizations.py +77 -0
  13. spiral/api/projects.py +197 -0
  14. spiral/api/telemetry.py +19 -0
  15. spiral/api/types.py +20 -0
  16. spiral/api/workloads.py +52 -0
  17. spiral/arrow_.py +221 -0
  18. spiral/cli/__init__.py +79 -0
  19. spiral/cli/__main__.py +4 -0
  20. spiral/cli/admin.py +16 -0
  21. spiral/cli/app.py +65 -0
  22. spiral/cli/console.py +95 -0
  23. spiral/cli/fs.py +112 -0
  24. spiral/cli/iceberg/__init__.py +7 -0
  25. spiral/cli/iceberg/namespaces.py +47 -0
  26. spiral/cli/iceberg/tables.py +60 -0
  27. spiral/cli/indexes/__init__.py +19 -0
  28. spiral/cli/login.py +22 -0
  29. spiral/cli/orgs.py +90 -0
  30. spiral/cli/printer.py +53 -0
  31. spiral/cli/projects.py +136 -0
  32. spiral/cli/state.py +5 -0
  33. spiral/cli/tables/__init__.py +121 -0
  34. spiral/cli/telemetry.py +18 -0
  35. spiral/cli/types.py +51 -0
  36. spiral/cli/workloads.py +59 -0
  37. spiral/client.py +79 -0
  38. spiral/core/__init__.pyi +0 -0
  39. spiral/core/client/__init__.pyi +117 -0
  40. spiral/core/index/__init__.pyi +15 -0
  41. spiral/core/table/__init__.pyi +108 -0
  42. spiral/core/table/manifests/__init__.pyi +35 -0
  43. spiral/core/table/metastore/__init__.pyi +62 -0
  44. spiral/core/table/spec/__init__.pyi +214 -0
  45. spiral/datetime_.py +27 -0
  46. spiral/expressions/__init__.py +245 -0
  47. spiral/expressions/base.py +149 -0
  48. spiral/expressions/http.py +86 -0
  49. spiral/expressions/io.py +100 -0
  50. spiral/expressions/list_.py +68 -0
  51. spiral/expressions/mp4.py +62 -0
  52. spiral/expressions/png.py +18 -0
  53. spiral/expressions/qoi.py +18 -0
  54. spiral/expressions/refs.py +58 -0
  55. spiral/expressions/str_.py +39 -0
  56. spiral/expressions/struct.py +59 -0
  57. spiral/expressions/text.py +62 -0
  58. spiral/expressions/tiff.py +223 -0
  59. spiral/expressions/udf.py +46 -0
  60. spiral/grpc_.py +32 -0
  61. spiral/iceberg/__init__.py +3 -0
  62. spiral/iceberg/client.py +33 -0
  63. spiral/indexes/__init__.py +5 -0
  64. spiral/indexes/client.py +137 -0
  65. spiral/indexes/index.py +34 -0
  66. spiral/indexes/scan.py +22 -0
  67. spiral/project.py +46 -0
  68. spiral/protogen/_/__init__.py +0 -0
  69. spiral/protogen/_/arrow/__init__.py +0 -0
  70. spiral/protogen/_/arrow/flight/__init__.py +0 -0
  71. spiral/protogen/_/arrow/flight/protocol/__init__.py +0 -0
  72. spiral/protogen/_/arrow/flight/protocol/sql/__init__.py +1990 -0
  73. spiral/protogen/_/scandal/__init__.py +178 -0
  74. spiral/protogen/_/spiral/__init__.py +0 -0
  75. spiral/protogen/_/spiral/table/__init__.py +22 -0
  76. spiral/protogen/_/substrait/__init__.py +3399 -0
  77. spiral/protogen/_/substrait/extensions/__init__.py +115 -0
  78. spiral/protogen/__init__.py +0 -0
  79. spiral/protogen/substrait/__init__.py +3399 -0
  80. spiral/protogen/substrait/extensions/__init__.py +115 -0
  81. spiral/protogen/util.py +41 -0
  82. spiral/py.typed +0 -0
  83. spiral/server.py +17 -0
  84. spiral/settings.py +101 -0
  85. spiral/substrait_.py +279 -0
  86. spiral/tables/__init__.py +12 -0
  87. spiral/tables/client.py +130 -0
  88. spiral/tables/dataset.py +250 -0
  89. spiral/tables/debug/__init__.py +0 -0
  90. spiral/tables/debug/manifests.py +70 -0
  91. spiral/tables/debug/metrics.py +56 -0
  92. spiral/tables/debug/scan.py +248 -0
  93. spiral/tables/maintenance.py +12 -0
  94. spiral/tables/scan.py +193 -0
  95. spiral/tables/snapshot.py +78 -0
  96. spiral/tables/table.py +157 -0
  97. spiral/tables/transaction.py +52 -0
  98. spiral/types_.py +6 -0
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyspiral
3
+ Version: 0.4.0
4
+ Classifier: Intended Audience :: Science/Research
5
+ Classifier: Operating System :: OS Independent
6
+ Classifier: Programming Language :: Python
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Rust
14
+ Classifier: License :: Other/Proprietary License
15
+ Requires-Dist: betterproto==2.0.0b7
16
+ Requires-Dist: google-re2>=1.1.20240702
17
+ Requires-Dist: grpclib>=0.4.7
18
+ Requires-Dist: hishel>=0.0.30
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: numpy>=2
21
+ Requires-Dist: pyarrow>=21.0.0
22
+ Requires-Dist: pydantic-settings>=2.3.4
23
+ Requires-Dist: pydantic[email]>=2.5.3
24
+ Requires-Dist: pyjwt[crypto]>=2.9.0
25
+ Requires-Dist: questionary>=2.0.1
26
+ Requires-Dist: tqdm>=4.66.5
27
+ Requires-Dist: typer>=0.16
28
+ Requires-Dist: xxhash>=3.4.1
29
+ Requires-Dist: nanoid>=2.0.0
30
+ Requires-Dist: sqlglot[rs]>=25.25.1
31
+ Requires-Dist: pyperclip>=1.9.0
32
+ Requires-Dist: polars>=1.31.0 ; extra == 'polars'
33
+ Requires-Dist: duckdb>=1.3.2 ; extra == 'duckdb'
34
+ Requires-Dist: pyiceberg>=0.9.1 ; extra == 'pyiceberg'
35
+ Provides-Extra: polars
36
+ Provides-Extra: duckdb
37
+ Provides-Extra: pyiceberg
38
+ Summary: Python client for Spiral.
39
+ Home-Page: https://spiraldb.com
40
+ Author-email: SpiralDB <hello@spiraldb.com>
41
+ License: Proprietary License
42
+ Requires-Python: >=3.10
43
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
44
+
45
+ # PySpiral
46
+
@@ -0,0 +1,98 @@
1
+ pyspiral-0.4.0.dist-info/METADATA,sha256=B17wQZ-DamgU1NYanta7NGFB-8iPqpYmASeHv5QBQgk,1610
2
+ pyspiral-0.4.0.dist-info/WHEEL,sha256=JiwhipTW0_LsGDAT-A3mDb2ZFAL_o8nDI5ZvqBdvwBg,113
3
+ pyspiral-0.4.0.dist-info/entry_points.txt,sha256=uft7u-a6g40NLt4Q6BleWbK4NY0M8nZuYPpP8DV0EOk,45
4
+ spiral/__init__.py,sha256=Jv1vbcnnmcTsBLN5mSNjnX3ae4C_mgojXDSBFaqIhN0,208
5
+ spiral/_lib.pypy310-pp73-darwin.so,sha256=-KxxizeEA9H_eAvIutS_wHZdCySET1QpQkc5fYUKtrg,66358892
6
+ spiral/adbc.py,sha256=HcvR60uQeEK2oggSAK6y5VYtIrACIiCQ-85MEf18EZc,14199
7
+ spiral/api/__init__.py,sha256=_7BS1RhqEFjnt3XwFWZNCHVEQeSKpezPevAiGCsvDbE,1776
8
+ spiral/api/admin.py,sha256=A1iVR1XYJSObZivPAD5UzmPuMgupXc9kaHNYYa_kwfs,585
9
+ spiral/api/client.py,sha256=9-L6T8niQAXo90jRxllJD4hXXmcGfHj7CW9X3XTYa5Q,4551
10
+ spiral/api/filesystems.py,sha256=5Ky_otnresGj7WdsR8Xi7DDM3lkB8UES6Lru_xWAGDM,4559
11
+ spiral/api/organizations.py,sha256=B-8zZ7lFJANGK7dUNbo_aU-cgI959JBP9VcWb6wdgi0,1895
12
+ spiral/api/projects.py,sha256=JBGof9A2Ivasu2jrULMjHBwlna0M8WRrTNqU-Es4GJ8,5673
13
+ spiral/api/telemetry.py,sha256=tfdA3E_EWJwFVxkQfkm8tiYGRubnx2LuE5nbfsk1oG4,474
14
+ spiral/api/types.py,sha256=zx-BRKsi1GHg9aL9gMUaVQWYYMXJcP0A8OQUc7jSIAc,653
15
+ spiral/api/workloads.py,sha256=XAyXV7vgZcoyyoPoGvOT4jTpyFKFMvrrAfhL6d1h1kE,1748
16
+ spiral/arrow_.py,sha256=T1LZ7bh9aMDbXfpUsf0dR0E1roTQyAYSgZ2mL4s8J_4,7681
17
+ spiral/cli/__init__.py,sha256=ooAFz_iCpVCKHE0TiVElIynbP2PtTgD9cUw46Vh1lcw,2145
18
+ spiral/cli/__main__.py,sha256=kNaKM2xgJo7GRogf83nYldLM-RGUR6vymdGwZxywQu0,71
19
+ spiral/cli/admin.py,sha256=7WbU_tr05clUjmZ-RkKTlvcf1pbXIElRfHRJlCItFGk,326
20
+ spiral/cli/app.py,sha256=-k0rrLbfJRLay_2_MOCt57PLcx0VnNMCkrnKV7j7nos,1725
21
+ spiral/cli/console.py,sha256=6JHbAQV6MFWz3P-VzqPOjhHpkIQagsCdzTMvmuDKMkU,2580
22
+ spiral/cli/fs.py,sha256=dVPoAoAbuQ9yJlfI-JiFgS9VdnPmeBMygVHgehJRj34,4367
23
+ spiral/cli/iceberg/__init__.py,sha256=IQV_gwCFSj6Ubxs58VM9Pal1ymgG2bxdDgOPuk9E5bs,214
24
+ spiral/cli/iceberg/namespaces.py,sha256=x9pvHlcXtcATYYjqimHa6CtkyL3taQUJ--ni_Bfoemc,1510
25
+ spiral/cli/iceberg/tables.py,sha256=nSR4-t54otJfCmubB6vXnbOkbqPVGV0sHBlc-t9cIVg,1930
26
+ spiral/cli/indexes/__init__.py,sha256=-USfxCIdckzZKBNQ-DXqe3V5ttWVo_Fsa1Mfcx5hdIw,467
27
+ spiral/cli/login.py,sha256=InKMnpV8NATW5RPgB3ZL-DSVPzUuUByyK4Fx7pZEgfg,607
28
+ spiral/cli/orgs.py,sha256=V-4ZTT3FwFQLcs1-BenC8uCgvWOJcxkZPSdCPfsexhc,2848
29
+ spiral/cli/printer.py,sha256=W83KAE-7meoDD1yRltLQrZqrA2olGapBGy_2USWkY08,1778
30
+ spiral/cli/projects.py,sha256=TKXu_VzkIUccwXzdlg-wQMkrB-Py33g052NrbuJx-D4,5096
31
+ spiral/cli/state.py,sha256=10wTIVQ0SJkY67Z6-KQ1LFlt3aVIPmZhoHFdTwp4kNA,130
32
+ spiral/cli/tables/__init__.py,sha256=lkGLDeU28IVnuxJdlYSUh6QSB9fQ4_1MeZJL73iXcHo,3660
33
+ spiral/cli/telemetry.py,sha256=ABDCyV5QJGOIJp4AxvK0LG5xNPIysP37K5haL38T7P4,586
34
+ spiral/cli/types.py,sha256=YG1eHhRLaqlVU_18DQBuF_YMsabhMZLBY0V9CvbSxjY,1369
35
+ spiral/cli/workloads.py,sha256=SbxgwiBlX1AuqpOLV3gs7DFkH-Tbeend7qJTwq0Je84,1994
36
+ spiral/client.py,sha256=K-OuMOTgYxOA9vef5jSANjmPRBfGrzQ65fg6Fd-rHMY,2683
37
+ spiral/core/__init__.pyi,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ spiral/core/client/__init__.pyi,sha256=FFpUCPyQjH_VxldflqWmvniSugLuDiQv11vMim9HhDY,3466
39
+ spiral/core/index/__init__.pyi,sha256=NPOG1ztFO6siBGpmJU3boRzX26xfxw--2TiCydosGvo,314
40
+ spiral/core/table/__init__.pyi,sha256=dwOaxcOl6ZIlxoLjOnC3CNUgGetWfnEV1Jx06aCH8M8,3265
41
+ spiral/core/table/manifests/__init__.pyi,sha256=3V59-K1qr1z2dGfgRKXaHSVheK8NNw8Q8PFhfbeQd_4,1065
42
+ spiral/core/table/metastore/__init__.pyi,sha256=dMqySDnsjPUTBuFU2MaQGyocKEoGkWpeTQmUP2iIKbc,1880
43
+ spiral/core/table/spec/__init__.pyi,sha256=D4GQp9RWwyLKTlRW7eDXcQE-xA5rF2iBcXZ8y7b48EE,5595
44
+ spiral/datetime_.py,sha256=1TA1RYIRU22qcUuipIjVhAtGnPDVn2z9WttuhkmfkwY,964
45
+ spiral/expressions/__init__.py,sha256=QAEtghxWoT-obmMsHpWwPloRh5EZFpeQ7H_i9F4PG5c,6556
46
+ spiral/expressions/base.py,sha256=q_W9XslcdFQtOIE_d1VkEmLickaXKOAoIcFeMoh-nqQ,4751
47
+ spiral/expressions/http.py,sha256=begUydWoFHEqjeLkATvI_v66Ez6_rR-OQBWO5cHbb9c,2742
48
+ spiral/expressions/io.py,sha256=gJ2a0FKMmdxarWKENulPRwH7KDvSJTIh_OUxX306xAM,3045
49
+ spiral/expressions/list_.py,sha256=MMt5lf5H1M3O-x6N_PvqOLGq9NOk6Ukv0fPWwPC_uy4,1809
50
+ spiral/expressions/mp4.py,sha256=R-fcVYRI6KaH1Nwpmqnsc1VYd9wA7Nuiy2UDcNxEzpw,2165
51
+ spiral/expressions/png.py,sha256=KO8X0OmMzUFwpg2I_j0JTyldPzVXDWIMzjWMWDV9vIY,506
52
+ spiral/expressions/qoi.py,sha256=gvIbb6fXb_Bb080sn9wkpbGGrPs2UEcTXCfuv4-kcYQ,506
53
+ spiral/expressions/refs.py,sha256=ISMtJtUL--BjHF6rsvgN3Um4QcvVqQE9URngOxjQrhw,2115
54
+ spiral/expressions/str_.py,sha256=tY8RXW3JWvr1-bEfCZtk5FAf11wKJnXPuA9EoeJ9tA4,1265
55
+ spiral/expressions/struct.py,sha256=pGAnCDh6AK0BK1XfZ1qG4ce4ranIQEE1HQsgmzBcfwQ,2038
56
+ spiral/expressions/text.py,sha256=-02gBWYoyNQ3qQ1--9HTa8IryUDojYQVIp8C7rgnOWQ,1893
57
+ spiral/expressions/tiff.py,sha256=fQwIn0kLFBM2Y3YYIHmTgb_EIRHKT2fNc77nioDQQw4,8044
58
+ spiral/expressions/udf.py,sha256=r6398z2Aj7KnXtwEvCiGNbgOXa6xsb_bnnG-FEvFxV4,1370
59
+ spiral/grpc_.py,sha256=f3czdP1Mxme42Y5--a5ogYq1TTiWn-J_MlGjwJ2mWwM,1015
60
+ spiral/iceberg/__init__.py,sha256=jSIlTxWauAbJV5gsWglZisFbnfNNzLYN90scoYcdWzc,65
61
+ spiral/iceberg/client.py,sha256=E6FyE_h2HLgDW1cAFg1XgglJr6rbVOCWjRtRmqoMVkM,1003
62
+ spiral/indexes/__init__.py,sha256=TXLQ-_3xso3lFIp2lM58_ip9OPNwPKFv1FdsWiUF-d8,178
63
+ spiral/indexes/client.py,sha256=NsFBILEHMjyCUruFrUEKucRQRrN4OvqgbL4pmzWs07g,5600
64
+ spiral/indexes/index.py,sha256=4CmSFlZYp46B2CjqtiyZ7VF5EH3duiutz3nWFnyApLA,973
65
+ spiral/indexes/scan.py,sha256=B2m-UgNuawNB90HXK33GTQfMy2WLdNNxiiB6cIjFW2Y,697
66
+ spiral/project.py,sha256=0uJ1Jb88Ie-cCNnSdX3QfFtCUqrjLka4zCm_TxCpVak,1189
67
+ spiral/protogen/_/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
+ spiral/protogen/_/arrow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
+ spiral/protogen/_/arrow/flight/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
+ spiral/protogen/_/arrow/flight/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
+ spiral/protogen/_/arrow/flight/protocol/sql/__init__.py,sha256=_xhj9QkWEW1qZ-iVxcQ8k4EjYr7KJ5ofitJGqVUGQi4,79921
72
+ spiral/protogen/_/scandal/__init__.py,sha256=X5YJqErZDIXxTESw8fLqJp3P2wZlqAglBzPs3LpTd-w,5145
73
+ spiral/protogen/_/spiral/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ spiral/protogen/_/spiral/table/__init__.py,sha256=o_aNyTuJBIRY6MlAWceMsjbfaSUuZphRxiG_IXmC0mU,629
75
+ spiral/protogen/_/substrait/__init__.py,sha256=pV4-T-lwAHKkfFrNYSUGY4IkbIvuKjSo_imzF7BLj_s,126526
76
+ spiral/protogen/_/substrait/extensions/__init__.py,sha256=yD7dg0TBqn-GK_L0qeVof1GKnwSLg_kPyQSV3kcSljs,3655
77
+ spiral/protogen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
+ spiral/protogen/substrait/__init__.py,sha256=pV4-T-lwAHKkfFrNYSUGY4IkbIvuKjSo_imzF7BLj_s,126526
79
+ spiral/protogen/substrait/extensions/__init__.py,sha256=yD7dg0TBqn-GK_L0qeVof1GKnwSLg_kPyQSV3kcSljs,3655
80
+ spiral/protogen/util.py,sha256=smnvVo6nYH3FfDm9jqhNLaXz4bbTBaQezHQDCTvZyiQ,1486
81
+ spiral/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
+ spiral/server.py,sha256=ztBmB5lBnUz-smQxR_tC8AI5SOhz17wH0MI3GuzDUdM,600
83
+ spiral/settings.py,sha256=PIQV2ljtB3pEOWoMRVSRzSGJNrXviO2JBgZ5ZY_Nq2E,2794
84
+ spiral/substrait_.py,sha256=RNSmfbGFT_5uyo8AFtzS9A7IHW3DkacMTw2vKnj0Das,12762
85
+ spiral/tables/__init__.py,sha256=iiP7BkHA117em37_e75jtdvoZC10xCXtld18gRnPbTw,430
86
+ spiral/tables/client.py,sha256=l_wJJRf3BPD5lg4Q1Ll2lAqQIuBCnKwC6JtsAui91Tc,4915
87
+ spiral/tables/dataset.py,sha256=DuHeKVCJfXLsbxmde9QW6yvesW5uhswG6qAxV5X0ZgA,7890
88
+ spiral/tables/debug/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
+ spiral/tables/debug/manifests.py,sha256=E_-DiMBg2EPL97cl9hLWhiqEsFtjEBgh_C7jZy8EWYc,2594
90
+ spiral/tables/debug/metrics.py,sha256=XdRDcjggtsLNGCAjam6IxG9072pz_d2C8iLApNRFUtk,2044
91
+ spiral/tables/debug/scan.py,sha256=-IWX_UjO4QP9Hj7PtZ1rLlbswJcryOin56GT-exqFm4,8942
92
+ spiral/tables/maintenance.py,sha256=7Xa2Jdu_OY1Qu6iN1sPVdywVZtk_Mv3EaC3G93cmQvI,305
93
+ spiral/tables/scan.py,sha256=3lPf5fSyF1fHGdGJ-pvu5HxPWoonf_XL7neWTqzB-0I,7582
94
+ spiral/tables/snapshot.py,sha256=2NTuVEp2uJ1pV3Q5tLj7FOzPSc9axlfb6uOITwHnj0g,2229
95
+ spiral/tables/table.py,sha256=4B2drwwfaoL6aIJ-5Ll-Bqza-EBeDIfMkuszSOZqSpk,5326
96
+ spiral/tables/transaction.py,sha256=3a64R-mf_cmR54BNn8U-05jmWonp6Ivxhe6u01Dyjzo,1573
97
+ spiral/types_.py,sha256=W_jyO7F6rpPiH69jhgSgV7OxQZbOlb1Ho3InpKUP6Eo,155
98
+ pyspiral-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.9.3)
3
+ Root-Is-Purelib: false
4
+ Tag: pp310-pypy310_pp73-macosx_10_12_x86_64
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spiral=spiral.cli.app:main
spiral/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Python client for Spiral"""
2
+
3
+ # This is here to make sure we load the native extension first
4
+ from spiral import _lib
5
+
6
+ assert _lib
7
+
8
+ from spiral.client import Spiral # noqa: E402, I001
9
+
10
+ __all__ = ["Spiral"]
Binary file
spiral/adbc.py ADDED
@@ -0,0 +1,393 @@
1
+ import abc
2
+ import functools
3
+ import logging
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from urllib.parse import urlparse
6
+
7
+ import pyarrow as pa
8
+ import pyarrow.compute as pc
9
+ import sqlglot
10
+ import sqlglot.expressions as exp
11
+ from betterproto.lib.google.protobuf import Any
12
+ from pyarrow.flight import (
13
+ Action,
14
+ FlightDescriptor,
15
+ FlightEndpoint,
16
+ FlightError,
17
+ FlightInfo,
18
+ FlightMetadataWriter,
19
+ FlightServerBase,
20
+ MetadataRecordBatchReader,
21
+ RecordBatchStream,
22
+ ServerCallContext,
23
+ Ticket,
24
+ )
25
+
26
+ from spiral import Spiral
27
+ from spiral.api.projects import TableResource
28
+ from spiral.protogen._.arrow.flight.protocol import sql as rpc
29
+ from spiral.protogen._.arrow.flight.protocol.sql import (
30
+ CommandGetCatalogs,
31
+ CommandGetDbSchemas,
32
+ CommandGetSqlInfo,
33
+ CommandGetTables,
34
+ CommandStatementQuery,
35
+ SqlInfo,
36
+ SqlSupportedTransaction,
37
+ )
38
+
39
+ log = logging.getLogger(__name__)
40
+ logging.getLogger("sqlx").setLevel(logging.WARNING)
41
+
42
+
43
+ def debuggable(func):
44
+ """A decorator to enable GUI (i.e. PyCharm) debugging in the
45
+ decorated Arrow Flight RPC Server function.
46
+
47
+ See: https://github.com/apache/arrow/issues/36844
48
+ for more details...
49
+ """
50
+
51
+ @functools.wraps(func)
52
+ def wrapper_decorator(*args, **kwargs):
53
+ try:
54
+ import pydevd
55
+
56
+ pydevd.connected = True
57
+ pydevd.settrace(suspend=False)
58
+ except ImportError:
59
+ # Not running in debugger
60
+ pass
61
+ value = func(*args, **kwargs)
62
+ return value
63
+
64
+ return wrapper_decorator
65
+
66
+
67
+ # TODO(marko): This should work for Iceberg tables.
68
+ class ADBCServerBase:
69
+ def get_sql_info(self, _req: CommandGetSqlInfo) -> pa.RecordBatchReader:
70
+ """Default implementation that reports no support for any complex features."""
71
+ info = {
72
+ SqlInfo.FLIGHT_SQL_SERVER_NAME: "Spiral ADBC Server",
73
+ SqlInfo.FLIGHT_SQL_SERVER_VERSION: "0.0.1",
74
+ SqlInfo.FLIGHT_SQL_SERVER_ARROW_VERSION: pa.__version__,
75
+ SqlInfo.FLIGHT_SQL_SERVER_READ_ONLY: True,
76
+ SqlInfo.FLIGHT_SQL_SERVER_TRANSACTION: SqlSupportedTransaction.NONE.value,
77
+ }
78
+
79
+ # See https://github.com/apache/arrow-adbc/blob/38c21c2311a59803559cb0091b3f34180c28b25f/rust/core/src/schemas.rs#L35
80
+ union_fields = [
81
+ pa.field("string_value", pa.string()),
82
+ pa.field("bool_value", pa.bool_()),
83
+ pa.field("int64_value", pa.int64()),
84
+ pa.field("int32_bitmask", pa.int32()),
85
+ pa.field("string_list", pa.list_(pa.string())),
86
+ pa.field(
87
+ "int32_to_int32_list_map",
88
+ pa.map_(pa.int32(), pa.list_(pa.int32()), keys_sorted=False),
89
+ ),
90
+ ]
91
+ schema = pa.schema(
92
+ [
93
+ pa.field("info_name", pa.uint32(), nullable=False),
94
+ pa.field("info_value", pa.dense_union(union_fields), nullable=False),
95
+ ]
96
+ )
97
+
98
+ # PyArrow doesn't support creating a dense union for us :(
99
+ types = []
100
+ offsets = []
101
+ ints = []
102
+ bools = []
103
+ strs = []
104
+ for value in info.values():
105
+ if isinstance(value, str):
106
+ types.append(0)
107
+ offsets.append(len(strs))
108
+ strs.append(value)
109
+ elif isinstance(value, bool):
110
+ types.append(1)
111
+ offsets.append(len(bools))
112
+ bools.append(value)
113
+ else:
114
+ types.append(1)
115
+ offsets.append(len(ints))
116
+ ints.append(value)
117
+
118
+ values = pa.UnionArray.from_dense(
119
+ pa.array(types, type=pa.int8()),
120
+ pa.array(offsets, type=pa.int32()),
121
+ [pa.array(data, type=f.type) for data, f in zip([strs, bools, ints, [], [], []], union_fields)],
122
+ [f.name for f in union_fields],
123
+ )
124
+
125
+ return pa.table(data=[pa.array(list(info.keys()), type=pa.uint32()), values], schema=schema).to_reader()
126
+
127
+ @abc.abstractmethod
128
+ def get_catalogs(self, req: CommandGetCatalogs) -> pa.RecordBatchReader: ...
129
+
130
+ @abc.abstractmethod
131
+ def get_db_schemas(self, req: CommandGetDbSchemas) -> pa.RecordBatchReader: ...
132
+
133
+ @abc.abstractmethod
134
+ def get_tables(self, req: CommandGetTables) -> pa.RecordBatchReader: ...
135
+
136
+ @abc.abstractmethod
137
+ def statement_query(self, req: CommandStatementQuery, limit: int | None = None) -> pa.RecordBatchReader: ...
138
+
139
+
140
+ class SpiralADBCServer(ADBCServerBase):
141
+ def __init__(self, spiral: Spiral):
142
+ self.sp = spiral
143
+
144
+ self.pool = ThreadPoolExecutor()
145
+
146
+ def get_catalogs(self, req: CommandGetCatalogs) -> pa.RecordBatchReader:
147
+ schema = pa.schema([pa.field("catalog_name", pa.string(), nullable=False)])
148
+
149
+ @debuggable
150
+ def batches():
151
+ yield pa.RecordBatch.from_arrays(
152
+ [[p.id for p in self.sp.list_projects()]],
153
+ schema=schema,
154
+ )
155
+
156
+ return pa.RecordBatchReader.from_batches(schema, batches())
157
+
158
+ def get_db_schemas(self, req: CommandGetDbSchemas) -> pa.RecordBatchReader:
159
+ """Get the schemas from the database."""
160
+
161
+ schema = pa.schema(
162
+ [
163
+ pa.field("catalog_name", pa.string()),
164
+ pa.field("db_schema_name", pa.string(), nullable=False),
165
+ ]
166
+ )
167
+
168
+ @debuggable
169
+ def batches():
170
+ if req.catalog == "":
171
+ # Empty string means databases _without_ a catalog, which we don't support
172
+ return
173
+
174
+ # Otherwise, catalog is either the project ID, or None.
175
+ if req.catalog is None:
176
+ projects = self.sp.list_projects()
177
+ else:
178
+ projects = [self.sp.project(req.catalog)]
179
+
180
+ for project in projects:
181
+ datasets = {dt.dataset for dt in project.tables.list_tables()}
182
+
183
+ batch = pa.RecordBatch.from_arrays(
184
+ [
185
+ [project.id] * len(datasets),
186
+ list(datasets),
187
+ ],
188
+ schema=schema,
189
+ )
190
+
191
+ if req.db_schema_filter_pattern:
192
+ mask = pc.match_like(batch["db_schema_name"], req.db_schema_filter_pattern)
193
+ batch = batch.filter(mask)
194
+
195
+ yield batch
196
+
197
+ return pa.RecordBatchReader.from_batches(schema, batches())
198
+
199
+ def get_tables(self, req: CommandGetTables) -> pa.RecordBatchReader:
200
+ schema = pa.schema(
201
+ [
202
+ pa.field("catalog_name", pa.string()),
203
+ pa.field("db_schema_name", pa.string()),
204
+ pa.field("table_name", pa.string(), nullable=False),
205
+ pa.field("table_type", pa.string(), nullable=False),
206
+ ]
207
+ + [pa.field("table_schema", pa.binary(), nullable=False)]
208
+ if req.include_schema
209
+ else []
210
+ )
211
+
212
+ @debuggable
213
+ def batches():
214
+ if req.catalog == "":
215
+ # Empty string means databases _without_ a catalog, which we don't support
216
+ return
217
+
218
+ if req.catalog is None:
219
+ projects = list(self.sp.list_projects())
220
+ else:
221
+ projects = [self.sp.project(req.catalog)]
222
+
223
+ def _process_project(project):
224
+ tables: list[TableResource] = project.tables.list_tables()
225
+
226
+ rows = []
227
+ for table in tables:
228
+ row = {
229
+ "catalog_name": project.id,
230
+ "db_schema_name": table.dataset,
231
+ "table_name": table.table,
232
+ "table_type": "TABLE",
233
+ }
234
+
235
+ if req.include_schema:
236
+ open_table = project.tables.table(f"{table.project_id}.{table.dataset}.{table.table}")
237
+ row["table_schema"] = open_table.snapshot().to_dataset().schema.serialize().to_pybytes()
238
+
239
+ rows.append(row)
240
+
241
+ return pa.RecordBatch.from_pylist(rows, schema=schema)
242
+
243
+ yield from self.pool.map(_process_project, projects)
244
+
245
+ return pa.RecordBatchReader.from_batches(schema, batches())
246
+
247
+ @debuggable
248
+ def statement_query(self, req: CommandStatementQuery, limit: int | None = None) -> pa.RecordBatchReader:
249
+ # Extract the tables from the query, and bring them into the Python locals scope.
250
+ expr = sqlglot.parse_one(req.query, dialect="duckdb")
251
+ for tbl in expr.find_all(exp.Table):
252
+ # We swap the three-part identifier out for a single identifier
253
+ # This lets us insert a PyArrow Dataset into Python locals such that
254
+ # DuckDB will pick up on it for the query.
255
+ name = exp.table_name(tbl)
256
+ locals()[name] = self.sp.tables.table(f"{tbl.catalog}.{tbl.db}.{tbl.name}").snapshot().to_dataset()
257
+ tbl.replace(exp.table_(table=name))
258
+
259
+ try:
260
+ import duckdb
261
+ except ImportError:
262
+ raise FlightError("DuckDB is required for SQL queries.")
263
+
264
+ try:
265
+ sql = duckdb.sql(expr.sql(dialect="duckdb"))
266
+ except Exception as e:
267
+ raise FlightError(str(e))
268
+
269
+ if limit is not None:
270
+ sql = sql.limit(limit)
271
+
272
+ return sql.fetch_arrow_reader(batch_size=1_000)
273
+
274
+
275
+ class ADBCFlightServer(FlightServerBase):
276
+ """An implementation of a FlightSQL ADBC server."""
277
+
278
+ def __init__(self, abdc: ADBCServerBase, *, location=None, **kwargs):
279
+ super().__init__(location=location, **kwargs)
280
+ self.location = location
281
+ self.adbc = abdc
282
+
283
+ self.host = "localhost"
284
+ self.tls = False
285
+ if location:
286
+ parts = urlparse(location)
287
+ self.host = parts.hostname
288
+ self.tls = parts.scheme.endswith("s")
289
+
290
+ @debuggable
291
+ def do_action(self, context: ServerCallContext, action: Action):
292
+ log.info("DoAction %s: %s", context.peer(), action)
293
+ super().do_action(context, action)
294
+
295
+ @debuggable
296
+ def do_exchange(self, context: ServerCallContext, descriptor: FlightDescriptor, reader, writer):
297
+ log.info("DoExchange %s: %s", context.peer(), descriptor)
298
+ super().do_exchange(context, descriptor, reader, writer)
299
+
300
+ @debuggable
301
+ def do_get(self, context: ServerCallContext, ticket: Ticket):
302
+ log.info("DoGet %s: %s", context.peer(), ticket)
303
+ req = self.parse_command(ticket.ticket)
304
+ match req:
305
+ case CommandGetSqlInfo():
306
+ return RecordBatchStream(self.adbc.get_sql_info(req))
307
+ case CommandGetCatalogs():
308
+ return RecordBatchStream(self.adbc.get_catalogs(req))
309
+ case CommandGetDbSchemas():
310
+ return RecordBatchStream(self.adbc.get_db_schemas(req))
311
+ case CommandGetTables():
312
+ return RecordBatchStream(self.adbc.get_tables(req))
313
+ case CommandStatementQuery():
314
+ return RecordBatchStream(self.adbc.statement_query(req))
315
+ case _:
316
+ raise NotImplementedError(f"Unsupported do_Get: {req}")
317
+
318
+ @debuggable
319
+ def do_put(
320
+ self,
321
+ context: ServerCallContext,
322
+ descriptor: FlightDescriptor,
323
+ reader: MetadataRecordBatchReader,
324
+ writer: FlightMetadataWriter,
325
+ ):
326
+ log.info("DoPut %s: %s", context.peer(), descriptor)
327
+ super().do_put(context, descriptor, reader, writer)
328
+
329
+ @debuggable
330
+ def get_flight_info(self, context: ServerCallContext, descriptor: FlightDescriptor) -> FlightInfo:
331
+ log.info("GetFlightInfo %s: %s", context.peer(), descriptor)
332
+ req = self.parse_command(descriptor.command)
333
+ match req:
334
+ case CommandGetSqlInfo():
335
+ # Each metadata type contributes to the schema.
336
+ schema = self.adbc.get_sql_info(req).schema
337
+ case CommandGetCatalogs():
338
+ schema = self.adbc.get_catalogs(req).schema
339
+ case CommandGetDbSchemas():
340
+ schema = self.adbc.get_db_schemas(req).schema
341
+ case CommandGetTables():
342
+ schema = self.adbc.get_tables(req).schema
343
+ case CommandStatementQuery():
344
+ schema = self.adbc.statement_query(req, limit=0).schema
345
+ case _:
346
+ raise NotImplementedError(f"Unsupported command: {req}")
347
+
348
+ return self._make_flight_info(self.descriptor_to_key(descriptor), descriptor, schema)
349
+
350
+ @staticmethod
351
+ def parse_command(command: bytes):
352
+ command = Any().parse(command)
353
+
354
+ if not command.type_url.startswith("type.googleapis.com/arrow.flight.protocol.sql."):
355
+ raise NotImplementedError(f"Unsupported command: {command.type_url}")
356
+
357
+ proto_cls_name = command.type_url[len("type.googleapis.com/arrow.flight.protocol.sql.") :]
358
+ proto_cls = getattr(rpc, proto_cls_name)
359
+ return proto_cls().parse(command.value)
360
+
361
+ @staticmethod
362
+ def descriptor_to_key(descriptor):
363
+ return descriptor.command
364
+
365
+ @debuggable
366
+ def get_schema(self, context: ServerCallContext, descriptor: FlightDescriptor):
367
+ log.info("GetSchema %s: %s", context.peer(), descriptor)
368
+ return super().get_schema(context, descriptor)
369
+
370
+ @debuggable
371
+ def list_actions(self, context: ServerCallContext):
372
+ log.info("ListActions %s", context.peer())
373
+ super().list_actions(context)
374
+
375
+ @debuggable
376
+ def list_flights(self, context: ServerCallContext, criteria):
377
+ log.info("ListFlights %s: %s", context.peer(), criteria)
378
+ super().list_flights(context, criteria)
379
+
380
+ def _make_flight_info(self, key, descriptor, schema: pa.Schema):
381
+ # If we pass zero locations, the FlightSQL client should attempt to use the original connection.
382
+ endpoints = [FlightEndpoint(key, [])]
383
+ return FlightInfo(schema, descriptor, endpoints, -1, -1)
384
+
385
+
386
+ if __name__ == "__main__":
387
+ import logging
388
+
389
+ logging.basicConfig()
390
+ logging.getLogger("spiral").setLevel(logging.DEBUG)
391
+
392
+ server = ADBCFlightServer(SpiralADBCServer(Spiral()), location="grpc://localhost:5005")
393
+ server.serve()
spiral/api/__init__.py ADDED
@@ -0,0 +1,64 @@
1
+ import os
2
+ from typing import TYPE_CHECKING
3
+
4
+ import httpx
5
+
6
+ from .client import _Client
7
+
8
+ if TYPE_CHECKING:
9
+ from spiral.core.client import Authn
10
+
11
+ from .admin import AdminService
12
+ from .filesystems import FileSystemService
13
+ from .organizations import OrganizationService
14
+ from .projects import ProjectService
15
+ from .telemetry import TelemetryService
16
+ from .workloads import WorkloadService
17
+
18
+
19
+ class SpiralAPI:
20
+ def __init__(self, authn: "Authn", base_url: str | None = None):
21
+ self.base_url = base_url or os.environ.get("SPIRAL_URL", "https://api.spiraldb.com")
22
+ self.client = _Client(
23
+ httpx.Client(
24
+ base_url=self.base_url,
25
+ timeout=None if ("PYTEST_VERSION" in os.environ or bool(os.environ.get("SPIRAL_DEV", None))) else 60,
26
+ ),
27
+ authn,
28
+ )
29
+
30
+ @property
31
+ def _admin(self) -> "AdminService":
32
+ from .admin import AdminService
33
+
34
+ return AdminService(self.client)
35
+
36
+ @property
37
+ def organization(self) -> "OrganizationService":
38
+ from .organizations import OrganizationService
39
+
40
+ return OrganizationService(self.client)
41
+
42
+ @property
43
+ def project(self) -> "ProjectService":
44
+ from .projects import ProjectService
45
+
46
+ return ProjectService(self.client)
47
+
48
+ @property
49
+ def file_system(self) -> "FileSystemService":
50
+ from .filesystems import FileSystemService
51
+
52
+ return FileSystemService(self.client)
53
+
54
+ @property
55
+ def workload(self) -> "WorkloadService":
56
+ from .workloads import WorkloadService
57
+
58
+ return WorkloadService(self.client)
59
+
60
+ @property
61
+ def telemetry(self) -> "TelemetryService":
62
+ from .telemetry import TelemetryService
63
+
64
+ return TelemetryService(self.client)
spiral/api/admin.py ADDED
@@ -0,0 +1,15 @@
1
+ from .client import Paged, PagedResponse, ServiceBase
2
+ from .organizations import OrgMembership
3
+ from .types import OrgId
4
+
5
+
6
+ class AdminService(ServiceBase):
7
+ def sync_memberships(self, org_id: OrgId | None = None) -> Paged[OrgMembership]:
8
+ params = {}
9
+ if org_id:
10
+ params["org_id"] = str(org_id)
11
+ return self.client.paged("/v1/admin/sync-memberships", PagedResponse[OrgMembership], params=params)
12
+
13
+ def sync_orgs(self) -> Paged[OrgId]:
14
+ params = {}
15
+ return self.client.paged("/v1/admin/sync-orgs", PagedResponse[OrgId], params=params)