pyspiral 0.3.1__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 (109) hide show
  1. {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/METADATA +9 -13
  2. pyspiral-0.4.0.dist-info/RECORD +98 -0
  3. {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/WHEEL +1 -1
  4. spiral/__init__.py +6 -9
  5. spiral/_lib.abi3.so +0 -0
  6. spiral/adbc.py +21 -14
  7. spiral/api/__init__.py +14 -175
  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 +82 -17
  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} +23 -37
  36. spiral/core/client/__init__.pyi +117 -0
  37. spiral/core/index/__init__.pyi +15 -0
  38. spiral/core/{core → table}/__init__.pyi +44 -17
  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 +41 -66
  42. spiral/datetime_.py +27 -0
  43. spiral/expressions/__init__.py +26 -18
  44. spiral/expressions/base.py +5 -5
  45. spiral/expressions/list_.py +1 -1
  46. spiral/expressions/mp4.py +2 -9
  47. spiral/expressions/png.py +1 -1
  48. spiral/expressions/qoi.py +1 -1
  49. spiral/expressions/refs.py +3 -9
  50. spiral/expressions/struct.py +7 -5
  51. spiral/expressions/text.py +62 -0
  52. spiral/expressions/udf.py +3 -3
  53. spiral/iceberg/__init__.py +3 -0
  54. spiral/iceberg/client.py +33 -0
  55. spiral/indexes/__init__.py +5 -0
  56. spiral/indexes/client.py +137 -0
  57. spiral/indexes/index.py +34 -0
  58. spiral/indexes/scan.py +22 -0
  59. spiral/project.py +19 -110
  60. spiral/{proto → protogen}/_/scandal/__init__.py +23 -135
  61. spiral/protogen/_/spiral/table/__init__.py +22 -0
  62. spiral/protogen/substrait/__init__.py +3399 -0
  63. spiral/protogen/substrait/extensions/__init__.py +115 -0
  64. spiral/server.py +17 -0
  65. spiral/settings.py +29 -91
  66. spiral/substrait_.py +9 -5
  67. spiral/tables/__init__.py +12 -0
  68. spiral/tables/client.py +130 -0
  69. spiral/{dataset.py → tables/dataset.py} +9 -199
  70. spiral/tables/debug/manifests.py +70 -0
  71. spiral/tables/debug/metrics.py +56 -0
  72. spiral/{debug.py → tables/debug/scan.py} +6 -9
  73. spiral/{maintenance.py → tables/maintenance.py} +1 -1
  74. spiral/{scan_.py → tables/scan.py} +63 -89
  75. spiral/tables/snapshot.py +78 -0
  76. spiral/{table.py → tables/table.py} +59 -73
  77. spiral/{txn.py → tables/transaction.py} +7 -3
  78. pyspiral-0.3.1.dist-info/RECORD +0 -85
  79. spiral/api/tables.py +0 -91
  80. spiral/api/tokens.py +0 -56
  81. spiral/authn/authn.py +0 -89
  82. spiral/authn/device.py +0 -206
  83. spiral/authn/github_.py +0 -33
  84. spiral/authn/modal_.py +0 -18
  85. spiral/cli/org.py +0 -90
  86. spiral/cli/project.py +0 -109
  87. spiral/cli/table.py +0 -20
  88. spiral/cli/token.py +0 -27
  89. spiral/core/metastore/__init__.pyi +0 -91
  90. spiral/proto/_/spfs/__init__.py +0 -36
  91. spiral/proto/_/spiral/table/__init__.py +0 -276
  92. spiral/proto/_/spiraldb/metastore/__init__.py +0 -499
  93. spiral/proto/__init__.py +0 -0
  94. spiral/proto/scandal/__init__.py +0 -45
  95. spiral/proto/spiral/__init__.py +0 -0
  96. spiral/proto/spiral/table/__init__.py +0 -96
  97. {pyspiral-0.3.1.dist-info → pyspiral-0.4.0.dist-info}/entry_points.txt +0 -0
  98. /spiral/{authn/__init__.py → core/__init__.pyi} +0 -0
  99. /spiral/{core → protogen/_}/__init__.py +0 -0
  100. /spiral/{proto/_ → protogen/_/arrow}/__init__.py +0 -0
  101. /spiral/{proto/_/arrow → protogen/_/arrow/flight}/__init__.py +0 -0
  102. /spiral/{proto/_/arrow/flight → protogen/_/arrow/flight/protocol}/__init__.py +0 -0
  103. /spiral/{proto → protogen}/_/arrow/flight/protocol/sql/__init__.py +0 -0
  104. /spiral/{proto/_/arrow/flight/protocol → protogen/_/spiral}/__init__.py +0 -0
  105. /spiral/{proto → protogen/_}/substrait/__init__.py +0 -0
  106. /spiral/{proto → protogen/_}/substrait/extensions/__init__.py +0 -0
  107. /spiral/{proto/_/spiral → protogen}/__init__.py +0 -0
  108. /spiral/{proto → protogen}/util.py +0 -0
  109. /spiral/{proto/_/spiraldb → tables/debug}/__init__.py +0 -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)
