pyspiral 0.6.9__cp312-abi3-macosx_11_0_arm64.whl → 0.7.12__cp312-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 (58) hide show
  1. {pyspiral-0.6.9.dist-info → pyspiral-0.7.12.dist-info}/METADATA +9 -8
  2. {pyspiral-0.6.9.dist-info → pyspiral-0.7.12.dist-info}/RECORD +53 -45
  3. {pyspiral-0.6.9.dist-info → pyspiral-0.7.12.dist-info}/entry_points.txt +1 -0
  4. spiral/__init__.py +20 -0
  5. spiral/_lib.abi3.so +0 -0
  6. spiral/api/__init__.py +1 -1
  7. spiral/api/client.py +1 -1
  8. spiral/api/types.py +1 -0
  9. spiral/cli/admin.py +2 -2
  10. spiral/cli/app.py +8 -4
  11. spiral/cli/fs.py +4 -4
  12. spiral/cli/iceberg.py +1 -1
  13. spiral/cli/key_spaces.py +15 -1
  14. spiral/cli/login.py +4 -3
  15. spiral/cli/orgs.py +8 -7
  16. spiral/cli/projects.py +4 -4
  17. spiral/cli/state.py +5 -3
  18. spiral/cli/tables.py +59 -36
  19. spiral/cli/telemetry.py +1 -1
  20. spiral/cli/types.py +2 -2
  21. spiral/cli/workloads.py +3 -3
  22. spiral/client.py +69 -22
  23. spiral/core/client/__init__.pyi +48 -13
  24. spiral/core/config/__init__.pyi +47 -0
  25. spiral/core/expr/__init__.pyi +15 -0
  26. spiral/core/expr/images/__init__.pyi +3 -0
  27. spiral/core/expr/list_/__init__.pyi +4 -0
  28. spiral/core/expr/refs/__init__.pyi +4 -0
  29. spiral/core/expr/str_/__init__.pyi +3 -0
  30. spiral/core/expr/struct_/__init__.pyi +6 -0
  31. spiral/core/expr/text/__init__.pyi +5 -0
  32. spiral/core/expr/udf/__init__.pyi +14 -0
  33. spiral/core/expr/video/__init__.pyi +3 -0
  34. spiral/core/table/__init__.pyi +37 -2
  35. spiral/core/table/spec/__init__.pyi +6 -4
  36. spiral/dataloader.py +52 -38
  37. spiral/dataset.py +10 -1
  38. spiral/enrichment.py +304 -0
  39. spiral/expressions/__init__.py +21 -23
  40. spiral/expressions/base.py +9 -4
  41. spiral/expressions/file.py +17 -0
  42. spiral/expressions/http.py +11 -80
  43. spiral/expressions/s3.py +16 -0
  44. spiral/expressions/tiff.py +2 -3
  45. spiral/expressions/udf.py +38 -24
  46. spiral/iceberg.py +3 -3
  47. spiral/project.py +34 -6
  48. spiral/scan.py +80 -33
  49. spiral/settings.py +19 -97
  50. spiral/streaming_/stream.py +1 -1
  51. spiral/table.py +40 -10
  52. spiral/transaction.py +99 -2
  53. spiral/expressions/io.py +0 -100
  54. spiral/expressions/mp4.py +0 -62
  55. spiral/expressions/png.py +0 -18
  56. spiral/expressions/qoi.py +0 -18
  57. spiral/expressions/refs.py +0 -58
  58. {pyspiral-0.6.9.dist-info → pyspiral-0.7.12.dist-info}/WHEEL +0 -0
spiral/cli/telemetry.py CHANGED
@@ -8,7 +8,7 @@ app = AsyncTyper(short_help="Client-side telemetry.")
8
8
 
9
9
  @app.command(help="Issue new telemetry export token.")
10
10
  def export():
11
- res: IssueExportTokenResponse = state.settings.api.telemetry.issue_export_token()
11
+ res: IssueExportTokenResponse = state.spiral.api.telemetry.issue_export_token()
12
12
 
13
13
  command = f"export SPIRAL_OTEL_TOKEN={res.token}"
14
14
  pyperclip.copy(command)
spiral/cli/types.py CHANGED
@@ -10,7 +10,7 @@ from spiral.cli import ERR_CONSOLE, state
10
10
 
11
11
 
12
12
  def ask_project(title="Select a project"):
