pyspiral 0.2.5__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 (114) hide show
  1. {pyspiral-0.2.5.dist-info → pyspiral-0.4.0.dist-info}/METADATA +12 -14
  2. pyspiral-0.4.0.dist-info/RECORD +98 -0
  3. {pyspiral-0.2.5.dist-info → pyspiral-0.4.0.dist-info}/WHEEL +1 -1
  4. spiral/__init__.py +6 -7
  5. spiral/_lib.abi3.so +0 -0
  6. spiral/adbc.py +21 -14
  7. spiral/api/__init__.py +15 -172
  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 +83 -18
  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} +22 -21
  36. spiral/core/client/__init__.pyi +117 -0
  37. spiral/core/index/__init__.pyi +15 -0
  38. spiral/core/table/__init__.pyi +108 -0
  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 +49 -92
  42. spiral/datetime_.py +27 -0
  43. spiral/expressions/__init__.py +40 -17
  44. spiral/expressions/base.py +5 -5
  45. spiral/expressions/list_.py +1 -1
  46. spiral/expressions/mp4.py +62 -0
  47. spiral/expressions/png.py +18 -0
  48. spiral/expressions/qoi.py +18 -0
  49. spiral/expressions/refs.py +23 -9
  50. spiral/expressions/struct.py +7 -5
  51. spiral/expressions/text.py +62 -0
  52. spiral/expressions/tiff.py +88 -88
  53. spiral/expressions/udf.py +3 -3
  54. spiral/iceberg/__init__.py +3 -0
  55. spiral/iceberg/client.py +33 -0
  56. spiral/indexes/__init__.py +5 -0
  57. spiral/indexes/client.py +137 -0
  58. spiral/indexes/index.py +34 -0
  59. spiral/indexes/scan.py +22 -0
  60. spiral/project.py +19 -110
  61. spiral/{proto → protogen}/_/scandal/__init__.py +32 -77
  62. spiral/protogen/_/spiral/table/__init__.py +22 -0
  63. spiral/protogen/substrait/__init__.py +3399 -0
  64. spiral/protogen/substrait/extensions/__init__.py +115 -0
  65. spiral/server.py +17 -0
  66. spiral/settings.py +31 -87
  67. spiral/substrait_.py +10 -6
  68. spiral/tables/__init__.py +12 -0
  69. spiral/tables/client.py +130 -0
  70. spiral/{dataset.py → tables/dataset.py} +36 -25
  71. spiral/tables/debug/manifests.py +70 -0
  72. spiral/tables/debug/metrics.py +56 -0
  73. spiral/{debug.py → tables/debug/scan.py} +6 -9
  74. spiral/tables/maintenance.py +12 -0
  75. spiral/tables/scan.py +193 -0
  76. spiral/tables/snapshot.py +78 -0
  77. spiral/tables/table.py +157 -0
  78. spiral/tables/transaction.py +52 -0
  79. pyspiral-0.2.5.dist-info/RECORD +0 -81
  80. spiral/api/tables.py +0 -94
  81. spiral/api/tokens.py +0 -56
  82. spiral/authn/authn.py +0 -89
  83. spiral/authn/device.py +0 -206
  84. spiral/authn/github_.py +0 -33
  85. spiral/authn/modal_.py +0 -18
  86. spiral/cli/org.py +0 -90
  87. spiral/cli/project.py +0 -107
  88. spiral/cli/table.py +0 -20
  89. spiral/cli/token.py +0 -27
  90. spiral/config.py +0 -26
  91. spiral/core/core/__init__.pyi +0 -53
  92. spiral/core/metastore/__init__.pyi +0 -91
  93. spiral/proto/_/spfs/__init__.py +0 -36
  94. spiral/proto/_/spiral/table/__init__.py +0 -225
  95. spiral/proto/_/spiraldb/metastore/__init__.py +0 -499
  96. spiral/proto/__init__.py +0 -0
  97. spiral/proto/scandal/__init__.py +0 -45
  98. spiral/proto/spiral/__init__.py +0 -0
  99. spiral/proto/spiral/table/__init__.py +0 -96
  100. spiral/scan_.py +0 -168
  101. spiral/table.py +0 -157
  102. {pyspiral-0.2.5.dist-info → pyspiral-0.4.0.dist-info}/entry_points.txt +0 -0
  103. /spiral/{authn/__init__.py → core/__init__.pyi} +0 -0
  104. /spiral/{core → protogen/_}/__init__.py +0 -0
  105. /spiral/{proto/_ → protogen/_/arrow}/__init__.py +0 -0
  106. /spiral/{proto/_/arrow → protogen/_/arrow/flight}/__init__.py +0 -0
  107. /spiral/{proto/_/arrow/flight → protogen/_/arrow/flight/protocol}/__init__.py +0 -0
  108. /spiral/{proto → protogen}/_/arrow/flight/protocol/sql/__init__.py +0 -0
  109. /spiral/{proto/_/arrow/flight/protocol → protogen/_/spiral}/__init__.py +0 -0
  110. /spiral/{proto → protogen/_}/substrait/__init__.py +0 -0
  111. /spiral/{proto → protogen/_}/substrait/extensions/__init__.py +0 -0
  112. /spiral/{proto/_/spiral → protogen}/__init__.py +0 -0
  113. /spiral/{proto → protogen}/util.py +0 -0
  114. /spiral/{proto/_/spiraldb → tables/debug}/__init__.py +0 -0
