pyspiral 0.4.0__pp310-pypy310_pp73-macosx_10_12_x86_64.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.4.0.dist-info/METADATA +46 -0
- pyspiral-0.4.0.dist-info/RECORD +98 -0
- pyspiral-0.4.0.dist-info/WHEEL +4 -0
- pyspiral-0.4.0.dist-info/entry_points.txt +2 -0
- spiral/__init__.py +10 -0
- spiral/_lib.pypy310-pp73-darwin.so +0 -0
- spiral/adbc.py +393 -0
- spiral/api/__init__.py +64 -0
- spiral/api/admin.py +15 -0
- spiral/api/client.py +160 -0
- spiral/api/filesystems.py +153 -0
- spiral/api/organizations.py +77 -0
- spiral/api/projects.py +197 -0
- spiral/api/telemetry.py +19 -0
- spiral/api/types.py +20 -0
- spiral/api/workloads.py +52 -0
- spiral/arrow_.py +221 -0
- spiral/cli/__init__.py +79 -0
- spiral/cli/__main__.py +4 -0
- spiral/cli/admin.py +16 -0
- spiral/cli/app.py +65 -0
- spiral/cli/console.py +95 -0
- spiral/cli/fs.py +112 -0
- 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 +22 -0
- spiral/cli/orgs.py +90 -0
- spiral/cli/printer.py +53 -0
- spiral/cli/projects.py +136 -0
- spiral/cli/state.py +5 -0
- spiral/cli/tables/__init__.py +121 -0
- spiral/cli/telemetry.py +18 -0
- spiral/cli/types.py +51 -0
- spiral/cli/workloads.py +59 -0
- spiral/client.py +79 -0
- spiral/core/__init__.pyi +0 -0
- spiral/core/client/__init__.pyi +117 -0
- spiral/core/index/__init__.pyi +15 -0
- spiral/core/table/__init__.pyi +108 -0
- spiral/core/table/manifests/__init__.pyi +35 -0
- spiral/core/table/metastore/__init__.pyi +62 -0
- spiral/core/table/spec/__init__.pyi +214 -0
- spiral/datetime_.py +27 -0
- spiral/expressions/__init__.py +245 -0
- spiral/expressions/base.py +149 -0
- spiral/expressions/http.py +86 -0
- spiral/expressions/io.py +100 -0
- spiral/expressions/list_.py +68 -0
- spiral/expressions/mp4.py +62 -0
- spiral/expressions/png.py +18 -0
- spiral/expressions/qoi.py +18 -0
- spiral/expressions/refs.py +58 -0
- spiral/expressions/str_.py +39 -0
- spiral/expressions/struct.py +59 -0
- spiral/expressions/text.py +62 -0
- spiral/expressions/tiff.py +223 -0
- spiral/expressions/udf.py +46 -0
- spiral/grpc_.py +32 -0
- 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 +46 -0
- spiral/protogen/_/__init__.py +0 -0
- spiral/protogen/_/arrow/__init__.py +0 -0
- spiral/protogen/_/arrow/flight/__init__.py +0 -0
- spiral/protogen/_/arrow/flight/protocol/__init__.py +0 -0
- spiral/protogen/_/arrow/flight/protocol/sql/__init__.py +1990 -0
- spiral/protogen/_/scandal/__init__.py +178 -0
- spiral/protogen/_/spiral/__init__.py +0 -0
- spiral/protogen/_/spiral/table/__init__.py +22 -0
- spiral/protogen/_/substrait/__init__.py +3399 -0
- spiral/protogen/_/substrait/extensions/__init__.py +115 -0
- spiral/protogen/__init__.py +0 -0
- spiral/protogen/substrait/__init__.py +3399 -0
- spiral/protogen/substrait/extensions/__init__.py +115 -0
- spiral/protogen/util.py +41 -0
- spiral/py.typed +0 -0
- spiral/server.py +17 -0
- spiral/settings.py +101 -0
- spiral/substrait_.py +279 -0
- spiral/tables/__init__.py +12 -0
- spiral/tables/client.py +130 -0
- spiral/tables/dataset.py +250 -0
- spiral/tables/debug/__init__.py +0 -0
- spiral/tables/debug/manifests.py +70 -0
- spiral/tables/debug/metrics.py +56 -0
- spiral/tables/debug/scan.py +248 -0
- spiral/tables/maintenance.py +12 -0
- spiral/tables/scan.py +193 -0
- spiral/tables/snapshot.py +78 -0
- spiral/tables/table.py +157 -0
- spiral/tables/transaction.py +52 -0
- spiral/types_.py +6 -0
spiral/api/client.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
import logging
|
2
|
+
from collections.abc import Iterable, Iterator, Mapping
|
3
|
+
from typing import Any, Generic, TypeVar
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
from httpx import HTTPStatusError
|
7
|
+
from pydantic import BaseModel, Field, TypeAdapter
|
8
|
+
|
9
|
+
from spiral.core.client import Authn
|
10
|
+
|
11
|
+
log = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
E = TypeVar("E")
|
15
|
+
|
16
|
+
|
17
|
+
class PagedRequest(BaseModel):
|
18
|
+
page_token: str | None = None
|
19
|
+
page_size: int = 50
|
20
|
+
|
21
|
+
|
22
|
+
class PagedResponse(BaseModel, Generic[E]):
|
23
|
+
items: list[E] = Field(default_factory=list)
|
24
|
+
next_page_token: str | None = None
|
25
|
+
|
26
|
+
|
27
|
+
PagedReqT = TypeVar("PagedReqT", bound=PagedRequest)
|
28
|
+
|
29
|
+
|
30
|
+
class Paged(Iterable[E], Generic[E]):
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
client: "_Client",
|
34
|
+
path: str,
|
35
|
+
page_token: str | None,
|
36
|
+
page_size: int,
|
37
|
+
response_cls: type[PagedResponse[E]],
|
38
|
+
params: Mapping[str, str] | None = None,
|
39
|
+
):
|
40
|
+
self._client = client
|
41
|
+
self._path = path
|
42
|
+
self._response_cls = response_cls
|
43
|
+
|
44
|
+
self._page_size = page_size
|
45
|
+
|
46
|
+
self._params = params or {}
|
47
|
+
if page_token is not None:
|
48
|
+
self._params["page_token"] = str(page_token)
|
49
|
+
# TODO(marko): Support paging.
|
50
|
+
# if page_size is not None:
|
51
|
+
# self._params["page_size"] = str(page_size)
|
52
|
+
|
53
|
+
self._response: PagedResponse[E] = client.get(path, response_cls, params=self._params)
|
54
|
+
|
55
|
+
@property
|
56
|
+
def page(self) -> PagedResponse[E]:
|
57
|
+
return self._response
|
58
|
+
|
59
|
+
def __iter__(self) -> Iterator[E]:
|
60
|
+
while True:
|
61
|
+
yield from self._response.items
|
62
|
+
|
63
|
+
if self._response.next_page_token is None:
|
64
|
+
break
|
65
|
+
|
66
|
+
params = self._params.copy()
|
67
|
+
params["page_token"] = self._response.next_page_token
|
68
|
+
self._response = self._client.get(self._path, self._response_cls, params=params)
|
69
|
+
|
70
|
+
|
71
|
+
class ServiceBase:
|
72
|
+
def __init__(self, client: "_Client"):
|
73
|
+
self.client = client
|
74
|
+
|
75
|
+
|
76
|
+
class SpiralHTTPError(Exception):
|
77
|
+
def __init__(self, body: str, code: int):
|
78
|
+
super().__init__(body)
|
79
|
+
self.body = body
|
80
|
+
self.code = code
|
81
|
+
|
82
|
+
|
83
|
+
class _Client:
|
84
|
+
RequestT = TypeVar("RequestT")
|
85
|
+
ResponseT = TypeVar("ResponseT")
|
86
|
+
|
87
|
+
def __init__(self, http: httpx.Client, authn: Authn):
|
88
|
+
self.http = http
|
89
|
+
self.authn = authn
|
90
|
+
|
91
|
+
def get(
|
92
|
+
self, path: str, response_cls: type[ResponseT], *, params: Mapping[str, str | list[str]] | None = None
|
93
|
+
) -> ResponseT:
|
94
|
+
return self.request("GET", path, None, response_cls, params=params)
|
95
|
+
|
96
|
+
def post(
|
97
|
+
self,
|
98
|
+
path: str,
|
99
|
+
req: RequestT,
|
100
|
+
response_cls: type[ResponseT],
|
101
|
+
*,
|
102
|
+
params: Mapping[str, str | list[str]] | None = None,
|
103
|
+
) -> ResponseT:
|
104
|
+
return self.request("POST", path, req, response_cls, params=params)
|
105
|
+
|
106
|
+
def put(
|
107
|
+
self,
|
108
|
+
path: str,
|
109
|
+
req: RequestT,
|
110
|
+
response_cls: type[ResponseT],
|
111
|
+
*,
|
112
|
+
params: Mapping[str, str | list[str]] | None = None,
|
113
|
+
) -> ResponseT:
|
114
|
+
return self.request("PUT", path, req, response_cls, params=params)
|
115
|
+
|
116
|
+
def delete(
|
117
|
+
self, path: str, response_cls: type[ResponseT], *, params: Mapping[str, str | list[str]] | None = None
|
118
|
+
) -> ResponseT:
|
119
|
+
return self.request("DELETE", path, None, response_cls, params=params)
|
120
|
+
|
121
|
+
def request(
|
122
|
+
self,
|
123
|
+
method: str,
|
124
|
+
path: str,
|
125
|
+
req: RequestT | None,
|
126
|
+
response_cls: type[ResponseT],
|
127
|
+
*,
|
128
|
+
params: Mapping[str, str | list[str]] | None = None,
|
129
|
+
) -> ResponseT:
|
130
|
+
req_data: dict[str, Any] = {}
|
131
|
+
if req is not None:
|
132
|
+
req_data = dict(json=TypeAdapter(req.__class__).dump_python(req, mode="json"))
|
133
|
+
|
134
|
+
token = self.authn.token()
|
135
|
+
resp = self.http.request(
|
136
|
+
method,
|
137
|
+
path,
|
138
|
+
params=params or {},
|
139
|
+
headers={"authorization": f"Bearer {token.expose_secret()}"} if token else None,
|
140
|
+
**req_data,
|
141
|
+
)
|
142
|
+
|
143
|
+
try:
|
144
|
+
resp.raise_for_status()
|
145
|
+
except HTTPStatusError as e:
|
146
|
+
# Enrich the exception with the response body
|
147
|
+
raise SpiralHTTPError(body=resp.text, code=resp.status_code) from e
|
148
|
+
|
149
|
+
return TypeAdapter(response_cls).validate_python(resp.json())
|
150
|
+
|
151
|
+
def paged(
|
152
|
+
self,
|
153
|
+
path: str,
|
154
|
+
response_cls: type[PagedResponse[E]],
|
155
|
+
*,
|
156
|
+
page_token: str | None = None,
|
157
|
+
page_size: int = 50,
|
158
|
+
params: Mapping[str, str] | None = None,
|
159
|
+
) -> Paged[E]:
|
160
|
+
return Paged(self, path, page_token, page_size, response_cls, params)
|
@@ -0,0 +1,153 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Annotated, Literal
|
3
|
+
|
4
|
+
from pydantic import AfterValidator, BaseModel, Field
|
5
|
+
|
6
|
+
from .client import Paged, PagedResponse, ServiceBase
|
7
|
+
from .types import ProjectId
|
8
|
+
|
9
|
+
|
10
|
+
def _validate_directory_path(path: str) -> str:
|
11
|
+
if not path.startswith("/"):
|
12
|
+
raise ValueError("Directory path must start with a slash.")
|
13
|
+
if not path.endswith("/"):
|
14
|
+
raise ValueError("Directory path must not end with a slash.")
|
15
|
+
return path
|
16
|
+
|
17
|
+
|
18
|
+
DirectoryPath = Annotated[str, AfterValidator(_validate_directory_path)]
|
19
|
+
FilePath = str # Path or directory
|
20
|
+
|
21
|
+
|
22
|
+
class BuiltinFileSystem(BaseModel):
|
23
|
+
"""Spiral supports several builtin file systems in different cloud provider regions."""
|
24
|
+
|
25
|
+
type: Literal["builtin"] = "builtin"
|
26
|
+
provider: str
|
27
|
+
|
28
|
+
|
29
|
+
class UpstreamFileSystem(BaseModel):
|
30
|
+
"""File system that points to another project, usually a "file system" project.
|
31
|
+
|
32
|
+
Upstream project must have an external file system configured,
|
33
|
+
and not a builtin file system or another upstream file system.
|
34
|
+
"""
|
35
|
+
|
36
|
+
type: Literal["upstream"] = "upstream"
|
37
|
+
project_id: ProjectId
|
38
|
+
|
39
|
+
|
40
|
+
class S3FileSystem(BaseModel):
|
41
|
+
"""File system backed by an S3-compatible bucket."""
|
42
|
+
|
43
|
+
type: Literal["s3"] = "s3"
|
44
|
+
endpoint: str = "https://s3.amazonaws.com"
|
45
|
+
region: str = "auto"
|
46
|
+
bucket: str
|
47
|
+
directory: DirectoryPath | None
|
48
|
+
|
49
|
+
|
50
|
+
class GCSFileSystem(BaseModel):
|
51
|
+
"""File system backed by a Google Cloud Storage bucket."""
|
52
|
+
|
53
|
+
type: Literal["gcs"] = "gcs"
|
54
|
+
region: str
|
55
|
+
bucket: str
|
56
|
+
directory: DirectoryPath | None
|
57
|
+
|
58
|
+
|
59
|
+
FileSystem = Annotated[
|
60
|
+
BuiltinFileSystem | UpstreamFileSystem | S3FileSystem | GCSFileSystem, Field(discriminator="type")
|
61
|
+
]
|
62
|
+
|
63
|
+
|
64
|
+
class Mode(str, Enum):
|
65
|
+
READ_ONLY = "ro"
|
66
|
+
READ_WRITE = "rw"
|
67
|
+
|
68
|
+
|
69
|
+
class Mount(BaseModel):
|
70
|
+
"""Mount grants permission to a Spiral resource to use a specific directory within the file system."""
|
71
|
+
|
72
|
+
id: str
|
73
|
+
project_id: ProjectId
|
74
|
+
directory: DirectoryPath
|
75
|
+
mode: Mode
|
76
|
+
principal: str
|
77
|
+
|
78
|
+
|
79
|
+
class AWSSecretAccessKey(BaseModel):
|
80
|
+
"""AWS secret access key credentials to be used with an S3 file system.
|
81
|
+
The access key must have read/write access to the bucket specified in the file system.
|
82
|
+
"""
|
83
|
+
|
84
|
+
access_key_id: str
|
85
|
+
secret_access_key: str
|
86
|
+
|
87
|
+
|
88
|
+
class UpdateS3FileSystem(S3FileSystem):
|
89
|
+
credentials: AWSSecretAccessKey
|
90
|
+
|
91
|
+
|
92
|
+
class GCPServiceAccount(BaseModel):
|
93
|
+
"""Google Cloud Platform service account credentials to be used with a GCS file system.
|
94
|
+
The service account must have read/write access to the bucket specified in the file system.
|
95
|
+
"""
|
96
|
+
|
97
|
+
service_account: str
|
98
|
+
|
99
|
+
|
100
|
+
class UpdateGCSFileSystem(GCSFileSystem):
|
101
|
+
credentials: GCPServiceAccount
|
102
|
+
|
103
|
+
|
104
|
+
UpdateFileSystemRequest = Annotated[
|
105
|
+
BuiltinFileSystem | UpstreamFileSystem | UpdateS3FileSystem | UpdateGCSFileSystem, Field(discriminator="type")
|
106
|
+
]
|
107
|
+
|
108
|
+
|
109
|
+
class UpdateFileSystemResponse(BaseModel):
|
110
|
+
file_system: FileSystem
|
111
|
+
|
112
|
+
|
113
|
+
class CreateMountRequest(BaseModel):
|
114
|
+
directory: DirectoryPath
|
115
|
+
mode: Mode
|
116
|
+
principal: str
|
117
|
+
|
118
|
+
|
119
|
+
class CreateMountResponse(BaseModel):
|
120
|
+
mount: Mount
|
121
|
+
|
122
|
+
|
123
|
+
class FileSystemService(ServiceBase):
|
124
|
+
"""Service for file system operations."""
|
125
|
+
|
126
|
+
def list_providers(self) -> list[str]:
|
127
|
+
"""List builtin providers."""
|
128
|
+
response = self.client.get("/v1/file-systems/builtin-providers", dict)
|
129
|
+
return response.get("providers", [])
|
130
|
+
|
131
|
+
def update_file_system(self, project_id: ProjectId, request: UpdateFileSystemRequest) -> UpdateFileSystemResponse:
|
132
|
+
"""Update project's default file system."""
|
133
|
+
return self.client.post(f"/v1/file-systems/{project_id}", request, UpdateFileSystemResponse)
|
134
|
+
|
135
|
+
def get_file_system(self, project_id: ProjectId) -> FileSystem:
|
136
|
+
"""Get project's default file system."""
|
137
|
+
return self.client.get(f"/v1/file-systems/{project_id}", FileSystem)
|
138
|
+
|
139
|
+
def create_mount(self, project_id: ProjectId, request: CreateMountRequest) -> CreateMountResponse:
|
140
|
+
"""Create a mount."""
|
141
|
+
return self.client.post(f"/v1/file-systems/{project_id}/mounts", request, CreateMountResponse)
|
142
|
+
|
143
|
+
def list_mounts(self, project_id: ProjectId) -> Paged[Mount]:
|
144
|
+
"""List active mounts in project's file system."""
|
145
|
+
return self.client.paged(f"/v1/file-systems/{project_id}/mounts", PagedResponse[Mount])
|
146
|
+
|
147
|
+
def get_mount(self, mount_id: str) -> Mount:
|
148
|
+
"""Get a mount."""
|
149
|
+
return self.client.get(f"/v1/mounts/{mount_id}", Mount)
|
150
|
+
|
151
|
+
def remove_mount(self, mount_id: str) -> None:
|
152
|
+
"""Remove mount."""
|
153
|
+
return self.client.delete(f"/v1/mounts/{mount_id}", None)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from .client import Paged, PagedResponse, ServiceBase
|
6
|
+
from .types import OrgId
|
7
|
+
|
8
|
+
|
9
|
+
class OrgRole(str, Enum):
|
10
|
+
OWNER = "owner"
|
11
|
+
MEMBER = "member"
|
12
|
+
GUEST = "guest"
|
13
|
+
|
14
|
+
|
15
|
+
class Org(BaseModel):
|
16
|
+
id: OrgId
|
17
|
+
name: str | None = None
|
18
|
+
|
19
|
+
|
20
|
+
class OrgMembership(BaseModel):
|
21
|
+
user_id: str
|
22
|
+
org: Org
|
23
|
+
role: str
|
24
|
+
|
25
|
+
|
26
|
+
class CreateOrgRequest(BaseModel):
|
27
|
+
name: str | None = None
|
28
|
+
|
29
|
+
|
30
|
+
class CreateOrgResponse(BaseModel):
|
31
|
+
org: Org
|
32
|
+
|
33
|
+
|
34
|
+
class PortalLinkIntent(str, Enum):
|
35
|
+
SSO = "sso"
|
36
|
+
DIRECTORY_SYNC = "directory-sync"
|
37
|
+
AUDIT_LOGS = "audit-logs"
|
38
|
+
LOG_STREAMS = "log-streams"
|
39
|
+
DOMAIN_VERIFICATION = "domain-verification"
|
40
|
+
|
41
|
+
|
42
|
+
class PortalLinkRequest(BaseModel):
|
43
|
+
intent: PortalLinkIntent
|
44
|
+
|
45
|
+
|
46
|
+
class PortalLinkResponse(BaseModel):
|
47
|
+
url: str
|
48
|
+
|
49
|
+
|
50
|
+
class InviteUserRequest(BaseModel):
|
51
|
+
email: str
|
52
|
+
role: OrgRole
|
53
|
+
expires_in_days: int | None = 7
|
54
|
+
|
55
|
+
|
56
|
+
class InviteUserResponse(BaseModel):
|
57
|
+
invite_id: str
|
58
|
+
|
59
|
+
|
60
|
+
class OrganizationService(ServiceBase):
|
61
|
+
"""Service for organization operations."""
|
62
|
+
|
63
|
+
def create(self, request: CreateOrgRequest) -> CreateOrgResponse:
|
64
|
+
"""Create a new organization."""
|
65
|
+
return self.client.post("/v1/organizations", request, CreateOrgResponse)
|
66
|
+
|
67
|
+
def list_memberships(self) -> Paged[OrgMembership]:
|
68
|
+
"""List organization memberships."""
|
69
|
+
return self.client.paged("/v1/organizations", PagedResponse[OrgMembership])
|
70
|
+
|
71
|
+
def invite_user(self, request: InviteUserRequest) -> InviteUserResponse:
|
72
|
+
"""Invite a user to the organization."""
|
73
|
+
return self.client.post("/v1/organizations/invite-user", request, InviteUserResponse)
|
74
|
+
|
75
|
+
def portal_link(self, request: PortalLinkRequest) -> PortalLinkResponse:
|
76
|
+
"""Get configuration portal link for the organization."""
|
77
|
+
return self.client.put("/v1/organizations/portal-link", request, PortalLinkResponse)
|
spiral/api/projects.py
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
from typing import Annotated, Literal
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from .client import Paged, PagedResponse, ServiceBase
|
6
|
+
from .types import OrgId, ProjectId, RoleId
|
7
|
+
|
8
|
+
|
9
|
+
class Project(BaseModel):
|
10
|
+
id: ProjectId
|
11
|
+
org_id: OrgId
|
12
|
+
name: str | None = None
|
13
|
+
|
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
|
+
|
24
|
+
class Grant(BaseModel):
|
25
|
+
id: str
|
26
|
+
project_id: ProjectId
|
27
|
+
role_id: RoleId
|
28
|
+
principal: str
|
29
|
+
conditions: dict | None = None
|
30
|
+
|
31
|
+
|
32
|
+
class OrgRolePrincipalConditions(BaseModel):
|
33
|
+
type: Literal["org_role"] = "org_role"
|
34
|
+
org_id: OrgId
|
35
|
+
role: str
|
36
|
+
|
37
|
+
|
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
|
139
|
+
|
140
|
+
|
141
|
+
class ProjectService(ServiceBase):
|
142
|
+
"""Service for project operations."""
|
143
|
+
|
144
|
+
def create(self, request: CreateProjectRequest) -> CreateProjectResponse:
|
145
|
+
"""Create a new project."""
|
146
|
+
return self.client.post("/v1/projects", request, CreateProjectResponse)
|
147
|
+
|
148
|
+
def list(self) -> Paged[Project]:
|
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
|
+
)
|
171
|
+
|
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
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
from .client import Paged, PagedResponse, ServiceBase
|
4
|
+
from .types import ProjectId
|
5
|
+
|
6
|
+
|
7
|
+
class Workload(BaseModel):
|
8
|
+
id: str
|
9
|
+
project_id: ProjectId
|
10
|
+
name: str | None = None
|
11
|
+
|
12
|
+
|
13
|
+
class CreateWorkloadRequest(BaseModel):
|
14
|
+
name: str | None = None
|
15
|
+
|
16
|
+
|
17
|
+
class CreateWorkloadResponse(BaseModel):
|
18
|
+
workload: Workload
|
19
|
+
|
20
|
+
|
21
|
+
class IssueWorkloadCredentialsResponse(BaseModel):
|
22
|
+
client_id: str
|
23
|
+
client_secret: str
|
24
|
+
revoked_client_id: str | None = None
|
25
|
+
|
26
|
+
|
27
|
+
class WorkloadService(ServiceBase):
|
28
|
+
"""Service for workload operations."""
|
29
|
+
|
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
|
+
|
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])
|
37
|
+
|
38
|
+
def get(self, workload_id: str) -> Workload:
|
39
|
+
"""Get a workload."""
|
40
|
+
return self.client.get(f"/v1/workloads/{workload_id}", Workload)
|
41
|
+
|
42
|
+
def deactivate(self, workload_id: str) -> None:
|
43
|
+
"""De-activate a workload."""
|
44
|
+
return self.client.delete(f"/v1/workloads/{workload_id}", None)
|
45
|
+
|
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)
|
49
|
+
|
50
|
+
def revoke_credentials(self, client_id: str) -> None:
|
51
|
+
"""Revoke workload credentials."""
|
52
|
+
return self.client.delete(f"/v1/credentials/{client_id}", None)
|