spiral/api/filesystems.py CHANGED
@@ -1,125 +1,153 @@
1
- from datetime import timedelta
2
1
  from enum import Enum
3
2
  from typing import Annotated, Literal
4
3
 
5
4
  from pydantic import AfterValidator, BaseModel, Field
6
5
 
7
- from . import ProjectId, ServiceBase
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
8
20
 
9
21
 
10
22
  class BuiltinFileSystem(BaseModel):
23
+ """Spiral supports several builtin file systems in different cloud provider regions."""
24
+
11
25
  type: Literal["builtin"] = "builtin"
12
26
  provider: str
13
27
 
14
28
 
15
- FileSystem = Annotated[BuiltinFileSystem, Field(discriminator="type")]
29
+ class UpstreamFileSystem(BaseModel):
30
+ """File system that points to another project, usually a "file system" project.
16
31
 
32
+ Upstream project must have an external file system configured,
33
+ and not a builtin file system or another upstream file system.
34
+ """
17
35
 
18
- def _validate_file_path(path: str) -> str:
19
- if "//" in path:
20
- raise ValueError("FilePath must not contain multiple slashes")
21
- if not path.startswith("/"):
22
- raise ValueError("FilePath must start with /")
23
- if path.endswith("/"):
24
- raise ValueError("FilePath must not end with /")
25
- return path
36
+ type: Literal["upstream"] = "upstream"
37
+ project_id: ProjectId
26
38
 
27
39
 
28
- FilePath = Annotated[
29
- str,
30
- AfterValidator(_validate_file_path),
31
- Field(description="A path to an individual file in the file system. Must not end with a slash."),
32
- ]
40
+ class S3FileSystem(BaseModel):
41
+ """File system backed by an S3-compatible bucket."""
33
42
 
43
+ type: Literal["s3"] = "s3"
44
+ endpoint: str = "https://s3.amazonaws.com"
45
+ region: str = "auto"
46
+ bucket: str
47
+ directory: DirectoryPath | None
34
48
 
35
- def _validate_prefix(path: str) -> str:
36
- if "//" in path:
37
- raise ValueError("Prefix must not contain multiple slashes")
38
- if not path.startswith("/"):
39
- raise ValueError("Prefix must start with /")
40
- if not path.endswith("/"):
41
- raise ValueError("Prefix must end with /")
42
- return path
43
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
44
57
 