spiral/api/projects.py CHANGED
@@ -1,160 +1,197 @@
1
- from enum import Enum
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 OrganizationId, Paged, PagedRequest, PagedResponse, ProjectId, RoleId, ServiceBase
7
- from .organizations import OrganizationRole
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
- organization_id: OrganizationId
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 CreateProject:
24
- class Request(BaseModel):
25
- organization_id: OrganizationId
26
- id_prefix: str | None = Field(default=None, description="Optional prefix for a random project ID")
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
- class Response(BaseModel):
35
- project: Project
36
-
37
-
38
- class GetProject:
39
- class Request(BaseModel):
40
- id: ProjectId
41
-
42
- class Response(BaseModel):
43
- project: Project
44
-
45
-
46
- class ListProjects:
47
- class Request(PagedRequest): ...
48
-
49
- class Response(PagedResponse[Project]): ...
50
-
51
-
52
- class ListGrants:
53
- class Request(PagedRequest):
54
- project_id: ProjectId
55
-
56
- class Response(PagedResponse[Grant]): ...
57
-
58
-
59
- class GrantRole:
60
- class Request(BaseModel):
61
- project_id: ProjectId
62
- role_id: RoleId
63
- principal: Annotated[
64
- Union[
65
- "GrantRole.OrgRolePrincipal",
66
- "GrantRole.OrgUserPrincipal",
67
- "GrantRole.WorkloadPrincipal",
68
- "GrantRole.GitHubPrincipal",
69
- "GrantRole.ModalPrincipal",
70
- ],
71
- Field(discriminator="type"),
72
- ]
73
-
74
- class Response(BaseModel): ...
75
-
76
- class OrgRolePrincipal(BaseModel):
77
- type: Literal["org"] = "org"
78
-
79
- org_id: OrganizationId
80
- role: OrganizationRole
81
-
82
- class OrgUserPrincipal(BaseModel):
83
- type: Literal["org_user"] = "org_user"
84
-
85
- org_id: OrganizationId
86
- user_id: str
87
-
88
- class WorkloadPrincipal(BaseModel):
89
- type: Literal["workload"] = "workload"
90
-
91
- workload_id: str
92
-
93
- class GitHubClaim(Enum):
94
- environment = "environment"
95
- ref = "ref"
96
- sha = "sha"
97
- repository = "repository"
98
- repository_owner = "repository_owner"
99
- actor_id = "actor_id"
100
- repository_visibility = "repository_visibility"
101
- repository_id = "repository_id"
102
- repository_owner_id = "repository_owner_id"
103
- run_id = "run_id"
104
- run_number = "run_number"
105
- run_attempt = "run_attempt"
106
- runner_environment = "runner_environment"
107
- actor = "actor"
108
- workflow = "workflow"
109
- head_ref = "head_ref"
110
- base_ref = "base_ref"
111
- event_name = "event_name"
112
- ref_type = "ref_type"
113
- job_workflow_ref = "job_workflow_ref"
114
-
115
- class GitHubPrincipal(BaseModel):
116
- type: Literal["github"] = "github"
117
-
118
- org: str
119
- repo: str
120
- conditions: dict["GrantRole.GitHubClaim", str] | None = None
121
-
122
- class ModalClaim(Enum):
123
- workspace_id = "workspace_id"
124
- environment_id = "environment_id"
125
- environment_name = "environment_name"
126
- app_id = "app_id"
127
- app_name = "app_name"
128
- function_id = "function_id"
129
- function_name = "function_name"
130
- container_id = "container_id"
131
-
132
- class ModalPrincipal(BaseModel):
133
- type: Literal["modal"] = "modal"
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
- def get(self, request: GetProject.Request) -> GetProject.Response:
148
- return self.client.put("/project/get", request, GetProject.Response)
142
+ """Service for project operations."""
149
143
 
