pixeltable 0.4.19__py3-none-any.whl → 0.4.21__py3-none-any.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.

Potentially problematic release.


This version of pixeltable might be problematic. Click here for more details.

Files changed (36) hide show
  1. pixeltable/_version.py +1 -1
  2. pixeltable/catalog/catalog.py +76 -50
  3. pixeltable/catalog/column.py +29 -16
  4. pixeltable/catalog/insertable_table.py +2 -2
  5. pixeltable/catalog/path.py +4 -10
  6. pixeltable/catalog/table.py +51 -0
  7. pixeltable/catalog/table_version.py +40 -7
  8. pixeltable/catalog/view.py +2 -2
  9. pixeltable/config.py +1 -0
  10. pixeltable/env.py +2 -0
  11. pixeltable/exprs/column_ref.py +2 -1
  12. pixeltable/functions/__init__.py +1 -0
  13. pixeltable/functions/image.py +2 -8
  14. pixeltable/functions/reve.py +250 -0
  15. pixeltable/functions/video.py +534 -1
  16. pixeltable/globals.py +2 -1
  17. pixeltable/index/base.py +5 -18
  18. pixeltable/index/btree.py +6 -2
  19. pixeltable/index/embedding_index.py +4 -4
  20. pixeltable/metadata/schema.py +7 -32
  21. pixeltable/share/__init__.py +1 -1
  22. pixeltable/share/packager.py +22 -18
  23. pixeltable/share/protocol/__init__.py +34 -0
  24. pixeltable/share/protocol/common.py +170 -0
  25. pixeltable/share/protocol/operation_types.py +33 -0
  26. pixeltable/share/protocol/replica.py +109 -0
  27. pixeltable/share/publish.py +91 -56
  28. pixeltable/store.py +11 -15
  29. pixeltable/utils/av.py +87 -1
  30. pixeltable/utils/dbms.py +15 -11
  31. pixeltable/utils/image.py +10 -0
  32. {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/METADATA +2 -1
  33. {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/RECORD +36 -31
  34. {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/WHEEL +0 -0
  35. {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/entry_points.txt +0 -0
  36. {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@ import dataclasses
2
2
  import types
3
3
  import typing
4
4
  import uuid
5
- from typing import Any, NamedTuple, TypeVar, Union, get_type_hints
5
+ from typing import Any, TypeVar, Union, get_type_hints
6
6
 
7
7
  import sqlalchemy as sql
8
8
  from sqlalchemy import BigInteger, ForeignKey, Integer, LargeBinary, orm
@@ -218,6 +218,12 @@ class TableMd:
218
218
  and len(self.column_md) == 0
219
219
  )
220
220
 
221
+ @property
222
+ def ancestor_ids(self) -> list[str]:
223
+ if self.view_md is None:
224
+ return []
225
+ return [id for id, _ in self.view_md.base_versions]
226
+
221
227
 
222
228
  class Table(Base):
223
229
  """
@@ -349,34 +355,3 @@ class Function(Base):
349
355
  dir_id: orm.Mapped[uuid.UUID] = orm.mapped_column(UUID(as_uuid=True), ForeignKey('dirs.id'), nullable=True)
350
356
  md: orm.Mapped[dict[str, Any]] = orm.mapped_column(JSONB, nullable=False) # FunctionMd
351
357
  binary_obj: orm.Mapped[bytes | None] = orm.mapped_column(LargeBinary, nullable=True)
352
-
353
-
354
- class FullTableMd(NamedTuple):
355
- tbl_md: TableMd
356
- version_md: TableVersionMd
357
- schema_version_md: TableSchemaVersionMd
358
-
359
- @property
360
- def is_pure_snapshot(self) -> bool:
361
- return (
362
- self.tbl_md.view_md is not None
363
- and self.tbl_md.view_md.is_snapshot
364
- and self.tbl_md.view_md.predicate is None
365
- and len(self.schema_version_md.columns) == 0
366
- )
367
-
368
- def as_dict(self) -> dict[str, Any]:
369
- return {
370
- 'table_id': self.tbl_md.tbl_id,
371
- 'table_md': dataclasses.asdict(self.tbl_md),
372
- 'table_version_md': dataclasses.asdict(self.version_md),
373
- 'table_schema_version_md': dataclasses.asdict(self.schema_version_md),
374
- }
375
-
376
- @classmethod
377
- def from_dict(cls, data_dict: dict[str, Any]) -> 'FullTableMd':
378
- return FullTableMd(
379
- tbl_md=md_from_dict(TableMd, data_dict['table_md']),
380
- version_md=md_from_dict(TableVersionMd, data_dict['table_version_md']),
381
- schema_version_md=md_from_dict(TableSchemaVersionMd, data_dict['table_schema_version_md']),
382
- )
@@ -1,3 +1,3 @@
1
1
  # ruff: noqa: F401
2
2
 
3
- from .publish import delete_replica, pull_replica, push_replica
3
+ from .publish import delete_replica, list_table_versions, pull_replica, push_replica
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import dataclasses
2
3
  import io
3
4
  import json
4
5
  import logging
@@ -20,6 +21,7 @@ import sqlalchemy as sql
20
21
 
21
22
  import pixeltable as pxt
22
23
  from pixeltable import catalog, exceptions as excs, metadata, type_system as ts
24
+ from pixeltable.catalog.table_version import TableVersionCompleteMd
23
25
  from pixeltable.env import Env
24
26
  from pixeltable.exprs.data_row import CellMd
25
27
  from pixeltable.metadata import schema
@@ -51,7 +53,7 @@ class TablePackager:
51
53
  tmp_dir: Path # Temporary directory where the package will reside
52
54
  tables_dir: Path # Directory where the Parquet tables will be written
53
55
  media_files: dict[Path, str] # Mapping from local media file paths to their tarball names
54
- md: dict[str, Any]
56
+ bundle_md: dict[str, Any]
55
57
 
56
58
  bundle_path: Path
57
59
  preview_header: dict[str, str]
@@ -62,16 +64,16 @@ class TablePackager:
62
64
  self.tmp_dir = TempStore.create_path()
63
65
  self.media_files = {}
64
66
 
65
- # Load metadata
67
+ # Load metadata and convert to JSON immediately
66
68
  with catalog.Catalog.get().begin_xact(for_write=False):
67
69
  tbl_md = catalog.Catalog.get().load_replica_md(table)
68
- self.md = {
70
+ self.bundle_md = {
69
71
  'pxt_version': pxt.__version__,
70
72
  'pxt_md_version': metadata.VERSION,
71
- 'md': {'tables': [md.as_dict() for md in tbl_md]},
73
+ 'md': [dataclasses.asdict(md) for md in tbl_md],
72
74
  }
73
75
  if additional_md is not None:
74
- self.md.update(additional_md)
76
+ self.bundle_md.update(additional_md)
75
77
 
76
78
  def package(self) -> Path:
77
79
  """
@@ -82,7 +84,7 @@ class TablePackager:
82
84
  _logger.info(f'Packaging table {self.table._path()!r} and its ancestors in: {self.tmp_dir}')
83
85
  self.tmp_dir.mkdir()
84
86
  with open(self.tmp_dir / 'metadata.json', 'w', encoding='utf8') as fp:
85
- json.dump(self.md, fp)
87
+ json.dump(self.bundle_md, fp)
86
88
  self.tables_dir = self.tmp_dir / 'tables'
87
89
  self.tables_dir.mkdir()
88
90
  with catalog.Catalog.get().begin_xact(for_write=False):
@@ -94,10 +96,10 @@ class TablePackager:
94
96
  self.bundle_path = self.__build_tarball()
95
97
 
96
98
  _logger.info('Extracting preview data.')
97
- self.md['row_count'] = self.table.count()
99
+ self.bundle_md['row_count'] = self.table.count()
98
100
  preview_header, preview = self.__extract_preview_data()
99
- self.md['preview_header'] = preview_header
100
- self.md['preview_data'] = preview
101
+ self.bundle_md['preview_header'] = preview_header
102
+ self.bundle_md['preview_data'] = preview
101
103
 
102
104
  _logger.info(f'Packaging complete: {self.bundle_path}')
103
105
  return self.bundle_path
@@ -358,19 +360,20 @@ class TableRestorer:
358
360
 
359
361
  Args:
360
362
  tbl_path: Pixeltable path (such as 'my_dir.my_table') where the materialized table will be made visible.
361
- md: Optional metadata dictionary. If not provided, metadata will be read from the tarball's `metadata.json`.
363
+ bundle_md: Optional metadata dictionary.
364
+ If not provided, metadata will be read from the tarball's `metadata.json`.
362
365
  The metadata contains table_md, table_version_md, and table_schema_version_md entries for each ancestor
363
366
  of the table being restored, as written out by `TablePackager`.
364
367
  """
365
368
 
366
369
  tbl_path: str
367
- md: dict[str, Any] | None
370
+ bundle_md: dict[str, Any] | None
368
371
  tmp_dir: Path
369
372
  media_files: dict[str, str] # Mapping from pxtmedia:// URLs to local file:// URLs
370
373
 
371
- def __init__(self, tbl_path: str, md: dict[str, Any] | None = None) -> None:
374
+ def __init__(self, tbl_path: str, bundle_md: dict[str, Any] | None = None) -> None:
372
375
  self.tbl_path = tbl_path
373
- self.md = md
376
+ self.bundle_md = bundle_md
374
377
  self.tmp_dir = TempStore.create_path()
375
378
  self.media_files = {}
376
379
 
@@ -380,12 +383,12 @@ class TableRestorer:
380
383
  with tarfile.open(bundle_path, 'r:bz2') as tf:
381
384
  tf.extractall(path=self.tmp_dir)
382
385
 
383
- if self.md is None:
386
+ if self.bundle_md is None:
384
387
  # No metadata supplied; read it from the archive
385
388
  with open(self.tmp_dir / 'metadata.json', 'r', encoding='utf8') as fp:
386
- self.md = json.load(fp)
389
+ self.bundle_md = json.load(fp)
387
390
 
388
- pxt_md_version = self.md['pxt_md_version']
391
+ pxt_md_version = self.bundle_md['pxt_md_version']
389
392
  assert isinstance(pxt_md_version, int)
390
393
 
391
394
  if pxt_md_version != metadata.VERSION:
@@ -393,8 +396,9 @@ class TableRestorer:
393
396
  f'Pixeltable metadata version mismatch: {pxt_md_version} != {metadata.VERSION}.\n'
394
397
  'Please upgrade Pixeltable to use this dataset: pip install -U pixeltable'
395
398
  )
399
+ # Convert tables metadata from dict to list of TableVersionCompleteMd
400
+ tbl_md = [schema.md_from_dict(TableVersionCompleteMd, t) for t in self.bundle_md['md']]
396
401
 
397
- tbl_md = [schema.FullTableMd.from_dict(t) for t in self.md['md']['tables']]
398
402
  for md in tbl_md:
399
403
  md.tbl_md.is_replica = True
400
404
 
@@ -421,7 +425,7 @@ class TableRestorer:
421
425
 
422
426
  return cat.get_table_by_id(UUID(tbl_md[0].tbl_md.tbl_id))
423
427
 
424
- def __import_table(self, bundle_path: Path, tv: catalog.TableVersion, tbl_md: schema.FullTableMd) -> None:
428
+ def __import_table(self, bundle_path: Path, tv: catalog.TableVersion, tbl_md: TableVersionCompleteMd) -> None:
425
429
  """
426
430
  Import the Parquet table into the Pixeltable catalog.
427
431
  """
@@ -0,0 +1,34 @@
1
+ """
2
+ Pixeltable Core Protocol
3
+
4
+ This module contains the core protocol structures for pixeltable table operations
5
+ that can be shared between pixeltable core and cloud implementations.
6
+ """
7
+
8
+ from .common import PxtUri, RequestBaseModel, StorageDestination
9
+ from .operation_types import ReplicaOperationType
10
+ from .replica import (
11
+ DeleteRequest,
12
+ DeleteResponse,
13
+ FinalizeRequest,
14
+ FinalizeResponse,
15
+ PublishRequest,
16
+ PublishResponse,
17
+ ReplicateRequest,
18
+ ReplicateResponse,
19
+ )
20
+
21
+ __all__ = [
22
+ 'DeleteRequest',
23
+ 'DeleteResponse',
24
+ 'FinalizeRequest',
25
+ 'FinalizeResponse',
26
+ 'PublishRequest',
27
+ 'PublishResponse',
28
+ 'PxtUri',
29
+ 'ReplicaOperationType',
30
+ 'ReplicateRequest',
31
+ 'ReplicateResponse',
32
+ 'RequestBaseModel',
33
+ 'StorageDestination',
34
+ ]
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from abc import ABC, abstractmethod
5
+ from enum import Enum
6
+ from typing import Any
7
+ from urllib.parse import urlparse
8
+ from uuid import UUID
9
+
10
+ from pydantic import BaseModel, model_validator
11
+
12
+
13
+ class StorageDestination(str, Enum):
14
+ """Storage destination types for table snapshots."""
15
+
16
+ S3 = 's3'
17
+ R2 = 'r2'
18
+ GCS = 'gcs'
19
+
20
+
21
+ def is_valid_uuid(uuid_string: str) -> bool:
22
+ """Check if a string is a valid UUID."""
23
+ try:
24
+ uuid.UUID(uuid_string)
25
+ return True
26
+ except (ValueError, TypeError):
27
+ return False
28
+
29
+
30
+ class PxtUri(BaseModel):
31
+ """Pixeltable URI model for pxt:// URIs with validation and parsing."""
32
+
33
+ uri: str # The full URI string
34
+
35
+ # Parsed components
36
+ org: str # Organization slug from the URI
37
+ db: str | None # Database slug from the URI (optional)
38
+ path: str | None = None # The table or directory path (None if using UUID)
39
+ id: UUID | None = None # The table UUID (None if using path)
40
+ version: int | None = None # Optional version number parsed from URI (format: identifier:<version>)
41
+
42
+ def __init__(self, uri: str | dict | None = None, **kwargs: Any) -> None:
43
+ # Handle dict input directly (from JSON deserialization or explicit dict)
44
+ if isinstance(uri, dict):
45
+ # Dict input goes directly to Pydantic, which will call parse_uri
46
+ kwargs.update(uri)
47
+ elif uri is not None:
48
+ # Validate that uri is a string when passed as positional argument
49
+ if not isinstance(uri, str):
50
+ raise ValueError(f'Invalid data type for PxtUri: expected str or dict, got {type(uri)}')
51
+ kwargs['uri'] = uri
52
+ super().__init__(**kwargs)
53
+
54
+ @model_validator(mode='before')
55
+ @classmethod
56
+ def parse_uri(cls, data: Any) -> dict:
57
+ # Handle case where data is already a string (from JSON deserialization)
58
+ if isinstance(data, str):
59
+ uri = data
60
+ elif isinstance(data, dict):
61
+ uri = data.get('uri')
62
+ if uri is None:
63
+ raise ValueError('URI must be provided in dict with "uri" key')
64
+ if not isinstance(uri, str):
65
+ raise ValueError(f'URI in dict must be a string, got {type(uri)}')
66
+ else:
67
+ raise ValueError(f'Invalid data type for PxtUri: expected str or dict, got {type(data)}')
68
+
69
+ return {'uri': uri, **cls._parse_and_validate_uri(uri)}
70
+
71
+ def __str__(self) -> str:
72
+ """Return the URI string."""
73
+ return self.uri
74
+
75
+ @classmethod
76
+ def _parse_and_validate_uri(cls, uri: str) -> dict:
77
+ """Parse and validate a URI string, return parsed components."""
78
+ if not uri.startswith('pxt://'):
79
+ raise ValueError('URI must start with pxt://')
80
+
81
+ parsed = urlparse(uri)
82
+ if parsed.scheme != 'pxt':
83
+ raise ValueError('URI must use pxt:// scheme')
84
+
85
+ if not parsed.netloc:
86
+ raise ValueError('URI must have an organization')
87
+
88
+ # Parse netloc for org and optional db
89
+ netloc_parts = parsed.netloc.split(':')
90
+ org = netloc_parts[0]
91
+ if not org:
92
+ raise ValueError('URI must have an organization')
93
+
94
+ db = netloc_parts[1] if len(netloc_parts) > 1 else None
95
+
96
+ # Allow root path (/) as valid, but reject missing path
97
+ if parsed.path is None:
98
+ raise ValueError('URI must have a path')
99
+
100
+ # Get path and remove leading slash (but keep empty string for root path)
101
+ # path will be '/' for root directory or '/path/to/table' for regular paths
102
+ path_part = parsed.path.lstrip('/') if parsed.path else ''
103
+
104
+ # Handle version parsing (format: identifier:version)
105
+ # For root path, path_part will be empty string after lstrip
106
+ if path_part and ':' in path_part:
107
+ parts = path_part.rsplit(':', 1) # Split from right, only once
108
+ if len(parts) == 2 and parts[1].isdigit():
109
+ identifier, version = parts[0], int(parts[1])
110
+ else:
111
+ identifier, version = path_part, None
112
+ else:
113
+ identifier, version = path_part, None
114
+
115
+ # Parse identifier into either a path string or UUID
116
+ path: str | None = None
117
+ id: UUID | None = None
118
+ if identifier:
119
+ if is_valid_uuid(identifier):
120
+ # It's a UUID
121
+ id = UUID(identifier)
122
+ else:
123
+ # It's a path - keep as string (URI format uses / as separator)
124
+ # Empty string means root path
125
+ path = identifier or ''
126
+ else:
127
+ # Empty identifier means root path
128
+ path = ''
129
+
130
+ return {'org': org, 'db': db, 'path': path, 'id': id, 'version': version}
131
+
132
+ @classmethod
133
+ def from_components(
134
+ cls,
135
+ org: str,
136
+ path: str | None = None,
137
+ id: UUID | None = None,
138
+ db: str | None = None,
139
+ version: int | None = None,
140
+ ) -> PxtUri:
141
+ """Construct a PxtUri from its components."""
142
+ if path is None and id is None:
143
+ raise ValueError('Either path or id must be provided')
144
+ if path is not None and id is not None:
145
+ raise ValueError('Cannot specify both path and id')
146
+
147
+ # Build the URI string from components
148
+ netloc = org if db is None else f'{org}:{db}'
149
+
150
+ # Use path or UUID as identifier
151
+ if id is not None:
152
+ identifier = str(id)
153
+ elif path is not None:
154
+ # Path is already in URI format (slash-separated)
155
+ identifier = path or ''
156
+ else:
157
+ identifier = ''
158
+
159
+ path_part = f'{identifier}:{version}' if version is not None else identifier
160
+ uri = f'pxt://{netloc}/{path_part}'
161
+ return cls(uri=uri)
162
+
163
+
164
+ class RequestBaseModel(BaseModel, ABC):
165
+ """Abstract base model for protocol requests that must have a PxtUri."""
166
+
167
+ @abstractmethod
168
+ def get_pxt_uri(self) -> PxtUri:
169
+ """Get the PxtUri from this request. Must be implemented by subclasses."""
170
+ pass
@@ -0,0 +1,33 @@
1
+ """
2
+ Replica operation types for pixeltable table replica operations.
3
+
4
+ This module defines the replica operation types that are shared between
5
+ pixeltable core and cloud implementations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+
12
+
13
+ class ReplicaOperationType(str, Enum):
14
+ """Replica operation types for table replica operations."""
15
+
16
+ # Table replica operations
17
+ PUBLISH_REPLICA = 'publish_replica'
18
+ FINALIZE_REPLICA = 'finalize_replica'
19
+ CLONE_REPLICA = 'clone_replica'
20
+ DELETE_REPLICA = 'delete_replica'
21
+
22
+ def is_replica_operation(self) -> bool:
23
+ """Check if operation is a replica operation."""
24
+ return self in REPLICA_OPERATIONS
25
+
26
+
27
+ # Define the operation sets as module-level constants
28
+ REPLICA_OPERATIONS: set[ReplicaOperationType] = {
29
+ ReplicaOperationType.PUBLISH_REPLICA,
30
+ ReplicaOperationType.FINALIZE_REPLICA,
31
+ ReplicaOperationType.CLONE_REPLICA,
32
+ ReplicaOperationType.DELETE_REPLICA,
33
+ }
@@ -0,0 +1,109 @@
1
+ """
2
+ Core table protocol for pixeltable operations.
3
+
4
+ This module contains the core table protocol structures that can be shared
5
+ between pixeltable core and cloud implementations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Literal
11
+ from uuid import UUID
12
+
13
+ from pydantic import AnyUrl, BaseModel
14
+
15
+ from pixeltable.catalog.table_version import TableVersionCompleteMd
16
+
17
+ from .common import PxtUri, RequestBaseModel, StorageDestination
18
+ from .operation_types import ReplicaOperationType
19
+
20
+
21
+ class PublishRequest(RequestBaseModel):
22
+ """Request to publish or push table replica."""
23
+
24
+ operation_type: Literal[ReplicaOperationType.PUBLISH_REPLICA] = ReplicaOperationType.PUBLISH_REPLICA
25
+ table_uri: PxtUri # If PxtUri#id is not None then it's considered a push replica request
26
+ pxt_version: str
27
+ pxt_md_version: int
28
+ md: list[TableVersionCompleteMd]
29
+ is_public: bool = False
30
+ bucket_name: str | None = None # Optional bucket name, falls back to org's default bucket if not provided
31
+
32
+ def get_pxt_uri(self) -> PxtUri:
33
+ """Get the PxtUri from this request."""
34
+ return self.table_uri
35
+
36
+
37
+ class PublishResponse(BaseModel):
38
+ """Response from publishing a table replica."""
39
+
40
+ upload_id: UUID
41
+ destination: StorageDestination
42
+ destination_uri: AnyUrl
43
+ max_size: int | None = None # Maximum size that can be used by this replica, used for R2 home buckets
44
+
45
+
46
+ class FinalizeRequest(RequestBaseModel):
47
+ """Request to finalize a table replica."""
48
+
49
+ operation_type: Literal[ReplicaOperationType.FINALIZE_REPLICA] = ReplicaOperationType.FINALIZE_REPLICA
50
+ table_uri: PxtUri # Use same table_uri that was given during publish replica request
51
+ upload_id: UUID
52
+ size: int
53
+ sha256: str
54
+ datafile: str
55
+ row_count: int
56
+ preview_header: dict[str, str]
57
+ preview_data: list[list[Any]]
58
+
59
+ def get_pxt_uri(self) -> PxtUri:
60
+ """Get the PxtUri from this request."""
61
+ return self.table_uri
62
+
63
+
64
+ class FinalizeResponse(BaseModel):
65
+ """Response from finalizing a table replica."""
66
+
67
+ confirmed_table_uri: PxtUri
68
+ version: int | None = None # Version that was pushed to replica
69
+
70
+
71
+ class DeleteRequest(RequestBaseModel):
72
+ """Request to delete a table replica."""
73
+
74
+ operation_type: Literal[ReplicaOperationType.DELETE_REPLICA] = ReplicaOperationType.DELETE_REPLICA
75
+ table_uri: PxtUri
76
+ version: int | None = None # Delete a version in replica
77
+
78
+ def get_pxt_uri(self) -> PxtUri:
79
+ """Get the PxtUri from this request."""
80
+ return self.table_uri
81
+
82
+
83
+ class DeleteResponse(BaseModel):
84
+ """Response from deleting a table replica."""
85
+
86
+ table_uri: PxtUri
87
+ version: int | None = None
88
+
89
+
90
+ class ReplicateRequest(RequestBaseModel):
91
+ """Request to clone a table replica."""
92
+
93
+ operation_type: Literal[ReplicaOperationType.CLONE_REPLICA] = ReplicaOperationType.CLONE_REPLICA
94
+ table_uri: PxtUri
95
+
96
+ def get_pxt_uri(self) -> PxtUri:
97
+ """Get the PxtUri from this request."""
98
+ return self.table_uri
99
+
100
+
101
+ class ReplicateResponse(BaseModel):
102
+ """Response from cloning a table replica."""
103
+
104
+ table_uri: PxtUri
105
+ pxt_md_version: int
106
+ destination: StorageDestination
107
+ destination_uri: AnyUrl
108
+ md: list[TableVersionCompleteMd]
109
+ version: int | None = None