fleet-python 0.2.100__tar.gz → 0.2.101__tar.gz

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 (119) hide show
  1. {fleet_python-0.2.100/fleet_python.egg-info → fleet_python-0.2.101}/PKG-INFO +1 -1
  2. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/__init__.py +1 -1
  3. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/base.py +1 -1
  5. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/instance/client.py +6 -2
  6. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/sqlite.py +79 -2
  7. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/base.py +1 -1
  8. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/client.py +6 -2
  9. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/sqlite.py +79 -2
  10. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/db.py +483 -0
  11. {fleet_python-0.2.100 → fleet_python-0.2.101/fleet_python.egg-info}/PKG-INFO +1 -1
  12. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/SOURCES.txt +1 -0
  13. {fleet_python-0.2.100 → fleet_python-0.2.101}/pyproject.toml +1 -1
  14. fleet_python-0.2.101/tests/test_expect_exactly.py +4148 -0
  15. {fleet_python-0.2.100 → fleet_python-0.2.101}/LICENSE +0 -0
  16. {fleet_python-0.2.100 → fleet_python-0.2.101}/README.md +0 -0
  17. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/diff_example.py +0 -0
  18. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/dsl_example.py +0 -0
  19. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example.py +0 -0
  20. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/exampleResume.py +0 -0
  21. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_account.py +0 -0
  22. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_action_log.py +0 -0
  23. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_client.py +0 -0
  24. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_mcp_anthropic.py +0 -0
  25. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_mcp_openai.py +0 -0
  26. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_sync.py +0 -0
  27. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_task.py +0 -0
  28. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_tasks.py +0 -0
  29. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_verifier.py +0 -0
  30. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/export_tasks.py +0 -0
  31. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/fetch_tasks.py +0 -0
  32. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/gemini_example.py +0 -0
  33. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/import_tasks.py +0 -0
  34. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/iterate_verifiers.py +0 -0
  35. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/json_tasks_example.py +0 -0
  36. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/nova_act_example.py +0 -0
  37. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/openai_example.py +0 -0
  38. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/openai_simple_example.py +0 -0
  39. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/query_builder_example.py +0 -0
  40. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/quickstart.py +0 -0
  41. {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/test_cdp_logging.py +0 -0
  42. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/client.py +0 -0
  43. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/env/__init__.py +0 -0
  44. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/env/client.py +0 -0
  45. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/exceptions.py +0 -0
  46. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/global_client.py +0 -0
  47. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/instance/__init__.py +0 -0
  48. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/instance/base.py +0 -0
  49. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/models.py +0 -0
  50. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/__init__.py +0 -0
  51. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/api.py +0 -0
  52. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/base.py +0 -0
  53. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/browser.py +0 -0
  54. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/mcp.py +0 -0
  55. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/tasks.py +0 -0
  56. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/verifiers/__init__.py +0 -0
  57. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/verifiers/bundler.py +0 -0
  58. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/verifiers/verifier.py +0 -0
  59. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/__init__.py +0 -0
  60. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/Dockerfile +0 -0
  61. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/__init__.py +0 -0
  62. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/agent.py +0 -0
  63. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp/main.py +0 -0
  64. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
  65. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
  66. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
  67. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/requirements.txt +0 -0
  68. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/start.sh +0 -0
  69. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/orchestrator.py +0 -0
  70. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/types.py +0 -0
  71. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/utils.py +0 -0
  72. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/cli.py +0 -0
  73. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/client.py +0 -0
  74. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/config.py +0 -0
  75. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/env/__init__.py +0 -0
  76. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/env/client.py +0 -0
  77. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/eval/__init__.py +0 -0
  78. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/eval/uploader.py +0 -0
  79. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/exceptions.py +0 -0
  80. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/global_client.py +0 -0
  81. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/__init__.py +0 -0
  82. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/base.py +0 -0
  83. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/models.py +0 -0
  84. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/models.py +0 -0
  85. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/proxy/__init__.py +0 -0
  86. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/proxy/proxy.py +0 -0
  87. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/proxy/whitelist.py +0 -0
  88. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/__init__.py +0 -0
  89. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/api.py +0 -0
  90. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/base.py +0 -0
  91. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/browser.py +0 -0
  92. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/mcp.py +0 -0
  93. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/tasks.py +0 -0
  94. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/types.py +0 -0
  95. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/__init__.py +0 -0
  96. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/http_logging.py +0 -0
  97. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/logging.py +0 -0
  98. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/playwright.py +0 -0
  99. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/__init__.py +0 -0
  100. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/bundler.py +0 -0
  101. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/code.py +0 -0
  102. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/decorator.py +0 -0
  103. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/parse.py +0 -0
  104. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/sql_differ.py +0 -0
  105. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/verifier.py +0 -0
  106. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/dependency_links.txt +0 -0
  107. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/entry_points.txt +0 -0
  108. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/requires.txt +0 -0
  109. {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/top_level.txt +0 -0
  110. {fleet_python-0.2.100 → fleet_python-0.2.101}/scripts/fix_sync_imports.py +0 -0
  111. {fleet_python-0.2.100 → fleet_python-0.2.101}/scripts/unasync.py +0 -0
  112. {fleet_python-0.2.100 → fleet_python-0.2.101}/setup.cfg +0 -0
  113. {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/__init__.py +0 -0
  114. {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_app_method.py +0 -0
  115. {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_expect_only.py +0 -0
  116. {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_instance_dispatch.py +0 -0
  117. {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_sqlite_resource_dual_mode.py +0 -0
  118. {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  119. {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.100
3
+ Version: 0.2.101
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.100"
76
+ __version__ = "0.2.101"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
@@ -44,7 +44,7 @@ from ..types import VerifierFunction
44
44
  from .. import env
45
45
  from . import global_client as _async_global_client
46
46
 
47
- __version__ = "0.2.100"
47
+ __version__ = "0.2.101"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.100"
29
+ __version__ = "0.2.101"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -166,8 +166,12 @@ class AsyncInstanceClient:
166
166
  if self._resources is None:
167
167
  response = await self.client.request("GET", "/resources")
168
168
  if response.status_code != 200:
169
- self._resources = []
170
- return
169
+ response_body = response.text[:500] if response.text else "empty"
170
+ self._resources = [] # Mark as loaded (empty) to prevent retries
171
+ raise FleetEnvironmentError(
172
+ f"Failed to load instance resources: status_code={response.status_code} "
173
+ f"(url={self.base_url}, response={response_body})"
174
+ )
171
175
 
172
176
  # Handle both old and new response formats
173
177
  response_data = response.json()
@@ -22,6 +22,7 @@ from fleet.verifiers.db import (
22
22
  _get_row_identifier,
23
23
  _format_row_for_error,
24
24
  _values_equivalent,
25
+ validate_diff_expect_exactly,
25
26
  )
26
27
 
27
28
 
@@ -1755,7 +1756,8 @@ class AsyncSnapshotDiff:
1755
1756
  return await self._expect_no_changes()
1756
1757
 
1757
1758
  resource = self.after.resource
1758
- if resource.client is not None and resource._mode == "http":
1759
+ # Disabled: structured diff endpoint not yet available
1760
+ if False and resource.client is not None and resource._mode == "http":
1759
1761
  api_diff = None
1760
1762
  try:
1761
1763
  payload = {}
@@ -1779,7 +1781,7 @@ class AsyncSnapshotDiff:
1779
1781
  # Fall back to local diff if API call fails
1780
1782
  print(f"Warning: Failed to fetch structured diff from API: {e}")
1781
1783
  print("Falling back to local diff computation...")
1782
-
1784
+
1783
1785
  # Validate outside try block so AssertionError propagates
1784
1786
  if api_diff is not None:
1785
1787
  return await self._validate_diff_against_allowed_changes_v2(api_diff, allowed_changes)
@@ -1794,6 +1796,81 @@ class AsyncSnapshotDiff:
1794
1796
  diff, allowed_changes
1795
1797
  )
1796
1798
 
1799
+ async def expect_exactly(self, expected_changes: List[Dict[str, Any]]):
1800
+ """Verify that EXACTLY the specified changes occurred.
1801
+
1802
+ This is stricter than expect_only_v2:
1803
+ 1. All changes in diff must match a spec (no unexpected changes)
1804
+ 2. All specs must have a matching change in diff (no missing expected changes)
1805
+
1806
+ This method is ideal for verifying that an agent performed exactly what was expected -
1807
+ not more, not less.
1808
+
1809
+ Args:
1810
+ expected_changes: List of expected change specs. Each spec requires:
1811
+ - "type": "insert", "modify", or "delete" (required)
1812
+ - "table": table name (required)
1813
+ - "pk": primary key value (required)
1814
+
1815
+ Spec formats by type:
1816
+ - Insert: {"type": "insert", "table": "t", "pk": 1, "fields": [...]}
1817
+ - Modify: {"type": "modify", "table": "t", "pk": 1, "resulting_fields": [...], "no_other_changes": True/False}
1818
+ - Delete: {"type": "delete", "table": "t", "pk": 1}
1819
+
1820
+ Field specs are 2-tuples: (field_name, expected_value)
1821
+ - ("name", "Alice"): check field equals "Alice"
1822
+ - ("name", ...): accept any value (ellipsis)
1823
+ - ("name", None): check field is SQL NULL
1824
+
1825
+ Note: Legacy specs without explicit "type" are not supported.
1826
+
1827
+ Returns:
1828
+ self for method chaining
1829
+
1830
+ Raises:
1831
+ AssertionError: If there are unexpected changes OR if expected changes are missing
1832
+ ValueError: If specs are missing required fields or have invalid format
1833
+ """
1834
+ # Get the diff (using HTTP if available, otherwise local)
1835
+ resource = self.after.resource
1836
+ diff = None
1837
+
1838
+ if resource.client is not None and resource._mode == "http":
1839
+ try:
1840
+ payload = {}
1841
+ if self.ignore_config:
1842
+ payload["ignore_config"] = {
1843
+ "tables": list(self.ignore_config.tables),
1844
+ "fields": list(self.ignore_config.fields),
1845
+ "table_fields": {
1846
+ table: list(fields) for table, fields in self.ignore_config.table_fields.items()
1847
+ }
1848
+ }
1849
+ response = await resource.client.request(
1850
+ "POST",
1851
+ "/diff/structured",
1852
+ json=payload,
1853
+ )
1854
+ result = response.json()
1855
+ if result.get("success") and "diff" in result:
1856
+ diff = result["diff"]
1857
+ except Exception as e:
1858
+ print(f"Warning: Failed to fetch structured diff from API: {e}")
1859
+ print("Falling back to local diff computation...")
1860
+
1861
+ if diff is None:
1862
+ diff = await self._collect()
1863
+
1864
+ # Use shared validation logic
1865
+ success, error_msg, _ = validate_diff_expect_exactly(
1866
+ diff, expected_changes, self.ignore_config
1867
+ )
1868
+
1869
+ if not success:
1870
+ raise AssertionError(error_msg)
1871
+
1872
+ return self
1873
+
1797
1874
  async def _ensure_all_fetched(self):
1798
1875
  """Fetch ALL data from ALL tables upfront (non-lazy loading).
1799
1876
 
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.100"
30
+ __version__ = "0.2.101"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -164,8 +164,12 @@ class InstanceClient:
164
164
  if self._resources is None:
165
165
  response = self.client.request("GET", "/resources")
166
166
  if response.status_code != 200:
167
- self._resources = []
168
- return
167
+ response_body = response.text[:500] if response.text else "empty"
168
+ self._resources = [] # Mark as loaded (empty) to prevent retries
169
+ raise FleetEnvironmentError(
170
+ f"Failed to load instance resources: status_code={response.status_code} "
171
+ f"(url={self.base_url}, response={response_body})"
172
+ )
169
173
 
170
174
  # Handle both old and new response formats
171
175
  response_data = response.json()
@@ -21,6 +21,7 @@ from fleet.verifiers.db import (
21
21
  _get_row_identifier,
22
22
  _format_row_for_error,
23
23
  _values_equivalent,
24
+ validate_diff_expect_exactly,
24
25
  )
25
26
 
26
27
 
@@ -1803,7 +1804,8 @@ class SyncSnapshotDiff:
1803
1804
  return self._expect_no_changes()
1804
1805
 
1805
1806
  resource = self.after.resource
1806
- if resource.client is not None and resource._mode == "http":
1807
+ # Disabled: structured diff endpoint not yet available
1808
+ if False and resource.client is not None and resource._mode == "http":
1807
1809
  api_diff = None
1808
1810
  try:
1809
1811
  payload = {}
@@ -1827,7 +1829,7 @@ class SyncSnapshotDiff:
1827
1829
  # Fall back to local diff if API call fails
1828
1830
  print(f"Warning: Failed to fetch structured diff from API: {e}")
1829
1831
  print("Falling back to local diff computation...")
1830
-
1832
+
1831
1833
  # Validate outside try block so AssertionError propagates
1832
1834
  if api_diff is not None:
1833
1835
  return self._validate_diff_against_allowed_changes_v2(api_diff, allowed_changes)
@@ -1840,6 +1842,81 @@ class SyncSnapshotDiff:
1840
1842
  diff = self._collect()
1841
1843
  return self._validate_diff_against_allowed_changes_v2(diff, allowed_changes)
1842
1844
 
1845
+ def expect_exactly(self, expected_changes: List[Dict[str, Any]]):
1846
+ """Verify that EXACTLY the specified changes occurred.
1847
+
1848
+ This is stricter than expect_only_v2:
1849
+ 1. All changes in diff must match a spec (no unexpected changes)
1850
+ 2. All specs must have a matching change in diff (no missing expected changes)
1851
+
1852
+ This method is ideal for verifying that an agent performed exactly what was expected -
1853
+ not more, not less.
1854
+
1855
+ Args:
1856
+ expected_changes: List of expected change specs. Each spec requires:
1857
+ - "type": "insert", "modify", or "delete" (required)
1858
+ - "table": table name (required)
1859
+ - "pk": primary key value (required)
1860
+
1861
+ Spec formats by type:
1862
+ - Insert: {"type": "insert", "table": "t", "pk": 1, "fields": [...]}
1863
+ - Modify: {"type": "modify", "table": "t", "pk": 1, "resulting_fields": [...], "no_other_changes": True/False}
1864
+ - Delete: {"type": "delete", "table": "t", "pk": 1}
1865
+
1866
+ Field specs are 2-tuples: (field_name, expected_value)
1867
+ - ("name", "Alice"): check field equals "Alice"
1868
+ - ("name", ...): accept any value (ellipsis)
1869
+ - ("name", None): check field is SQL NULL
1870
+
1871
+ Note: Legacy specs without explicit "type" are not supported.
1872
+
1873
+ Returns:
1874
+ self for method chaining
1875
+
1876
+ Raises:
1877
+ AssertionError: If there are unexpected changes OR if expected changes are missing
1878
+ ValueError: If specs are missing required fields or have invalid format
1879
+ """
1880
+ # Get the diff (using HTTP if available, otherwise local)
1881
+ resource = self.after.resource
1882
+ diff = None
1883
+
1884
+ if resource.client is not None and resource._mode == "http":
1885
+ try:
1886
+ payload = {}
1887
+ if self.ignore_config:
1888
+ payload["ignore_config"] = {
1889
+ "tables": list(self.ignore_config.tables),
1890
+ "fields": list(self.ignore_config.fields),
1891
+ "table_fields": {
1892
+ table: list(fields) for table, fields in self.ignore_config.table_fields.items()
1893
+ }
1894
+ }
1895
+ response = resource.client.request(
1896
+ "POST",
1897
+ "/diff/structured",
1898
+ json=payload,
1899
+ )
1900
+ result = response.json()
1901
+ if result.get("success") and "diff" in result:
1902
+ diff = result["diff"]
1903
+ except Exception as e:
1904
+ print(f"Warning: Failed to fetch structured diff from API: {e}")
1905
+ print("Falling back to local diff computation...")
1906
+
1907
+ if diff is None:
1908
+ diff = self._collect()
1909
+
1910
+ # Use shared validation logic
1911
+ success, error_msg, _ = validate_diff_expect_exactly(
1912
+ diff, expected_changes, self.ignore_config
1913
+ )
1914
+
1915
+ if not success:
1916
+ raise AssertionError(error_msg)
1917
+
1918
+ return self
1919
+
1843
1920
  def _ensure_all_fetched(self):
1844
1921
  """Fetch ALL data from ALL tables upfront (non-lazy loading).
1845
1922