150
- def create(self, request: CreateProject.Request) -> CreateProject.Response:
151
- return self.client.post("/project/create", request, CreateProject.Response)
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
- return self.client.paged("/project/list", ListProjects.Request(), ListProjects.Response)
155
-
156
- def list_grants(self, request: ListGrants.Request) -> Paged[Grant]:
157
- return self.client.paged("/project/list-grants", request, ListGrants.Response)
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 grant_role(self, request: GrantRole.Request) -> GrantRole.Response:
160
- return self.client.post("/project/grant-role", request, GrantRole.Response)
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)
@@ -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, Field
1
+ from pydantic import BaseModel
2
2
 
3
- from . import Paged, PagedRequest, PagedResponse, ProjectId, ServiceBase
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 CreateWorkload:
13
- class Request(BaseModel):
14
- project_id: str
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
- class Response(BaseModel):
18
- workload: Workload
17
+ class CreateWorkloadResponse(BaseModel):
18
+ workload: Workload
19
19
 
20
20
 
21
- class IssueToken:
22
- class Request(BaseModel):
23
- workload_id: str
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
- class ListWorkloads:
31
- class Request(PagedRequest):
32
- project_id: str
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
- class Response(PagedResponse[Workload]): ...
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
- class WorkloadService(ServiceBase):
38
- def create(self, request: CreateWorkload.Request) -> CreateWorkload.Response:
39
- return self.client.post("/workload/create", request, CreateWorkload.Response)
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 issue_token(self, request: IssueToken.Request) -> IssueToken.Response:
42
- return self.client.post("/workload/issue-token", request, IssueToken.Response)
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 list(self, request: ListWorkloads.Request) -> Paged[Workload]:
45
- return self.client.paged("/workload/list", request, ListWorkloads.Response)
50
+ def revoke_credentials(self, client_id: str) -> None:
51
+ """Revoke workload credentials."""
52
+ return self.client.delete(f"/v1/credentials/{client_id}", None)
@@ -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, Optional
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.admin import SyncMemberships, SyncOrgs
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(orgs: bool = False, memberships: bool = False):
11
- run_all = True
12
- if any([orgs, memberships]):
13
- run_all = False
10
+ def sync(
11
+ org_id: OrgId | None = None,
12
+ ):
13
+ state.settings.api._admin.sync_orgs()
14
14
 
15
- if run_all or orgs:
16
- for org_id in state.settings.api._admin.sync_orgs(SyncOrgs.Request()):
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 AsyncTyper, admin, console, fs, login, org, project, state, table, token, workload
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(org.app, name="org")
22
- app.add_typer(project.app, name="project")
23
- app.add_typer(table.app, name="table")
24
- app.add_typer(workload.app, name="workload")
25
- app.add_typer(token.app, name="token")
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
 
spiral/cli/console.py CHANGED
@@ -3,7 +3,7 @@ import subprocess
3
3
 
4
4
  from spiral import Spiral
5
5
  from spiral.adbc import ADBCFlightServer, SpiralADBCServer
6
- from spiral.api import wait_for_port
6
+ from spiral.server import wait_for_port
7
7
 
8
8
 
9
9
  def command():