pyspiral 0.8.9__cp311-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.8.9.dist-info/METADATA +53 -0
  2. pyspiral-0.8.9.dist-info/RECORD +114 -0
  3. pyspiral-0.8.9.dist-info/WHEEL +4 -0
  4. pyspiral-0.8.9.dist-info/entry_points.txt +3 -0
  5. spiral/__init__.py +55 -0
  6. spiral/_lib.abi3.so +0 -0
  7. spiral/adbc.py +411 -0
  8. spiral/api/__init__.py +78 -0
  9. spiral/api/admin.py +15 -0
  10. spiral/api/client.py +165 -0
  11. spiral/api/filesystems.py +152 -0
  12. spiral/api/key_space_indexes.py +23 -0
  13. spiral/api/organizations.py +78 -0
  14. spiral/api/projects.py +219 -0
  15. spiral/api/telemetry.py +19 -0
  16. spiral/api/text_indexes.py +56 -0
  17. spiral/api/types.py +23 -0
  18. spiral/api/workers.py +40 -0
  19. spiral/api/workloads.py +52 -0
  20. spiral/arrow_.py +202 -0
  21. spiral/cli/__init__.py +89 -0
  22. spiral/cli/__main__.py +4 -0
  23. spiral/cli/admin.py +33 -0
  24. spiral/cli/app.py +108 -0
  25. spiral/cli/console.py +95 -0
  26. spiral/cli/fs.py +109 -0
  27. spiral/cli/iceberg.py +97 -0
  28. spiral/cli/key_spaces.py +103 -0
  29. spiral/cli/login.py +25 -0
  30. spiral/cli/orgs.py +81 -0
  31. spiral/cli/printer.py +53 -0
  32. spiral/cli/projects.py +148 -0
  33. spiral/cli/state.py +7 -0
  34. spiral/cli/tables.py +225 -0
  35. spiral/cli/telemetry.py +17 -0
  36. spiral/cli/text.py +115 -0
  37. spiral/cli/types.py +50 -0
  38. spiral/cli/workloads.py +86 -0
  39. spiral/client.py +279 -0
  40. spiral/core/__init__.pyi +0 -0
  41. spiral/core/_tools/__init__.pyi +5 -0
  42. spiral/core/authn/__init__.pyi +21 -0
  43. spiral/core/client/__init__.pyi +270 -0
  44. spiral/core/config/__init__.pyi +35 -0
  45. spiral/core/expr/__init__.pyi +15 -0
  46. spiral/core/expr/images/__init__.pyi +3 -0
  47. spiral/core/expr/list_/__init__.pyi +4 -0
  48. spiral/core/expr/pushdown/__init__.pyi +3 -0
  49. spiral/core/expr/refs/__init__.pyi +4 -0
  50. spiral/core/expr/s3/__init__.pyi +3 -0
  51. spiral/core/expr/str_/__init__.pyi +3 -0
  52. spiral/core/expr/struct_/__init__.pyi +6 -0
  53. spiral/core/expr/text/__init__.pyi +5 -0
  54. spiral/core/expr/udf/__init__.pyi +14 -0
  55. spiral/core/expr/video/__init__.pyi +3 -0
  56. spiral/core/table/__init__.pyi +142 -0
  57. spiral/core/table/manifests/__init__.pyi +35 -0
  58. spiral/core/table/metastore/__init__.pyi +58 -0
  59. spiral/core/table/spec/__init__.pyi +214 -0
  60. spiral/dataloader.py +310 -0
  61. spiral/dataset.py +264 -0
  62. spiral/datetime_.py +27 -0
  63. spiral/debug/__init__.py +0 -0
  64. spiral/debug/manifests.py +103 -0
  65. spiral/debug/metrics.py +56 -0
  66. spiral/debug/scan.py +266 -0
  67. spiral/demo.py +100 -0
  68. spiral/enrichment.py +290 -0
  69. spiral/expressions/__init__.py +274 -0
  70. spiral/expressions/base.py +186 -0
  71. spiral/expressions/file.py +17 -0
  72. spiral/expressions/http.py +17 -0
  73. spiral/expressions/list_.py +77 -0
  74. spiral/expressions/pushdown.py +12 -0
  75. spiral/expressions/s3.py +16 -0
  76. spiral/expressions/str_.py +39 -0
  77. spiral/expressions/struct.py +59 -0
  78. spiral/expressions/text.py +62 -0
  79. spiral/expressions/tiff.py +225 -0
  80. spiral/expressions/udf.py +66 -0
  81. spiral/grpc_.py +32 -0
  82. spiral/iceberg.py +31 -0
  83. spiral/iterable_dataset.py +106 -0
  84. spiral/key_space_index.py +44 -0
  85. spiral/project.py +247 -0
  86. spiral/protogen/_/__init__.py +0 -0
  87. spiral/protogen/_/arrow/__init__.py +0 -0
  88. spiral/protogen/_/arrow/flight/__init__.py +0 -0
  89. spiral/protogen/_/arrow/flight/protocol/__init__.py +0 -0
  90. spiral/protogen/_/arrow/flight/protocol/sql/__init__.py +2548 -0
  91. spiral/protogen/_/google/__init__.py +0 -0
  92. spiral/protogen/_/google/protobuf/__init__.py +2310 -0
  93. spiral/protogen/_/message_pool.py +3 -0
  94. spiral/protogen/_/py.typed +0 -0
  95. spiral/protogen/_/scandal/__init__.py +190 -0
  96. spiral/protogen/_/spfs/__init__.py +72 -0
  97. spiral/protogen/_/spql/__init__.py +61 -0
  98. spiral/protogen/_/substrait/__init__.py +6196 -0
  99. spiral/protogen/_/substrait/extensions/__init__.py +169 -0
  100. spiral/protogen/__init__.py +0 -0
  101. spiral/protogen/util.py +41 -0
  102. spiral/py.typed +0 -0
  103. spiral/scan.py +383 -0
  104. spiral/server.py +37 -0
  105. spiral/settings.py +36 -0
  106. spiral/snapshot.py +61 -0
  107. spiral/streaming_/__init__.py +3 -0
  108. spiral/streaming_/reader.py +133 -0
  109. spiral/streaming_/stream.py +156 -0
  110. spiral/substrait_.py +274 -0
  111. spiral/table.py +216 -0
  112. spiral/text_index.py +17 -0
  113. spiral/transaction.py +156 -0
  114. spiral/types_.py +6 -0
