Skip to content

Commit 5be3e74

Browse files
committed
PR comments
1 parent 407dc14 commit 5be3e74

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

pyiceberg/catalog/rest/planning_models.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pydantic import Field
2323

2424
from pyiceberg.catalog.rest.expression import Expression
25+
from pyiceberg.catalog.rest.response import ErrorResponse as IcebergErrorResponse
2526
from pyiceberg.typedef import IcebergBaseModel, IcebergRootModel
2627

2728

@@ -345,3 +346,37 @@ class ScanTasks(IcebergBaseModel):
345346
)
346347
file_scan_tasks: Optional[List[FileScanTask]] = Field(None, alias="file-scan-tasks")
347348
plan_tasks: Optional[List[PlanTask]] = Field(None, alias="plan-tasks")
349+
350+
351+
class FailedPlanningResult(IcebergErrorResponse):
352+
"""Failed server-side planning result."""
353+
354+
status: Literal["failed"]
355+
356+
357+
class AsyncPlanningResult(IcebergBaseModel):
358+
status: Literal["submitted"]
359+
plan_id: str = Field(..., alias="plan-id", description="ID used to track a planning request")
360+
361+
362+
class CancelledPlanningResult(IcebergBaseModel):
363+
"""A cancelled planning result."""
364+
365+
status: Literal["cancelled"]
366+
367+
368+
class CompletedPlanningWithIDResult(ScanTasks):
369+
"""Completed server-side planning result."""
370+
371+
status: Literal["completed"]
372+
plan_id: Optional[str] = Field(None, alias="plan-id", description="ID used to track a planning request")
373+
374+
375+
class PlanTableScanResult(
376+
IcebergRootModel[Union[CompletedPlanningWithIDResult, FailedPlanningResult, AsyncPlanningResult, CancelledPlanningResult]]
377+
):
378+
"""Result of server-side scan planning for planTableScan."""
379+
380+
root: Union[CompletedPlanningWithIDResult, FailedPlanningResult, AsyncPlanningResult, CancelledPlanningResult] = Field(
381+
..., discriminator="status"
382+
)

tests/catalog/test_rest_serializers.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,29 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
import json
18+
1719
from pyiceberg.catalog.rest.expression import (
1820
AndOrExpression,
1921
Expression,
2022
LiteralExpression,
2123
Term,
2224
)
2325
from pyiceberg.catalog.rest.planning_models import (
26+
AsyncPlanningResult,
27+
CancelledPlanningResult,
28+
CompletedPlanningWithIDResult,
2429
DataFile,
2530
DeleteFile,
2631
EqualityDeleteFile,
32+
FailedPlanningResult,
2733
FileScanTask,
2834
PlanTableScanRequest,
35+
PlanTableScanResult,
2936
PositionDeleteFile,
3037
ScanTasks,
3138
)
39+
from pyiceberg.catalog.rest.response import ErrorResponseMessage
3240

3341

3442
def test_serialize_plan_table_scan_request() -> None:
@@ -165,3 +173,118 @@ def snapshot_json_for_plan_table_scan_request() -> str:
165173

