fleet-python 0.2.75b3__tar.gz → 0.2.76__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.75b3/fleet_python.egg-info → fleet_python-0.2.76}/PKG-INFO +1 -1
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/iterate_verifiers.py +0 -6
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/resources/sqlite.py +316 -1
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/verifiers/verifier.py +19 -1
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/resources/sqlite.py +334 -1
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/verifier.py +19 -1
- {fleet_python-0.2.75b3 → fleet_python-0.2.76/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/pyproject.toml +1 -1
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/LICENSE +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/README.md +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/diff_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_account.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_sync.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_task.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/openai_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/quickstart.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/base.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/base.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/config.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/env/client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/global_client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/models.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/tasks.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/types.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet_python.egg-info/SOURCES.txt +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/scripts/unasync.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/setup.cfg +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/tests/__init__.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.75b3 → fleet_python-0.2.76}/tests/test_verifier_from_string.py +0 -0
|
@@ -387,12 +387,6 @@ def apply_verifiers_to_json(json_path: str, python_path: str) -> None:
|
|
|
387
387
|
|
|
388
388
|
if task_key in verifiers:
|
|
389
389
|
new_code = verifiers[task_key]
|
|
390
|
-
|
|
391
|
-
# Escape newlines in debug print patterns (>>> and <<<)
|
|
392
|
-
# These should be \n escape sequences, not actual newlines
|
|
393
|
-
new_code = new_code.replace(">>>\n", ">>>\\n")
|
|
394
|
-
new_code = new_code.replace("\n<<<", "\\n<<<")
|
|
395
|
-
|
|
396
390
|
old_code = task.get("verifier_func", "").strip()
|
|
397
391
|
|
|
398
392
|
# Only update if the code actually changed
|
|
@@ -761,6 +761,317 @@ class AsyncSnapshotDiff:
|
|
|
761
761
|
|
|
762
762
|
return self
|
|
763
763
|
|
|
764
|
+
async def _expect_only_targeted_v2(self, allowed_changes: List[Dict[str, Any]]):
|
|
765
|
+
"""Optimized v2 version that only queries specific rows mentioned in allowed_changes."""
|
|
766
|
+
import asyncio
|
|
767
|
+
|
|
768
|
+
# Helper functions for v2 validation (same as in _validate_diff_against_allowed_changes_v2)
|
|
769
|
+
def _parse_fields_spec(
|
|
770
|
+
fields_spec: List[Tuple[str, Any]]
|
|
771
|
+
) -> Dict[str, Tuple[bool, Any]]:
|
|
772
|
+
"""Parse a fields spec into a mapping of field_name -> (should_check_value, expected_value)."""
|
|
773
|
+
spec_map: Dict[str, Tuple[bool, Any]] = {}
|
|
774
|
+
for spec_tuple in fields_spec:
|
|
775
|
+
if len(spec_tuple) != 2:
|
|
776
|
+
raise ValueError(
|
|
777
|
+
f"Invalid field spec tuple: {spec_tuple}. "
|
|
778
|
+
f"Expected 2-tuple like ('field', value), ('field', None), or ('field', ...)"
|
|
779
|
+
)
|
|
780
|
+
field_name, expected_value = spec_tuple
|
|
781
|
+
if expected_value is ...:
|
|
782
|
+
spec_map[field_name] = (False, None)
|
|
783
|
+
else:
|
|
784
|
+
spec_map[field_name] = (True, expected_value)
|
|
785
|
+
return spec_map
|
|
786
|
+
|
|
787
|
+
def _validate_row_with_fields_spec(
|
|
788
|
+
table: str,
|
|
789
|
+
row_id: Any,
|
|
790
|
+
row_data: Dict[str, Any],
|
|
791
|
+
fields_spec: List[Tuple[str, Any]],
|
|
792
|
+
) -> Optional[List[Tuple[str, Any, str]]]:
|
|
793
|
+
"""Validate an inserted/deleted row against a bulk fields spec."""
|
|
794
|
+
spec_map = _parse_fields_spec(fields_spec)
|
|
795
|
+
unmatched_fields: List[Tuple[str, Any, str]] = []
|
|
796
|
+
|
|
797
|
+
for field_name, field_value in row_data.items():
|
|
798
|
+
if field_name == "rowid":
|
|
799
|
+
continue
|
|
800
|
+
if self.ignore_config.should_ignore_field(table, field_name):
|
|
801
|
+
continue
|
|
802
|
+
|
|
803
|
+
if field_name not in spec_map:
|
|
804
|
+
unmatched_fields.append(
|
|
805
|
+
(field_name, field_value, "NOT_IN_FIELDS_SPEC")
|
|
806
|
+
)
|
|
807
|
+
else:
|
|
808
|
+
should_check, expected_value = spec_map[field_name]
|
|
809
|
+
if should_check and not _values_equivalent(
|
|
810
|
+
expected_value, field_value
|
|
811
|
+
):
|
|
812
|
+
unmatched_fields.append(
|
|
813
|
+
(field_name, field_value, f"expected {repr(expected_value)}")
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
return unmatched_fields if unmatched_fields else None
|
|
817
|
+
|
|
818
|
+
def _validate_modification_with_fields_spec(
|
|
819
|
+
table: str,
|
|
820
|
+
row_id: Any,
|
|
821
|
+
row_changes: Dict[str, Dict[str, Any]],
|
|
822
|
+
resulting_fields: List[Tuple[str, Any]],
|
|
823
|
+
no_other_changes: bool,
|
|
824
|
+
) -> Optional[List[Tuple[str, Any, str]]]:
|
|
825
|
+
"""Validate a modification against a resulting_fields spec."""
|
|
826
|
+
spec_map = _parse_fields_spec(resulting_fields)
|
|
827
|
+
unmatched_fields: List[Tuple[str, Any, str]] = []
|
|
828
|
+
|
|
829
|
+
for field_name, vals in row_changes.items():
|
|
830
|
+
if self.ignore_config.should_ignore_field(table, field_name):
|
|
831
|
+
continue
|
|
832
|
+
|
|
833
|
+
after_value = vals["after"]
|
|
834
|
+
|
|
835
|
+
if field_name not in spec_map:
|
|
836
|
+
if no_other_changes:
|
|
837
|
+
unmatched_fields.append(
|
|
838
|
+
(field_name, after_value, "NOT_IN_RESULTING_FIELDS")
|
|
839
|
+
)
|
|
840
|
+
else:
|
|
841
|
+
should_check, expected_value = spec_map[field_name]
|
|
842
|
+
if should_check and not _values_equivalent(
|
|
843
|
+
expected_value, after_value
|
|
844
|
+
):
|
|
845
|
+
unmatched_fields.append(
|
|
846
|
+
(field_name, after_value, f"expected {repr(expected_value)}")
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
return unmatched_fields if unmatched_fields else None
|
|
850
|
+
|
|
851
|
+
# Group allowed changes by table
|
|
852
|
+
changes_by_table: Dict[str, List[Dict[str, Any]]] = {}
|
|
853
|
+
for change in allowed_changes:
|
|
854
|
+
table = change["table"]
|
|
855
|
+
if table not in changes_by_table:
|
|
856
|
+
changes_by_table[table] = []
|
|
857
|
+
changes_by_table[table].append(change)
|
|
858
|
+
|
|
859
|
+
errors = []
|
|
860
|
+
|
|
861
|
+
def _get_change_spec(table_changes: List[Dict[str, Any]], pk: Any) -> Optional[Dict[str, Any]]:
|
|
862
|
+
"""Get the change spec for a given pk."""
|
|
863
|
+
for change in table_changes:
|
|
864
|
+
if str(change.get("pk")) == str(pk):
|
|
865
|
+
return change
|
|
866
|
+
return None
|
|
867
|
+
|
|
868
|
+
async def check_row(
|
|
869
|
+
table: str,
|
|
870
|
+
pk: Any,
|
|
871
|
+
table_changes: List[Dict[str, Any]],
|
|
872
|
+
pk_columns: List[str],
|
|
873
|
+
):
|
|
874
|
+
"""Check a single row against v2 specs."""
|
|
875
|
+
try:
|
|
876
|
+
where_sql = self._build_pk_where_clause(pk_columns, pk)
|
|
877
|
+
|
|
878
|
+
# Query both snapshots
|
|
879
|
+
before_query = f"SELECT * FROM {table} WHERE {where_sql}"
|
|
880
|
+
before_response = await self.before.resource.query(before_query)
|
|
881
|
+
before_row = (
|
|
882
|
+
dict(zip(before_response.columns, before_response.rows[0]))
|
|
883
|
+
if before_response.rows
|
|
884
|
+
else None
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
after_response = await self.after.resource.query(before_query)
|
|
888
|
+
after_row = (
|
|
889
|
+
dict(zip(after_response.columns, after_response.rows[0]))
|
|
890
|
+
if after_response.rows
|
|
891
|
+
else None
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# Get the spec for this pk
|
|
895
|
+
spec = _get_change_spec(table_changes, pk)
|
|
896
|
+
if spec is None:
|
|
897
|
+
return # No spec found, shouldn't happen
|
|
898
|
+
|
|
899
|
+
change_type = spec.get("type")
|
|
900
|
+
|
|
901
|
+
if not before_row and after_row:
|
|
902
|
+
# Inserted row
|
|
903
|
+
if change_type != "insert":
|
|
904
|
+
error_msg = f"Row {pk} in table '{table}' was inserted but spec type is '{change_type}'"
|
|
905
|
+
errors.append(AssertionError(error_msg))
|
|
906
|
+
return
|
|
907
|
+
|
|
908
|
+
fields_spec = spec.get("fields")
|
|
909
|
+
if fields_spec is not None:
|
|
910
|
+
unmatched = _validate_row_with_fields_spec(
|
|
911
|
+
table, pk, after_row, fields_spec
|
|
912
|
+
)
|
|
913
|
+
if unmatched:
|
|
914
|
+
error_msg = (
|
|
915
|
+
f"Insert validation failed for table '{table}', row {pk}:\n"
|
|
916
|
+
+ "\n".join(
|
|
917
|
+
f" - {f}: {repr(v)} ({issue})"
|
|
918
|
+
for f, v, issue in unmatched[:5]
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
errors.append(AssertionError(error_msg))
|
|
922
|
+
|
|
923
|
+
elif before_row and not after_row:
|
|
924
|
+
# Deleted row
|
|
925
|
+
if change_type != "delete":
|
|
926
|
+
error_msg = f"Row {pk} in table '{table}' was deleted but spec type is '{change_type}'"
|
|
927
|
+
errors.append(AssertionError(error_msg))
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
fields_spec = spec.get("fields")
|
|
931
|
+
if fields_spec is not None:
|
|
932
|
+
unmatched = _validate_row_with_fields_spec(
|
|
933
|
+
table, pk, before_row, fields_spec
|
|
934
|
+
)
|
|
935
|
+
if unmatched:
|
|
936
|
+
error_msg = (
|
|
937
|
+
f"Delete validation failed for table '{table}', row {pk}:\n"
|
|
938
|
+
+ "\n".join(
|
|
939
|
+
f" - {f}: {repr(v)} ({issue})"
|
|
940
|
+
for f, v, issue in unmatched[:5]
|
|
941
|
+
)
|
|
942
|
+
)
|
|
943
|
+
errors.append(AssertionError(error_msg))
|
|
944
|
+
|
|
945
|
+
elif before_row and after_row:
|
|
946
|
+
# Modified row - compute changes
|
|
947
|
+
row_changes = {}
|
|
948
|
+
for field in set(before_row.keys()) | set(after_row.keys()):
|
|
949
|
+
if self.ignore_config.should_ignore_field(table, field):
|
|
950
|
+
continue
|
|
951
|
+
before_val = before_row.get(field)
|
|
952
|
+
after_val = after_row.get(field)
|
|
953
|
+
if not _values_equivalent(before_val, after_val):
|
|
954
|
+
row_changes[field] = {"before": before_val, "after": after_val}
|
|
955
|
+
|
|
956
|
+
if not row_changes:
|
|
957
|
+
# No actual changes (after ignores), this is fine
|
|
958
|
+
return
|
|
959
|
+
|
|
960
|
+
if change_type != "modify":
|
|
961
|
+
error_msg = f"Row {pk} in table '{table}' was modified but spec type is '{change_type}'"
|
|
962
|
+
errors.append(AssertionError(error_msg))
|
|
963
|
+
return
|
|
964
|
+
|
|
965
|
+
resulting_fields = spec.get("resulting_fields")
|
|
966
|
+
if resulting_fields is not None:
|
|
967
|
+
if "no_other_changes" not in spec:
|
|
968
|
+
error_msg = (
|
|
969
|
+
f"Modify spec for table '{table}' pk={pk} "
|
|
970
|
+
f"has 'resulting_fields' but missing required 'no_other_changes' field."
|
|
971
|
+
)
|
|
972
|
+
errors.append(ValueError(error_msg))
|
|
973
|
+
return
|
|
974
|
+
|
|
975
|
+
no_other_changes = spec["no_other_changes"]
|
|
976
|
+
if not isinstance(no_other_changes, bool):
|
|
977
|
+
error_msg = (
|
|
978
|
+
f"Modify spec for table '{table}' pk={pk} "
|
|
979
|
+
f"has 'no_other_changes' but it must be a boolean."
|
|
980
|
+
)
|
|
981
|
+
errors.append(ValueError(error_msg))
|
|
982
|
+
return
|
|
983
|
+
|
|
984
|
+
unmatched = _validate_modification_with_fields_spec(
|
|
985
|
+
table, pk, row_changes, resulting_fields, no_other_changes
|
|
986
|
+
)
|
|
987
|
+
if unmatched:
|
|
988
|
+
error_msg = (
|
|
989
|
+
f"Modify validation failed for table '{table}', row {pk}:\n"
|
|
990
|
+
+ "\n".join(
|
|
991
|
+
f" - {f}: {repr(v)} ({issue})"
|
|
992
|
+
for f, v, issue in unmatched[:5]
|
|
993
|
+
)
|
|
994
|
+
)
|
|
995
|
+
errors.append(AssertionError(error_msg))
|
|
996
|
+
|
|
997
|
+
else:
|
|
998
|
+
# Row doesn't exist in either snapshot
|
|
999
|
+
error_msg = f"Row {pk} not found in table '{table}' in either snapshot"
|
|
1000
|
+
errors.append(AssertionError(error_msg))
|
|
1001
|
+
|
|
1002
|
+
except Exception as e:
|
|
1003
|
+
errors.append(e)
|
|
1004
|
+
|
|
1005
|
+
# Prepare all row checks
|
|
1006
|
+
row_checks = []
|
|
1007
|
+
for table, table_changes in changes_by_table.items():
|
|
1008
|
+
if self.ignore_config.should_ignore_table(table):
|
|
1009
|
+
continue
|
|
1010
|
+
|
|
1011
|
+
pk_columns = await self._get_primary_key_columns(table)
|
|
1012
|
+
pks_to_check = {change["pk"] for change in table_changes}
|
|
1013
|
+
|
|
1014
|
+
for pk in pks_to_check:
|
|
1015
|
+
row_checks.append((table, pk, table_changes, pk_columns))
|
|
1016
|
+
|
|
1017
|
+
# Execute row checks in parallel
|
|
1018
|
+
if row_checks:
|
|
1019
|
+
await asyncio.gather(
|
|
1020
|
+
*[
|
|
1021
|
+
check_row(table, pk, table_changes, pk_columns)
|
|
1022
|
+
for table, pk, table_changes, pk_columns in row_checks
|
|
1023
|
+
]
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
if errors:
|
|
1027
|
+
raise errors[0]
|
|
1028
|
+
|
|
1029
|
+
# Verify tables not mentioned have no changes
|
|
1030
|
+
all_tables = set(await self.before.tables()) | set(await self.after.tables())
|
|
1031
|
+
tables_to_verify = []
|
|
1032
|
+
|
|
1033
|
+
for table in all_tables:
|
|
1034
|
+
if (
|
|
1035
|
+
table not in changes_by_table
|
|
1036
|
+
and not self.ignore_config.should_ignore_table(table)
|
|
1037
|
+
):
|
|
1038
|
+
tables_to_verify.append(table)
|
|
1039
|
+
|
|
1040
|
+
async def verify_no_changes(table: str):
|
|
1041
|
+
try:
|
|
1042
|
+
before_count_response = await self.before.resource.query(
|
|
1043
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1044
|
+
)
|
|
1045
|
+
before_count = (
|
|
1046
|
+
before_count_response.rows[0][0]
|
|
1047
|
+
if before_count_response.rows
|
|
1048
|
+
else 0
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
after_count_response = await self.after.resource.query(
|
|
1052
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1053
|
+
)
|
|
1054
|
+
after_count = (
|
|
1055
|
+
after_count_response.rows[0][0] if after_count_response.rows else 0
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
if before_count != after_count:
|
|
1059
|
+
error_msg = (
|
|
1060
|
+
f"Unexpected change in table '{table}': "
|
|
1061
|
+
f"row count changed from {before_count} to {after_count}"
|
|
1062
|
+
)
|
|
1063
|
+
errors.append(AssertionError(error_msg))
|
|
1064
|
+
except Exception as e:
|
|
1065
|
+
errors.append(e)
|
|
1066
|
+
|
|
1067
|
+
if tables_to_verify:
|
|
1068
|
+
await asyncio.gather(*[verify_no_changes(table) for table in tables_to_verify])
|
|
1069
|
+
|
|
1070
|
+
if errors:
|
|
1071
|
+
raise errors[0]
|
|
1072
|
+
|
|
1073
|
+
return self
|
|
1074
|
+
|
|
764
1075
|
async def _validate_diff_against_allowed_changes(
|
|
765
1076
|
self, diff: Dict[str, Any], allowed_changes: List[Dict[str, Any]]
|
|
766
1077
|
):
|
|
@@ -1421,7 +1732,11 @@ class AsyncSnapshotDiff:
|
|
|
1421
1732
|
if not allowed_changes:
|
|
1422
1733
|
return await self._expect_no_changes()
|
|
1423
1734
|
|
|
1424
|
-
#
|
|
1735
|
+
# Use targeted optimization when possible (all changes have table and pk)
|
|
1736
|
+
if self._can_use_targeted_queries(allowed_changes):
|
|
1737
|
+
return await self._expect_only_targeted_v2(allowed_changes)
|
|
1738
|
+
|
|
1739
|
+
# Fall back to full diff for complex cases
|
|
1425
1740
|
diff = await self._collect()
|
|
1426
1741
|
return await self._validate_diff_against_allowed_changes_v2(
|
|
1427
1742
|
diff, allowed_changes
|
|
@@ -232,7 +232,25 @@ Remote traceback:
|
|
|
232
232
|
) -> "VerifiersExecuteResponse":
|
|
233
233
|
"""Remote execution of the verifier function that returns the full response model."""
|
|
234
234
|
args_array = list(args)
|
|
235
|
-
|
|
235
|
+
# Send complete environment data for remote reconstruction
|
|
236
|
+
env_data = {
|
|
237
|
+
"instance_id": env.instance_id,
|
|
238
|
+
"env_key": env.env_key,
|
|
239
|
+
"version": env.version,
|
|
240
|
+
"status": env.status,
|
|
241
|
+
"subdomain": env.subdomain,
|
|
242
|
+
"created_at": env.created_at,
|
|
243
|
+
"updated_at": env.updated_at,
|
|
244
|
+
"terminated_at": env.terminated_at,
|
|
245
|
+
"team_id": env.team_id,
|
|
246
|
+
"region": env.region,
|
|
247
|
+
"env_variables": env.env_variables,
|
|
248
|
+
"data_key": env.data_key,
|
|
249
|
+
"data_version": env.data_version,
|
|
250
|
+
"urls": env.urls.model_dump() if env.urls else None,
|
|
251
|
+
"health": env.health,
|
|
252
|
+
}
|
|
253
|
+
args_array.append({"env": env_data})
|
|
236
254
|
args = tuple(args_array)
|
|
237
255
|
|
|
238
256
|
try:
|
|
@@ -783,6 +783,335 @@ class SyncSnapshotDiff:
|
|
|
783
783
|
|
|
784
784
|
return self
|
|
785
785
|
|
|
786
|
+
def _expect_only_targeted_v2(self, allowed_changes: List[Dict[str, Any]]):
|
|
787
|
+
"""Optimized v2 version that only queries specific rows mentioned in allowed_changes."""
|
|
788
|
+
import concurrent.futures
|
|
789
|
+
from threading import Lock
|
|
790
|
+
|
|
791
|
+
# Helper functions for v2 validation (same as in _validate_diff_against_allowed_changes_v2)
|
|
792
|
+
def _parse_fields_spec(
|
|
793
|
+
fields_spec: List[Tuple[str, Any]]
|
|
794
|
+
) -> Dict[str, Tuple[bool, Any]]:
|
|
795
|
+
"""Parse a fields spec into a mapping of field_name -> (should_check_value, expected_value)."""
|
|
796
|
+
spec_map: Dict[str, Tuple[bool, Any]] = {}
|
|
797
|
+
for spec_tuple in fields_spec:
|
|
798
|
+
if len(spec_tuple) != 2:
|
|
799
|
+
raise ValueError(
|
|
800
|
+
f"Invalid field spec tuple: {spec_tuple}. "
|
|
801
|
+
f"Expected 2-tuple like ('field', value), ('field', None), or ('field', ...)"
|
|
802
|
+
)
|
|
803
|
+
field_name, expected_value = spec_tuple
|
|
804
|
+
if expected_value is ...:
|
|
805
|
+
spec_map[field_name] = (False, None)
|
|
806
|
+
else:
|
|
807
|
+
spec_map[field_name] = (True, expected_value)
|
|
808
|
+
return spec_map
|
|
809
|
+
|
|
810
|
+
def _validate_row_with_fields_spec(
|
|
811
|
+
table: str,
|
|
812
|
+
row_id: Any,
|
|
813
|
+
row_data: Dict[str, Any],
|
|
814
|
+
fields_spec: List[Tuple[str, Any]],
|
|
815
|
+
) -> Optional[List[Tuple[str, Any, str]]]:
|
|
816
|
+
"""Validate an inserted/deleted row against a bulk fields spec."""
|
|
817
|
+
spec_map = _parse_fields_spec(fields_spec)
|
|
818
|
+
unmatched_fields: List[Tuple[str, Any, str]] = []
|
|
819
|
+
|
|
820
|
+
for field_name, field_value in row_data.items():
|
|
821
|
+
if field_name == "rowid":
|
|
822
|
+
continue
|
|
823
|
+
if self.ignore_config.should_ignore_field(table, field_name):
|
|
824
|
+
continue
|
|
825
|
+
|
|
826
|
+
if field_name not in spec_map:
|
|
827
|
+
unmatched_fields.append(
|
|
828
|
+
(field_name, field_value, "NOT_IN_FIELDS_SPEC")
|
|
829
|
+
)
|
|
830
|
+
else:
|
|
831
|
+
should_check, expected_value = spec_map[field_name]
|
|
832
|
+
if should_check and not _values_equivalent(
|
|
833
|
+
expected_value, field_value
|
|
834
|
+
):
|
|
835
|
+
unmatched_fields.append(
|
|
836
|
+
(field_name, field_value, f"expected {repr(expected_value)}")
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
return unmatched_fields if unmatched_fields else None
|
|
840
|
+
|
|
841
|
+
def _validate_modification_with_fields_spec(
|
|
842
|
+
table: str,
|
|
843
|
+
row_id: Any,
|
|
844
|
+
row_changes: Dict[str, Dict[str, Any]],
|
|
845
|
+
resulting_fields: List[Tuple[str, Any]],
|
|
846
|
+
no_other_changes: bool,
|
|
847
|
+
) -> Optional[List[Tuple[str, Any, str]]]:
|
|
848
|
+
"""Validate a modification against a resulting_fields spec."""
|
|
849
|
+
spec_map = _parse_fields_spec(resulting_fields)
|
|
850
|
+
unmatched_fields: List[Tuple[str, Any, str]] = []
|
|
851
|
+
|
|
852
|
+
for field_name, vals in row_changes.items():
|
|
853
|
+
if self.ignore_config.should_ignore_field(table, field_name):
|
|
854
|
+
continue
|
|
855
|
+
|
|
856
|
+
after_value = vals["after"]
|
|
857
|
+
|
|
858
|
+
if field_name not in spec_map:
|
|
859
|
+
if no_other_changes:
|
|
860
|
+
unmatched_fields.append(
|
|
861
|
+
(field_name, after_value, "NOT_IN_RESULTING_FIELDS")
|
|
862
|
+
)
|
|
863
|
+
else:
|
|
864
|
+
should_check, expected_value = spec_map[field_name]
|
|
865
|
+
if should_check and not _values_equivalent(
|
|
866
|
+
expected_value, after_value
|
|
867
|
+
):
|
|
868
|
+
unmatched_fields.append(
|
|
869
|
+
(field_name, after_value, f"expected {repr(expected_value)}")
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
return unmatched_fields if unmatched_fields else None
|
|
873
|
+
|
|
874
|
+
# Group allowed changes by table
|
|
875
|
+
changes_by_table: Dict[str, List[Dict[str, Any]]] = {}
|
|
876
|
+
for change in allowed_changes:
|
|
877
|
+
table = change["table"]
|
|
878
|
+
if table not in changes_by_table:
|
|
879
|
+
changes_by_table[table] = []
|
|
880
|
+
changes_by_table[table].append(change)
|
|
881
|
+
|
|
882
|
+
errors = []
|
|
883
|
+
errors_lock = Lock()
|
|
884
|
+
|
|
885
|
+
def _get_change_spec(table_changes: List[Dict[str, Any]], pk: Any) -> Optional[Dict[str, Any]]:
|
|
886
|
+
"""Get the change spec for a given pk."""
|
|
887
|
+
for change in table_changes:
|
|
888
|
+
if str(change.get("pk")) == str(pk):
|
|
889
|
+
return change
|
|
890
|
+
return None
|
|
891
|
+
|
|
892
|
+
def check_row(
|
|
893
|
+
table: str,
|
|
894
|
+
pk: Any,
|
|
895
|
+
table_changes: List[Dict[str, Any]],
|
|
896
|
+
pk_columns: List[str],
|
|
897
|
+
):
|
|
898
|
+
"""Check a single row against v2 specs."""
|
|
899
|
+
try:
|
|
900
|
+
where_sql = self._build_pk_where_clause(pk_columns, pk)
|
|
901
|
+
|
|
902
|
+
# Query both snapshots
|
|
903
|
+
before_query = f"SELECT * FROM {table} WHERE {where_sql}"
|
|
904
|
+
before_response = self.before.resource.query(before_query)
|
|
905
|
+
before_row = (
|
|
906
|
+
dict(zip(before_response.columns, before_response.rows[0]))
|
|
907
|
+
if before_response.rows
|
|
908
|
+
else None
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
after_response = self.after.resource.query(before_query)
|
|
912
|
+
after_row = (
|
|
913
|
+
dict(zip(after_response.columns, after_response.rows[0]))
|
|
914
|
+
if after_response.rows
|
|
915
|
+
else None
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# Get the spec for this pk
|
|
919
|
+
spec = _get_change_spec(table_changes, pk)
|
|
920
|
+
if spec is None:
|
|
921
|
+
return # No spec found, shouldn't happen
|
|
922
|
+
|
|
923
|
+
change_type = spec.get("type")
|
|
924
|
+
|
|
925
|
+
if not before_row and after_row:
|
|
926
|
+
# Inserted row
|
|
927
|
+
if change_type != "insert":
|
|
928
|
+
error_msg = f"Row {pk} in table '{table}' was inserted but spec type is '{change_type}'"
|
|
929
|
+
with errors_lock:
|
|
930
|
+
errors.append(AssertionError(error_msg))
|
|
931
|
+
return
|
|
932
|
+
|
|
933
|
+
fields_spec = spec.get("fields")
|
|
934
|
+
if fields_spec is not None:
|
|
935
|
+
unmatched = _validate_row_with_fields_spec(
|
|
936
|
+
table, pk, after_row, fields_spec
|
|
937
|
+
)
|
|
938
|
+
if unmatched:
|
|
939
|
+
error_msg = (
|
|
940
|
+
f"Insert validation failed for table '{table}', row {pk}:\n"
|
|
941
|
+
+ "\n".join(
|
|
942
|
+
f" - {f}: {repr(v)} ({issue})"
|
|
943
|
+
for f, v, issue in unmatched[:5]
|
|
944
|
+
)
|
|
945
|
+
)
|
|
946
|
+
with errors_lock:
|
|
947
|
+
errors.append(AssertionError(error_msg))
|
|
948
|
+
|
|
949
|
+
elif before_row and not after_row:
|
|
950
|
+
# Deleted row
|
|
951
|
+
if change_type != "delete":
|
|
952
|
+
error_msg = f"Row {pk} in table '{table}' was deleted but spec type is '{change_type}'"
|
|
953
|
+
with errors_lock:
|
|
954
|
+
errors.append(AssertionError(error_msg))
|
|
955
|
+
return
|
|
956
|
+
|
|
957
|
+
fields_spec = spec.get("fields")
|
|
958
|
+
if fields_spec is not None:
|
|
959
|
+
unmatched = _validate_row_with_fields_spec(
|
|
960
|
+
table, pk, before_row, fields_spec
|
|
961
|
+
)
|
|
962
|
+
if unmatched:
|
|
963
|
+
error_msg = (
|
|
964
|
+
f"Delete validation failed for table '{table}', row {pk}:\n"
|
|
965
|
+
+ "\n".join(
|
|
966
|
+
f" - {f}: {repr(v)} ({issue})"
|
|
967
|
+
for f, v, issue in unmatched[:5]
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
with errors_lock:
|
|
971
|
+
errors.append(AssertionError(error_msg))
|
|
972
|
+
|
|
973
|
+
elif before_row and after_row:
|
|
974
|
+
# Modified row - compute changes
|
|
975
|
+
row_changes = {}
|
|
976
|
+
for field in set(before_row.keys()) | set(after_row.keys()):
|
|
977
|
+
if self.ignore_config.should_ignore_field(table, field):
|
|
978
|
+
continue
|
|
979
|
+
before_val = before_row.get(field)
|
|
980
|
+
after_val = after_row.get(field)
|
|
981
|
+
if not _values_equivalent(before_val, after_val):
|
|
982
|
+
row_changes[field] = {"before": before_val, "after": after_val}
|
|
983
|
+
|
|
984
|
+
if not row_changes:
|
|
985
|
+
# No actual changes (after ignores), this is fine
|
|
986
|
+
return
|
|
987
|
+
|
|
988
|
+
if change_type != "modify":
|
|
989
|
+
error_msg = f"Row {pk} in table '{table}' was modified but spec type is '{change_type}'"
|
|
990
|
+
with errors_lock:
|
|
991
|
+
errors.append(AssertionError(error_msg))
|
|
992
|
+
return
|
|
993
|
+
|
|
994
|
+
resulting_fields = spec.get("resulting_fields")
|
|
995
|
+
if resulting_fields is not None:
|
|
996
|
+
if "no_other_changes" not in spec:
|
|
997
|
+
error_msg = (
|
|
998
|
+
f"Modify spec for table '{table}' pk={pk} "
|
|
999
|
+
f"has 'resulting_fields' but missing required 'no_other_changes' field."
|
|
1000
|
+
)
|
|
1001
|
+
with errors_lock:
|
|
1002
|
+
errors.append(ValueError(error_msg))
|
|
1003
|
+
return
|
|
1004
|
+
|
|
1005
|
+
no_other_changes = spec["no_other_changes"]
|
|
1006
|
+
if not isinstance(no_other_changes, bool):
|
|
1007
|
+
error_msg = (
|
|
1008
|
+
f"Modify spec for table '{table}' pk={pk} "
|
|
1009
|
+
f"has 'no_other_changes' but it must be a boolean."
|
|
1010
|
+
)
|
|
1011
|
+
with errors_lock:
|
|
1012
|
+
errors.append(ValueError(error_msg))
|
|
1013
|
+
return
|
|
1014
|
+
|
|
1015
|
+
unmatched = _validate_modification_with_fields_spec(
|
|
1016
|
+
table, pk, row_changes, resulting_fields, no_other_changes
|
|
1017
|
+
)
|
|
1018
|
+
if unmatched:
|
|
1019
|
+
error_msg = (
|
|
1020
|
+
f"Modify validation failed for table '{table}', row {pk}:\n"
|
|
1021
|
+
+ "\n".join(
|
|
1022
|
+
f" - {f}: {repr(v)} ({issue})"
|
|
1023
|
+
for f, v, issue in unmatched[:5]
|
|
1024
|
+
)
|
|
1025
|
+
)
|
|
1026
|
+
with errors_lock:
|
|
1027
|
+
errors.append(AssertionError(error_msg))
|
|
1028
|
+
|
|
1029
|
+
else:
|
|
1030
|
+
# Row doesn't exist in either snapshot
|
|
1031
|
+
error_msg = f"Row {pk} not found in table '{table}' in either snapshot"
|
|
1032
|
+
with errors_lock:
|
|
1033
|
+
errors.append(AssertionError(error_msg))
|
|
1034
|
+
|
|
1035
|
+
except Exception as e:
|
|
1036
|
+
with errors_lock:
|
|
1037
|
+
errors.append(e)
|
|
1038
|
+
|
|
1039
|
+
# Prepare all row checks
|
|
1040
|
+
row_checks = []
|
|
1041
|
+
for table, table_changes in changes_by_table.items():
|
|
1042
|
+
if self.ignore_config.should_ignore_table(table):
|
|
1043
|
+
continue
|
|
1044
|
+
|
|
1045
|
+
pk_columns = self._get_primary_key_columns(table)
|
|
1046
|
+
pks_to_check = {change["pk"] for change in table_changes}
|
|
1047
|
+
|
|
1048
|
+
for pk in pks_to_check:
|
|
1049
|
+
row_checks.append((table, pk, table_changes, pk_columns))
|
|
1050
|
+
|
|
1051
|
+
# Execute row checks in parallel
|
|
1052
|
+
if row_checks:
|
|
1053
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
1054
|
+
futures = [
|
|
1055
|
+
executor.submit(check_row, table, pk, table_changes, pk_columns)
|
|
1056
|
+
for table, pk, table_changes, pk_columns in row_checks
|
|
1057
|
+
]
|
|
1058
|
+
concurrent.futures.wait(futures)
|
|
1059
|
+
|
|
1060
|
+
if errors:
|
|
1061
|
+
raise errors[0]
|
|
1062
|
+
|
|
1063
|
+
# Verify tables not mentioned have no changes
|
|
1064
|
+
all_tables = set(self.before.tables()) | set(self.after.tables())
|
|
1065
|
+
tables_to_verify = []
|
|
1066
|
+
|
|
1067
|
+
for table in all_tables:
|
|
1068
|
+
if (
|
|
1069
|
+
table not in changes_by_table
|
|
1070
|
+
and not self.ignore_config.should_ignore_table(table)
|
|
1071
|
+
):
|
|
1072
|
+
tables_to_verify.append(table)
|
|
1073
|
+
|
|
1074
|
+
def verify_no_changes(table: str):
|
|
1075
|
+
try:
|
|
1076
|
+
before_count_response = self.before.resource.query(
|
|
1077
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1078
|
+
)
|
|
1079
|
+
before_count = (
|
|
1080
|
+
before_count_response.rows[0][0]
|
|
1081
|
+
if before_count_response.rows
|
|
1082
|
+
else 0
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
after_count_response = self.after.resource.query(
|
|
1086
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1087
|
+
)
|
|
1088
|
+
after_count = (
|
|
1089
|
+
after_count_response.rows[0][0] if after_count_response.rows else 0
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
if before_count != after_count:
|
|
1093
|
+
error_msg = (
|
|
1094
|
+
f"Unexpected change in table '{table}': "
|
|
1095
|
+
f"row count changed from {before_count} to {after_count}"
|
|
1096
|
+
)
|
|
1097
|
+
with errors_lock:
|
|
1098
|
+
errors.append(AssertionError(error_msg))
|
|
1099
|
+
except Exception as e:
|
|
1100
|
+
with errors_lock:
|
|
1101
|
+
errors.append(e)
|
|
1102
|
+
|
|
1103
|
+
if tables_to_verify:
|
|
1104
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
1105
|
+
futures = [
|
|
1106
|
+
executor.submit(verify_no_changes, table) for table in tables_to_verify
|
|
1107
|
+
]
|
|
1108
|
+
concurrent.futures.wait(futures)
|
|
1109
|
+
|
|
1110
|
+
if errors:
|
|
1111
|
+
raise errors[0]
|
|
1112
|
+
|
|
1113
|
+
return self
|
|
1114
|
+
|
|
786
1115
|
def _validate_diff_against_allowed_changes(
|
|
787
1116
|
self, diff: Dict[str, Any], allowed_changes: List[Dict[str, Any]]
|
|
788
1117
|
):
|
|
@@ -1443,7 +1772,11 @@ class SyncSnapshotDiff:
|
|
|
1443
1772
|
if not allowed_changes:
|
|
1444
1773
|
return self._expect_no_changes()
|
|
1445
1774
|
|
|
1446
|
-
#
|
|
1775
|
+
# Use targeted optimization when possible (all changes have table and pk)
|
|
1776
|
+
if self._can_use_targeted_queries(allowed_changes):
|
|
1777
|
+
return self._expect_only_targeted_v2(allowed_changes)
|
|
1778
|
+
|
|
1779
|
+
# Fall back to full diff for complex cases
|
|
1447
1780
|
diff = self._collect()
|
|
1448
1781
|
return self._validate_diff_against_allowed_changes_v2(diff, allowed_changes)
|
|
1449
1782
|
|
|
@@ -243,7 +243,25 @@ Remote traceback:
|
|
|
243
243
|
) -> "VerifiersExecuteResponse":
|
|
244
244
|
"""Remote execution of the verifier function that returns the full response model."""
|
|
245
245
|
args_array = list(args)
|
|
246
|
-
|
|
246
|
+
# Send complete environment data for remote reconstruction
|
|
247
|
+
env_data = {
|
|
248
|
+
"instance_id": env.instance_id,
|
|
249
|
+
"env_key": env.env_key,
|
|
250
|
+
"version": env.version,
|
|
251
|
+
"status": env.status,
|
|
252
|
+
"subdomain": env.subdomain,
|
|
253
|
+
"created_at": env.created_at,
|
|
254
|
+
"updated_at": env.updated_at,
|
|
255
|
+
"terminated_at": env.terminated_at,
|
|
256
|
+
"team_id": env.team_id,
|
|
257
|
+
"region": env.region,
|
|
258
|
+
"env_variables": env.env_variables,
|
|
259
|
+
"data_key": env.data_key,
|
|
260
|
+
"data_version": env.data_version,
|
|
261
|
+
"urls": env.urls.model_dump() if env.urls else None,
|
|
262
|
+
"health": env.health,
|
|
263
|
+
}
|
|
264
|
+
args_array.append({"env": env_data})
|
|
247
265
|
args = tuple(args_array)
|
|
248
266
|
|
|
249
267
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|