pyspiral 0.8.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.
Files changed (114) hide show
  1. pyspiral-0.8.9.dist-info/METADATA +53 -0
  2. pyspiral-0.8.9.dist-info/RECORD +114 -0
  3. pyspiral-0.8.9.dist-info/WHEEL +4 -0
  4. pyspiral-0.8.9.dist-info/entry_points.txt +3 -0
  5. spiral/__init__.py +55 -0
  6. spiral/_lib.abi3.so +0 -0
  7. spiral/adbc.py +411 -0
  8. spiral/api/__init__.py +78 -0
  9. spiral/api/admin.py +15 -0
  10. spiral/api/client.py +165 -0
  11. spiral/api/filesystems.py +152 -0
  12. spiral/api/key_space_indexes.py +23 -0
  13. spiral/api/organizations.py +78 -0
  14. spiral/api/projects.py +219 -0
  15. spiral/api/telemetry.py +19 -0
  16. spiral/api/text_indexes.py +56 -0
  17. spiral/api/types.py +23 -0
  18. spiral/api/workers.py +40 -0
  19. spiral/api/workloads.py +52 -0
  20. spiral/arrow_.py +202 -0
  21. spiral/cli/__init__.py +89 -0
  22. spiral/cli/__main__.py +4 -0
  23. spiral/cli/admin.py +33 -0
  24. spiral/cli/app.py +108 -0
  25. spiral/cli/console.py +95 -0
  26. spiral/cli/fs.py +109 -0
  27. spiral/cli/iceberg.py +97 -0
  28. spiral/cli/key_spaces.py +103 -0
  29. spiral/cli/login.py +25 -0
  30. spiral/cli/orgs.py +81 -0
  31. spiral/cli/printer.py +53 -0
  32. spiral/cli/projects.py +148 -0
  33. spiral/cli/state.py +7 -0
  34. spiral/cli/tables.py +225 -0
  35. spiral/cli/telemetry.py +17 -0
  36. spiral/cli/text.py +115 -0
  37. spiral/cli/types.py +50 -0
  38. spiral/cli/workloads.py +86 -0
  39. spiral/client.py +279 -0
  40. spiral/core/__init__.pyi +0 -0
  41. spiral/core/_tools/__init__.pyi +5 -0
  42. spiral/core/authn/__init__.pyi +21 -0
  43. spiral/core/client/__init__.pyi +270 -0
  44. spiral/core/config/__init__.pyi +35 -0
  45. spiral/core/expr/__init__.pyi +15 -0
  46. spiral/core/expr/images/__init__.pyi +3 -0
  47. spiral/core/expr/list_/__init__.pyi +4 -0
  48. spiral/core/expr/pushdown/__init__.pyi +3 -0
  49. spiral/core/expr/refs/__init__.pyi +4 -0
  50. spiral/core/expr/s3/__init__.pyi +3 -0
  51. spiral/core/expr/str_/__init__.pyi +3 -0
  52. spiral/core/expr/struct_/__init__.pyi +6 -0
  53. spiral/core/expr/text/__init__.pyi +5 -0
  54. spiral/core/expr/udf/__init__.pyi +14 -0
  55. spiral/core/expr/video/__init__.pyi +3 -0
  56. spiral/core/table/__init__.pyi +142 -0
  57. spiral/core/table/manifests/__init__.pyi +35 -0
  58. spiral/core/table/metastore/__init__.pyi +58 -0
  59. spiral/core/table/spec/__init__.pyi +214 -0
  60. spiral/dataloader.py +310 -0
  61. spiral/dataset.py +264 -0
  62. spiral/datetime_.py +27 -0
  63. spiral/debug/__init__.py +0 -0
  64. spiral/debug/manifests.py +103 -0
  65. spiral/debug/metrics.py +56 -0
  66. spiral/debug/scan.py +266 -0
  67. spiral/demo.py +100 -0
  68. spiral/enrichment.py +290 -0
  69. spiral/expressions/__init__.py +274 -0
  70. spiral/expressions/base.py +186 -0
  71. spiral/expressions/file.py +17 -0
  72. spiral/expressions/http.py +17 -0
  73. spiral/expressions/list_.py +77 -0
  74. spiral/expressions/pushdown.py +12 -0
  75. spiral/expressions/s3.py +16 -0
  76. spiral/expressions/str_.py +39 -0
  77. spiral/expressions/struct.py +59 -0
  78. spiral/expressions/text.py +62 -0
  79. spiral/expressions/tiff.py +225 -0
  80. spiral/expressions/udf.py +66 -0
  81. spiral/grpc_.py +32 -0
  82. spiral/iceberg.py +31 -0
  83. spiral/iterable_dataset.py +106 -0
  84. spiral/key_space_index.py +44 -0
  85. spiral/project.py +247 -0
  86. spiral/protogen/_/__init__.py +0 -0
  87. spiral/protogen/_/arrow/__init__.py +0 -0
  88. spiral/protogen/_/arrow/flight/__init__.py +0 -0
  89. spiral/protogen/_/arrow/flight/protocol/__init__.py +0 -0
  90. spiral/protogen/_/arrow/flight/protocol/sql/__init__.py +2548 -0
  91. spiral/protogen/_/google/__init__.py +0 -0
  92. spiral/protogen/_/google/protobuf/__init__.py +2310 -0
  93. spiral/protogen/_/message_pool.py +3 -0
  94. spiral/protogen/_/py.typed +0 -0
  95. spiral/protogen/_/scandal/__init__.py +190 -0
  96. spiral/protogen/_/spfs/__init__.py +72 -0
  97. spiral/protogen/_/spql/__init__.py +61 -0
  98. spiral/protogen/_/substrait/__init__.py +6196 -0
  99. spiral/protogen/_/substrait/extensions/__init__.py +169 -0
  100. spiral/protogen/__init__.py +0 -0
  101. spiral/protogen/util.py +41 -0
  102. spiral/py.typed +0 -0
  103. spiral/scan.py +383 -0
  104. spiral/server.py +37 -0
  105. spiral/settings.py +36 -0
  106. spiral/snapshot.py +61 -0
  107. spiral/streaming_/__init__.py +3 -0
  108. spiral/streaming_/reader.py +133 -0
  109. spiral/streaming_/stream.py +156 -0
  110. spiral/substrait_.py +274 -0
  111. spiral/table.py +216 -0
  112. spiral/text_index.py +17 -0
  113. spiral/transaction.py +156 -0
  114. spiral/types_.py +6 -0
