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.
- pixeltable/_version.py +1 -1
- pixeltable/catalog/catalog.py +76 -50
- pixeltable/catalog/column.py +29 -16
- pixeltable/catalog/insertable_table.py +2 -2
- pixeltable/catalog/path.py +4 -10
- pixeltable/catalog/table.py +51 -0
- pixeltable/catalog/table_version.py +40 -7
- pixeltable/catalog/view.py +2 -2
- pixeltable/config.py +1 -0
- pixeltable/env.py +2 -0
- pixeltable/exprs/column_ref.py +2 -1
- pixeltable/functions/__init__.py +1 -0
- pixeltable/functions/image.py +2 -8
- pixeltable/functions/reve.py +250 -0
- pixeltable/functions/video.py +534 -1
- pixeltable/globals.py +2 -1
- pixeltable/index/base.py +5 -18
- pixeltable/index/btree.py +6 -2
- pixeltable/index/embedding_index.py +4 -4
- pixeltable/metadata/schema.py +7 -32
- pixeltable/share/__init__.py +1 -1
- pixeltable/share/packager.py +22 -18
- pixeltable/share/protocol/__init__.py +34 -0
- pixeltable/share/protocol/common.py +170 -0
- pixeltable/share/protocol/operation_types.py +33 -0
- pixeltable/share/protocol/replica.py +109 -0
- pixeltable/share/publish.py +91 -56
- pixeltable/store.py +11 -15
- pixeltable/utils/av.py +87 -1
- pixeltable/utils/dbms.py +15 -11
- pixeltable/utils/image.py +10 -0
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/METADATA +2 -1
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/RECORD +36 -31
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/WHEEL +0 -0
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/entry_points.txt +0 -0
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.21.dist-info}/licenses/LICENSE +0 -0
pixeltable/metadata/schema.py
CHANGED
|
@@ -2,7 +2,7 @@ import dataclasses
|
|
|
2
2
|
import types
|
|
3
3
|
import typing
|
|
4
4
|
import uuid
|
|
5
|
-
from typing import Any,
|
|
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
|
-
)
|
pixeltable/share/__init__.py
CHANGED
pixeltable/share/packager.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
70
|
+
self.bundle_md = {
|
|
69
71
|
'pxt_version': pxt.__version__,
|
|
70
72
|
'pxt_md_version': metadata.VERSION,
|
|
71
|
-
'md':
|
|
73
|
+
'md': [dataclasses.asdict(md) for md in tbl_md],
|
|
72
74
|
}
|
|
73
75
|
if additional_md is not None:
|
|
74
|
-
self.
|
|
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.
|
|
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.
|
|
99
|
+
self.bundle_md['row_count'] = self.table.count()
|
|
98
100
|
preview_header, preview = self.__extract_preview_data()
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
374
|
+
def __init__(self, tbl_path: str, bundle_md: dict[str, Any] | None = None) -> None:
|
|
372
375
|
self.tbl_path = tbl_path
|
|
373
|
-
self.
|
|
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.
|
|
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.
|
|
389
|
+
self.bundle_md = json.load(fp)
|
|
387
390
|
|
|
388
|
-
pxt_md_version = self.
|
|
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:
|
|
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
|