pyspiral 0.3.1__cp310-abi3-macosx_11_0_arm64.whl → 0.4.0__cp310-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 (109) hide show
  1. {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/METADATA +9 -13
  2. pyspiral-0.4.0.dist-info/RECORD +98 -0
  3. {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/WHEEL +1 -1
  4. spiral/__init__.py +6 -9
  5. spiral/_lib.abi3.so +0 -0
  6. spiral/adbc.py +21 -14
  7. spiral/api/__init__.py +14 -175
  8. spiral/api/admin.py +12 -26
  9. spiral/api/client.py +160 -0
  10. spiral/api/filesystems.py +100 -72
  11. spiral/api/organizations.py +45 -58
  12. spiral/api/projects.py +171 -134
  13. spiral/api/telemetry.py +19 -0
  14. spiral/api/types.py +20 -0
  15. spiral/api/workloads.py +32 -25
  16. spiral/{arrow.py → arrow_.py} +12 -0
  17. spiral/cli/__init__.py +2 -5
  18. spiral/cli/admin.py +7 -12
  19. spiral/cli/app.py +23 -6
  20. spiral/cli/console.py +1 -1
  21. spiral/cli/fs.py +82 -17
  22. spiral/cli/iceberg/__init__.py +7 -0
  23. spiral/cli/iceberg/namespaces.py +47 -0
  24. spiral/cli/iceberg/tables.py +60 -0
  25. spiral/cli/indexes/__init__.py +19 -0
  26. spiral/cli/login.py +14 -5
  27. spiral/cli/orgs.py +90 -0
  28. spiral/cli/printer.py +9 -1
  29. spiral/cli/projects.py +136 -0
  30. spiral/cli/state.py +2 -0
  31. spiral/cli/tables/__init__.py +121 -0
  32. spiral/cli/telemetry.py +18 -0
  33. spiral/cli/types.py +8 -10
  34. spiral/cli/{workload.py → workloads.py} +11 -11
  35. spiral/{catalog.py → client.py} +23 -37
  36. spiral/core/client/__init__.pyi +117 -0
  37. spiral/core/index/__init__.pyi +15 -0
  38. spiral/core/{core → table}/__init__.pyi +44 -17
  39. spiral/core/{manifests → table/manifests}/__init__.pyi +5 -23
  40. spiral/core/table/metastore/__init__.pyi +62 -0
  41. spiral/core/{spec → table/spec}/__init__.pyi +41 -66
  42. spiral/datetime_.py +27 -0
  43. spiral/expressions/__init__.py +26 -18
  44. spiral/expressions/base.py +5 -5
  45. spiral/expressions/list_.py +1 -1
  46. spiral/expressions/mp4.py +2 -9
  47. spiral/expressions/png.py +1 -1
  48. spiral/expressions/qoi.py +1 -1
  49. spiral/expressions/refs.py +3 -9
  50. spiral/expressions/struct.py +7 -5
  51. spiral/expressions/text.py +62 -0
  52. spiral/expressions/udf.py +3 -3
  53. spiral/iceberg/__init__.py +3 -0
  54. spiral/iceberg/client.py +33 -0
  55. spiral/indexes/__init__.py +5 -0
  56. spiral/indexes/client.py +137 -0
  57. spiral/indexes/index.py +34 -0
  58. spiral/indexes/scan.py +22 -0
  59. spiral/project.py +19 -110
  60. spiral/{proto → protogen}/_/scandal/__init__.py +23 -135
  61. spiral/protogen/_/spiral/table/__init__.py +22 -0
  62. spiral/protogen/substrait/__init__.py +3399 -0
  63. spiral/protogen/substrait/extensions/__init__.py +115 -0
  64. spiral/server.py +17 -0
  65. spiral/settings.py +29 -91
  66. spiral/substrait_.py +9 -5
  67. spiral/tables/__init__.py +12 -0
  68. spiral/tables/client.py +130 -0
  69. spiral/{dataset.py → tables/dataset.py} +9 -199
  70. spiral/tables/debug/manifests.py +70 -0
  71. spiral/tables/debug/metrics.py +56 -0
  72. spiral/{debug.py → tables/debug/scan.py} +6 -9
  73. spiral/{maintenance.py → tables/maintenance.py} +1 -1
  74. spiral/{scan_.py → tables/scan.py} +63 -89
  75. spiral/tables/snapshot.py +78 -0
  76. spiral/{table.py → tables/table.py} +59 -73
  77. spiral/{txn.py → tables/transaction.py} +7 -3
  78. pyspiral-0.3.1.dist-info/RECORD +0 -85
  79. spiral/api/tables.py +0 -91
  80. spiral/api/tokens.py +0 -56
  81. spiral/authn/authn.py +0 -89
  82. spiral/authn/device.py +0 -206
  83. spiral/authn/github_.py +0 -33
  84. spiral/authn/modal_.py +0 -18
  85. spiral/cli/org.py +0 -90
  86. spiral/cli/project.py +0 -109
  87. spiral/cli/table.py +0 -20
  88. spiral/cli/token.py +0 -27
  89. spiral/core/metastore/__init__.pyi +0 -91
  90. spiral/proto/_/spfs/__init__.py +0 -36
  91. spiral/proto/_/spiral/table/__init__.py +0 -276
  92. spiral/proto/_/spiraldb/metastore/__init__.py +0 -499
  93. spiral/proto/__init__.py +0 -0
  94. spiral/proto/scandal/__init__.py +0 -45
  95. spiral/proto/spiral/__init__.py +0 -0
  96. spiral/proto/spiral/table/__init__.py +0 -96
  97. {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/entry_points.txt +0 -0
  98. /spiral/{authn/__init__.py → core/__init__.pyi} +0 -0
  99. /spiral/{core → protogen/_}/__init__.py +0 -0
  100. /spiral/{proto/_ → protogen/_/arrow}/__init__.py +0 -0
  101. /spiral/{proto/_/arrow → protogen/_/arrow/flight}/__init__.py +0 -0
  102. /spiral/{proto/_/arrow/flight → protogen/_/arrow/flight/protocol}/__init__.py +0 -0
  103. /spiral/{proto → protogen}/_/arrow/flight/protocol/sql/__init__.py +0 -0
  104. /spiral/{proto/_/arrow/flight/protocol → protogen/_/spiral}/__init__.py +0 -0
  105. /spiral/{proto → protogen/_}/substrait/__init__.py +0 -0
  106. /spiral/{proto → protogen/_}/substrait/extensions/__init__.py +0 -0
  107. /spiral/{proto/_/spiral → protogen}/__init__.py +0 -0
  108. /spiral/{proto → protogen}/util.py +0 -0
  109. /spiral/{proto/_/spiraldb → tables/debug}/__init__.py +0 -0
spiral/authn/device.py DELETED
@@ -1,206 +0,0 @@
1
- import logging
2
- import sys
3
- import textwrap
4
- import time
5
- import webbrowser
6
- from pathlib import Path
7
-
8
- import httpx
9
- import jwt
10
- from pydantic import BaseModel
11
-
12
- log = logging.getLogger(__name__)
13
-
14
-
15
- class TokensModel(BaseModel):
16
- access_token: str
17
- refresh_token: str
18
-
19
- @property
20
- def organization_id(self) -> str | None:
21
- return self.unverified_access_token().get("org_id")
22
-
23
- def unverified_access_token(self):
24
- return jwt.decode(self.access_token, options={"verify_signature": False})
25
-
26
-
27
- class AuthModel(BaseModel):
28
- tokens: TokensModel | None = None
29
-
30
-
31
- class DeviceAuth:
32
- def __init__(
33
- self,
34
- auth_file: Path,
35
- domain: str,
36
- client_id: str,
37
- http: httpx.Client = None,
38
- ):
39
- self._auth_file = auth_file
40
- self._domain = domain
41
- self._client_id = client_id
42
- self._http = http or httpx.Client()
43
-
44
- if self._auth_file.exists():
45
- with self._auth_file.open("r") as f:
46
- self._auth = AuthModel.model_validate_json(f.read())
47
- else:
48
- self._auth = AuthModel()
49
-
50
- self._default_scope = ["email", "profile"]
51
-
52
- def is_authenticated(self) -> bool:
53
- """Check if the user is authenticated."""
54
- tokens = self._auth.tokens
55
- if tokens is None:
56
- return False
57
-
58
- # Give ourselves a 30-second buffer before the token expires.
59
- return tokens.unverified_access_token()["exp"] - 30 > time.time()
60
-
61
- def authenticate(self, force: bool = False, refresh: bool = False, organization_id: str = None) -> TokensModel:
62
- """Blocking call to authenticate the user.
63
-
64
- Triggers a device code flow and polls for the user to login.
65
- """
66
- if force:
67
- return self._device_code(organization_id)
68
-
69
- if refresh:
70
- if self._auth.tokens is None:
71
- raise ValueError("No tokens to refresh.")
72
- tokens = self._refresh(self._auth.tokens, organization_id)
73
- if not tokens:
74
- raise ValueError("Failed to refresh token.")
75
- return tokens
76
-
77
- # Check for mis-matched organization.
78
- if organization_id is not None:
79
- tokens = self._auth.tokens
80
- if tokens is not None and tokens.unverified_access_token().get("org_id") != organization_id:
81
- tokens = self._refresh(self._auth.tokens, organization_id)
82
- if tokens is None:
83
- return self._device_code(organization_id)
84
-
85
- if self.is_authenticated():
86
- return self._auth.tokens
87
-
88
- # Try to refresh.
89
- tokens = self._auth.tokens
90
- if tokens is not None:
91
- tokens = self._refresh(tokens)
92
- if tokens is not None:
93
- return tokens
94
-
95
- # Otherwise, we kick off the device code flow.
96
- return self._device_code(organization_id)
97
-
98
- def logout(self):
99
- self._remove_tokens()
100
-
101
- def _device_code(self, organization_id: str | None):
102
- scope = " ".join(self._default_scope)
103
- res = self._http.post(
104
- f"{self._domain}/auth/device/code",
105
- data={
106
- "client_id": self._client_id,
107
- "scope": scope,
108
- "organization_id": organization_id,
109
- },
110
- )
111
- res = res.raise_for_status().json()
112
- device_code = res["device_code"]
113
- user_code = res["user_code"]
114
- expires_at = res["expires_in"] + time.time()
115
- interval = res["interval"]
116
- verification_uri_complete = res["verification_uri_complete"]
117
-
118
- # We need to detect if the user is running in a terminal, in Jupyter, etc.
119
- # For now, we'll try to open the browser.
120
- sys.stderr.write(
121
- textwrap.dedent(
122
- f"""
123
- Please login here: {verification_uri_complete}
124
- Your code is {user_code}.
125
- """
126
- )
127
- )
128
-
129
- # Try to open the browser (this also works if the Jupiter notebook is running on the user's machine).
130
- opened = webbrowser.open(verification_uri_complete)
131
-
132
- # If we have a server-side Jupyter notebook, we can try to open with client-side JavaScript.
133
- if not opened and _in_notebook():
134
- from IPython.display import Javascript, display
135
-
136
- display(Javascript(f'window.open("{verification_uri_complete}");'))
137
-
138
- # In the meantime, we need to poll for the user to login.
139
- while True:
140
- if time.time() > expires_at:
141
- raise TimeoutError("Login timed out.")
142
- time.sleep(interval)
143
- res = self._http.post(
144
- f"{self._domain}/auth/token",
145
- data={
146
- "client_id": self._client_id,
147
- "device_code": device_code,
148
- "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
149
- },
150
- )
151
- if not res.is_success:
152
- continue
153
-
154
- tokens = TokensModel(
155
- access_token=res.json()["access_token"],
156
- refresh_token=res.json()["refresh_token"],
157
- )
158
- self._save_tokens(tokens)
159
- return self._auth.tokens
160
-
161
- def _refresh(self, tokens: TokensModel, organization_id: str = None) -> TokensModel | None:
162
- """Attempt to use the refresh token."""
163
- log.debug("Refreshing token %s", self._client_id)
164
-
165
- res = self._http.post(
166
- f"{self._domain}/auth/refresh",
167
- data={
168
- "client_id": self._client_id,
169
- "grant_type": "refresh_token",
170
- "refresh_token": tokens.refresh_token,
171
- "organization_id": organization_id,
172
- },
173
- )
174
- if not res.is_success:
175
- print("Failed to refresh token", res.status_code, res.text)
176
- return None
177
-
178
- tokens = TokensModel(
179
- access_token=res.json()["access_token"],
180
- refresh_token=res.json()["refresh_token"],
181
- )
182
- self._save_tokens(tokens)
183
- return tokens
184
-
185
- def _save_tokens(self, tokens: TokensModel):
186
- self._auth = self._auth.model_copy(update={"tokens": tokens})
187
- self._auth_file.parent.mkdir(parents=True, exist_ok=True)
188
- with self._auth_file.open("w") as f:
189
- f.write(self._auth.model_dump_json(exclude_defaults=True))
190
-
191
- def _remove_tokens(self):
192
- self._auth_file.unlink(missing_ok=True)
193
- self._auth = self._auth.model_copy(update={"tokens": None})
194
-
195
-
196
- def _in_notebook():
197
- try:
198
- from IPython import get_ipython
199
-
200
- if "IPKernelApp" not in get_ipython().config: # pragma: no cover
201
- return False
202
- except ImportError:
203
- return False
204
- except AttributeError:
205
- return False
206
- return True
spiral/authn/github_.py DELETED
@@ -1,33 +0,0 @@
1
- import os
2
-
3
- import httpx
4
-
5
- from spiral.api import Authn
6
-
7
-
8
- class GitHubActionsProvider(Authn):
9
- AUDIENCE = "https://iss.spiraldb.com"
10
-
11
- def __init__(self):
12
- self._gh_token = None
13
-
14
- def token(self) -> str | None:
15
- if self._gh_token is not None:
16
- return self._gh_token
17
-
18
- if os.environ.get("GITHUB_ACTIONS") == "true":
19
- # Next, we check to see if we're running in GitHub actions and if so, grab an ID token.
20
- if "ACTIONS_ID_TOKEN_REQUEST_TOKEN" in os.environ and "ACTIONS_ID_TOKEN_REQUEST_URL" in os.environ:
21
- if not hasattr(self, "__gh_token"):
22
- resp = httpx.get(
23
- f"{os.environ['ACTIONS_ID_TOKEN_REQUEST_URL']}&audience={self.AUDIENCE}",
24
- headers={"Authorization": f'Bearer {os.environ["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]}'},
25
- )
26
- if not resp.is_success:
27
- raise ValueError(f"Failed to get GitHub Actions ID token: {resp.text}", resp)
28
- self._gh_token = resp.json()["value"]
29
- else:
30
- raise ValueError("Please set 'id-token: write' permission for this GitHub Actions workflow.")
31
-
32
- # For now, we don't exchange the token for a Spiral one.
33
- return self._gh_token
spiral/authn/modal_.py DELETED
@@ -1,18 +0,0 @@
1
- import os
2
-
3
- from spiral.api import Authn
4
-
5
-
6
- class ModalProvider(Authn):
7
- def __init__(self):
8
- self._modal_token = None
9
-
10
- def token(self) -> str | None:
11
- if self._modal_token is not None:
12
- return self._modal_token
13
-
14
- if os.environ.get("MODAL_IDENTITY_TOKEN") is not None:
15
- self._modal_token = os.environ["MODAL_IDENTITY_TOKEN"]
16
-
17
- # For now, we don't exchange the token for a Spiral one.
18
- return self._modal_token
spiral/cli/org.py DELETED
@@ -1,90 +0,0 @@
1
- import webbrowser
2
- from typing import Annotated
3
-
4
- import jwt
5
- import rich
6
- import typer
7
- from rich.table import Table
8
- from typer import Option
9
-
10
- from spiral.api.organizations import CreateOrganization, InviteUser, OrganizationRole, PortalLink
11
- from spiral.cli import AsyncTyper, OptionalStr, state
12
- from spiral.cli.types import OrganizationArg
13
-
14
- app = AsyncTyper()
15
-
16
-
17
- @app.command(help="Switch the active organization.")
18
- def switch(org_id: OrganizationArg):
19
- state.settings.spiraldb.device_auth().authenticate(refresh=True, organization_id=org_id)
20
- rich.print(f"Switched to organization: {org_id}")
21
-
22
-
23
- @app.command(help="Create a new organization.")
24
- def create(
25
- name: Annotated[OptionalStr, Option(help="The human-readable name of the organization.")] = None,
26
- ):
27
- res = state.settings.api.organization.create_organization(CreateOrganization.Request(name=name))
28
-
29
- # Authenticate to the new organization
30
- state.settings.spiraldb.device_auth().authenticate(refresh=True, organization_id=res.organization.id)
31
-
32
- rich.print(f"{res.organization.name} [dim]{res.organization.id}[/dim]")
33
-
34
-
35
- @app.command(help="List organizations.")
36
- def ls():
37
- org_id = current_org_id()
38
-
39
- table = Table("", "id", "name", "role", title="Organizations")
40
- for m in state.settings.api.organization.list_user_memberships():
41
- table.add_row("👉" if m.organization.id == org_id else "", m.organization.id, m.organization.name, m.role)
42
-
43
- rich.print(table)
44
-
45
-
46
- @app.command(help="Invite a user to the organization.")
47
- def invite(email: str, role: OrganizationRole = "member", expires_in_days: int = 7):
48
- state.settings.api.organization.invite_user(
49
- InviteUser.Request(email=email, role=role, expires_in_days=expires_in_days)
50
- )
51
- rich.print(f"Invited {email} as a {role.value}.")
52
-
53
-
54
- @app.command(help="Configure single sign-on for your organization.")
55
- def sso():
56
- _do_action(PortalLink.Intent.SSO)
57
-
58
-
59
- @app.command(help="Configure directory services for your organization.")
60
- def directory():
61
- _do_action(PortalLink.Intent.DIRECTORY)
62
-
63
-
64
- @app.command(help="Configure audit logs for your organization.")
65
- def audit_logs():
66
- _do_action(PortalLink.Intent.AUDIT_LOGS)
67
-
68
-
69
- @app.command(help="Configure log streams for your organization.")
70
- def log_streams():
71
- _do_action(PortalLink.Intent.LOG_STREAMS)
72
-
73
-
74
- @app.command(help="Configure domains for your organization.")
75
- def domains():
76
- _do_action(PortalLink.Intent.DOMAIN_VERIFICATION)
77
-
78
-
79
- def _do_action(intent: PortalLink.Intent):
80
- res = state.settings.api.organization.portal_link(PortalLink.Request(intent=intent))
81
- rich.print(f"Opening the configuration portal:\n{res.url}")
82
- webbrowser.open(res.url)
83
-
84
-
85
- def current_org_id():
86
- org_id = jwt.decode(state.settings.authn.token(), options={"verify_signature": False}).get("org_id")
87
- if not org_id:
88
- rich.print("[red]You are not logged in to an organization.[/red]")
89
- raise typer.Exit(1)
90
- return org_id
spiral/cli/project.py DELETED
@@ -1,109 +0,0 @@
1
- from typing import Annotated
2
-
3
- import rich
4
- import typer
5
- from typer import Option
6
-
7
- from spiral.api.organizations import OrganizationRole
8
- from spiral.api.projects import CreateProject, Grant, GrantRole, ListGrants, Project
9
- from spiral.cli import AsyncTyper, OptionalStr, printer, state
10
- from spiral.cli.org import current_org_id
11
- from spiral.cli.types import ProjectArg
12
-
13
- app = AsyncTyper()
14
-
15
-
16
- @app.command(help="List projects.")
17
- def ls():
18
- projects = list(state.settings.api.project.list())
19
- rich.print(printer.table_of_models(Project, projects))
20
-
21
-
22
- @app.command(help="Create a new project.")
23
- def create(
24
- id_prefix: Annotated[
25
- OptionalStr, Option(help="An optional ID prefix to which a random number will be appended.")
26
- ] = None,
27
- org_id: Annotated[OptionalStr, Option(help="Organization ID in which to create the project.")] = None,
28
- name: Annotated[OptionalStr, Option(help="Friendly name for the project.")] = None,
29
- ):
30
- res = state.settings.api.project.create(
31
- CreateProject.Request(organization_id=org_id or current_org_id(), id_prefix=id_prefix, name=name)
32
- )
33
- rich.print(f"Created project {res.project.id}")
34
-
35
-
36
- @app.command(help="Grant a role on a project.")
37
- def grant(
38
- project: ProjectArg,
39
- role: Annotated[str, Option(help="Role to grant.")],
40
- org_id: Annotated[
41
- OptionalStr, Option(help="Pass an organization ID to grant a role to an organization user(s).")
42
- ] = None,
43
- user_id: Annotated[
44
- OptionalStr, Option(help="Pass a user ID when using --org-id to grant a role to grant a role to a user.")
45
- ] = None,
46
- org_role: Annotated[
47
- OptionalStr,
48
- Option(help="Pass an organization role when using --org-id to grant a role to all users with that role."),
49
- ] = None,
50
- workload_id: Annotated[OptionalStr, Option(help="Pass a workload ID to grant a role to a workload.")] = None,
51
- github: Annotated[
52
- OptionalStr, Option(help="Pass an `<org>/<repo>` string to grant a role to a job running in GitHub Actions.")
53
- ] = None,
54
- modal: Annotated[
55
- OptionalStr,
56
- Option(help="Pass a `<workspace_id>/<env_name>` string to grant a role to a job running in Modal environment."),
57
- ] = None,
58
- conditions: list[str] | None = Option(
59
- default=None,
60
- help="`<key>=<value>` token conditions to apply to the grant when using --github or --modal.",
61
- ),
62
- ):
63
- conditions = conditions or []
64
-
65
- # Check mutual exclusion
66
- if sum(int(bool(opt)) for opt in {org_id, workload_id, github, modal}) != 1:
67
- raise typer.BadParameter("Only one of --org-id, --github or --modal may be specified.")
68
-
69
- if github:
70
- org, repo = github.split("/", 1)
71
- conditions = {GrantRole.GitHubClaim(k): v for k, v in dict(c.split("=", 1) for c in conditions).items()}
72
- principal = GrantRole.GitHubPrincipal(org=org, repo=repo, conditions=conditions)
73
- elif modal:
74
- workspace_id, environment_name = modal.split("/", 1)
75
- conditions = {GrantRole.ModalClaim(k): v for k, v in dict(c.split("=", 1) for c in conditions).items()}
76
- principal = GrantRole.ModalPrincipal(
77
- workspace_id=workspace_id, environment_name=environment_name, conditions=conditions
78
- )
79
- elif org_id:
80
- # Check mutual exclusion
81
- if sum(int(bool(opt)) for opt in {user_id, org_role}) != 1:
82
- raise typer.BadParameter("Only one of --user-id or --org-role may be specified.")
83
-
84
- if user_id is not None:
85
- principal = GrantRole.OrgUserPrincipal(org_id=org_id, user_id=user_id)
86
- elif org_role is not None:
87
- principal = GrantRole.OrgRolePrincipal(org_id=org_id, role=OrganizationRole(org_role))
88
- else:
89
- raise NotImplementedError("Only user or role principal is supported at this time.")
90
- elif workload_id:
91
- principal = GrantRole.WorkloadPrincipal(workload_id=workload_id)
92
- else:
93
- raise NotImplementedError("Only organization, GitHub or Modal principal is supported at this time.")
94
-
95
- state.settings.api.project.grant_role(
96
- GrantRole.Request(
97
- project_id=project,
98
- role_id=role,
99
- principal=principal,
100
- )
101
- )
102
-
103
- rich.print(f"Granted role {role} on project {project}")
104
-
105
-
106
- @app.command(help="List project grants.")
107
- def grants(project: ProjectArg):
108
- project_grants = list(state.settings.api.project.list_grants(ListGrants.Request(project_id=project)))
109
- rich.print(printer.table_of_models(Grant, project_grants, title="Project Grants"))
spiral/cli/table.py DELETED
@@ -1,20 +0,0 @@
1
- from typing import Annotated
2
-
3
- import rich
4
- from typer import Option
5
-
6
- from spiral.api.tables import ListTables, Table
7
- from spiral.cli import AsyncTyper, OptionalStr, printer, state
8
- from spiral.cli.types import ProjectArg
9
-
10
- app = AsyncTyper()
11
-
12
-
13
- @app.command(help="List tables.")
14
- def ls(
15
- project: ProjectArg,
16
- dataset: Annotated[OptionalStr, Option(help="Filter by dataset name.")] = None,
17
- ):
18
- """List tables."""
19
- tables = list(state.settings.api.table.list(ListTables.Request(project_id=project, dataset=dataset)))
20
- rich.print(printer.table_of_models(Table, tables, fields=["id", "project_id", "dataset", "table"]))
spiral/cli/token.py DELETED
@@ -1,27 +0,0 @@
1
- from typing import Annotated
2
-
3
- import rich
4
- from typer import Argument, Option
5
-
6
- from spiral.api.tokens import ListTokens, RevokeToken, Token
7
- from spiral.cli import AsyncTyper, OptionalStr, printer, state
8
- from spiral.cli.types import ProjectArg
9
-
10
- app = AsyncTyper()
11
-
12
-
13
- @app.command(help="List tokens.")
14
- def ls(
15
- project: ProjectArg,
16
- on_behalf_of: Annotated[OptionalStr, Option(help="Filter by on behalf of.")] = None,
17
- ):
18
- tokens = list(state.settings.api.token.list(ListTokens.Request(project_id=project, on_behalf_of=on_behalf_of)))
19
- rich.print(printer.table_of_models(Token, tokens, fields=["id", "project_id", "on_behalf_of"]))
20
-
21
-
22
- @app.command(help="Revoke a token.")
23
- def revoke(token_id: Annotated[str, Argument(help="Token ID.")]):
24
- res = state.settings.api.token.revoke(RevokeToken.Request(token_id=token_id))
25
- rich.print(
26
- f"Revoked token {res.token.id} for project {res.token.project_id} acting on behalf of {res.token.on_behalf_of}"
27
- )
@@ -1,91 +0,0 @@
1
- """The SpiralDB metastore API."""
2
-
3
- from collections.abc import Callable
4
-
5
- from spiral.core.spec import ColumnGroup, ColumnGroupMetadata, FileFormat, LogEntry, Schema, WriteAheadLog
6
- from spiral.types_ import Timestamp, Uri
7
- from spiraldb.proto.spiral.table import ManifestHandle
8
-
9
- class FileHandle:
10
- def __init__(self, *, uri: str, format: FileFormat, spfs_token: str | None): ...
11
-
12
- uri: str
13
- format: FileFormat
14
- spfs_token: str | None
15
-
16
- class FileRef:
17
- def __init__(self, *, id: str, file_type: FileType, file_format: FileFormat): ...
18
-
19
- id: str
20
- file_type: FileType
21
- file_format: FileFormat
22
-
23
- def resolve_uri(self, root_uri: str) -> str:
24
- """Resolves the file reference URI given the root URI."""
25
-
26
- class FileType:
27
- FragmentFile: FileType
28
- FragmentManifest: FileType
29
- ReferenceFile: FileType
30
-
31
- def __int__(self) -> int:
32
- """Returns the protobuf enum int value."""
33
-
34
- class PyMetastore:
35
- """Rust implementation of the metastore API."""
36
-
37
- @property
38
- def table_id(self) -> str: ...
39
- @property
40
- def root_uri(self) -> Uri: ...
41
- @property
42
- def key_schema(self) -> Schema: ...
43
- def get_wal(self) -> WriteAheadLog:
44
- """Return the log for the table."""
45
- ...
46
-
47
- def append_wal(self, prev_last_modified_at: Timestamp, entries: list[LogEntry]) -> WriteAheadLog:
48
- """Append additional entries into the write-ahead log given the previous write-ahead log timestamp.
49
-
50
- The given entries should have a timestamp of zero and will be assigned an actual timestamp by the server.
51
-
52
- This API is designed to support both a trivial compare-and-swap on the WAL, and also to support more advanced
53
- conflict resolution within the metastore.
54
- """
55
- ...
56
-
57
- def update_wal(
58
- self,
59
- prev_ks_manifest_handle_id: str,
60
- truncate_ts_max: Timestamp | None = None,
61
- new_ks_manifest_handle: ManifestHandle | None = None,
62
- ) -> WriteAheadLog:
63
- """Update the write-ahead log atomically.
64
-
65
- Supports WAL truncation and manifest handle updates necessary for flushing.
66
- """
67
- ...
68
-
69
- def get_column_group_metadata(self, column_group: ColumnGroup) -> ColumnGroupMetadata:
70
- """Return the metadata for column group."""
71
- ...
72
-
73
- def update_column_group_metadata(
74
- self, prev_last_modified_at: Timestamp, column_group_metadata: ColumnGroupMetadata
75
- ) -> ColumnGroupMetadata:
76
- """Update the column group metadata to the metastore given the previous metadata timestamp."""
77
- ...
78
-
79
- def list_column_groups(self) -> tuple[list[ColumnGroup], Timestamp]:
80
- """List all column groups in the table, or None if no index is available."""
81
- ...
82
-
83
- @staticmethod
84
- def http(
85
- table_id: str, root_uri: str, key_schema: Schema, base_url: str, token_provider: Callable[[], str]
86
- ) -> PyMetastore:
87
- """Construct a PyMetastore backed by an HTTP metastore service."""
88
-
89
- @staticmethod
90
- def test(table_id: str, root_uri: str, key_schema: Schema) -> PyMetastore:
91
- """Construct a PyMetastore backed by an in-memory mock metastore service."""
@@ -1,36 +0,0 @@
1
- # Generated by the protocol buffer compiler. DO NOT EDIT!
2
- # sources: spfs/spfs.proto
3
- # plugin: python-betterproto
4
- # This file has been @generated
5
-
6
- from dataclasses import dataclass
7
-
8
- import betterproto
9
-
10
-
11
- @dataclass(eq=False, repr=False)
12
- class FileMetadata(betterproto.Message):
13
- protobuf: "ProtobufFileSpecificMetadata" = betterproto.message_field(
14
- 1, group="format_specific"
15
- )
16
- parquet: "ParquetFileSpecificMetadata" = betterproto.message_field(
17
- 2, group="format_specific"
18
- )
19
- vortex: "VortexFileSpecificMetadata" = betterproto.message_field(
20
- 3, group="format_specific"
21
- )
22
-
23
-
24
- @dataclass(eq=False, repr=False)
25
- class ProtobufFileSpecificMetadata(betterproto.Message):
26
- pass
27
-
28
-
29
- @dataclass(eq=False, repr=False)
30
- class ParquetFileSpecificMetadata(betterproto.Message):
31
- metadata_size_bytes: int = betterproto.uint32_field(1)
32
-
33
-
34
- @dataclass(eq=False, repr=False)
35
- class VortexFileSpecificMetadata(betterproto.Message):
36
- metadata_size_bytes: int = betterproto.uint32_field(1)