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.
- {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/METADATA +9 -13
- pyspiral-0.4.0.dist-info/RECORD +98 -0
- {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/WHEEL +1 -1
- spiral/__init__.py +6 -9
- spiral/_lib.abi3.so +0 -0
- spiral/adbc.py +21 -14
- spiral/api/__init__.py +14 -175
- spiral/api/admin.py +12 -26
- spiral/api/client.py +160 -0
- spiral/api/filesystems.py +100 -72
- spiral/api/organizations.py +45 -58
- spiral/api/projects.py +171 -134
- spiral/api/telemetry.py +19 -0
- spiral/api/types.py +20 -0
- spiral/api/workloads.py +32 -25
- spiral/{arrow.py → arrow_.py} +12 -0
- spiral/cli/__init__.py +2 -5
- spiral/cli/admin.py +7 -12
- spiral/cli/app.py +23 -6
- spiral/cli/console.py +1 -1
- spiral/cli/fs.py +82 -17
- spiral/cli/iceberg/__init__.py +7 -0
- spiral/cli/iceberg/namespaces.py +47 -0
- spiral/cli/iceberg/tables.py +60 -0
- spiral/cli/indexes/__init__.py +19 -0
- spiral/cli/login.py +14 -5
- spiral/cli/orgs.py +90 -0
- spiral/cli/printer.py +9 -1
- spiral/cli/projects.py +136 -0
- spiral/cli/state.py +2 -0
- spiral/cli/tables/__init__.py +121 -0
- spiral/cli/telemetry.py +18 -0
- spiral/cli/types.py +8 -10
- spiral/cli/{workload.py → workloads.py} +11 -11
- spiral/{catalog.py → client.py} +23 -37
- spiral/core/client/__init__.pyi +117 -0
- spiral/core/index/__init__.pyi +15 -0
- spiral/core/{core → table}/__init__.pyi +44 -17
- spiral/core/{manifests → table/manifests}/__init__.pyi +5 -23
- spiral/core/table/metastore/__init__.pyi +62 -0
- spiral/core/{spec → table/spec}/__init__.pyi +41 -66
- spiral/datetime_.py +27 -0
- spiral/expressions/__init__.py +26 -18
- spiral/expressions/base.py +5 -5
- spiral/expressions/list_.py +1 -1
- spiral/expressions/mp4.py +2 -9
- spiral/expressions/png.py +1 -1
- spiral/expressions/qoi.py +1 -1
- spiral/expressions/refs.py +3 -9
- spiral/expressions/struct.py +7 -5
- spiral/expressions/text.py +62 -0
- spiral/expressions/udf.py +3 -3
- spiral/iceberg/__init__.py +3 -0
- spiral/iceberg/client.py +33 -0
- spiral/indexes/__init__.py +5 -0
- spiral/indexes/client.py +137 -0
- spiral/indexes/index.py +34 -0
- spiral/indexes/scan.py +22 -0
- spiral/project.py +19 -110
- spiral/{proto → protogen}/_/scandal/__init__.py +23 -135
- spiral/protogen/_/spiral/table/__init__.py +22 -0
- spiral/protogen/substrait/__init__.py +3399 -0
- spiral/protogen/substrait/extensions/__init__.py +115 -0
- spiral/server.py +17 -0
- spiral/settings.py +29 -91
- spiral/substrait_.py +9 -5
- spiral/tables/__init__.py +12 -0
- spiral/tables/client.py +130 -0
- spiral/{dataset.py → tables/dataset.py} +9 -199
- spiral/tables/debug/manifests.py +70 -0
- spiral/tables/debug/metrics.py +56 -0
- spiral/{debug.py → tables/debug/scan.py} +6 -9
- spiral/{maintenance.py → tables/maintenance.py} +1 -1
- spiral/{scan_.py → tables/scan.py} +63 -89
- spiral/tables/snapshot.py +78 -0
- spiral/{table.py → tables/table.py} +59 -73
- spiral/{txn.py → tables/transaction.py} +7 -3
- pyspiral-0.3.1.dist-info/RECORD +0 -85
- spiral/api/tables.py +0 -91
- spiral/api/tokens.py +0 -56
- spiral/authn/authn.py +0 -89
- spiral/authn/device.py +0 -206
- spiral/authn/github_.py +0 -33
- spiral/authn/modal_.py +0 -18
- spiral/cli/org.py +0 -90
- spiral/cli/project.py +0 -109
- spiral/cli/table.py +0 -20
- spiral/cli/token.py +0 -27
- spiral/core/metastore/__init__.pyi +0 -91
- spiral/proto/_/spfs/__init__.py +0 -36
- spiral/proto/_/spiral/table/__init__.py +0 -276
- spiral/proto/_/spiraldb/metastore/__init__.py +0 -499
- spiral/proto/__init__.py +0 -0
- spiral/proto/scandal/__init__.py +0 -45
- spiral/proto/spiral/__init__.py +0 -0
- spiral/proto/spiral/table/__init__.py +0 -96
- {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/entry_points.txt +0 -0
- /spiral/{authn/__init__.py → core/__init__.pyi} +0 -0
- /spiral/{core → protogen/_}/__init__.py +0 -0
- /spiral/{proto/_ → protogen/_/arrow}/__init__.py +0 -0
- /spiral/{proto/_/arrow → protogen/_/arrow/flight}/__init__.py +0 -0
- /spiral/{proto/_/arrow/flight → protogen/_/arrow/flight/protocol}/__init__.py +0 -0
- /spiral/{proto → protogen}/_/arrow/flight/protocol/sql/__init__.py +0 -0
- /spiral/{proto/_/arrow/flight/protocol → protogen/_/spiral}/__init__.py +0 -0
- /spiral/{proto → protogen/_}/substrait/__init__.py +0 -0
- /spiral/{proto → protogen/_}/substrait/extensions/__init__.py +0 -0
- /spiral/{proto/_/spiral → protogen}/__init__.py +0 -0
- /spiral/{proto → protogen}/util.py +0 -0
- /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."""
|
spiral/proto/_/spfs/__init__.py
DELETED
@@ -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)
|