166174
def snapshot_json_for_scan_tasks() -> str:
167175
return """{"delete-files":[{"content":"position-deletes","file-path":"/path/to/delete-a.parquet","file-format":"parquet","spec-id":0,"partition":[],"file-size-in-bytes":256,"record-count":10},{"content":"equality-deletes","file-path":"/path/to/delete-b.parquet","file-format":"parquet","spec-id":0,"partition":[],"file-size-in-bytes":256,"record-count":10,"equality-ids":[1,2]}],"file-scan-tasks":[{"data-file":{"content":"data","file-path":"/path/to/data-a.parquet","file-format":"parquet","spec-id":0,"partition":[],"file-size-in-bytes":1024,"record-count":56},"delete-file-references":[0,1]}]}"""
176+
177+
178+
def test_deserialize_async_planning_result() -> None:
179+
"""Test deserializing a dict to an AsyncPlanningResult"""
180+
result = PlanTableScanResult.model_validate_json(snapshot_json_for_async_planning_result())
181+
expected = AsyncPlanningResult(status="submitted", plan_id="plan-123")
182+
# Assert that deserialized dict == Python object
183+
assert result.root == expected
184+
185+
186+
def test_serialize_async_planning_result() -> None:
187+
"""Test serializing an AsyncPlanningResult to a dict"""
188+
result = PlanTableScanResult(root=AsyncPlanningResult(status="submitted", plan_id="plan-123"))
189+
# Assert that JSON matches
190+
assert json.loads(result.model_dump_json(by_alias=True)) == json.loads(snapshot_json_for_async_planning_result())
191+
192+
193+
def snapshot_json_for_async_planning_result() -> str:
194+
return """{"status":"submitted","plan-id":"plan-123"}"""
195+
196+
197+
def test_deserialize_failed_planning_result() -> None:
198+
"""Test deserializing a dict to a FailedPlanningResult"""
199+
result = PlanTableScanResult.model_validate_json(snapshot_json_for_failed_planning_result())
200+
expected = FailedPlanningResult(
201+
status="failed",
202+
error=ErrorResponseMessage(
203+
message="The plan is invalid",
204+
type="NoSuchPlanException",
205+
code=404,
206+
),
207+
)
208+
# Assert that deserialized dict == Python object
209+
assert result.root == expected
210+
211+
212+
def test_serialize_failed_planning_result() -> None:
213+
"""Test serializing a FailedPlanningResult to a dict"""
214+
result = PlanTableScanResult(
215+
root=FailedPlanningResult(
216+
status="failed",
217+
error=ErrorResponseMessage(
218+
message="The plan is invalid",
219+
type="NoSuchPlanException",
220+
code=404,
221+
),
222+
)
223+
)
224+
# Assert that JSON matches
225+
assert json.loads(result.model_dump_json(by_alias=True, exclude_none=True)) == json.loads(
226+
snapshot_json_for_failed_planning_result()
227+
)
228+
229+
230+
def snapshot_json_for_failed_planning_result() -> str:
231+
return """{"status":"failed","error":{"message":"The plan is invalid","type":"NoSuchPlanException","code":404}}"""
232+
233+
234+
def test_deserialize_cancelled_planning_result() -> None:
235+
"""Test deserializing a dict to an CancelledPlanningResult"""
236+
result = PlanTableScanResult.model_validate_json(snapshot_json_for_cancelled_planning_result())
237+
expected = CancelledPlanningResult(status="cancelled")
238+
# Assert that deserialized dict == Python object
239+
assert result.root == expected
240+
241+
242+
def test_serialize_cancelled_planning_result() -> None:
243+
"""Test serializing an CancelledPlanningResult to a dict"""
244+
result = PlanTableScanResult(root=CancelledPlanningResult(status="cancelled"))
245+
# Assert that JSON matches
246+
assert json.loads(result.model_dump_json(by_alias=True)) == json.loads(snapshot_json_for_cancelled_planning_result())
247+
248+
249+
def snapshot_json_for_cancelled_planning_result() -> str:
250+
return """{"status":"cancelled"}"""
251+
252+
253+
def test_deserialize_completed_planning_with_id_result() -> None:
254+
"""Test deserializing a dict to a CompletedPlanningWithIDResult"""
255+
scan_tasks_dict = json.loads(snapshot_json_for_scan_tasks())
256+
scan_tasks_dict["status"] = "completed"
257+
scan_tasks_dict["plan-id"] = "plan-456"
258+
json_str = json.dumps(scan_tasks_dict)
259+
260+
result = PlanTableScanResult.model_validate_json(json_str)
261+
expected_scan_tasks = ScanTasks.model_validate_json(snapshot_json_for_scan_tasks())
262+
263+
expected = CompletedPlanningWithIDResult(
264+
status="completed",
265+
plan_id="plan-456",
266+
file_scan_tasks=expected_scan_tasks.file_scan_tasks,
267+
delete_files=expected_scan_tasks.delete_files,
268+
)
269+
# Assert that deserialized dict == Python object
270+
assert result.root == expected
271+
272+
273+
def test_serialize_completed_planning_with_id_result() -> None:
274+
"""Test serializing a CompletedPlanningWithIDResult to a dict"""
275+
expected_scan_tasks = ScanTasks.model_validate_json(snapshot_json_for_scan_tasks())
276+
result = PlanTableScanResult(
277+
root=CompletedPlanningWithIDResult(
278+
status="completed",
279+
plan_id="plan-456",
280+
file_scan_tasks=expected_scan_tasks.file_scan_tasks,
281+
delete_files=expected_scan_tasks.delete_files,
282+
)
283+
)
284+
285+
scan_tasks_dict = json.loads(snapshot_json_for_scan_tasks())
286+
scan_tasks_dict["status"] = "completed"
287+
scan_tasks_dict["plan-id"] = "plan-456"
288+
289+
# Assert that JSON matches
290+
assert json.loads(result.model_dump_json(exclude_none=True, by_alias=True)) == scan_tasks_dict

0 commit comments

Comments
 (0)