@@ -0,0 +1,86 @@
1
+ from typing import Annotated
2
+
3
+ import pyperclip
4
+ import questionary
5
+ from questionary import Choice
6
+ from typer import Argument, Option
7
+
8
+ from spiral.api.workloads import (
9
+ CreateWorkloadRequest,
10
+ CreateWorkloadResponse,
11
+ IssueWorkloadCredentialsResponse,
12
+ Workload,
13
+ )
14
+ from spiral.cli import CONSOLE, ERR_CONSOLE, AsyncTyper, printer, state
15
+ from spiral.cli.types import ProjectArg
16
+
17
+ app = AsyncTyper()
18
+
19
+
20
+ @app.command(help="List workloads.")
21
+ def ls(
22
+ project: ProjectArg,
23
+ ):
24
+ workloads = list(state.spiral.api.workloads.list(project))
25
+ CONSOLE.print(printer.table_of_models(Workload, workloads, fields=["id", "project_id", "name"]))
26
+
27
+
28
+ @app.command(help="Create a new workload.")
29
+ def create(
30
+ project: ProjectArg,
31
+ name: Annotated[str | None, Option(help="Friendly name for the workload.")] = None,
32
+ ):
33
+ res: CreateWorkloadResponse = state.spiral.api.workloads.create(project, CreateWorkloadRequest(name=name))
34
+ CONSOLE.print(f"{res.workload.id}")
35
+
36
+
37
+ @app.command(help="Deactivate a workload. Removes all associated credentials.")
38
+ def deactivate(
39
+ workload_id: Annotated[str, Argument(help="Workload ID.")],
40
+ ):
41
+ state.spiral.api.workloads.deactivate(workload_id)
42
+ CONSOLE.print(f"Deactivated workload {workload_id}")
43
+
44
+
45
+ @app.command(help="Issue new workflow credentials.")
46
+ def issue_creds(
47
+ workload_id: Annotated[str, Argument(help="Workload ID.")],
48
+ skip_prompt: Annotated[bool, Option(help="Skip prompt and print secret to console.")] = False,
49
+ ):
50
+ res: IssueWorkloadCredentialsResponse = state.spiral.api.workloads.issue_credentials(workload_id)
51
+
52
+ if skip_prompt:
53
+ CONSOLE.print(f"[green]SPIRAL_CLIENT_ID[/green] {res.client_id}")
54
+ CONSOLE.print(f"[green]SPIRAL_CLIENT_SECRET[/green] {res.client_secret}")
55
+ else:
56
+ while True:
57
+ choice = questionary.select(
58
+ "What would you like to do with the secret? You will not be able to see this secret again!",
59
+ choices=[
60
+ Choice(title="Copy to clipboard", value=1),
61
+ Choice(title="Print to console", value=2),
62
+ Choice(title="Exit", value=3),
63
+ ],
64
+ ).ask()
65
+
66
+ if choice == 1:
67
+ pyperclip.copy(res.client_secret)
68
+ CONSOLE.print("[green]Secret copied to clipboard![/green]")
69
+ break
70
+ elif choice == 2:
71
+ CONSOLE.print(f"[green]SPIRAL_CLIENT_SECRET[/green] {res.client_secret}")
72
+ break
73
+ elif choice == 3:
74
+ break
75
+ else:
76
+ ERR_CONSOLE.print("Invalid choice. Please try again.")
77
+
78
+ CONSOLE.print(f"[green]SPIRAL_CLIENT_ID[/green] {res.client_id}")
79
+
80
+
81
+ @app.command(help="Revoke workflow credentials.")
82
+ def revoke_creds(
83
+ client_id: Annotated[str, Argument(help="Client ID to revoke.")],
84
+ ):
85
+ state.spiral.api.workloads.revoke_credentials(client_id)
86
+ CONSOLE.print(f"Revoked credentials for client ID {client_id}")
spiral/client.py ADDED
@@ -0,0 +1,279 @@
1
+ import os
2
+ from datetime import datetime, timedelta
3
+ from typing import TYPE_CHECKING
4
+
5
+ import jwt
6
+ import pyarrow as pa
7
+
8
+ from spiral.api import SpiralAPI
9
+ from spiral.api.projects import CreateProjectRequest, CreateProjectResponse
10
+ from spiral.core.authn import Authn
11
+ from spiral.core.client import Internal, Shard
12
+ from spiral.core.client import Spiral as CoreSpiral
13
+ from spiral.core.config import ClientSettings
14
+ from spiral.datetime_ import timestamp_micros
15
+ from spiral.expressions import ExprLike
16
+ from spiral.scan import Scan
17
+
18
+ if TYPE_CHECKING:
19
+ from spiral.iceberg import Iceberg
20
+ from spiral.key_space_index import KeySpaceIndex
21
+ from spiral.project import Project
22
+ from spiral.table import Table
23
+ from spiral.text_index import TextIndex
24
+
25
+
26
+ class Spiral:
27
+ """Main client for interacting with the Spiral data platform.
28
+
29
+ Configuration is loaded with the following priority (highest to lowest):
30
+ 1. Explicit parameters.
31
+ 2. Environment variables (`SPIRAL__*`)
32
+ 3. Config file (`~/.spiral.toml`)
33
+ 4. Default values (production URLs)
34
+
35
+ Examples:
36
+
37
+ ```python
38
+ import spiral
39
+ # Default configuration
40
+ sp = spiral.Spiral()
41
+
42
+ # With config overrides
43
+ sp = spiral.Spiral(overrides={"limits.concurrency": "16"})
44
+ ```
45
+
46
+ Args:
47
+ config: Custom ClientSettings object. Defaults to global settings.
48
+ overrides: Configuration overrides using dot notation,
49
+ see the [Client Configuration](https://docs.spiraldb.com/config) page for a full list.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ config: ClientSettings | None = None,
56
+ overrides: dict[str, str] | None = None,
57
+ ):
58
+ if bool(os.environ.get("SPIRAL__DEV", None)):
59
+ overrides = overrides or {}
60
+ overrides["server.url"] = "http://localhost:4279"
61
+ overrides["spfs.url"] = "http://localhost:4295"
62
+
63
+ self._overrides = overrides
64
+ self._config = config
65
+ self._org = None
66
+ self._core = None
67
+ self._api = None
68
+
69
+ @property
70
+ def config(self) -> ClientSettings:
71
+ """Returns the client's configuration"""
72
+ return self.core.config()
73
+
74
+ @property
75
+ def authn(self) -> Authn:
76
+ """Get the authentication handler for this client."""
77
+ return self.core.authn()
78
+
79
+ @property
80
+ def api(self) -> SpiralAPI:
81
+ if self._api is None:
82
+ self._api = SpiralAPI(self.config.server_url, self.authn)
83
+ return self._api
84
+
85
+ @property
86
+ def core(self) -> CoreSpiral:
87
+ if self._core is None:
88
+ self._core = CoreSpiral(
89
+ config=self._config,
90
+ overrides=self._overrides,
91
+ )
92
+
93
+ return self._core
94
+
95
+ @property
96
+ def internal(self) -> Internal:
97
+ return self.core.internal(format=self.config.file_format)
98
+
99
+ @property
100
+ def organization(self) -> str:
101
+ if self._org is None:
102
+ token = self.authn.token()
103
+ if token is None:
104
+ raise ValueError("Authentication failed.")
105
+ token_payload = jwt.decode(token.expose_secret(), options={"verify_signature": False})
106
+ if "org_id" not in token_payload:
107
+ raise ValueError("Please create an organization.")
108
+ self._org = token_payload["org_id"]
109
+ return self._org
110
+
111
+ def list_projects(self) -> list["Project"]:
112
+ """List project IDs."""
113
+ from .project import Project
114
+
115
+ return [Project(self, project_id=p.id, name=p.name) for p in self.api.projects.list()]
116
+
117
+ def create_project(
118
+ self,
119
+ id_prefix: str | None = None,
120
+ *,
121
+ name: str | None = None,
122
+ ) -> "Project":
123
+ """Create a project in the current, or given, organization."""
124
+ from .project import Project
125
+
126
+ res: CreateProjectResponse = self.api.projects.create(CreateProjectRequest(id_prefix=id_prefix, name=name))
127
+ return Project(self, res.project.id, name=res.project.name)
128
+
129
+ def project(self, project_id: str) -> "Project":
130
+ """Open an existing project."""
131
+ from spiral.project import Project
132
+
133
+ # We avoid an API call since we'd just be fetching a human-readable name. Seems a waste in most cases.
134
+ return Project(self, project_id=project_id, name=project_id)
135
+
136
+ def table(self, table_id: str) -> "Table":
137
+ """Open a table using an ID."""
138
+ from spiral.table import Table
139
+
140
+ return Table(self, self.core.table(table_id))
141
+
142
+ def text_index(self, index_id: str) -> "TextIndex":
143
+ """Open a text index using an ID."""
144
+ from spiral.text_index import TextIndex
145
+
146
+ return TextIndex(self.core.text_index(index_id))
147
+
148
+ def key_space_index(self, index_id: str) -> "KeySpaceIndex":
149
+ """Open a key space index using an ID."""
150
+ from spiral.key_space_index import KeySpaceIndex
151
+
152
+ return KeySpaceIndex(self.core.key_space_index(index_id))
153
+
154
+ def scan(
155
+ self,
156
+ *projections: ExprLike,
157
+ where: ExprLike | None = None,
158
+ asof: datetime | int | None = None,
159
+ shard: Shard | None = None,
160
+ ) -> Scan:
161
+ """Starts a read transaction on the Spiral.
162
+
163
+ Args:
164
+ projections: a set of expressions that return struct arrays.
165
+ where: a query expression to apply to the data.
166
+ asof: execute the scan on the version of the table as of the given timestamp.
167
+ shard: if provided, opens the scan only for the given shard.
168
+ While shards can be provided when executing the scan, providing a shard here
169
+ optimizes the scan planning phase and can significantly reduce metadata download.
170
+ """
171
+ from spiral import expressions as se
172
+
173
+ if isinstance(asof, datetime):
174
+ asof = timestamp_micros(asof)
175
+
176
+ # Combine all projections into a single struct.
177
+ if not projections:
178
+ raise ValueError("At least one projection is required.")
179
+ projection = se.merge(*projections)
180
+ if where is not None:
181
+ where = se.lift(where)
182
+
183
+ return Scan(
184
+ self,
185
+ self.core.scan(projection.__expr__, filter=where.__expr__ if where else None, asof=asof, shard=shard),
186
+ )
187
+
188
+ # TODO(marko): This should be query, and search should be query + scan.
189
+ def search(
190
+ self,
191
+ top_k: int,
192
+ *rank_by: ExprLike,
193
+ filters: ExprLike | None = None,
194
+ freshness_window: timedelta | None = None,
195
+ ) -> pa.RecordBatchReader:
196
+ """Queries the index with the given rank by and filters clauses. Returns a stream of scored keys.
197
+
198
+ Args:
199
+ top_k: The number of top results to return.
200
+ rank_by: Rank by expressions are combined for scoring.
201
+ See `se.text.find` and `se.text.boost` for scoring expressions.
202
+ filters: The `filters` expression is used to filter the results.
203
+ It must return a boolean value and use only conjunctions (ANDs). Expressions in filters
204
+ statement are considered either a `must` or `must_not` clause in search terminology.
205
+ freshness_window: If provided, the index will not be refreshed if its freshness does not exceed this window.
206
+ """
207
+ from spiral import expressions as se
208
+
209
+ if not rank_by:
210
+ raise ValueError("At least one rank by expression is required.")
211
+ rank_by = se.or_(*rank_by)
212
+ if filters is not None:
213
+ filters = se.lift(filters)
214
+
215
+ if freshness_window is None:
216
+ freshness_window = timedelta(seconds=0)
217
+ freshness_window_s = int(freshness_window.total_seconds())
218
+
219
+ return self.core.search(
220
+ top_k=top_k,
221
+ rank_by=rank_by.__expr__,
222
+ filters=filters.__expr__ if filters else None,
223
+ freshness_window_s=freshness_window_s,
224
+ )
225
+
226
+ def resume_scan(self, state_json: str) -> Scan:
227
+ """Resumes a previously started scan using its scan state.
228
+
229
+ Args:
230
+ state_json: The scan state returned by a previous scan.
231
+ """
232
+ from spiral.core.table import ScanState
233
+
234
+ state = ScanState.from_json(state_json)
235
+ return Scan(self, self.core.load_scan(state))
236
+
237
+ def compute_shards(
238
+ self,
239
+ max_batch_size: int,
240
+ *projections: ExprLike,
241
+ where: ExprLike | None = None,
242
+ asof: datetime | int | None = None,
243
+ stream: bool = False,
244
+ ) -> list[Shard]:
245
+ """Computes shards over the given projections and filter.
246
+
247
+ Args:
248
+ max_batch_size: The maximum number of rows per shard.
249
+ projections: a set of expressions that return struct arrays.
250
+ where: a query expression to apply to the data.
251
+ asof: execute the scan on the version of the table as of the given timestamp.
252
+ stream: if true, builds shards in a streaming fashion, suitable for very large tables.
253
+ """
254
+ from spiral import expressions as se
255
+
256
+ if isinstance(asof, datetime):
257
+ asof = timestamp_micros(asof)
258
+
259
+ # Combine all projections into a single struct.
260
+ if not projections:
261
+ raise ValueError("At least one projection is required.")
262
+ projection = se.merge(*projections)
263
+ if where is not None:
264
+ where = se.lift(where)
265
+
266
+ return self.core.compute_shards(
267
+ max_batch_size, projection.__expr__, where.__expr__ if where else None, asof=asof, stream=stream
268
+ )
269
+
270
+ @property
271
+ def iceberg(self) -> "Iceberg":
272
+ """
273
+ Apache Iceberg is a powerful open-source table format designed for high-performance data lakes.
274
+ Iceberg brings reliability, scalability, and advanced features like time travel, schema evolution,
275
+ and ACID transactions to your warehouse.
276
+ """
277
+ from spiral.iceberg import Iceberg
278
+
279
+ return Iceberg(self)
File without changes
@@ -0,0 +1,5 @@
1
+ from ..table.spec import Schema
2
+
3
+ def pretty_key(key: bytes, schema: Schema) -> str:
4
+ """Represent a key in a human-readable way."""
5
+ ...
@@ -0,0 +1,21 @@
1
+ from spiral.api.types import OrgId
2
+
3
+ class Token:
4
+ def __init__(self, value: str): ...
5
+ def expose_secret(self) -> str: ...
6
+
7
+ class Authn:
8
+ def token(self) -> Token | None: ...
9
+
10
+ class DeviceCodeAuth:
11
+ @staticmethod
12
+ def default() -> DeviceCodeAuth:
13
+ """Return the static device code instance."""
14
+ ...
15
+ def authenticate(self, force: bool = False, org_id: OrgId | None = None) -> Token:
16
+ """Authenticate using device code flow."""
17
+ ...
18
+
19
+ def logout(self) -> None:
20
+ """Logout from the device authentication session."""
21
+ ...
@@ -0,0 +1,270 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ import pyarrow as pa
5
+ from spiral.api.types import DatasetName, IndexName, ProjectId, RootUri, TableId, TableName
6
+ from spiral.core.authn import Authn
7
+ from spiral.core.config import ClientSettings
8
+ from spiral.core.table import ColumnGroupState, KeyRange, KeySpaceState, Scan, ScanState, Snapshot, Table, Transaction
9
+ from spiral.core.table.spec import ColumnGroup, Schema
10
+ from spiral.expressions import Expr
11
+
12
+ class Spiral:
13
+ """A client for Spiral database"""
14
+ def __init__(
15
+ self,
16
+ config: ClientSettings | None = None,
17
+ overrides: dict[str, str] | None = None,
18
+ ):
19
+ """Initialize the Spiral client.
20
+
21
+ Args:
22
+ config: Client configuration, defaults to the global config.
23
+ overrides: Configuration overrides using dot notation,
24
+ see the [Client Configuration](/python-client) page for a full list.
25
+ """
26
+ ...
27
+
28
+ def authn(self) -> Authn:
29
+ """Get the current authentication context."""
30
+ ...
31
+
32
+ def scan(
33
+ self,
34
+ projection: Expr,
35
+ filter: Expr | None = None,
36
+ asof: int | None = None,
37
+ shard: Shard | None = None,
38
+ key_columns: KeyColumns | None = None,
39
+ ) -> Scan:
40
+ """Construct a table scan."""
41
+ ...
42
+
43
+ def load_scan(self, plan_state: ScanState) -> Scan:
44
+ """Load a scan from a serialized scan state."""
45
+ ...
46
+
47
+ def transaction(self, table: Table, *, partition_max_bytes: int | None = None) -> Transaction:
48
+ """Being a table transaction."""
49
+ ...
50
+
51
+ def search(
52
+ self,
53
+ top_k: int,
54
+ rank_by: Expr,
55
+ *,
56
+ filters: Expr | None = None,
57
+ freshness_window_s: int | None = None,
58
+ ) -> pa.RecordBatchReader:
59
+ """Search an index.
60
+
61
+ Searching an index returns a stream of record batches that match table's key schema + float score column.
62
+ """
63
+ ...
64
+
65
+ def table(self, table_id: str) -> Table:
66
+ """Get a table."""
67
+ ...
68
+
69
+ def create_table(
70
+ self,
71
+ project_id: ProjectId,
72
+ dataset: DatasetName,
73
+ table: TableName,
74
+ key_schema: Schema,
75
+ *,
76
+ root_uri: RootUri | None = None,
77
+ exist_ok: bool = False,
78
+ ) -> Table:
79
+ """Create a new table in the specified project."""
80
+ ...
81
+
82
+ def move_table(
83
+ self,
84
+ table_id: TableId,
85
+ new_dataset: DatasetName,
86
+ ):
87
+ """Move a table to a dataset in the same project."""
88
+ ...
89
+
90
+ def rename_table(
91
+ self,
92
+ table_id: TableId,
93
+ new_table: TableName,
94
+ ):
95
+ """Rename a table."""
96
+ ...
97
+
98
+ def drop_table(self, table_id: TableId):
99
+ """Drop a table."""
100
+ ...
101
+
102
+ def text_index(self, index_id: str) -> TextIndex:
103
+ """Get a text index."""
104
+ ...
105
+
106
+ def create_text_index(
107
+ self,
108
+ project_id: ProjectId,
109
+ name: IndexName,
110
+ projection: Expr,
111
+ filter: Expr | None = None,
112
+ *,
113
+ root_uri: RootUri | None = None,
114
+ exist_ok: bool = False,
115
+ ) -> TextIndex:
116
+ """Create a new index in the specified project."""
117
+ ...
118
+
119
+ def key_space_index(self, index_id: str) -> KeySpaceIndex:
120
+ """Get a key space index."""
121
+ ...
122
+
123
+ def create_key_space_index(
124
+ self,
125
+ project_id: ProjectId,
126
+ name: IndexName,
127
+ granularity: int,
128
+ projection: Expr,
129
+ filter: Expr | None = None,
130
+ *,
131
+ root_uri: RootUri | None = None,
132
+ exist_ok: bool = False,
133
+ ) -> KeySpaceIndex:
134
+ """Create a new key space index in the specified project."""
135
+ ...
136
+
137
+ def compute_shards(
138
+ self,
139
+ max_batch_size: int,
140
+ projection: Expr,
141
+ filter: Expr | None = None,
142
+ asof: int | None = None,
143
+ stream: bool = False,
144
+ ) -> list[Shard]:
145
+ """Constructs shards for a given projection (and filter).
146
+
147
+ Useful for distributing work.
148
+ """
149
+ ...
150
+
151
+ def internal(self, *, format: str | None = None) -> Internal:
152
+ """Internal client APIs. It can change without notice."""
153
+ ...
154
+
155
+ def config(self) -> ClientSettings:
156
+ """Client-side configuration."""
157
+ ...
158
+
159
+ class KeyColumns(Enum):
160
+ IfProjected = 0
161
+ Included = 1
162
+ Only = 2
163
+
164
+ class TextIndex:
165
+ id: str
166
+
167
+ class KeySpaceIndex:
168
+ id: str
169
+ table_id: str
170
+ granularity: int
171
+ projection: Expr
172
+ filter: Expr
173
+ asof: int
174
+
175
+ class Shard:
176
+ """A shard representing a partition of data.
177
+
178
+ Attributes:
179
+ key_range: The key range for this shard.
180
+ cardinality: The number of rows in this shard, if known.
181
+ """
182
+
183
+ key_range: KeyRange
184
+ cardinality: int | None
185
+
186
+ def __init__(self, key_range: KeyRange, cardinality: int | None): ...
187
+ def __getnewargs__(self) -> tuple[KeyRange, int | None]: ...
188
+ def union(self, other: KeyRange) -> KeyRange: ...
189
+ def __or__(self, other):
190
+ """Combine two shards into one that covers both key ranges.
191
+
192
+ The cardinality of the resulting shard is set to None.
193
+ """
194
+ ...
195
+
196
+ class ShuffleConfig:
197
+ """Configuration for within-shard sample shuffling.
198
+
199
+ This controls how samples are shuffled within a buffer, separate from
200
+ which shards to read (which is specified as a parameter to the scan).
201
+
202
+ Attributes:
203
+ buffer_size: Size of the buffer pool for shuffling samples.
204
+ seed: Random seed for reproducibility. If None, uses OS randomness.
205
+ """
206
+
207
+ buffer_size: int
208
+ seed: int | None
209
+
210
+ def __init__(
211
+ self,
212
+ buffer_size: int,
213
+ *,
214
+ seed: int | None = None,
215
+ ): ...
216
+
217
+ class Internal:
218
+ def flush_wal(self, table: Table) -> None:
219
+ """
220
+ Flush the write-ahead log of the table.
221
+ """
222
+ ...
223
+ def update_text_index(self, index: TextIndex, snapshot: Snapshot) -> None:
224
+ """
225
+ Index table changes up to the given snapshot.
226
+ """
227
+ ...
228
+ def update_key_space_index(self, index: KeySpaceIndex, snapshot: Snapshot) -> None:
229
+ """
230
+ Index table changes up to the given snapshot.
231
+ """
232
+ ...
233
+ def key_space_state(self, snapshot: Snapshot) -> KeySpaceState:
234
+ """
235
+ The key space state for the table.
236
+ """
237
+ ...
238
+ def column_group_state(
239
+ self, snapshot: Snapshot, key_space_state: KeySpaceState, column_group: ColumnGroup
240
+ ) -> ColumnGroupState:
241
+ """
242
+ The state the column group of the table.
243
+ """
244
+ ...
245
+ def column_groups_states(self, snapshot: Snapshot, key_space_state: KeySpaceState) -> list[ColumnGroupState]:
246
+ """
247
+ The state of each column group of the table.
248
+ """
249
+ ...
250
+ def key_space_index_shards(self, index: KeySpaceIndex) -> list[Shard]:
251
+ """
252
+ Compute the scan shards from a key space index.
253
+ """
254
+ ...
255
+ def prepare_shard(
256
+ self,
257
+ output_path: str,
258
+ scan: Scan,
259
+ shard: Shard,
260
+ row_block_size: int = 8192,
261
+ ) -> None:
262
+ """
263
+ Prepare a shard locally. Used for `SpiralStream` integration with `streaming` which requires on-disk shards.
264
+ """
265
+ ...
266
+ def metrics(self) -> dict[str, Any]: ...
267
+
268
+ def flush_telemetry() -> None:
269
+ """Flush telemetry data to the configured exporter."""
270
+ ...
@@ -0,0 +1,35 @@
1
+ class ClientSettings:
2
+ """Client configuration loaded from ~/.spiral.toml and environment variables."""
3
+
4
+ @staticmethod
5
+ def load() -> ClientSettings:
6
+ """Load ClientSettings from ~/.spiral.toml and environment variables.
7
+
8
+ Configuration priority (highest to lowest):
9
+ 1. Environment variables (SPIRAL__*)
10
+ 2. Config file (~/.spiral.toml)
11
+ 3. Default values
12
+ """
13
+ ...
14
+
15
+ @property
16
+ def server_url(self) -> str:
17
+ """The Spiral API endpoint URL."""
18
+ ...
19
+
20
+ @property
21
+ def spfs_url(self) -> str:
22
+ """The SpFS endpoint URL."""
23
+ ...
24
+
25
+ @property
26
+ def file_format(self) -> str:
27
+ """File format for table storage (vortex or parquet)."""
28
+ ...
29
+
30
+ def to_json(self) -> str:
31
+ """Serialize to a JSON string"""
32
+ ...
33
+ @staticmethod
34
+ def from_json(json: str) -> ClientSettings:
35
+ """Deserialize from a JSON-formatted string"""