pyspiral 0.3.1__cp310-abi3-macosx_11_0_arm64.whl → 0.4.1__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.1.dist-info}/METADATA +9 -13
- pyspiral-0.4.1.dist-info/RECORD +98 -0
- {pyspiral-0.3.1.dist-info → pyspiral-0.4.1.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.1.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/api/projects.py
CHANGED
@@ -1,160 +1,197 @@
|
|
1
|
-
from
|
2
|
-
from typing import Annotated, Literal, Union
|
1
|
+
from typing import Annotated, Literal
|
3
2
|
|
4
3
|
from pydantic import BaseModel, Field
|
5
4
|
|
6
|
-
from . import
|
7
|
-
from .
|
5
|
+
from .client import Paged, PagedResponse, ServiceBase
|
6
|
+
from .types import OrgId, ProjectId, RoleId
|
8
7
|
|
9
8
|
|
10
9
|
class Project(BaseModel):
|
11
10
|
id: ProjectId
|
12
|
-
|
11
|
+
org_id: OrgId
|
13
12
|
name: str | None = None
|
14
13
|
|
15
14
|
|
15
|
+
class CreateProjectRequest(BaseModel):
|
16
|
+
id_prefix: str | None = None
|
17
|
+
name: str | None = None
|
18
|
+
|
19
|
+
|
20
|
+
class CreateProjectResponse(BaseModel):
|
21
|
+
project: Project
|
22
|
+
|
23
|
+
|
16
24
|
class Grant(BaseModel):
|
17
25
|
id: str
|
18
26
|
project_id: ProjectId
|
19
27
|
role_id: RoleId
|
20
28
|
principal: str
|
29
|
+
conditions: dict | None = None
|
21
30
|
|
22
31
|
|
23
|
-
class
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
name: str | None = Field(default=None, description="Optional human-readable name for the project")
|
32
|
+
class OrgRolePrincipalConditions(BaseModel):
|
33
|
+
type: Literal["org_role"] = "org_role"
|
34
|
+
org_id: OrgId
|
35
|
+
role: str
|
28
36
|
|
29
|
-
id: str | None = Field(
|
30
|
-
default=None,
|
31
|
-
description="Exact project ID to use. Requires elevated permissions.",
|
32
|
-
)
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
workspace_id: str
|
136
|
-
# Environments are sub-divisions of workspaces. Name is unique within a workspace.
|
137
|
-
# See https://modal.com/docs/guide/environments
|
138
|
-
environment_name: str
|
139
|
-
# A Modal App is a group of functions and classes that are deployed together.
|
140
|
-
# See https://modal.com/docs/guide/apps. Nick and Marko discussed having an app_name
|
141
|
-
# here as well and decided to leave it out for now with the assumption that people
|
142
|
-
# will want to authorize the whole Modal environment to access Spiral (their data).
|
143
|
-
conditions: dict["GrantRole.ModalClaim", str] | None = None
|
38
|
+
class OrgUserPrincipalConditions(BaseModel):
|
39
|
+
type: Literal["org_user"] = "org_user"
|
40
|
+
org_id: OrgId
|
41
|
+
user_id: str
|
42
|
+
|
43
|
+
|
44
|
+
class WorkloadPrincipalConditions(BaseModel):
|
45
|
+
type: Literal["workload"] = "workload"
|
46
|
+
workload_id: str
|
47
|
+
|
48
|
+
|
49
|
+
class GitHubConditions(BaseModel):
|
50
|
+
environment: str | None = None
|
51
|
+
ref: str | None = None
|
52
|
+
ref_type: str | None = None
|
53
|
+
sha: str | None = None
|
54
|
+
repository: str | None = None
|
55
|
+
repository_owner: str | None = None
|
56
|
+
repository_visibility: str | None = None
|
57
|
+
repository_id: str | None = None
|
58
|
+
repository_owner_id: str | None = None
|
59
|
+
run_id: str | None = None
|
60
|
+
run_number: str | None = None
|
61
|
+
run_attempt: str | None = None
|
62
|
+
runner_environment: str | None = None
|
63
|
+
actor_id: str | None = None
|
64
|
+
actor: str | None = None
|
65
|
+
workflow: str | None = None
|
66
|
+
head_ref: str | None = None
|
67
|
+
base_ref: str | None = None
|
68
|
+
job_workflow_ref: str | None = None
|
69
|
+
event_name: str | None = None
|
70
|
+
|
71
|
+
|
72
|
+
class GitHubPrincipalConditions(BaseModel):
|
73
|
+
type: Literal["github"] = "github"
|
74
|
+
org: str
|
75
|
+
repo: str
|
76
|
+
conditions: GitHubConditions | None = None
|
77
|
+
|
78
|
+
|
79
|
+
class ModalConditions(BaseModel):
|
80
|
+
app_id: str | None = None
|
81
|
+
app_name: str | None = None
|
82
|
+
function_id: str | None = None
|
83
|
+
function_name: str | None = None
|
84
|
+
container_id: str | None = None
|
85
|
+
|
86
|
+
|
87
|
+
class ModalPrincipalConditions(BaseModel):
|
88
|
+
type: Literal["modal"] = "modal"
|
89
|
+
|
90
|
+
# A Modal App is a group of functions and classes that are deployed together.
|
91
|
+
# See https://modal.com/docs/guide/apps. Nick and Marko discussed having an app_name
|
92
|
+
# here as well and decided to leave it out for now with the assumption that people
|
93
|
+
# will want to authorize the whole Modal environment to access Spiral (their data).
|
94
|
+
workspace_id: str
|
95
|
+
# Environments are sub-divisions of workspaces. Name is unique within a workspace.
|
96
|
+
# See https://modal.com/docs/guide/environments
|
97
|
+
environment_name: str
|
98
|
+
|
99
|
+
conditions: ModalConditions | None = None
|
100
|
+
|
101
|
+
|
102
|
+
class GCPPrincipalConditions(BaseModel):
|
103
|
+
type: Literal["gcp"] = "gcp"
|
104
|
+
service_account: str
|
105
|
+
unique_id: str
|
106
|
+
|
107
|
+
|
108
|
+
PrincipalConditions = Annotated[
|
109
|
+
OrgRolePrincipalConditions
|
110
|
+
| OrgUserPrincipalConditions
|
111
|
+
| WorkloadPrincipalConditions
|
112
|
+
| GitHubPrincipalConditions
|
113
|
+
| ModalPrincipalConditions
|
114
|
+
| GCPPrincipalConditions,
|
115
|
+
Field(discriminator="type"),
|
116
|
+
]
|
117
|
+
|
118
|
+
|
119
|
+
class GrantRoleRequest(BaseModel):
|
120
|
+
role_id: RoleId
|
121
|
+
principal: PrincipalConditions
|
122
|
+
|
123
|
+
|
124
|
+
class GrantRoleResponse(BaseModel):
|
125
|
+
grant: Grant
|
126
|
+
|
127
|
+
|
128
|
+
class TableResource(BaseModel):
|
129
|
+
id: str
|
130
|
+
project_id: ProjectId
|
131
|
+
dataset: str
|
132
|
+
table: str
|
133
|
+
|
134
|
+
|
135
|
+
class TextIndexResource(BaseModel):
|
136
|
+
id: str
|
137
|
+
project_id: ProjectId
|
138
|
+
name: str
|
144
139
|
|
145
140
|
|
146
141
|
class ProjectService(ServiceBase):
|
147
|
-
|
148
|
-
return self.client.put("/project/get", request, GetProject.Response)
|
142
|
+
"""Service for project operations."""
|
149
143
|
|
150
|
-
def create(self, request:
|
151
|
-
|
144
|
+
def create(self, request: CreateProjectRequest) -> CreateProjectResponse:
|
145
|
+
"""Create a new project."""
|
146
|
+
return self.client.post("/v1/projects", request, CreateProjectResponse)
|
152
147
|
|
153
148
|
def list(self) -> Paged[Project]:
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
149
|
+
"""List projects."""
|
150
|
+
return self.client.paged("/v1/projects", PagedResponse[Project])
|
151
|
+
|
152
|
+
def list_tables(
|
153
|
+
self, project_id: ProjectId, dataset: str | None = None, table: str | None = None
|
154
|
+
) -> Paged[TableResource]:
|
155
|
+
"""List tables in a project."""
|
156
|
+
params = {}
|
157
|
+
if dataset:
|
158
|
+
params["dataset"] = dataset
|
159
|
+
if table:
|
160
|
+
params["table"] = table
|
161
|
+
return self.client.paged(f"/v1/projects/{project_id}/tables", PagedResponse[TableResource], params=params)
|
162
|
+
|
163
|
+
def list_text_indexes(self, project_id: ProjectId, name: str | None = None) -> Paged[TextIndexResource]:
|
164
|
+
"""List text indexes in a project."""
|
165
|
+
params = {}
|
166
|
+
if name:
|
167
|
+
params["name"] = name
|
168
|
+
return self.client.paged(
|
169
|
+
f"/v1/projects/{project_id}/text-indexes", PagedResponse[TextIndexResource], params=params
|
170
|
+
)
|
158
171
|
|
159
|
-
def
|
160
|
-
|
172
|
+
def get(self, project_id: ProjectId) -> Project:
|
173
|
+
"""Get a project."""
|
174
|
+
return self.client.get(f"/v1/projects/{project_id}", Project)
|
175
|
+
|
176
|
+
def grant_role(self, project_id: ProjectId, request: GrantRoleRequest) -> GrantRoleResponse:
|
177
|
+
"""Grant a role to a principal."""
|
178
|
+
return self.client.post(f"/v1/projects/{project_id}/grants", request, GrantRoleResponse)
|
179
|
+
|
180
|
+
def list_grants(
|
181
|
+
self,
|
182
|
+
project_id: ProjectId,
|
183
|
+
principal: str | None = None,
|
184
|
+
) -> Paged[Grant]:
|
185
|
+
"""List active project grants."""
|
186
|
+
params = {}
|
187
|
+
if principal:
|
188
|
+
params["principal"] = principal
|
189
|
+
return self.client.paged(f"/v1/projects/{project_id}/grants", PagedResponse[Grant], params=params)
|
190
|
+
|
191
|
+
def get_grant(self, grant_id: str) -> Grant:
|
192
|
+
"""Get a grant."""
|
193
|
+
return self.client.get(f"/v1/grants/{grant_id}", Grant)
|
194
|
+
|
195
|
+
def revoke_grant(self, grant_id: str) -> None:
|
196
|
+
"""Revoke a grant."""
|
197
|
+
return self.client.delete(f"/v1/grants/{grant_id}", None)
|
spiral/api/telemetry.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
from .client import ServiceBase
|
4
|
+
|
5
|
+
|
6
|
+
class IssueExportTokenRequest(BaseModel):
|
7
|
+
pass
|
8
|
+
|
9
|
+
|
10
|
+
class IssueExportTokenResponse(BaseModel):
|
11
|
+
token: str
|
12
|
+
|
13
|
+
|
14
|
+
class TelemetryService(ServiceBase):
|
15
|
+
"""Service for telemetry operations."""
|
16
|
+
|
17
|
+
def issue_export_token(self) -> IssueExportTokenResponse:
|
18
|
+
"""Issue telemetry export token."""
|
19
|
+
return self.client.put("/v1/telemetry/token", IssueExportTokenRequest(), IssueExportTokenResponse)
|
spiral/api/types.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
from typing import Annotated
|
2
|
+
|
3
|
+
from pydantic import AfterValidator, StringConstraints
|
4
|
+
|
5
|
+
|
6
|
+
def _validate_root_uri(uri: str) -> str:
|
7
|
+
if uri.endswith("/"):
|
8
|
+
raise ValueError("Root URI must not end with a slash.")
|
9
|
+
return uri
|
10
|
+
|
11
|
+
|
12
|
+
UserId = str
|
13
|
+
OrgId = str
|
14
|
+
ProjectId = str
|
15
|
+
RoleId = str
|
16
|
+
|
17
|
+
RootUri = Annotated[str, AfterValidator(_validate_root_uri)]
|
18
|
+
DatasetName = Annotated[str, StringConstraints(max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]+$")]
|
19
|
+
TableName = Annotated[str, StringConstraints(max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]*$")]
|
20
|
+
IndexName = Annotated[str, StringConstraints(max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]*$")]
|
spiral/api/workloads.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
from pydantic import BaseModel
|
1
|
+
from pydantic import BaseModel
|
2
2
|
|
3
|
-
from . import Paged,
|
3
|
+
from .client import Paged, PagedResponse, ServiceBase
|
4
|
+
from .types import ProjectId
|
4
5
|
|
5
6
|
|
6
7
|
class Workload(BaseModel):
|
@@ -9,37 +10,43 @@ class Workload(BaseModel):
|
|
9
10
|
name: str | None = None
|
10
11
|
|
11
12
|
|
12
|
-
class
|
13
|
-
|
14
|
-
|
15
|
-
name: str | None = Field(default=None, description="Optional human-readable name for the workload")
|
13
|
+
class CreateWorkloadRequest(BaseModel):
|
14
|
+
name: str | None = None
|
15
|
+
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
class CreateWorkloadResponse(BaseModel):
|
18
|
+
workload: Workload
|
19
19
|
|
20
20
|
|
21
|
-
class
|
22
|
-
|
23
|
-
|
21
|
+
class IssueWorkloadCredentialsResponse(BaseModel):
|
22
|
+
client_id: str
|
23
|
+
client_secret: str
|
24
|
+
revoked_client_id: str | None = None
|
24
25
|
|
25
|
-
class Response(BaseModel):
|
26
|
-
token_id: str
|
27
|
-
token_secret: str
|
28
26
|
|
27
|
+
class WorkloadService(ServiceBase):
|
28
|
+
"""Service for workload operations."""
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
project_id
|
30
|
+
def create(self, project_id: ProjectId, request: CreateWorkloadRequest) -> CreateWorkloadResponse:
|
31
|
+
"""Create a new workload."""
|
32
|
+
return self.client.post(f"/v1/projects/{project_id}/workloads", request, CreateWorkloadResponse)
|
33
33
|
|
34
|
-
|
34
|
+
def list(self, project_id: ProjectId) -> Paged[Workload]:
|
35
|
+
"""List active project workloads."""
|
36
|
+
return self.client.paged(f"/projects/{project_id}/workloads", PagedResponse[Workload])
|
35
37
|
|
38
|
+
def get(self, workload_id: str) -> Workload:
|
39
|
+
"""Get a workload."""
|
40
|
+
return self.client.get(f"/v1/workloads/{workload_id}", Workload)
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
return self.client.
|
42
|
+
def deactivate(self, workload_id: str) -> None:
|
43
|
+
"""De-activate a workload."""
|
44
|
+
return self.client.delete(f"/v1/workloads/{workload_id}", None)
|
40
45
|
|
41
|
-
def
|
42
|
-
|
46
|
+
def issue_credentials(self, workload_id: str) -> IssueWorkloadCredentialsResponse:
|
47
|
+
"""Issue workload credentials."""
|
48
|
+
return self.client.post(f"/v1/workloads/{workload_id}/credentials", None, IssueWorkloadCredentialsResponse)
|
43
49
|
|
44
|
-
def
|
45
|
-
|
50
|
+
def revoke_credentials(self, client_id: str) -> None:
|
51
|
+
"""Revoke workload credentials."""
|
52
|
+
return self.client.delete(f"/v1/credentials/{client_id}", None)
|
spiral/{arrow.py → arrow_.py}
RENAMED
@@ -149,6 +149,18 @@ def flatten_struct_table(table: pa.Table, separator=".") -> pa.Table:
|
|
149
149
|
return pa.Table.from_arrays(data, names=names)
|
150
150
|
|
151
151
|
|
152
|
+
def struct_array(fields: list[tuple[str, bool, pa.Array]], /, mask: list[bool] | None = None) -> pa.StructArray:
|
153
|
+
return pa.StructArray.from_arrays(
|
154
|
+
arrays=[x[2] for x in fields],
|
155
|
+
fields=[pa.field(x[0], type=x[2].type, nullable=x[1]) for x in fields],
|
156
|
+
mask=pa.array(mask) if mask else mask,
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
def table(fields: list[tuple[str, bool, pa.Array]], /) -> pa.Table:
|
161
|
+
return pa.Table.from_struct_array(struct_array(fields))
|
162
|
+
|
163
|
+
|
152
164
|
def dict_to_table(data) -> pa.Table:
|
153
165
|
return pa.Table.from_struct_array(dict_to_struct_array(data))
|
154
166
|
|
spiral/cli/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import asyncio
|
2
2
|
import functools
|
3
3
|
import inspect
|
4
|
-
from typing import IO
|
4
|
+
from typing import IO
|
5
5
|
|
6
6
|
import rich
|
7
7
|
import typer
|
@@ -9,9 +9,6 @@ from click import ClickException
|
|
9
9
|
from grpclib import GRPCError
|
10
10
|
from httpx import HTTPStatusError
|
11
11
|
|
12
|
-
# We need to use Optional[str] since Typer doesn't support str | None.
|
13
|
-
OptionalStr = Optional[str] # noqa: UP007
|
14
|
-
|
15
12
|
|
16
13
|
class AsyncTyper(typer.Typer):
|
17
14
|
"""Wrapper to allow async functions to be used as commands.
|
@@ -43,7 +40,7 @@ class AsyncTyper(typer.Typer):
|
|
43
40
|
|
44
41
|
class _ClickGRPCException(ClickException):
|
45
42
|
def __init__(self, err: GRPCError):
|
46
|
-
super().__init__(err.message)
|
43
|
+
super().__init__(err.message or "GRPCError message was None.")
|
47
44
|
self.err = err
|
48
45
|
self.exit_code = 1
|
49
46
|
|
spiral/cli/admin.py
CHANGED
@@ -1,21 +1,16 @@
|
|
1
1
|
from rich import print
|
2
2
|
|
3
|
-
from spiral.api.
|
3
|
+
from spiral.api.types import OrgId
|
4
4
|
from spiral.cli import AsyncTyper, state
|
5
5
|
|
6
6
|
app = AsyncTyper()
|
7
7
|
|
8
8
|
|
9
9
|
@app.command()
|
10
|
-
def sync(
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def sync(
|
11
|
+
org_id: OrgId | None = None,
|
12
|
+
):
|
13
|
+
state.settings.api._admin.sync_orgs()
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
print(org_id)
|
18
|
-
|
19
|
-
if run_all or memberships:
|
20
|
-
for membership in state.settings.api._admin.sync_memberships(SyncMemberships.Request()):
|
21
|
-
print(membership)
|
15
|
+
for membership in state.settings.api._admin.sync_memberships(org_id):
|
16
|
+
print(membership)
|
spiral/cli/app.py
CHANGED
@@ -2,7 +2,21 @@ import logging
|
|
2
2
|
import os
|
3
3
|
from logging.handlers import RotatingFileHandler
|
4
4
|
|
5
|
-
from spiral.cli import
|
5
|
+
from spiral.cli import (
|
6
|
+
AsyncTyper,
|
7
|
+
admin,
|
8
|
+
console,
|
9
|
+
fs,
|
10
|
+
iceberg,
|
11
|
+
indexes,
|
12
|
+
login,
|
13
|
+
orgs,
|
14
|
+
projects,
|
15
|
+
state,
|
16
|
+
tables,
|
17
|
+
telemetry,
|
18
|
+
workloads,
|
19
|
+
)
|
6
20
|
from spiral.settings import LOG_DIR, Settings
|
7
21
|
|
8
22
|
app = AsyncTyper(name="spiral")
|
@@ -18,16 +32,19 @@ def _callback(verbose: bool = False):
|
|
18
32
|
|
19
33
|
|
20
34
|
app.add_typer(fs.app, name="fs")
|
21
|
-
app.add_typer(
|
22
|
-
app.add_typer(
|
23
|
-
app.add_typer(
|
24
|
-
app.add_typer(
|
25
|
-
app.add_typer(
|
35
|
+
app.add_typer(orgs.app, name="orgs")
|
36
|
+
app.add_typer(projects.app, name="projects")
|
37
|
+
app.add_typer(iceberg.app, name="iceberg")
|
38
|
+
app.add_typer(tables.app, name="tables")
|
39
|
+
app.add_typer(indexes.app, name="indexes")
|
40
|
+
app.add_typer(telemetry.app, name="telemetry")
|
26
41
|
app.command("console")(console.command)
|
27
42
|
app.command("login")(login.command)
|
43
|
+
app.command("whoami")(login.whoami)
|
28
44
|
|
29
45
|
# Register unless we're building docs. Because Typer docs command does not skip hidden commands...
|
30
46
|
if not bool(os.environ.get("SPIRAL_DOCS", False)):
|
47
|
+
app.add_typer(workloads.app, name="workloads", hidden=True)
|
31
48
|
app.add_typer(admin.app, name="admin", hidden=True)
|
32
49
|
app.command("logout", hidden=True)(login.logout)
|
33
50
|
|