13
- projects = list(state.settings.api.project.list())
13
+ projects = list(state.spiral.api.project.list())
14
14
 
15
15
  if not projects:
16
16
  ERR_CONSOLE.print("No projects found")
@@ -29,7 +29,7 @@ ProjectArg = Annotated[ProjectId, Argument(help="Project ID", show_default=False
29
29
 
30
30
 
31
31
  def _org_default():
32
- memberships = list(state.settings.api.organization.list_memberships())
32
+ memberships = list(state.spiral.api.organization.list_memberships())
33
33
 
34
34
  if not memberships:
35
35
  ERR_CONSOLE.print("No organizations found")
spiral/cli/workloads.py CHANGED
@@ -17,7 +17,7 @@ def create(
17
17
  project: ProjectArg,
18
18
  name: Annotated[str | None, Option(help="Friendly name for the workload.")] = None,
19
19
  ):
20
- res = state.settings.api.workload.create(project, CreateWorkloadRequest(name=name))
20
+ res = state.spiral.api.workload.create(project, CreateWorkloadRequest(name=name))
21
21
  CONSOLE.print(f"Created workload {res.workload.id}")
22
22
 
23
23
 
@@ -25,13 +25,13 @@ def create(
25
25
  def ls(
26
26
  project: ProjectArg,
27
27
  ):
28
- workloads = list(state.settings.api.workload.list(project))
28
+ workloads = list(state.spiral.api.workload.list(project))
29
29
  CONSOLE.print(printer.table_of_models(Workload, workloads, fields=["id", "project_id", "name"]))
30
30
 
31
31
 
32
32
  @app.command(help="Issue new workflow credentials.")
33
33
  def issue_credentials(workload_id: Annotated[str, Argument(help="Workload ID.")]):
34
- res: IssueWorkloadCredentialsResponse = state.settings.api.workload.issue_credentials(workload_id)
34
+ res: IssueWorkloadCredentialsResponse = state.spiral.api.workload.issue_credentials(workload_id)
35
35
 
36
36
  while True:
37
37
  choice = questionary.select(
spiral/client.py CHANGED
@@ -6,12 +6,13 @@ import pyarrow as pa
6
6
 
7
7
  from spiral.api import SpiralAPI
8
8
  from spiral.api.projects import CreateProjectRequest, CreateProjectResponse
9
- from spiral.core.client import Operations
9
+ from spiral.core.authn import Authn
10
+ from spiral.core.client import Internal, KeyColumns
10
11
  from spiral.core.client import Spiral as CoreSpiral
12
+ from spiral.core.config import ClientSettings
11
13
  from spiral.datetime_ import timestamp_micros
12
14
  from spiral.expressions import ExprLike
13
15
  from spiral.scan import Scan
14
- from spiral.settings import Settings, settings
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from spiral.iceberg import Iceberg
@@ -22,26 +23,74 @@ if TYPE_CHECKING:
22
23
 
23
24
 
24
25
  class Spiral:
25
- def __init__(self, config: Settings | None = None):
26
- self._config = config or settings()
26
+ """Main client for interacting with the Spiral data platform.
27
+
28
+ Configuration is loaded with the following priority (highest to lowest):
29
+ 1. Explicit parameters.
30
+ 2. Environment variables (`SPIRAL__*`)
31
+ 3. Config file (`~/.spiral.toml`)
32
+ 4. Default values (production URLs)
33
+
34
+ Examples:
35
+ ```python
36
+ # Default configuration
37
+ client = Spiral()
38
+
39
+ # With config overrides
40
+ client = Spiral(overrides={"limits.concurrency": "16"})
41
+ ```
42
+
43
+ Args:
44
+ config: Custom ClientSettings object. Defaults to global settings.
45
+ overrides: Configuration overrides using dot notation,
46
+ see the [Client Configuration](/python-client.md) page for a full list.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ config: ClientSettings | None = None,
52
+ overrides: dict[str, str] | None = None,
53
+ ):
54
+ self._overrides = overrides
55
+ self._config = config
27
56
  self._org = None
57
+ self._core = None
58
+ self._api = None
59
+
60
+ @property
61
+ def config(self) -> ClientSettings:
62
+ """Returns the client's configuration"""
63
+ return self.core.config()
28
64
 
29
65
  @property
30
- def config(self) -> Settings:
31
- return self._config
66
+ def authn(self) -> Authn:
67
+ """Get the authentication handler for this client."""
68
+ return self.core.authn()
32
69
 
33
70
  @property
34
71
  def api(self) -> SpiralAPI:
35
- return self._config.api
72
+ if self._api is None:
73
+ self._api = SpiralAPI(self.authn, base_url=self.config.server_url)
74
+ return self._api
36
75
 
37
76
  @property
38
- def _core(self) -> CoreSpiral:
39
- return self._config.core
77
+ def core(self) -> CoreSpiral:
78
+ if self._core is None:
79
+ self._core = CoreSpiral(
80
+ config=self._config,
81
+ overrides=self._overrides,
82
+ )
83
+
84
+ return self._core
85
+
86
+ @property
87
+ def internal(self) -> Internal:
88
+ return self.core.internal(format=self.config.file_format)
40
89
 
41
90
  @property
42
91
  def organization(self) -> str:
43
92
  if self._org is None:
44
- token = self._config.authn.token()
93
+ token = self.authn.token()
45
94
  if token is None:
46
95
  raise ValueError("Authentication failed.")
47
96
  token_payload = jwt.decode(token.expose_secret(), options={"verify_signature": False})
@@ -79,25 +128,26 @@ class Spiral:
79
128
  """Open a table using an ID."""
80
129
  from spiral.table import Table
81
130
 
82
- return Table(self, self._core.table(table_id))
131
+ return Table(self, self.core.table(table_id))
83
132
 
84
133
  def text_index(self, index_id: str) -> "TextIndex":
85
134
  """Open a text index using an ID."""
86
135
  from spiral.text_index import TextIndex
87
136
 
88
- return TextIndex(self._core.text_index(index_id))
137
+ return TextIndex(self.core.text_index(index_id))
89
138
 
90
139
  def key_space_index(self, index_id: str) -> "KeySpaceIndex":
91
140
  """Open a key space index using an ID."""
92
141
  from spiral.key_space_index import KeySpaceIndex
93
142
 
94
- return KeySpaceIndex(self._core.key_space_index(index_id))
143
+ return KeySpaceIndex(self.core.key_space_index(index_id))
95
144
 
96
145
  def scan(
97
146
  self,
98
147
  *projections: ExprLike,
99
148
  where: ExprLike | None = None,
100
149
  asof: datetime | int | None = None,
150
+ _key_columns: KeyColumns | None = None,
101
151
  ) -> Scan:
102
152
  """Starts a read transaction on the Spiral.
103
153
 
@@ -112,15 +162,16 @@ class Spiral:
112
162
  asof = timestamp_micros(asof)
113
163
 
114
164
  # Combine all projections into a single struct.
165
+ if not projections:
166
+ raise ValueError("At least one projection is required.")
115
167
  projection = se.merge(*projections)
116
168
  if where is not None:
117
169
  where = se.lift(where)
118
170
 
119
171
  return Scan(
120
- self._core.scan(
121
- projection.__expr__,
122
- filter=where.__expr__ if where else None,
123
- asof=asof,
172
+ self,
173
+ self.core.scan(
174
+ projection.__expr__, filter=where.__expr__ if where else None, asof=asof, key_columns=_key_columns
124
175
  ),
125
176
  )
126
177
 
@@ -155,17 +206,13 @@ class Spiral:
155
206
  freshness_window = timedelta(seconds=0)
156
207
  freshness_window_s = int(freshness_window.total_seconds())
157
208
 
158
- return self._core.search(
209
+ return self.core.search(
159
210
  top_k=top_k,
160
211
  rank_by=rank_by.__expr__,
161
212
  filters=filters.__expr__ if filters else None,
162
213
  freshness_window_s=freshness_window_s,
163
214
  )
164
215
 
165
- def _ops(self) -> Operations:
166
- """Access maintenance operations."""
167
- return self._core._ops(format=settings().file_format)
168
-
169
216
  @property
170
217
  def iceberg(self) -> "Iceberg":
171
218
  """
@@ -1,22 +1,36 @@
1
+ from enum import Enum
1
2
  from typing import Any, Literal
2
3
 
3
4
  import pyarrow as pa
4
- from spiral.api.types import DatasetName, IndexName, ProjectId, RootUri, TableName
5
+ from spiral.api.types import DatasetName, IndexName, ProjectId, RootUri, TableId, TableName
5
6
  from spiral.core.authn import Authn
6
- from spiral.core.table import ColumnGroupState, KeyRange, KeySpaceState, Scan, Snapshot, Table, Transaction
7
+ from spiral.core.config import ClientSettings
8
+ from spiral.core.table import ColumnGroupState, KeyRange, KeySpaceState, Scan, ScanState, Snapshot, Table, Transaction
7
9
  from spiral.core.table.spec import ColumnGroup, Schema
8
10
  from spiral.expressions import Expr
9
11
 
12
+ # Only for typing, the actual definition is in Rust.
13
+ class KeyColumns(Enum):
14
+ IfProjected = 0
15
+ Included = 1
16
+ Only = 2
17
+
10
18
  class Spiral:
11
19
  """A client for Spiral database"""
12
20
  def __init__(
13
21
  self,
14
- api_url: str | None = None,
15
- spfs_url: str | None = None,
16
- authn: Authn | None = None,
22
+ config: ClientSettings | None = None,
23
+ overrides: dict[str, str] | None = None,
17
24
  ):
18
- """Initialize the Spiral client."""
25
+ """Initialize the Spiral client.
26
+
27
+ Args:
28
+ config: Client configuration, defaults to the global config.
29
+ overrides: Configuration overrides using dot notation,
30
+ see the [Client Configuration](/python-client) page for a full list.
31
+ """
19
32
  ...
33
+
20
34
  def authn(self) -> Authn:
21
35
  """Get the current authentication context."""
22
36
  ...
@@ -26,10 +40,15 @@ class Spiral:
26
40
  projection: Expr,
27
41
  filter: Expr | None = None,
28
42
  asof: int | None = None,
43
+ key_columns: KeyColumns | None = None,
29
44
  ) -> Scan:
30
45
  """Construct a table scan."""
31
46
  ...
32
47
 
48
+ def load_scan(self, plan_state: ScanState) -> Scan:
49
+ """Load a scan from a serialized scan state."""
50
+ ...
51
+
33
52
  def transaction(self, table: Table, format: str | None = None, retries: int | None = 3) -> Transaction:
34
53
  """Being a table transaction."""
35
54
  ...
@@ -65,6 +84,22 @@ class Spiral:
65
84
  """Create a new table in the specified project."""
66
85
  ...
67
86
 
87
+ def move_table(
88
+ self,
89
+ table_id: TableId,
90
+ new_dataset: DatasetName,
91
+ ):
92
+ """Move a table to a dataset in the same project."""
93
+ ...
94
+
95
+ def rename_table(
96
+ self,
97
+ table_id: TableId,
98
+ new_table: TableName,
99
+ ):
100
+ """Rename a table."""
101
+ ...
102
+
68
103
  def text_index(self, index_id: str) -> TextIndex:
69
104
  """Get a text index."""
70
105
  ...
@@ -100,12 +135,12 @@ class Spiral:
100
135
  """Create a new key space index in the specified project."""
101
136
  ...
102
137
 
103
- def _ops(self, *, format: str | None = None) -> Operations:
104
- """Access maintenance operations.
138
+ def internal(self, *, format: str | None = None) -> Internal:
139
+ """Internal client APIs. It can change without notice."""
140
+ ...
105
141
 
106
- IMPORTANT: This API is internal and is currently exposed for development & testing.
107
- Maintenance operations are run by SpiralDB.
108
- """
142
+ def config(self) -> ClientSettings:
143
+ """Client-side configuration."""
109
144
  ...
110
145
 
111
146
  class TextIndex:
@@ -158,8 +193,8 @@ class ShuffleConfig:
158
193
  max_batch_size: int | None = None,
159
194
  ): ...
160
195
 
161
- class Operations:
162
- def flush_wal(self, table: Table, *, keep_latest_s: int | None = None) -> None:
196
+ class Internal:
197
+ def flush_wal(self, table: Table) -> None:
163
198
  """
164
199
  Flush the write-ahead log of the table.
165
200
  """
@@ -0,0 +1,47 @@
1
+ from spiral.core.authn import Token
2
+
3
+ class ClientSettings:
4
+ """Client configuration loaded from ~/.spiral.toml and environment variables."""
5
+
6
+ @staticmethod
7
+ def load() -> ClientSettings:
8
+ """Load ClientSettings from ~/.spiral.toml and environment variables.
9
+
10
+ Configuration priority (highest to lowest):
11
+ 1. Environment variables (SPIRAL__*)
12
+ 2. Config file (~/.spiral.toml)
13
+ 3. Default values
14
+ """
15
+ ...
16
+
17
+ @property
18
+ def server_url(self) -> str:
19
+ """The Spiral API endpoint URL."""
20
+ ...
21
+
22
+ @property
23
+ def spfs_url(self) -> str:
24
+ """The SpFS endpoint URL."""
25
+ ...
26
+
27
+ @property
28
+ def file_format(self) -> str:
29
+ """File format for table storage (vortex or parquet)."""
30
+ ...
31
+
32
+ @property
33
+ def token(self) -> Token | None:
34
+ """Authentication token (if provided via SPIRAL__TOKEN)."""
35
+ ...
36
+
37
+ @token.setter
38
+ def token(self, token: Token):
39
+ """Set the config's authentication token"""
40
+ ...
41
+
42
+ def to_json(self) -> str:
43
+ """Serialize to a JSON string"""
44
+ ...
45
+ @staticmethod
46
+ def from_json(json: str) -> ClientSettings:
47
+ """Deserialize from a JSON-formatted string"""
@@ -0,0 +1,15 @@
1
+ from pyarrow import Array, DataType, Scalar
2
+
3
+ class Expr:
4
+ """Low level expression class."""
5
+
6
+ def aux(name: str, data_type: DataType) -> Expr: ...
7
+
8
+ # Array is correct (there is no ArrayData), see the table here:
9
+ # https://arrow.apache.org/rust/arrow_pyarrow/index.html
10
+ def scalar(array: Array[Scalar[DataType]]) -> Expr: ...
11
+ def not_(expr: Expr) -> Expr: ...
12
+ def is_null(expr: Expr) -> Expr: ...
13
+ def binary(op: str, expr: Expr, Expr: Expr) -> Expr: ...
14
+ def cast(_expr: Expr, _data_type: DataType) -> Expr: ...
15
+ def array_lit(array: Array[Scalar[DataType]]) -> Expr: ...
@@ -0,0 +1,3 @@
1
+ from .. import Expr
2
+
3
+ def encode_(images: Expr, format: str) -> Expr: ...
@@ -0,0 +1,4 @@
1
+ from .. import Expr
2
+
3
+ def contains(list: Expr, expr: Expr) -> Expr: ...
4
+ def element_at(list: Expr, element: Expr) -> Expr: ...
@@ -0,0 +1,4 @@
1
+ from .. import Expr
2
+
3
+ def ref(expr: Expr, field: str | None) -> Expr: ...
4
+ def deref(expr: Expr, field: str | None) -> Expr: ...
@@ -0,0 +1,3 @@
1
+ from .. import Expr
2
+
3
+ def substr(expr: Expr, begin: int, end: int | None) -> Expr: ...
@@ -0,0 +1,6 @@
1
+ from .. import Expr
2
+
3
+ def getitem(expr: Expr, item: str) -> Expr: ...
4
+ def select(expr: Expr, including: list[str] | None = None, excluding: list[str] | None = None) -> Expr: ...
5
+ def pack(names: list[str], children: list[str], nullable: bool) -> Expr: ...
6
+ def merge(names: list[Expr]) -> Expr: ...
@@ -0,0 +1,5 @@
1
+ from .. import Expr
2
+
3
+ def field(expr: Expr, tokeneizer: str | None) -> Expr: ...
4
+ def find(expr: Expr, term: str) -> Expr: ...
5
+ def boost(expr: Expr, factor: float) -> Expr: ...
@@ -0,0 +1,14 @@
1
+ from collections.abc import Callable
2
+
3
+ from pyarrow import Array, DataType, Scalar
4
+
5
+ from .. import Expr
6
+
7
+ class UDF:
8
+ def __call__(self, args: list[Expr]) -> Expr: ...
9
+
10
+ def create(
11
+ name: str,
12
+ return_type: Callable[[tuple[DataType, ...]], DataType],
13
+ invoke: Callable[[tuple[Array[Scalar[DataType]], ...]], Array[Scalar[DataType]]],
14
+ ) -> UDF: ...
@@ -0,0 +1,3 @@
1
+ from .. import Expr
2
+
3
+ def read(expr: Expr, ranges: Expr, crops: Expr, format: str) -> Expr: ...
@@ -5,7 +5,7 @@ from spiral.core.client import Shard, ShuffleConfig
5
5
 
6
6
  from .manifests import FragmentManifest
7
7
  from .metastore import PyMetastore
8
- from .spec import ColumnGroup, Key, Schema, WriteAheadLog
8
+ from .spec import ColumnGroup, Key, Operation, Schema, WriteAheadLog
9
9
 
10
10
  class KeyRange:
11
11
  """A right-exclusive range of keys."""
@@ -52,6 +52,21 @@ class Snapshot:
52
52
  table: Table
53
53
  wal: WriteAheadLog
54
54
 
55
+ class ScanState:
56
+ def to_json(self) -> str: ...
57
+ @staticmethod
58
+ def from_json(json: str) -> ScanState: ...
59
+
60
+ class MaterializablePlan:
61
+ pass
62
+
63
+ class EvaluatedExecutablePlan:
64
+ pass
65
+
66
+ class EvaluatedPlanStream:
67
+ def __next__(self) -> EvaluatedExecutablePlan: ...
68
+ def __iter__(self) -> EvaluatedPlanStream: ...
69
+
55
70
  class Scan:
56
71
  def key_schema(self) -> Schema: ...
57
72
  def schema(self) -> Schema: ...
@@ -62,10 +77,14 @@ class Scan:
62
77
  def column_groups(self) -> list[ColumnGroup]: ...
63
78
  def column_group_state(self, column_group: ColumnGroup) -> ColumnGroupState: ...
64
79
  def key_space_state(self, table_id: str) -> KeySpaceState: ...
80
+ def plan_state(self) -> ScanState: ...
81
+ def materializable_plan(self) -> MaterializablePlan: ...
65
82
  def to_record_batches(
66
83
  self,
84
+ key_range: KeyRange | None = None,
67
85
  key_table: pa.Table | pa.RecordBatch | None = None,
68
86
  batch_readahead: int | None = None,
87
+ progress: bool = True,
69
88
  ) -> pa.RecordBatchReader: ...
70
89
  def to_shuffled_record_batches(
71
90
  self,
@@ -78,6 +97,10 @@ class Scan:
78
97
  # If `infinite` is True, shards are shuffled after exhausted but not before the first pass.
79
98
  # Otherwise, shards are not shuffle and shuffle config is only used for shuffle buffer.
80
99
  ...
100
+
101
+ def evaluate_analyze(
102
+ self, key_table: pa.Table | pa.RecordBatch | None = None, batch_readahead: int | None = None
103
+ ) -> EvaluatedPlanStream: ...
81
104
  def metrics(self) -> dict[str, Any]: ...
82
105
 
83
106
  class KeySpaceState:
@@ -95,7 +118,19 @@ class Transaction:
95
118
  status: str
96
119
 
97
120
  def write(self, table: pa.RecordBatchReader, *, partition_size_bytes: int | None = None): ...
121
+ def writeback(
122
+ self,
123
+ scan: Scan,
124
+ *,
125
+ key_range: KeyRange | None = None,
126
+ partition_size_bytes: int | None = None,
127
+ batch_readahead: int | None = None,
128
+ ): ...
98
129
  def drop_columns(self, column_paths: list[str]): ...
99
- def commit(self): ...
130
+ def ops(self) -> list[Operation]: ...
131
+ def take(self) -> list[Operation]: ...
132
+ def include(self, ops: list[Operation]): ...
133
+ def commit(self, *, compact: bool = False): ...
100
134
  def abort(self): ...
135
+ def is_empty(self) -> bool: ...
101
136
  def metrics(self) -> dict[str, Any]: ...
@@ -62,6 +62,12 @@ class ColumnGroupMetadata:
62
62
  def apply_wal(self, wal: WriteAheadLog) -> ColumnGroupMetadata:
63
63
  """Applies the given WAL to the metadata."""
64
64
 
65
+ class Operation:
66
+ # Base class for all operations in the WAL.
67
+ def to_json(self) -> str: ...
68
+ @staticmethod
69
+ def from_json(json: str) -> Operation: ...
70
+
65
71
  class LogEntry:
66
72
  ts: int
67
73
  operation: (
@@ -172,11 +178,7 @@ class KeySpaceWriteOp:
172
178
 
173
179
  class ColumnGroupWriteOp:
174
180
  column_group: ColumnGroup
175
- level: FragmentLevel
176
181
  manifest_handle: ManifestHandle
177
- key_span: KeySpan
178
- key_extent: KeyExtent
179
- column_ids: list[str]
180
182
 
181
183
  class SchemaEvolutionOp:
182
184
  column_group: ColumnGroup