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.
Files changed (98) hide show
  1. pyspiral-0.4.0.dist-info/METADATA +46 -0
  2. pyspiral-0.4.0.dist-info/RECORD +98 -0
  3. pyspiral-0.4.0.dist-info/WHEEL +4 -0
  4. pyspiral-0.4.0.dist-info/entry_points.txt +2 -0
  5. spiral/__init__.py +10 -0
  6. spiral/_lib.pypy310-pp73-darwin.so +0 -0
  7. spiral/adbc.py +393 -0
  8. spiral/api/__init__.py +64 -0
  9. spiral/api/admin.py +15 -0
  10. spiral/api/client.py +160 -0
  11. spiral/api/filesystems.py +153 -0
  12. spiral/api/organizations.py +77 -0
  13. spiral/api/projects.py +197 -0
  14. spiral/api/telemetry.py +19 -0
  15. spiral/api/types.py +20 -0
  16. spiral/api/workloads.py +52 -0
  17. spiral/arrow_.py +221 -0
  18. spiral/cli/__init__.py +79 -0
  19. spiral/cli/__main__.py +4 -0
  20. spiral/cli/admin.py +16 -0
  21. spiral/cli/app.py +65 -0
  22. spiral/cli/console.py +95 -0
  23. spiral/cli/fs.py +112 -0
  24. spiral/cli/iceberg/__init__.py +7 -0
  25. spiral/cli/iceberg/namespaces.py +47 -0
  26. spiral/cli/iceberg/tables.py +60 -0
  27. spiral/cli/indexes/__init__.py +19 -0
  28. spiral/cli/login.py +22 -0
  29. spiral/cli/orgs.py +90 -0
  30. spiral/cli/printer.py +53 -0
  31. spiral/cli/projects.py +136 -0
  32. spiral/cli/state.py +5 -0
  33. spiral/cli/tables/__init__.py +121 -0
  34. spiral/cli/telemetry.py +18 -0
  35. spiral/cli/types.py +51 -0
  36. spiral/cli/workloads.py +59 -0
  37. spiral/client.py +79 -0
  38. spiral/core/__init__.pyi +0 -0
  39. spiral/core/client/__init__.pyi +117 -0
  40. spiral/core/index/__init__.pyi +15 -0
  41. spiral/core/table/__init__.pyi +108 -0
  42. spiral/core/table/manifests/__init__.pyi +35 -0
  43. spiral/core/table/metastore/__init__.pyi +62 -0
  44. spiral/core/table/spec/__init__.pyi +214 -0
  45. spiral/datetime_.py +27 -0
  46. spiral/expressions/__init__.py +245 -0
  47. spiral/expressions/base.py +149 -0
  48. spiral/expressions/http.py +86 -0
  49. spiral/expressions/io.py +100 -0
  50. spiral/expressions/list_.py +68 -0
  51. spiral/expressions/mp4.py +62 -0
  52. spiral/expressions/png.py +18 -0
  53. spiral/expressions/qoi.py +18 -0
  54. spiral/expressions/refs.py +58 -0
  55. spiral/expressions/str_.py +39 -0
  56. spiral/expressions/struct.py +59 -0
  57. spiral/expressions/text.py +62 -0
  58. spiral/expressions/tiff.py +223 -0
  59. spiral/expressions/udf.py +46 -0
  60. spiral/grpc_.py +32 -0
  61. spiral/iceberg/__init__.py +3 -0
  62. spiral/iceberg/client.py +33 -0
  63. spiral/indexes/__init__.py +5 -0
  64. spiral/indexes/client.py +137 -0
  65. spiral/indexes/index.py +34 -0
  66. spiral/indexes/scan.py +22 -0
  67. spiral/project.py +46 -0
  68. spiral/protogen/_/__init__.py +0 -0
  69. spiral/protogen/_/arrow/__init__.py +0 -0
  70. spiral/protogen/_/arrow/flight/__init__.py +0 -0
  71. spiral/protogen/_/arrow/flight/protocol/__init__.py +0 -0
  72. spiral/protogen/_/arrow/flight/protocol/sql/__init__.py +1990 -0
  73. spiral/protogen/_/scandal/__init__.py +178 -0
  74. spiral/protogen/_/spiral/__init__.py +0 -0
  75. spiral/protogen/_/spiral/table/__init__.py +22 -0
  76. spiral/protogen/_/substrait/__init__.py +3399 -0
  77. spiral/protogen/_/substrait/extensions/__init__.py +115 -0
  78. spiral/protogen/__init__.py +0 -0
  79. spiral/protogen/substrait/__init__.py +3399 -0
  80. spiral/protogen/substrait/extensions/__init__.py +115 -0
  81. spiral/protogen/util.py +41 -0
  82. spiral/py.typed +0 -0
  83. spiral/server.py +17 -0
  84. spiral/settings.py +101 -0
  85. spiral/substrait_.py +279 -0
  86. spiral/tables/__init__.py +12 -0
  87. spiral/tables/client.py +130 -0
  88. spiral/tables/dataset.py +250 -0
  89. spiral/tables/debug/__init__.py +0 -0
  90. spiral/tables/debug/manifests.py +70 -0
  91. spiral/tables/debug/metrics.py +56 -0
  92. spiral/tables/debug/scan.py +248 -0
  93. spiral/tables/maintenance.py +12 -0
  94. spiral/tables/scan.py +193 -0
  95. spiral/tables/snapshot.py +78 -0
  96. spiral/tables/table.py +157 -0
  97. spiral/tables/transaction.py +52 -0
  98. 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)
@@ -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_-]*$")]
@@ -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)