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
spiral/cli/app.py
CHANGED
|
@@ -20,6 +20,7 @@ from spiral.cli import (
|
|
|
20
20
|
tables,
|
|
21
21
|
telemetry,
|
|
22
22
|
text,
|
|
23
|
+
transactions,
|
|
23
24
|
workloads,
|
|
24
25
|
)
|
|
25
26
|
from spiral.settings import LOG_DIR, PACKAGE_NAME
|
|
@@ -77,18 +78,23 @@ app.add_typer(projects.app, name="projects")
|
|
|
77
78
|
app.add_typer(fs.app, name="fs")
|
|
78
79
|
app.add_typer(workloads.app, name="workloads")
|
|
79
80
|
app.add_typer(tables.app, name="tables")
|
|
80
|
-
app.add_typer(key_spaces.app, name="ks")
|
|
81
|
-
app.add_typer(text.app, name="text")
|
|
82
|
-
app.add_typer(telemetry.app, name="telemetry")
|
|
83
81
|
app.add_typer(iceberg.app, name="iceberg")
|
|
82
|
+
app.add_typer(telemetry.app, name="telemetry")
|
|
84
83
|
app.command("login")(login.command)
|
|
84
|
+
app.command("whoami")(login.whoami)
|
|
85
85
|
app.command("console")(console.command)
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
# Register unless we're building docs. Because Typer docs command does not skip hidden commands...
|
|
89
89
|
if not bool(os.environ.get("SPIRAL_DOCS", False)):
|
|
90
90
|
app.add_typer(admin.app, name="admin", hidden=True)
|
|
91
|
-
|
|
91
|
+
|
|
92
|
+
# Hide some "beta" commands.
|
|
93
|
+
app.add_typer(transactions.app, name="txn", hidden=True)
|
|
94
|
+
app.add_typer(key_spaces.app, name="ks", hidden=True)
|
|
95
|
+
app.add_typer(text.app, name="text", hidden=True)
|
|
96
|
+
|
|
97
|
+
# Hidden because there isn't really a logout. This just removes the stored credentials.
|
|
92
98
|
app.command("logout", hidden=True)(login.logout)
|
|
93
99
|
|
|
94
100
|
|
spiral/cli/chooser.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
import typer
|
|
6
|
+
from questionary import Choice
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def choose(title: str, choices: list[Choice] | list[str]) -> Any:
|
|
10
|
+
"""Interactively select one of the choices exiting the process if ctrl-c is pressed."""
|
|
11
|
+
if choices and isinstance(choices[0], Choice):
|
|
12
|
+
for choice in choices:
|
|
13
|
+
assert isinstance(choice, Choice)
|
|
14
|
+
choice.value = (choice.value,)
|
|
15
|
+
else:
|
|
16
|
+
choices = cast(list[str], choices)
|
|
17
|
+
choices = [Choice(choice, value=(choice,)) for choice in choices]
|
|
18
|
+
|
|
19
|
+
maybe_selection = questionary.select(title, choices=choices).ask()
|
|
20
|
+
if maybe_selection is None:
|
|
21
|
+
raise typer.Exit(2)
|
|
22
|
+
return maybe_selection[0]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def input_text(message: str, validate: Callable[[str], bool | str]) -> str:
|
|
26
|
+
"""Interactively receive string input which passes the validation exiting the process if ctrl-c is pressed."""
|
|
27
|
+
maybe_text = questionary.text(message, validate=validate).ask()
|
|
28
|
+
if maybe_text is None:
|
|
29
|
+
raise typer.Exit(2)
|
|
30
|
+
return maybe_text[0]
|
spiral/cli/fs.py
CHANGED
|
@@ -13,7 +13,8 @@ from spiral.api.filesystems import (
|
|
|
13
13
|
UpstreamFileSystem,
|
|
14
14
|
)
|
|
15
15
|
from spiral.cli import CONSOLE, AsyncTyper, state
|
|
16
|
-
from spiral.cli.
|
|
16
|
+
from spiral.cli.chooser import choose
|
|
17
|
+
from spiral.cli.types_ import ProjectArg, ask_project
|
|
17
18
|
|
|
18
19
|
app = AsyncTyper(short_help="File Systems.")
|
|
19
20
|
|
|
@@ -26,7 +27,7 @@ def show(project: ProjectArg):
|
|
|
26
27
|
|
|
27
28
|
def ask_provider():
|
|
28
29
|
res = state.spiral.api.file_systems.list_providers()
|
|
29
|
-
return
|
|
30
|
+
return choose("Select a file system provider", choices=res)
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@app.command(help="Update a project's default file system.")
|
spiral/cli/iceberg.py
CHANGED
spiral/cli/key_spaces.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
|
|
3
|
-
import questionary
|
|
4
3
|
import rich
|
|
5
4
|
import typer
|
|
6
5
|
from questionary import Choice
|
|
@@ -11,7 +10,8 @@ from spiral.api.projects import KeySpaceIndexResource
|
|
|
11
10
|
from spiral.api.types import IndexId
|
|
12
11
|
from spiral.api.workers import ResourceClass
|
|
13
12
|
from spiral.cli import CONSOLE, AsyncTyper, state
|
|
14
|
-
from spiral.cli.
|
|
13
|
+
from spiral.cli.chooser import choose
|
|
14
|
+
from spiral.cli.types_ import ProjectArg
|
|
15
15
|
|
|
16
16
|
app = AsyncTyper(short_help="Key Space Indexes.")
|
|
17
17
|
|
|
@@ -23,10 +23,10 @@ def ask_index(project_id, title="Select an index"):
|
|
|
23
23
|
CONSOLE.print("[red]No indexes found[/red]")
|
|
24
24
|
raise typer.Exit(1)
|
|
25
25
|
|
|
26
|
-
return
|
|
26
|
+
return choose(
|
|
27
27
|
title,
|
|
28
28
|
choices=[Choice(title=index.name, value=index.id) for index in sorted(indexes, key=lambda t: (t.name, t.id))],
|
|
29
|
-
)
|
|
29
|
+
)
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def get_index_id(
|
spiral/cli/orgs.py
CHANGED
|
@@ -6,7 +6,7 @@ from rich.table import Table
|
|
|
6
6
|
|
|
7
7
|
from spiral.api.organizations import InviteUserRequest, OrgRole, PortalLinkIntent, PortalLinkRequest
|
|
8
8
|
from spiral.cli import CONSOLE, ERR_CONSOLE, AsyncTyper, state
|
|
9
|
-
from spiral.cli.
|
|
9
|
+
from spiral.cli.types_ import OrganizationArg
|
|
10
10
|
from spiral.core.authn import DeviceCodeAuth
|
|
11
11
|
|
|
12
12
|
app = AsyncTyper(short_help="Org admin.")
|
spiral/cli/projects.py
CHANGED
|
@@ -21,14 +21,14 @@ from spiral.api.projects import (
|
|
|
21
21
|
WorkloadPrincipalConditions,
|
|
22
22
|
)
|
|
23
23
|
from spiral.cli import CONSOLE, AsyncTyper, printer, state
|
|
24
|
-
from spiral.cli.
|
|
24
|
+
from spiral.cli.types_ import ProjectArg
|
|
25
25
|
|
|
26
26
|
app = AsyncTyper(short_help="Projects and grants.")
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@app.command(help="List projects.")
|
|
30
30
|
def ls():
|
|
31
|
-
projects = list(state.spiral.api.projects.list())
|
|
31
|
+
projects = list(sorted(state.spiral.api.projects.list(), key=lambda p: p.id))
|
|
32
32
|
CONSOLE.print(printer.table_of_models(Project, projects))
|
|
33
33
|
|
|
34
34
|
|
spiral/cli/tables.py
CHANGED
|
@@ -11,8 +11,9 @@ from typer import Argument, Option
|
|
|
11
11
|
from spiral import Spiral
|
|
12
12
|
from spiral.api.projects import TableResource
|
|
13
13
|
from spiral.cli import CONSOLE, ERR_CONSOLE, AsyncTyper, state
|
|
14
|
-
from spiral.cli.
|
|
15
|
-
from spiral.
|
|
14
|
+
from spiral.cli.chooser import choose, input_text
|
|
15
|
+
from spiral.cli.types_ import ProjectArg
|
|
16
|
+
from spiral.debug.manifests import display_manifests, display_scan_manifests
|
|
16
17
|
from spiral.table import Table
|
|
17
18
|
|
|
18
19
|
app = AsyncTyper(short_help="Spiral Tables.")
|
|
@@ -25,13 +26,13 @@ def ask_table(project_id: str, title: str = "Select a table") -> str:
|
|
|
25
26
|
ERR_CONSOLE.print("No tables found")
|
|
26
27
|
raise typer.Exit(1)
|
|
27
28
|
|
|
28
|
-
return
|
|
29
|
+
return choose( # pyright: ignore[reportAny]
|
|
29
30
|
title,
|
|
30
31
|
choices=[
|
|
31
32
|
Choice(title=f"{table.dataset}.{table.table}", value=f"{table.dataset}.{table.table}")
|
|
32
33
|
for table in sorted(tables, key=lambda t: (t.dataset, t.table))
|
|
33
34
|
],
|
|
34
|
-
)
|
|
35
|
+
)
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def get_table(
|
|
@@ -89,7 +90,7 @@ def validate_non_empty_str(text: str) -> bool | str:
|
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
def get_string(message: str, validate: Callable[[str], bool | str] = validate_non_empty_str) -> str:
|
|
92
|
-
return
|
|
93
|
+
return input_text(message, validate=validate)
|
|
93
94
|
|
|
94
95
|
|
|
95
96
|
@app.command(help="Move table to a different dataset.")
|
|
@@ -182,27 +183,53 @@ def flush(
|
|
|
182
183
|
CONSOLE.print(f"Flushed WAL for table {identifier} in project {project}.")
|
|
183
184
|
|
|
184
185
|
|
|
185
|
-
@app.command(help="
|
|
186
|
+
@app.command(help="Truncate column group metadata.")
|
|
187
|
+
def truncate(
|
|
188
|
+
project: ProjectArg,
|
|
189
|
+
table: Annotated[str | None, Option(help="Table name.")] = None,
|
|
190
|
+
dataset: Annotated[str | None, Option(help="Dataset name.")] = None,
|
|
191
|
+
):
|
|
192
|
+
identifier, t = get_table(project, table, dataset)
|
|
193
|
+
|
|
194
|
+
# Ask for confirmation
|
|
195
|
+
confirm = questionary.confirm(
|
|
196
|
+
f"Are you sure you want to truncate metadata for table '{identifier}'? This will break as-of queries."
|
|
197
|
+
).ask()
|
|
198
|
+
if not confirm:
|
|
199
|
+
CONSOLE.print("Aborted.")
|
|
200
|
+
raise typer.Exit(0)
|
|
201
|
+
|
|
202
|
+
state.spiral.internal.truncate_metadata(t.core)
|
|
203
|
+
CONSOLE.print(f"Truncated metadata for table {identifier} in project {project}.")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command(help="Display all fragments from metadata.", deprecated="Use 'fragments' instead.")
|
|
186
207
|
def manifests(
|
|
187
208
|
project: ProjectArg,
|
|
188
209
|
table: Annotated[str | None, Option(help="Table name.")] = None,
|
|
189
210
|
dataset: Annotated[str | None, Option(help="Dataset name.")] = None,
|
|
190
211
|
max_rows: Annotated[int | None, Option(help="Maximum number of rows to show per manifest.")] = None,
|
|
191
212
|
):
|
|
192
|
-
|
|
193
|
-
s = t.snapshot()
|
|
213
|
+
fragments(project, table, dataset, max_rows)
|
|
194
214
|
|
|
195
|
-
key_space_state = state.spiral.internal.key_space_state(s.core) # pyright: ignore[reportPrivateUsage]
|
|
196
|
-
key_space_manifest = key_space_state.manifest
|
|
197
215
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
216
|
+
@app.command(help="Display all fragments from metadata.")
|
|
217
|
+
def fragments(
|
|
218
|
+
project: ProjectArg,
|
|
219
|
+
table: Annotated[str | None, Option(help="Table name.")] = None,
|
|
220
|
+
dataset: Annotated[str | None, Option(help="Dataset name.")] = None,
|
|
221
|
+
max_rows: Annotated[int | None, Option(help="Maximum number of fragments to show per manifest.")] = None,
|
|
222
|
+
):
|
|
223
|
+
_, t = get_table(project, table, dataset)
|
|
224
|
+
s = t.snapshot()
|
|
225
|
+
cgs = s.core.column_groups()
|
|
226
|
+
key_space_manifest = state.spiral.internal.key_space_manifest(s.core)
|
|
227
|
+
column_group_manifests = [(cg, state.spiral.internal.column_group_manifest(s.core, cg)) for cg in cgs]
|
|
228
|
+
display_manifests(key_space_manifest, column_group_manifests, t.key_schema, max_rows)
|
|
202
229
|
|
|
203
230
|
|
|
204
|
-
@app.command(help="
|
|
205
|
-
def
|
|
231
|
+
@app.command(help="Display the fragments used in a scan of a given column group.")
|
|
232
|
+
def fragments_scan(
|
|
206
233
|
project: ProjectArg,
|
|
207
234
|
table: Annotated[str | None, Option(help="Table name.")] = None,
|
|
208
235
|
dataset: Annotated[str | None, Option(help="Dataset name.")] = None,
|
|
@@ -210,11 +237,11 @@ def debug_scan(
|
|
|
210
237
|
):
|
|
211
238
|
_, t = get_table(project, table, dataset)
|
|
212
239
|
scan = state.spiral.scan(t[column_group] if column_group != "." else t)
|
|
213
|
-
scan.
|
|
240
|
+
display_scan_manifests(scan.core)
|
|
214
241
|
|
|
215
242
|
|
|
216
|
-
@app.command(help="
|
|
217
|
-
def
|
|
243
|
+
@app.command(help="Visualize the scan of a given column group.")
|
|
244
|
+
def debug_scan(
|
|
218
245
|
project: ProjectArg,
|
|
219
246
|
table: Annotated[str | None, Option(help="Table name.")] = None,
|
|
220
247
|
dataset: Annotated[str | None, Option(help="Dataset name.")] = None,
|
|
@@ -222,4 +249,4 @@ def dump_scan(
|
|
|
222
249
|
):
|
|
223
250
|
_, t = get_table(project, table, dataset)
|
|
224
251
|
scan = state.spiral.scan(t[column_group] if column_group != "." else t)
|
|
225
|
-
scan.
|
|
252
|
+
scan._debug() # pyright: ignore[reportPrivateUsage]
|
spiral/cli/telemetry.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import typer
|
|
2
2
|
|
|
3
3
|
from spiral.api.telemetry import IssueExportTokenResponse
|
|
4
4
|
from spiral.cli import CONSOLE, AsyncTyper, state
|
|
@@ -7,11 +7,18 @@ app = AsyncTyper(short_help="Client-side telemetry.")
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@app.command(help="Issue new telemetry export token.")
|
|
10
|
-
def export(
|
|
10
|
+
def export(
|
|
11
|
+
print_token: bool = typer.Option(False, "--print", help="Print the token instead of copying to clipboard."),
|
|
12
|
+
):
|
|
11
13
|
res: IssueExportTokenResponse = state.spiral.api.telemetry.issue_export_token()
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
if print_token:
|
|
16
|
+
print(res.token)
|
|
17
|
+
else:
|
|
18
|
+
import pyperclip
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
command = f"export SPIRAL_OTEL_TOKEN={res.token}"
|
|
21
|
+
pyperclip.copy(command)
|
|
22
|
+
|
|
23
|
+
CONSOLE.print("Export command copied to clipboard! Paste and run to set [green]SPIRAL_OTEL_TOKEN[/green].")
|
|
24
|
+
CONSOLE.print("[dim]Token is valid for 1h.[/dim]")
|
spiral/cli/text.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
|
|
3
|
-
import questionary
|
|
4
3
|
import rich
|
|
5
4
|
import typer
|
|
6
5
|
from questionary import Choice
|
|
@@ -11,7 +10,8 @@ from spiral.api.text_indexes import CreateWorkerRequest, SyncIndexRequest
|
|
|
11
10
|
from spiral.api.types import IndexId
|
|
12
11
|
from spiral.api.workers import CPU, GcpRegion, Memory, ResourceClass
|
|
13
12
|
from spiral.cli import CONSOLE, ERR_CONSOLE, AsyncTyper, state
|
|
14
|
-
from spiral.cli.
|
|
13
|
+
from spiral.cli.chooser import choose
|
|
14
|
+
from spiral.cli.types_ import ProjectArg
|
|
15
15
|
|
|
16
16
|
app = AsyncTyper(short_help="Text Indexes.")
|
|
17
17
|
|
|
@@ -23,10 +23,10 @@ def ask_index(project_id, title="Select an index"):
|
|
|
23
23
|
ERR_CONSOLE.print("No indexes found")
|
|
24
24
|
raise typer.Exit(1)
|
|
25
25
|
|
|
26
|
-
return
|
|
26
|
+
return choose(
|
|
27
27
|
title,
|
|
28
28
|
choices=[Choice(title=index.name, value=index.id) for index in sorted(indexes, key=lambda t: (t.name, t.id))],
|
|
29
|
-
)
|
|
29
|
+
)
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def get_index_id(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from datetime import UTC
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
import rich
|
|
6
|
+
import rich.table
|
|
7
|
+
import typer
|
|
8
|
+
from typer import Option
|
|
9
|
+
|
|
10
|
+
from spiral.cli import CONSOLE, AsyncTyper, state
|
|
11
|
+
from spiral.cli.tables import get_table
|
|
12
|
+
from spiral.cli.types_ import ProjectArg
|
|
13
|
+
|
|
14
|
+
app = AsyncTyper(short_help="Table Transactions.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("ls", help="List transactions for a table.")
|
|
18
|
+
def ls(
|
|
19
|
+
project: ProjectArg,
|
|
20
|
+
table: Annotated[str | None, Option(help="Table name.")] = None,
|
|
21
|
+
dataset: Annotated[str | None, Option(help="Dataset name.")] = None,
|
|
22
|
+
since: Annotated[
|
|
23
|
+
int | None, Option(help="List transactions committed after this timestamp (microseconds).")
|
|
24
|
+
] = None,
|
|
25
|
+
):
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
identifier, t = get_table(project, table, dataset)
|
|
29
|
+
|
|
30
|
+
# Get transactions from the API
|
|
31
|
+
transactions = state.spiral.api.tables.list_transactions(
|
|
32
|
+
table_id=t.table_id,
|
|
33
|
+
since=since,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not transactions:
|
|
37
|
+
CONSOLE.print("No transactions found.")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Create a rich table to display transactions
|
|
41
|
+
rich_table = rich.table.Table(
|
|
42
|
+
"Table ID", "Tx Index", "Committed At", "Operations", title=f"Transactions for {identifier}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for txn in transactions:
|
|
46
|
+
# Convert timestamp to readable format
|
|
47
|
+
dt = datetime.fromtimestamp(txn.committed_at / 1_000_000, tz=UTC)
|
|
48
|
+
committed_str = dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
49
|
+
|
|
50
|
+
# Count operations by type
|
|
51
|
+
op_counts: dict[str, int] = {}
|
|
52
|
+
for op in txn.operations:
|
|
53
|
+
# Operation is a dict with a single key indicating the type
|
|
54
|
+
op_type = op["type"]
|
|
55
|
+
op_counts[op_type] = op_counts.get(op_type, 0) + 1
|
|
56
|
+
|
|
57
|
+
op_summary = ", ".join(f"{count}x {op_type}" for op_type, count in op_counts.items())
|
|
58
|
+
|
|
59
|
+
rich_table.add_row(t.table_id, str(txn.txn_idx), committed_str, op_summary)
|
|
60
|
+
|
|
61
|
+
CONSOLE.print(rich_table)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("revert", help="Revert a transaction by table ID and transaction index.")
|
|
65
|
+
def revert(
|
|
66
|
+
table_id: str,
|
|
67
|
+
txn_idx: int,
|
|
68
|
+
):
|
|
69
|
+
# Ask for confirmation
|
|
70
|
+
CONSOLE.print(
|
|
71
|
+
"[yellow]Only transactions still in WAL can be reverted. "
|
|
72
|
+
"It is recommended to only revert the latest transaction. "
|
|
73
|
+
"Reverting historical transactions may break a table.[/yellow]"
|
|
74
|
+
)
|
|
75
|
+
confirm = questionary.confirm(
|
|
76
|
+
f"Are you sure you want to revert transaction {txn_idx} for table '{table_id}'?\n"
|
|
77
|
+
).ask()
|
|
78
|
+
if not confirm:
|
|
79
|
+
CONSOLE.print("Aborted.")
|
|
80
|
+
raise typer.Exit(0)
|
|
81
|
+
|
|
82
|
+
# Revert the transaction
|
|
83
|
+
state.spiral.api.tables.revert_transaction(table_id=table_id, txn_idx=txn_idx)
|
|
84
|
+
CONSOLE.print(f"Successfully reverted transaction {txn_idx} for table {table_id}.")
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
|
|
3
|
-
import questionary
|
|
4
3
|
import typer
|
|
5
4
|
from questionary import Choice
|
|
6
5
|
from typer import Argument
|
|
7
6
|
|
|
8
7
|
from spiral.api.types import OrgId, ProjectId
|
|
9
8
|
from spiral.cli import ERR_CONSOLE, state
|
|
9
|
+
from spiral.cli.chooser import choose
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def ask_project(title="Select a project"):
|
|
@@ -16,13 +16,13 @@ def ask_project(title="Select a project"):
|
|
|
16
16
|
ERR_CONSOLE.print("No projects found")
|
|
17
17
|
raise typer.Exit(1)
|
|
18
18
|
|
|
19
|
-
return
|
|
19
|
+
return choose(
|
|
20
20
|
title,
|
|
21
|
-
|
|
21
|
+
[
|
|
22
22
|
Choice(title=f"{project.id} - {project.name}" if project.name else project.id, value=project.id)
|
|
23
23
|
for project in projects
|
|
24
24
|
],
|
|
25
|
-
)
|
|
25
|
+
)
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
ProjectArg = Annotated[ProjectId, Argument(help="Project ID", show_default=False, default_factory=ask_project)]
|
|
@@ -35,7 +35,7 @@ def _org_default():
|
|
|
35
35
|
ERR_CONSOLE.print("No organizations found")
|
|
36
36
|
raise typer.Exit(1)
|
|
37
37
|
|
|
38
|
-
return
|
|
38
|
+
return choose(
|
|
39
39
|
"Select an organization",
|
|
40
40
|
choices=[
|
|
41
41
|
Choice(
|
|
@@ -44,7 +44,7 @@ def _org_default():
|
|
|
44
44
|
)
|
|
45
45
|
for m in memberships
|
|
46
46
|
],
|
|
47
|
-
)
|
|
47
|
+
)
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
OrganizationArg = Annotated[OrgId, Argument(help="Organization ID", show_default=False, default_factory=_org_default)]
|
spiral/cli/workloads.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
|
|
3
3
|
import pyperclip
|
|
4
|
-
import questionary
|
|
5
4
|
from questionary import Choice
|
|
6
5
|
from typer import Argument, Option
|
|
7
6
|
|
|
@@ -12,7 +11,8 @@ from spiral.api.workloads import (
|
|
|
12
11
|
Workload,
|
|
13
12
|
)
|
|
14
13
|
from spiral.cli import CONSOLE, ERR_CONSOLE, AsyncTyper, printer, state
|
|
15
|
-
from spiral.cli.
|
|
14
|
+
from spiral.cli.chooser import choose
|
|
15
|
+
from spiral.cli.types_ import ProjectArg
|
|
16
16
|
|
|
17
17
|
app = AsyncTyper()
|
|
18
18
|
|
|
@@ -54,14 +54,14 @@ def issue_creds(
|
|
|
54
54
|
CONSOLE.print(f"[green]SPIRAL_CLIENT_SECRET[/green] {res.client_secret}")
|
|
55
55
|
else:
|
|
56
56
|
while True:
|
|
57
|
-
choice =
|
|
57
|
+
choice = choose(
|
|
58
58
|
"What would you like to do with the secret? You will not be able to see this secret again!",
|
|
59
59
|
choices=[
|
|
60
60
|
Choice(title="Copy to clipboard", value=1),
|
|
61
61
|
Choice(title="Print to console", value=2),
|
|
62
62
|
Choice(title="Exit", value=3),
|
|
63
63
|
],
|
|
64
|
-
)
|
|
64
|
+
)
|
|
65
65
|
|
|
66
66
|
if choice == 1:
|
|
67
67
|
pyperclip.copy(res.client_secret)
|
spiral/client.py
CHANGED
|
@@ -8,7 +8,7 @@ import pyarrow as pa
|
|
|
8
8
|
from spiral.api import SpiralAPI
|
|
9
9
|
from spiral.api.projects import CreateProjectRequest, CreateProjectResponse
|
|
10
10
|
from spiral.core.authn import Authn
|
|
11
|
-
from spiral.core.client import Internal, Shard
|
|
11
|
+
from spiral.core.client import Internal, KeyColumns, Shard
|
|
12
12
|
from spiral.core.client import Spiral as CoreSpiral
|
|
13
13
|
from spiral.core.config import ClientSettings
|
|
14
14
|
from spiral.datetime_ import timestamp_micros
|
|
@@ -157,6 +157,7 @@ class Spiral:
|
|
|
157
157
|
where: ExprLike | None = None,
|
|
158
158
|
asof: datetime | int | None = None,
|
|
159
159
|
shard: Shard | None = None,
|
|
160
|
+
hide_progress_bar: bool = False,
|
|
160
161
|
) -> Scan:
|
|
161
162
|
"""Starts a read transaction on the Spiral.
|
|
162
163
|
|
|
@@ -167,7 +168,61 @@ class Spiral:
|
|
|
167
168
|
shard: if provided, opens the scan only for the given shard.
|
|
168
169
|
While shards can be provided when executing the scan, providing a shard here
|
|
169
170
|
optimizes the scan planning phase and can significantly reduce metadata download.
|
|
171
|
+
hide_progress_bar: if True, disables the progress bar during scan building.
|
|
170
172
|
"""
|
|
173
|
+
return self._scan_internal(
|
|
174
|
+
*projections,
|
|
175
|
+
where=where,
|
|
176
|
+
asof=asof,
|
|
177
|
+
shard=shard,
|
|
178
|
+
hide_progress_bar=hide_progress_bar,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def scan_keys(
|
|
182
|
+
self,
|
|
183
|
+
*projections: ExprLike,
|
|
184
|
+
where: ExprLike | None = None,
|
|
185
|
+
asof: datetime | int | None = None,
|
|
186
|
+
shard: Shard | None = None,
|
|
187
|
+
hide_progress_bar: bool = False,
|
|
188
|
+
) -> Scan:
|
|
189
|
+
"""Starts a keys-only read transaction on the Spiral.
|
|
190
|
+
|
|
191
|
+
To determine which keys are present in at least one column group of the table, key scan the
|
|
192
|
+
table itself:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
sp.scan_keys(table)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
projections: scan the keys of the column groups referenced by these expressions.
|
|
200
|
+
where: a query expression to apply to the data.
|
|
201
|
+
asof: execute the scan on the version of the table as of the given timestamp.
|
|
202
|
+
shard: if provided, opens the scan only for the given shard.
|
|
203
|
+
While shards can be provided when executing the scan, providing a shard here
|
|
204
|
+
optimizes the scan planning phase and can significantly reduce metadata download.
|
|
205
|
+
hide_progress_bar: if True, disables the progress bar during scan building.
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
return self._scan_internal(
|
|
209
|
+
*projections,
|
|
210
|
+
where=where,
|
|
211
|
+
asof=asof,
|
|
212
|
+
shard=shard,
|
|
213
|
+
hide_progress_bar=hide_progress_bar,
|
|
214
|
+
key_columns=KeyColumns.Only,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _scan_internal(
|
|
218
|
+
self,
|
|
219
|
+
*projections: ExprLike,
|
|
220
|
+
where: ExprLike | None = None,
|
|
221
|
+
asof: datetime | int | None = None,
|
|
222
|
+
shard: Shard | None = None,
|
|
223
|
+
key_columns: KeyColumns | None = None,
|
|
224
|
+
hide_progress_bar: bool = False,
|
|
225
|
+
) -> Scan:
|
|
171
226
|
from spiral import expressions as se
|
|
172
227
|
|
|
173
228
|
if isinstance(asof, datetime):
|
|
@@ -182,7 +237,14 @@ class Spiral:
|
|
|
182
237
|
|
|
183
238
|
return Scan(
|
|
184
239
|
self,
|
|
185
|
-
self.core.scan(
|
|
240
|
+
self.core.scan(
|
|
241
|
+
projection.__expr__,
|
|
242
|
+
filter=where.__expr__ if where else None,
|
|
243
|
+
asof=asof,
|
|
244
|
+
shard=shard,
|
|
245
|
+
key_columns=key_columns,
|
|
246
|
+
progress=(not hide_progress_bar),
|
|
247
|
+
),
|
|
186
248
|
)
|
|
187
249
|
|
|
188
250
|
# TODO(marko): This should be query, and search should be query + scan.
|
|
@@ -223,16 +285,16 @@ class Spiral:
|
|
|
223
285
|
freshness_window_s=freshness_window_s,
|
|
224
286
|
)
|
|
225
287
|
|
|
226
|
-
def resume_scan(self,
|
|
227
|
-
"""Resumes a previously started scan using its scan
|
|
288
|
+
def resume_scan(self, context_bytes: bytes) -> Scan:
|
|
289
|
+
"""Resumes a previously started scan using its scan context.
|
|
228
290
|
|
|
229
291
|
Args:
|
|
230
|
-
|
|
292
|
+
context_bytes: The compressed scan context returned by a previous scan.
|
|
231
293
|
"""
|
|
232
|
-
from spiral.core.table import
|
|
294
|
+
from spiral.core.table import ScanContext
|
|
233
295
|
|
|
234
|
-
|
|
235
|
-
return Scan(self, self.core.load_scan(
|
|
296
|
+
context = ScanContext.from_bytes_compressed(context_bytes)
|
|
297
|
+
return Scan(self, self.core.load_scan(context))
|
|
236
298
|
|
|
237
299
|
def compute_shards(
|
|
238
300
|
self,
|