45
- Prefix = Annotated[
46
- str,
47
- AfterValidator(_validate_prefix),
48
- Field(description="A prefix in a file system. Must end with a slash."),
58
+
59
+ FileSystem = Annotated[
60
+ BuiltinFileSystem | UpstreamFileSystem | S3FileSystem | GCSFileSystem, Field(discriminator="type")
49
61
  ]
50
62
 
51
63
 
52
- class Mode(Enum):
64
+ class Mode(str, Enum):
53
65
  READ_ONLY = "ro"
54
66
  READ_WRITE = "rw"
55
67
 
56
68
 
57
69
  class Mount(BaseModel):
70
+ """Mount grants permission to a Spiral resource to use a specific directory within the file system."""
71
+
58
72
  id: str
59
73
  project_id: ProjectId
60
- prefix: Prefix
74
+ directory: DirectoryPath
61
75
  mode: Mode
62
76
  principal: str
63
77
 
64
78
 
65
- class GetFileSystem:
66
- class Request(BaseModel):
67
- project_id: ProjectId
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
68
90
 
69
- class Response(BaseModel):
70
- file_system: FileSystem
71
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
+ """
72
96
 
73
- class UpdateFileSystem:
74
- class Request(BaseModel):
75
- project_id: ProjectId
76
- file_system: FileSystem
97
+ service_account: str
77
98
 
78
- class Response(BaseModel):
79
- file_system: FileSystem
80
99
 
100
+ class UpdateGCSFileSystem(GCSFileSystem):
101
+ credentials: GCPServiceAccount
81
102
 
82
- class ListProviders:
83
- class Response(BaseModel):
84
- providers: list[str]
103
+
104
+ UpdateFileSystemRequest = Annotated[
105
+ BuiltinFileSystem | UpstreamFileSystem | UpdateS3FileSystem | UpdateGCSFileSystem, Field(discriminator="type")
106
+ ]
85
107
 
86
108
 
87
- class CreateMount:
88
- class Request(BaseModel):
89
- project_id: ProjectId
90
- prefix: Prefix
91
- mode: Mode
92
- principal: str
109
+ class UpdateFileSystemResponse(BaseModel):
110
+ file_system: FileSystem
93
111
 
94
- class Response(BaseModel):
95
- mount: Mount
96
112
 
113
+ class CreateMountRequest(BaseModel):
114
+ directory: DirectoryPath
115
+ mode: Mode
116
+ principal: str
97
117
 
98
- class CreateMountToken:
99
- class Request(BaseModel):
100
- mount_id: str
101
- mode: Mode
102
- path: FilePath | Prefix
103
- ttl: int = Field(default_factory=lambda: int(timedelta(hours=1).total_seconds()))
104
118
 
105
- class Response(BaseModel):
106
- token: str
119
+ class CreateMountResponse(BaseModel):
120
+ mount: Mount
107
121
 
108
122
 
109
123
  class FileSystemService(ServiceBase):
110
- def get_file_system(self, request: GetFileSystem.Request) -> GetFileSystem.Response:
111
- """Get the file system currently configured for a project."""
112
- return self.client.put("/file-system/get", request, GetFileSystem.Response)
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)
113
138
 
114
- def update_file_system(self, request: UpdateFileSystem.Request) -> UpdateFileSystem.Response:
115
- """Update the file system for a project."""
116
- return self.client.put("/file-system/update", request, UpdateFileSystem.Response)
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)
117
142
 
118
- def list_providers(self) -> ListProviders.Response:
119
- return self.client.put("/file-system/list-providers", None, ListProviders.Response)
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])
120
146
 
121
- def create_mount(self, request: CreateMount.Request) -> CreateMount.Response:
122
- return self.client.post("/file-system/create-mount", request, CreateMount.Response)
147
+ def get_mount(self, mount_id: str) -> Mount:
148
+ """Get a mount."""
149
+ return self.client.get(f"/v1/mounts/{mount_id}", Mount)
123
150
 
124
- def create_mount_token(self, request: CreateMountToken.Request) -> CreateMountToken.Response:
125
- return self.client.post("/file-system/mount-token", request, CreateMountToken.Response)
151
+ def remove_mount(self, mount_id: str) -> None:
152
+ """Remove mount."""
153
+ return self.client.delete(f"/v1/mounts/{mount_id}", None)
@@ -1,90 +1,77 @@
1
1
  from enum import Enum
2
2
 
3
- from pydantic import BaseModel, EmailStr, Field
3
+ from pydantic import BaseModel
4
4
 
5
- from . import OrganizationId, Paged, PagedRequest, PagedResponse, ServiceBase
5
+ from .client import Paged, PagedResponse, ServiceBase
6
+ from .types import OrgId
6
7
 
7
8
 
8
- class OrganizationRole(Enum):
9
+ class OrgRole(str, Enum):
9
10
  OWNER = "owner"
10
11
  MEMBER = "member"
11
12
  GUEST = "guest"
12
13
 
13
14
 
14
- class Organization(BaseModel):
15
- id: OrganizationId
16
- name: str | None = Field(
17
- default=None,
18
- description="Optional human-readable name for the organization",
19
- )
15
+ class Org(BaseModel):
16
+ id: OrgId
17
+ name: str | None = None
20
18
 
21
19
 
22
- class OrganizationMembership(BaseModel):
23
- organization: Organization
24
- role: str = Field(description="The user's role in the organization")
20
+ class OrgMembership(BaseModel):
21
+ user_id: str
22
+ org: Org
23
+ role: str
25
24
 
26
25
 
27
- class CreateOrganization:
28
- class Request(BaseModel):
29
- name: str | None = Field(
30
- default=None,
31
- description="Optional human-readable name for the organization",
32
- )
26
+ class CreateOrgRequest(BaseModel):
27
+ name: str | None = None
33
28
 
34
- class Response(BaseModel):
35
- organization: Organization
36
29
 
30
+ class CreateOrgResponse(BaseModel):
31
+ org: Org
37
32
 
38
- class ListUserMemberships:
39
- class Request(PagedRequest):
40
- """List the organization memberships of the current user."""
41
33
 
42
- class Response(PagedResponse[OrganizationMembership]):
43
- """The user's organization memberships."""
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"
44
40
 
45
41
 
46
- class PortalLink:
47
- class Intent(Enum):
48
- SSO = "sso"
49
- DIRECTORY = "directory"
50
- AUDIT_LOGS = "audit-logs"
51
- LOG_STREAMS = "log-streams"
52
- DOMAIN_VERIFICATION = "domain-verification"
42
+ class PortalLinkRequest(BaseModel):
43
+ intent: PortalLinkIntent
53
44
 
54
- class Request(BaseModel):
55
- intent: "PortalLink.Intent"
56
45
 
57
- class Response(BaseModel):
58
- url: str
46
+ class PortalLinkResponse(BaseModel):
47
+ url: str
59
48
 
60
49
 
61
- class InviteUser:
62
- class Request(BaseModel):
63
- email: EmailStr
64
- role: OrganizationRole
65
- expires_in_days: int = 7
50
+ class InviteUserRequest(BaseModel):
51
+ email: str
52
+ role: OrgRole
53
+ expires_in_days: int | None = 7
66
54
 
67
- class Response(BaseModel):
68
- invite_id: str
55
+
56
+ class InviteUserResponse(BaseModel):
57
+ invite_id: str
69
58
 
70
59
 
71
60
  class OrganizationService(ServiceBase):
72
- def create_organization(self, request: CreateOrganization.Request) -> CreateOrganization.Response:
73
- """Create a new organization."""
74
- return self.client.post("/organization/create", request, CreateOrganization.Response)
61
+ """Service for organization operations."""
75
62
 
76
- def list_user_memberships(self) -> Paged[OrganizationMembership]:
77
- """List organizations that the user is a member of."""
78
- return self.client.paged(
79
- "/organization/list-user-memberships",
80
- ListUserMemberships.Request(),
81
- ListUserMemberships.Response,
82
- )
63
+ def create(self, request: CreateOrgRequest) -> CreateOrgResponse:
64
+ """Create a new organization."""
65
+ return self.client.post("/v1/organizations", request, CreateOrgResponse)
83
66
 
84
- def portal_link(self, request: PortalLink.Request) -> PortalLink.Response:
85
- """Get a link to the organization configuration portal."""
86
- return self.client.put("/organization/portal-link", request, PortalLink.Response)
67
+ def list_memberships(self) -> Paged[OrgMembership]:
68
+ """List organization memberships."""
69
+ return self.client.paged("/v1/organizations", PagedResponse[OrgMembership])
87
70
 
88
- def invite_user(self, request: InviteUser.Request) -> InviteUser.Response:
71
+ def invite_user(self, request: InviteUserRequest) -> InviteUserResponse:
89
72
  """Invite a user to the organization."""
90
- return self.client.post("/organization/invite-user", request, InviteUser.Response)
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)