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.
- {fleet_python-0.2.100/fleet_python.egg-info → fleet_python-0.2.101}/PKG-INFO +1 -1
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/__init__.py +1 -1
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/instance/client.py +6 -2
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/sqlite.py +79 -2
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/base.py +1 -1
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/client.py +6 -2
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/sqlite.py +79 -2
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/db.py +483 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/SOURCES.txt +1 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/pyproject.toml +1 -1
- fleet_python-0.2.101/tests/test_expect_exactly.py +4148 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/LICENSE +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/README.md +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/diff_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_account.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_client.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_sync.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_task.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/openai_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/quickstart.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/api.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp/main.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/orchestrator.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/cli.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/client.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/config.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/env/client.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/global_client.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/models.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/api.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/tasks.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/types.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/scripts/unasync.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/setup.cfg +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/__init__.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.100 → fleet_python-0.2.101}/tests/test_verifier_from_string.py +0 -0
|
@@ -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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
|