spiral/api/__init__.py ADDED
@@ -0,0 +1,78 @@
1
+ import os
2
+ from typing import TYPE_CHECKING
3
+
4
+ import httpx
5
+
6
+ from .client import _Client
7
+
8
+ if TYPE_CHECKING:
9
+ from spiral.core.authn import Authn
10
+
11
+ from .admin import AdminService
12
+ from .filesystems import FileSystemsService
13
+ from .key_space_indexes import KeySpaceIndexesService
14
+ from .organizations import OrganizationsService
15
+ from .projects import ProjectsService
16
+ from .telemetry import TelemetryService
17
+ from .text_indexes import TextIndexesService
18
+ from .workloads import WorkloadsService
19
+
20
+
21
+ class SpiralAPI:
22
+ def __init__(self, base_url: str, authn: "Authn"):
23
+ self.base_url = base_url
24
+ self.client = _Client(
25
+ httpx.Client(
26
+ base_url=self.base_url,
27
+ timeout=None if "PYTEST_VERSION" in os.environ else 60,
28
+ ),
29
+ authn,
30
+ )
31
+
32
+ @property
33
+ def _admin(self) -> "AdminService":
34
+ from .admin import AdminService
35
+
36
+ return AdminService(self.client)
37
+
38
+ @property
39
+ def organizations(self) -> "OrganizationsService":
40
+ from .organizations import OrganizationsService
41
+
42
+ return OrganizationsService(self.client)
43
+
44
+ @property
45
+ def projects(self) -> "ProjectsService":
46
+ from .projects import ProjectsService
47
+
48
+ return ProjectsService(self.client)
49
+
50
+ @property
51
+ def file_systems(self) -> "FileSystemsService":
52
+ from .filesystems import FileSystemsService
53
+
54
+ return FileSystemsService(self.client)
55
+
56
+ @property
57
+ def workloads(self) -> "WorkloadsService":
58
+ from .workloads import WorkloadsService
59
+
60
+ return WorkloadsService(self.client)
61
+
62
+ @property
63
+ def text_indexes(self) -> "TextIndexesService":
64
+ from .text_indexes import TextIndexesService
65
+
66
+ return TextIndexesService(self.client)
67
+
68
+ @property
69
+ def key_space_indexes(self) -> "KeySpaceIndexesService":
70
+ from .key_space_indexes import KeySpaceIndexesService
71
+
72
+ return KeySpaceIndexesService(self.client)
73
+
74
+ @property
75
+ def telemetry(self) -> "TelemetryService":
76
+ from .telemetry import TelemetryService
77
+
78
+ return TelemetryService(self.client)
spiral/api/admin.py ADDED
@@ -0,0 +1,15 @@
1
+ from .client import Paged, PagedResponse, ServiceBase
2
+ from .organizations import OrgMembership
3
+ from .types import OrgId
4
+
5
+
6
+ class AdminService(ServiceBase):
7
+ def sync_memberships(self, org_id: OrgId | None = None) -> Paged[OrgMembership]:
8
+ params = {}
9
+ if org_id:
10
+ params["org_id"] = str(org_id)
11
+ return self.client.paged("/v1/admin/sync-memberships", PagedResponse[OrgMembership], params=params)
12
+
13
+ def sync_orgs(self) -> Paged[OrgId]:
14
+ params = {}
15
+ return self.client.paged("/v1/admin/sync-orgs", PagedResponse[OrgId], params=params)
spiral/api/client.py ADDED
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Iterable, Iterator, Mapping
5
+ from typing import Any, Generic, TypeVar
6
+
7
+ import httpx
8
+ from httpx import HTTPStatusError
9
+ from pydantic import BaseModel, Field, TypeAdapter
10
+
11
+ from spiral.core.authn import Authn
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ E = TypeVar("E")
17
+
18
+
19
+ class PagedRequest(BaseModel):
20
+ page_token: str | None = None
21
+ page_size: int = 50
22
+
23
+
24
+ class PagedResponse(BaseModel, Generic[E]):
25
+ items: list[E] = Field(default_factory=list)
26
+ next_page_token: str | None = None
27
+
28
+
29
+ PagedReqT = TypeVar("PagedReqT", bound=PagedRequest)
30
+
31
+
32
+ class Paged(Iterable[E], Generic[E]):
33
+ def __init__(
34
+ self,
35
+ client: _Client,
36
+ path: str,
37
+ page_token: str | None,
38
+ page_size: int,
39
+ response_cls: type[PagedResponse[E]],
40
+ params: Mapping[str, str] | None = None,
41
+ ):
42
+ self._client = client
43
+ self._path = path
44
+ self._response_cls = response_cls
45
+
46
+ self._page_size = page_size
47
+
48
+ self._params = params or {}
49
+ if page_token is not None:
50
+ self._params["page_token"] = str(page_token)
51
+ # TODO(marko): Support paging.
52
+ # if page_size is not None:
53
+ # self._params["page_size"] = str(page_size)
54
+
55
+ self._response: PagedResponse[E] = client.get(path, response_cls, params=self._params)
56
+
57
+ @property
58
+ def page(self) -> PagedResponse[E]:
59
+ return self._response
60
+
61
+ def __iter__(self) -> Iterator[E]:
62
+ while True:
63
+ yield from self._response.items
64
+
65
+ if self._response.next_page_token is None:
66
+ break
67
+
68
+ params = self._params.copy()
69
+ params["page_token"] = self._response.next_page_token
70
+ self._response = self._client.get(self._path, self._response_cls, params=params)
71
+
72
+
73
+ class ServiceBase:
74
+ def __init__(self, client: _Client):
75
+ self.client = client
76
+
77
+
78
+ class SpiralHTTPError(Exception):
79
+ def __init__(self, body: str, code: int):
80
+ super().__init__(body)
81
+ self.body = body
82
+ self.code = code
83
+
84
+
85
+ class _Client:
86
+ RequestT = TypeVar("RequestT")
87
+ ResponseT = TypeVar("ResponseT")
88
+
89
+ def __init__(self, http: httpx.Client, authn: Authn):
90
+ self.http = http
91
+ self.authn = authn
92
+
93
+ def get(
94
+ self, path: str, response_cls: type[ResponseT], *, params: Mapping[str, str | list[str]] | None = None
95
+ ) -> ResponseT:
96
+ return self.request("GET", path, None, response_cls, params=params)
97
+
98
+ def post(
99
+ self,
100
+ path: str,
101
+ req: RequestT,
102
+ response_cls: type[ResponseT],
103
+ *,
104
+ params: Mapping[str, str | list[str]] | None = None,
105
+ ) -> ResponseT:
106
+ return self.request("POST", path, req, response_cls, params=params)
107
+
108
+ def put(
109
+ self,
110
+ path: str,
111
+ req: RequestT,
112
+ response_cls: type[ResponseT],
113
+ *,
114
+ params: Mapping[str, str | list[str]] | None = None,
115
+ ) -> ResponseT:
116
+ return self.request("PUT", path, req, response_cls, params=params)
117
+
118
+ def delete(
119
+ self, path: str, response_cls: type[ResponseT], *, params: Mapping[str, str | list[str]] | None = None
120
+ ) -> ResponseT:
121
+ return self.request("DELETE", path, None, response_cls, params=params)
122
+
123
+ def request(
124
+ self,
125
+ method: str,
126
+ path: str,
127
+ req: RequestT | None,
128
+ response_cls: type[ResponseT],
129
+ *,
130
+ params: Mapping[str, str | list[str]] | None = None,
131
+ ) -> ResponseT:
132
+ req_data: dict[str, Any] = {}
133
+ if req is not None:
134
+ req_data = dict(json=TypeAdapter(req.__class__).dump_python(req, mode="json", exclude_none=True))
135
+
136
+ token = self.authn.token()
137
+ resp = self.http.request(
138
+ method,
139
+ path,
140
+ params=params or {},
141
+ headers={"Authorization": f"Bearer {token.expose_secret()}"} if token else None,
142
+ **req_data,
143
+ )
144
+
145
+ try:
146
+ resp.raise_for_status()
147
+ except HTTPStatusError as e:
148
+ # Enrich the exception with the response body
149
+ raise SpiralHTTPError(body=resp.text, code=resp.status_code) from e
150
+
151
+ if response_cls == type[None]:
152
+ return None
153
+
154
+ return TypeAdapter(response_cls).validate_python(resp.json())
155
+
156
+ def paged(
157
+ self,
158
+ path: str,
159
+ response_cls: type[PagedResponse[E]],
160
+ *,
161
+ page_token: str | None = None,
162
+ page_size: int = 50,
163
+ params: Mapping[str, str] | None = None,
164
+ ) -> Paged[E]:
165
+ return Paged(self, path, page_token, page_size, response_cls, params)
@@ -0,0 +1,152 @@
1
+ from enum import Enum
2
+ from types import NoneType
3
+ from typing import Annotated, Literal
4
+
5
+ from pydantic import AfterValidator, BaseModel, Field
6
+
7
+ from .client import Paged, PagedResponse, ServiceBase
8
+ from .types import ProjectId
9
+
10
+
11
+ def _validate_directory_path(path: str) -> str:
12
+ if not path.startswith("/"):
13
+ raise ValueError("Directory path must start with a slash.")
14
+ if not path.endswith("/"):
15
+ raise ValueError("Directory path must not end with a slash.")
16
+ return path
17
+
18
+
19
+ DirectoryPath = Annotated[str, AfterValidator(_validate_directory_path)]
20
+ FilePath = str # Path or directory
21
+ FsLoc = str
22
+
23
+
24
+ class BuiltinFileSystem(BaseModel):
25
+ """Spiral supports several builtin file systems in different cloud provider regions."""
26
+
27
+ type: Literal["builtin"] = "builtin"
28
+ provider: str
29
+
30
+
31
+ class UpstreamFileSystem(BaseModel):
32
+ """File system that points to another project, usually a "file system" project.
33
+
34
+ Upstream project must have an external file system configured,
35
+ and not a builtin file system or another upstream file system.
36
+ """
37
+
38
+ type: Literal["upstream"] = "upstream"
39
+ project_id: ProjectId
40
+
41
+
42
+ class S3FileSystem(BaseModel):
43
+ """File system backed by an AWS S3 bucket."""
44
+
45
+ type: Literal["s3"] = "s3"
46
+ endpoint: str | None = None
47
+ region: str
48
+ bucket: str
49
+ directory: DirectoryPath | None = None
50
+ # ARN of the role to assume when accessing the bucket
51
+ role_arn: str
52
+
53
+
54
+ class VaultSecret(BaseModel):
55
+ type: Literal["secret"] = "secret"
56
+ # Reference to a secret stored in vault
57
+ secret: str
58
+
59
+
60
+ class S3LikeFileSystem(BaseModel):
61
+ """File system backed by an AWS S3 bucket that is compatible with S3 APIs."""
62
+
63
+ type: Literal["s3like"] = "s3like"
64
+ endpoint: str
65
+ region: str
66
+ bucket: str
67
+ directory: DirectoryPath | None = None
68
+ access_key_id: str
69
+ secret_access_key: str | VaultSecret
70
+
71
+
72
+ class GCSFileSystem(BaseModel):
73
+ """File system backed by a Google Cloud Storage bucket."""
74
+
75
+ type: Literal["gcs"] = "gcs"
76
+ region: str
77
+ bucket: str
78
+ directory: DirectoryPath | None = None
79
+
80
+
81
+ FileSystem = Annotated[
82
+ BuiltinFileSystem | UpstreamFileSystem | S3FileSystem | S3LikeFileSystem | GCSFileSystem,
83
+ Field(discriminator="type"),
84
+ ]
85
+
86
+
87
+ class Mode(str, Enum):
88
+ READ_ONLY = "ro"
89
+ READ_WRITE = "rw"
90
+
91
+
92
+ class Mount(BaseModel):
93
+ """Mount grants permission to a Spiral resource to use a specific directory within the file system."""
94
+
95
+ id: str
96
+ project_id: ProjectId
97
+ directory: DirectoryPath
98
+ mode: Mode
99
+ principal: str
100
+
101
+
102
+ class CreateMountRequest(BaseModel):
103
+ directory: DirectoryPath
104
+ mode: Mode
105
+ principal: str
106
+
107
+
108
+ class CreateMountResponse(BaseModel):
109
+ mount: Mount
110
+
111
+
112
+ class GetMountAndFileSystemResponse(BaseModel):
113
+ mount: Mount
114
+ file_system: FileSystem
115
+ fs_loc: FsLoc
116
+
117
+
118
+ class FileSystemsService(ServiceBase):
119
+ """Service for file system operations."""
120
+
121
+ def list_providers(self) -> list[str]:
122
+ """List builtin providers."""
123
+ response = self.client.get("/v1/file-systems/builtin-providers", dict)
124
+ return response.get("providers", [])
125
+
126
+ def update_file_system(self, project_id: ProjectId, request: FileSystem) -> FileSystem:
127
+ """Update project's default file system."""
128
+ return self.client.post(f"/v1/file-systems/{project_id}", request, FileSystem)
129
+
130
+ def get_file_system(self, project_id: ProjectId) -> FileSystem:
131
+ """Get project's default file system."""
132
+ return self.client.get(f"/v1/file-systems/{project_id}", FileSystem)
133
+
134
+ def create_mount(self, project_id: ProjectId, request: CreateMountRequest) -> CreateMountResponse:
135
+ """Create a mount."""
136
+ return self.client.post(f"/v1/file-systems/{project_id}/mounts", request, CreateMountResponse)
137
+
138
+ def list_mounts(self, project_id: ProjectId) -> Paged[Mount]:
139
+ """List active mounts in project's file system."""
140
+ return self.client.paged(f"/v1/file-systems/{project_id}/mounts", PagedResponse[Mount])
141
+
142
+ def get_mount(self, mount_id: str) -> Mount:
143
+ """Get a mount."""
144
+ return self.client.get(f"/v1/mounts/{mount_id}", Mount)
145
+
146
+ def get_mount_and_file_system(self, mount_id: str) -> GetMountAndFileSystemResponse:
147
+ """Get the mount and its associated file system."""
148
+ return self.client.get(f"/v1/mounts/{mount_id}/with-filesystem", GetMountAndFileSystemResponse)
149
+
150
+ def remove_mount(self, mount_id: str) -> None:
151
+ """Remove mount."""
152
+ return self.client.delete(f"/v1/mounts/{mount_id}", NoneType)
@@ -0,0 +1,23 @@
1
+ from pydantic import BaseModel
2
+
3
+ from .client import ServiceBase
4
+ from .types import IndexId, WorkerId
5
+ from .workers import ResourceClass
6
+
7
+
8
+ class SyncIndexRequest(BaseModel):
9
+ """Request to sync a text index."""
10
+
11
+ resources: ResourceClass
12
+
13
+
14
+ class SyncIndexResponse(BaseModel):
15
+ worker_id: WorkerId
16
+
17
+
18
+ class KeySpaceIndexesService(ServiceBase):
19
+ """Service for key space index operations."""
20
+
21
+ def sync_index(self, index_id: IndexId, request: SyncIndexRequest) -> SyncIndexResponse:
22
+ """Start a job to sync an index."""
23
+ return self.client.post(f"/v1/key-space-indexes/{index_id}/sync", request, SyncIndexResponse)
@@ -0,0 +1,78 @@
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
+ BYOK = "bring-your-own-key"
41
+
42
+
43
+ class PortalLinkRequest(BaseModel):
44
+ intent: PortalLinkIntent
45
+
46
+
47
+ class PortalLinkResponse(BaseModel):
48
+ url: str
49
+
50
+
51
+ class InviteUserRequest(BaseModel):
52
+ email: str
53
+ role: OrgRole
54
+ expires_in_days: int | None = 7
55
+
56
+
57
+ class InviteUserResponse(BaseModel):
58
+ invite_id: str
59
+
60
+
61
+ class OrganizationsService(ServiceBase):
62
+ """Service for organization operations."""
63
+
64
+ def create(self, request: CreateOrgRequest) -> CreateOrgResponse:
65
+ """Create a new organization."""
66
+ return self.client.post("/v1/organizations", request, CreateOrgResponse)
67
+
68
+ def list_memberships(self) -> Paged[OrgMembership]:
69
+ """List organization memberships."""
70
+ return self.client.paged("/v1/organizations", PagedResponse[OrgMembership])
71
+
72
+ def invite_user(self, request: InviteUserRequest) -> InviteUserResponse:
73
+ """Invite a user to the organization."""
74
+ return self.client.post("/v1/organizations/invite-user", request, InviteUserResponse)
75
+
76
+ def portal_link(self, request: PortalLinkRequest) -> PortalLinkResponse:
77
+ """Get configuration portal link for the organization."""
78
+ return self.client.put("/v1/organizations/portal-link", request, PortalLinkResponse)