fleet-python 0.2.72__py3-none-any.whl → 0.2.72b2__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.
@@ -24,7 +24,7 @@ from fleet.verifiers.db import (
24
24
 
25
25
 
26
26
  class AsyncDatabaseSnapshot:
27
- """Async database snapshot that fetches data through API and stores locally for diffing."""
27
+ """Lazy database snapshot that fetches data on-demand through API."""
28
28
 
29
29
  def __init__(self, resource: "AsyncSQLiteResource", name: Optional[str] = None):
30
30
  self.resource = resource
@@ -32,11 +32,12 @@ class AsyncDatabaseSnapshot:
32
32
  self.created_at = datetime.utcnow()
33
33
  self._data: Dict[str, List[Dict[str, Any]]] = {}
34
34
  self._schemas: Dict[str, List[str]] = {}
35
- self._fetched = False
35
+ self._table_names: Optional[List[str]] = None
36
+ self._fetched_tables: set = set()
36
37
 
37
- async def _ensure_fetched(self):
38
- """Fetch all data from remote database if not already fetched."""
39
- if self._fetched:
38
+ async def _ensure_tables_list(self):
39
+ """Fetch just the list of table names if not already fetched."""
40
+ if self._table_names is not None:
40
41
  return
41
42
 
42
43
  # Get all tables
@@ -45,35 +46,36 @@ class AsyncDatabaseSnapshot:
45
46
  )
46
47
 
47
48
  if not tables_response.rows:
48
- self._fetched = True
49
+ self._table_names = []
49
50
  return
50
51
 
51
- table_names = [row[0] for row in tables_response.rows]
52
-
53
- # Fetch data from each table
54
- for table in table_names:
55
- # Get table schema
56
- schema_response = await self.resource.query(f"PRAGMA table_info({table})")
57
- if schema_response.rows:
58
- self._schemas[table] = [
59
- row[1] for row in schema_response.rows
60
- ] # Column names
61
-
62
- # Get all data
63
- data_response = await self.resource.query(f"SELECT * FROM {table}")
64
- if data_response.rows and data_response.columns:
65
- self._data[table] = [
66
- dict(zip(data_response.columns, row)) for row in data_response.rows
67
- ]
68
- else:
69
- self._data[table] = []
52
+ self._table_names = [row[0] for row in tables_response.rows]
53
+
54
+ async def _ensure_table_data(self, table: str):
55
+ """Fetch data for a specific table on demand."""
56
+ if table in self._fetched_tables:
57
+ return
58
+
59
+ # Get table schema
60
+ schema_response = await self.resource.query(f"PRAGMA table_info({table})")
61
+ if schema_response.rows:
62
+ self._schemas[table] = [row[1] for row in schema_response.rows] # Column names
63
+
64
+ # Get all data for this table
65
+ data_response = await self.resource.query(f"SELECT * FROM {table}")
66
+ if data_response.rows and data_response.columns:
67
+ self._data[table] = [
68
+ dict(zip(data_response.columns, row)) for row in data_response.rows
69
+ ]
70
+ else:
71
+ self._data[table] = []
70
72
 
71
- self._fetched = True
73
+ self._fetched_tables.add(table)
72
74
 
73
75
  async def tables(self) -> List[str]:
74
76
  """Get list of all tables in the snapshot."""
75
- await self._ensure_fetched()
76
- return list(self._data.keys())
77
+ await self._ensure_tables_list()
78
+ return list(self._table_names) if self._table_names else []
77
79
 
78
80
  def table(self, table_name: str) -> "AsyncSnapshotQueryBuilder":
79
81
  """Create a query builder for snapshot data."""
@@ -85,13 +87,12 @@ class AsyncDatabaseSnapshot:
85
87
  ignore_config: Optional[IgnoreConfig] = None,
86
88
  ) -> "AsyncSnapshotDiff":
87
89
  """Compare this snapshot with another."""
88
- await self._ensure_fetched()
89
- await other._ensure_fetched()
90
+ # No need to fetch all data upfront - diff will fetch on demand
90
91
  return AsyncSnapshotDiff(self, other, ignore_config)
91
92
 
92
93
 
93
94
  class AsyncSnapshotQueryBuilder:
94
- """Query builder that works on local snapshot data."""
95
+ """Query builder that works on snapshot data - can use targeted queries when possible."""
95
96
 
96
97
  def __init__(self, snapshot: AsyncDatabaseSnapshot, table: str):
97
98
  self._snapshot = snapshot
@@ -101,10 +102,63 @@ class AsyncSnapshotQueryBuilder:
101
102
  self._limit: Optional[int] = None
102
103
  self._order_by: Optional[str] = None
103
104
  self._order_desc: bool = False
105
+ self._use_targeted_query = True # Try to use targeted queries when possible
106
+
107
+ def _can_use_targeted_query(self) -> bool:
108
+ """Check if we can use a targeted query instead of loading all data."""
109
+ # We can use targeted query if:
110
+ # 1. We have simple equality conditions
111
+ # 2. No complex operations like joins
112
+ # 3. The query is selective (has conditions)
113
+ if not self._conditions:
114
+ return False
115
+ for col, op, val in self._conditions:
116
+ if op not in ["=", "IS", "IS NOT"]:
117
+ return False
118
+ return True
119
+
120
+ async def _execute_targeted_query(self) -> List[Dict[str, Any]]:
121
+ """Execute a targeted query directly instead of loading all data."""
122
+ # Build WHERE clause
123
+ where_parts = []
124
+ for col, op, val in self._conditions:
125
+ if op == "=" and val is None:
126
+ where_parts.append(f"{col} IS NULL")
127
+ elif op == "IS":
128
+ where_parts.append(f"{col} IS NULL")
129
+ elif op == "IS NOT":
130
+ where_parts.append(f"{col} IS NOT NULL")
131
+ elif op == "=":
132
+ if isinstance(val, str):
133
+ escaped_val = val.replace("'", "''")
134
+ where_parts.append(f"{col} = '{escaped_val}'")
135
+ else:
136
+ where_parts.append(f"{col} = '{val}'")
137
+
138
+ where_clause = " AND ".join(where_parts)
139
+
140
+ # Build full query
141
+ cols = ", ".join(self._select_cols)
142
+ query = f"SELECT {cols} FROM {self._table} WHERE {where_clause}"
143
+
144
+ if self._order_by:
145
+ query += f" ORDER BY {self._order_by}"
146
+ if self._limit is not None:
147
+ query += f" LIMIT {self._limit}"
148
+
149
+ # Execute query
150
+ response = await self._snapshot.resource.query(query)
151
+ if response.rows and response.columns:
152
+ return [dict(zip(response.columns, row)) for row in response.rows]
153
+ return []
104
154
 
105
155
  async def _get_data(self) -> List[Dict[str, Any]]:
106
- """Get table data from snapshot."""
107
- await self._snapshot._ensure_fetched()
156
+ """Get table data - use targeted query if possible, otherwise load all data."""
157
+ if self._use_targeted_query and self._can_use_targeted_query():
158
+ return await self._execute_targeted_query()
159
+
160
+ # Fall back to loading all data
161
+ await self._snapshot._ensure_table_data(self._table)
108
162
  return self._snapshot._data.get(self._table, [])
109
163
 
110
164
  def eq(self, column: str, value: Any) -> "AsyncSnapshotQueryBuilder":
@@ -143,6 +197,11 @@ class AsyncSnapshotQueryBuilder:
143
197
  return rows[0] if rows else None
144
198
 
145
199
  async def all(self) -> List[Dict[str, Any]]:
200
+ # If we can use targeted query, _get_data already applies filters
201
+ if self._use_targeted_query and self._can_use_targeted_query():
202
+ return await self._get_data()
203
+
204
+ # Otherwise, get all data and apply filters manually
146
205
  data = await self._get_data()
147
206
 
148
207
  # Apply filters
@@ -207,6 +266,7 @@ class AsyncSnapshotDiff:
207
266
  self.after = after
208
267
  self.ignore_config = ignore_config or IgnoreConfig()
209
268
  self._cached: Optional[Dict[str, Any]] = None
269
+ self._targeted_mode = False # Flag to use targeted queries
210
270
 
211
271
  async def _get_primary_key_columns(self, table: str) -> List[str]:
212
272
  """Get primary key columns for a table."""
@@ -247,6 +307,10 @@ class AsyncSnapshotDiff:
247
307
  # Get primary key columns
248
308
  pk_columns = await self._get_primary_key_columns(tbl)
249
309
 
310
+ # Ensure data is fetched for this table
311
+ await self.before._ensure_table_data(tbl)
312
+ await self.after._ensure_table_data(tbl)
313
+
250
314
  # Get data from both snapshots
251
315
  before_data = self.before._data.get(tbl, [])
252
316
  after_data = self.after._data.get(tbl, [])
@@ -329,9 +393,378 @@ class AsyncSnapshotDiff:
329
393
  )
330
394
  return self._cached
331
395
 
332
- async def expect_only(self, allowed_changes: List[Dict[str, Any]]):
333
- """Ensure only specified changes occurred."""
334
- diff = await self._collect()
396
+ def _can_use_targeted_queries(self, allowed_changes: List[Dict[str, Any]]) -> bool:
397
+ """Check if we can use targeted queries for optimization."""
398
+ # We can use targeted queries if all allowed changes specify table and pk
399
+ for change in allowed_changes:
400
+ if "table" not in change or "pk" not in change:
401
+ return False
402
+ return True
403
+
404
+ def _build_pk_where_clause(self, pk_columns: List[str], pk_value: Any) -> str:
405
+ """Build WHERE clause for primary key lookup."""
406
+ # Escape single quotes in values to prevent SQL injection
407
+ def escape_value(val: Any) -> str:
408
+ if val is None:
409
+ return "NULL"
410
+ elif isinstance(val, str):
411
+ escaped = str(val).replace("'", "''")
412
+ return f"'{escaped}'"
413
+ else:
414
+ return f"'{val}'"
415
+
416
+ if len(pk_columns) == 1:
417
+ return f"{pk_columns[0]} = {escape_value(pk_value)}"
418
+ else:
419
+ # Composite key
420
+ if isinstance(pk_value, tuple):
421
+ conditions = [
422
+ f"{col} = {escape_value(val)}"
423
+ for col, val in zip(pk_columns, pk_value)
424
+ ]
425
+ return " AND ".join(conditions)
426
+ else:
427
+ # Shouldn't happen if data is consistent
428
+ return f"{pk_columns[0]} = {escape_value(pk_value)}"
429
+
430
+ async def _expect_no_changes(self):
431
+ """Efficiently verify that no changes occurred between snapshots using row counts."""
432
+ try:
433
+ import asyncio
434
+
435
+ # Get all tables from both snapshots
436
+ before_tables = set(await self.before.tables())
437
+ after_tables = set(await self.after.tables())
438
+
439
+ # Check for added/removed tables (excluding ignored ones)
440
+ added_tables = after_tables - before_tables
441
+ removed_tables = before_tables - after_tables
442
+
443
+ for table in added_tables:
444
+ if not self.ignore_config.should_ignore_table(table):
445
+ raise AssertionError(f"Unexpected table added: {table}")
446
+
447
+ for table in removed_tables:
448
+ if not self.ignore_config.should_ignore_table(table):
449
+ raise AssertionError(f"Unexpected table removed: {table}")
450
+
451
+ # Prepare tables to check
452
+ tables_to_check = []
453
+ all_tables = before_tables | after_tables
454
+ for table in all_tables:
455
+ if not self.ignore_config.should_ignore_table(table):
456
+ tables_to_check.append(table)
457
+
458
+ # If no tables to check, we're done
459
+ if not tables_to_check:
460
+ return self
461
+
462
+ # Track errors and tables needing verification
463
+ errors = []
464
+ tables_needing_verification = []
465
+
466
+ async def check_table_counts(table: str):
467
+ """Check row counts for a single table."""
468
+ try:
469
+ # Get row counts from both snapshots
470
+ before_count = 0
471
+ after_count = 0
472
+
473
+ if table in before_tables:
474
+ before_count_response = await self.before.resource.query(
475
+ f"SELECT COUNT(*) FROM {table}"
476
+ )
477
+ before_count = (
478
+ before_count_response.rows[0][0]
479
+ if before_count_response.rows
480
+ else 0
481
+ )
482
+
483
+ if table in after_tables:
484
+ after_count_response = await self.after.resource.query(
485
+ f"SELECT COUNT(*) FROM {table}"
486
+ )
487
+ after_count = (
488
+ after_count_response.rows[0][0]
489
+ if after_count_response.rows
490
+ else 0
491
+ )
492
+
493
+ if before_count != after_count:
494
+ error_msg = (
495
+ f"Unexpected change in table '{table}': "
496
+ f"row count changed from {before_count} to {after_count}"
497
+ )
498
+ errors.append(AssertionError(error_msg))
499
+ elif before_count > 0 and before_count <= 1000:
500
+ # Mark for detailed verification
501
+ tables_needing_verification.append(table)
502
+
503
+ except Exception as e:
504
+ errors.append(e)
505
+
506
+ # Execute count checks in parallel
507
+ await asyncio.gather(*[check_table_counts(table) for table in tables_to_check])
508
+
509
+ # Check if any errors occurred during count checking
510
+ if errors:
511
+ raise errors[0]
512
+
513
+ # Now verify small tables for data changes (also in parallel)
514
+ if tables_needing_verification:
515
+ verification_errors = []
516
+
517
+ async def verify_table(table: str):
518
+ """Verify a single table's data hasn't changed."""
519
+ try:
520
+ await self._verify_table_unchanged(table)
521
+ except AssertionError as e:
522
+ verification_errors.append(e)
523
+
524
+ await asyncio.gather(*[verify_table(table) for table in tables_needing_verification])
525
+
526
+ # Check if any errors occurred during verification
527
+ if verification_errors:
528
+ raise verification_errors[0]
529
+
530
+ return self
531
+
532
+ except AssertionError:
533
+ # Re-raise assertion errors (these are expected failures)
534
+ raise
535
+ except Exception as e:
536
+ # If the optimized check fails for other reasons, fall back to full diff
537
+ print(f"Warning: Optimized no-changes check failed: {e}")
538
+ print("Falling back to full diff...")
539
+ return await self._validate_diff_against_allowed_changes(
540
+ await self._collect(), []
541
+ )
542
+
543
+ async def _verify_table_unchanged(self, table: str):
544
+ """Verify that a table's data hasn't changed (for small tables)."""
545
+ # Get primary key columns
546
+ pk_columns = await self._get_primary_key_columns(table)
547
+
548
+ # Get sorted data from both snapshots
549
+ order_by = ", ".join(pk_columns) if pk_columns else "rowid"
550
+
551
+ before_response = await self.before.resource.query(
552
+ f"SELECT * FROM {table} ORDER BY {order_by}"
553
+ )
554
+ after_response = await self.after.resource.query(
555
+ f"SELECT * FROM {table} ORDER BY {order_by}"
556
+ )
557
+
558
+ # Quick check: if column counts differ, there's a schema change
559
+ if before_response.columns != after_response.columns:
560
+ raise AssertionError(f"Schema changed in table '{table}'")
561
+
562
+ # Compare row by row
563
+ if len(before_response.rows) != len(after_response.rows):
564
+ raise AssertionError(
565
+ f"Row count mismatch in table '{table}': "
566
+ f"{len(before_response.rows)} vs {len(after_response.rows)}"
567
+ )
568
+
569
+ for i, (before_row, after_row) in enumerate(
570
+ zip(before_response.rows, after_response.rows)
571
+ ):
572
+ before_dict = dict(zip(before_response.columns, before_row))
573
+ after_dict = dict(zip(after_response.columns, after_row))
574
+
575
+ # Compare fields, ignoring those in ignore config
576
+ for field in before_response.columns:
577
+ if self.ignore_config.should_ignore_field(table, field):
578
+ continue
579
+
580
+ if not _values_equivalent(
581
+ before_dict.get(field), after_dict.get(field)
582
+ ):
583
+ pk_val = before_dict.get(pk_columns[0]) if pk_columns else i
584
+ raise AssertionError(
585
+ f"Unexpected change in table '{table}', row {pk_val}, "
586
+ f"field '{field}': {repr(before_dict.get(field))} -> {repr(after_dict.get(field))}"
587
+ )
588
+
589
+ def _is_field_change_allowed(
590
+ self, table_changes: List[Dict[str, Any]], pk: Any, field: str, after_val: Any
591
+ ) -> bool:
592
+ """Check if a specific field change is allowed."""
593
+ for change in table_changes:
594
+ if (
595
+ str(change.get("pk")) == str(pk)
596
+ and change.get("field") == field
597
+ and _values_equivalent(change.get("after"), after_val)
598
+ ):
599
+ return True
600
+ return False
601
+
602
+ def _is_row_change_allowed(
603
+ self, table_changes: List[Dict[str, Any]], pk: Any, change_type: str
604
+ ) -> bool:
605
+ """Check if a row addition/deletion is allowed."""
606
+ for change in table_changes:
607
+ if str(change.get("pk")) == str(pk) and change.get("after") == change_type:
608
+ return True
609
+ return False
610
+
611
+ async def _expect_only_targeted(self, allowed_changes: List[Dict[str, Any]]):
612
+ """Optimized version that only queries specific rows mentioned in allowed_changes."""
613
+ import asyncio
614
+
615
+ # Group allowed changes by table
616
+ changes_by_table: Dict[str, List[Dict[str, Any]]] = {}
617
+ for change in allowed_changes:
618
+ table = change["table"]
619
+ if table not in changes_by_table:
620
+ changes_by_table[table] = []
621
+ changes_by_table[table].append(change)
622
+
623
+ errors = []
624
+
625
+ # Function to check a single row
626
+ async def check_row(
627
+ table: str,
628
+ pk: Any,
629
+ table_changes: List[Dict[str, Any]],
630
+ pk_columns: List[str],
631
+ ):
632
+ try:
633
+ # Build WHERE clause for this PK
634
+ where_sql = self._build_pk_where_clause(pk_columns, pk)
635
+
636
+ # Query before snapshot
637
+ before_query = f"SELECT * FROM {table} WHERE {where_sql}"
638
+ before_response = await self.before.resource.query(before_query)
639
+ before_row = (
640
+ dict(zip(before_response.columns, before_response.rows[0]))
641
+ if before_response.rows
642
+ else None
643
+ )
644
+
645
+ # Query after snapshot
646
+ after_response = await self.after.resource.query(before_query)
647
+ after_row = (
648
+ dict(zip(after_response.columns, after_response.rows[0]))
649
+ if after_response.rows
650
+ else None
651
+ )
652
+
653
+ # Check changes for this row
654
+ if before_row and after_row:
655
+ # Modified row - check fields
656
+ for field in set(before_row.keys()) | set(after_row.keys()):
657
+ if self.ignore_config.should_ignore_field(table, field):
658
+ continue
659
+ before_val = before_row.get(field)
660
+ after_val = after_row.get(field)
661
+ if not _values_equivalent(before_val, after_val):
662
+ # Check if this change is allowed
663
+ if not self._is_field_change_allowed(
664
+ table_changes, pk, field, after_val
665
+ ):
666
+ error_msg = (
667
+ f"Unexpected change in table '{table}', "
668
+ f"row {pk}, field '{field}': "
669
+ f"{repr(before_val)} -> {repr(after_val)}"
670
+ )
671
+ errors.append(AssertionError(error_msg))
672
+ return # Stop checking this row
673
+ elif not before_row and after_row:
674
+ # Added row
675
+ if not self._is_row_change_allowed(table_changes, pk, "__added__"):
676
+ error_msg = f"Unexpected row added in table '{table}': {pk}"
677
+ errors.append(AssertionError(error_msg))
678
+ elif before_row and not after_row:
679
+ # Removed row
680
+ if not self._is_row_change_allowed(table_changes, pk, "__removed__"):
681
+ error_msg = f"Unexpected row removed from table '{table}': {pk}"
682
+ errors.append(AssertionError(error_msg))
683
+ except Exception as e:
684
+ errors.append(e)
685
+
686
+ # Prepare all row checks
687
+ row_checks = []
688
+ for table, table_changes in changes_by_table.items():
689
+ if self.ignore_config.should_ignore_table(table):
690
+ continue
691
+
692
+ # Get primary key columns once per table
693
+ pk_columns = await self._get_primary_key_columns(table)
694
+
695
+ # Extract unique PKs to check
696
+ pks_to_check = {change["pk"] for change in table_changes}
697
+
698
+ for pk in pks_to_check:
699
+ row_checks.append((table, pk, table_changes, pk_columns))
700
+
701
+ # Execute row checks in parallel
702
+ if row_checks:
703
+ await asyncio.gather(
704
+ *[
705
+ check_row(table, pk, table_changes, pk_columns)
706
+ for table, pk, table_changes, pk_columns in row_checks
707
+ ]
708
+ )
709
+
710
+ # Check for errors from row checks
711
+ if errors:
712
+ raise errors[0]
713
+
714
+ # Now check tables not mentioned in allowed_changes to ensure no changes
715
+ all_tables = set(await self.before.tables()) | set(await self.after.tables())
716
+ tables_to_verify = []
717
+
718
+ for table in all_tables:
719
+ if (
720
+ table not in changes_by_table
721
+ and not self.ignore_config.should_ignore_table(table)
722
+ ):
723
+ tables_to_verify.append(table)
724
+
725
+ # Function to verify no changes in a table
726
+ async def verify_no_changes(table: str):
727
+ try:
728
+ # For tables with no allowed changes, just check row counts
729
+ before_count_response = await self.before.resource.query(
730
+ f"SELECT COUNT(*) FROM {table}"
731
+ )
732
+ before_count = (
733
+ before_count_response.rows[0][0]
734
+ if before_count_response.rows
735
+ else 0
736
+ )
737
+
738
+ after_count_response = await self.after.resource.query(
739
+ f"SELECT COUNT(*) FROM {table}"
740
+ )
741
+ after_count = (
742
+ after_count_response.rows[0][0] if after_count_response.rows else 0
743
+ )
744
+
745
+ if before_count != after_count:
746
+ error_msg = (
747
+ f"Unexpected change in table '{table}': "
748
+ f"row count changed from {before_count} to {after_count}"
749
+ )
750
+ errors.append(AssertionError(error_msg))
751
+ except Exception as e:
752
+ errors.append(e)
753
+
754
+ # Execute table verification in parallel
755
+ if tables_to_verify:
756
+ await asyncio.gather(*[verify_no_changes(table) for table in tables_to_verify])
757
+
758
+ # Final error check
759
+ if errors:
760
+ raise errors[0]
761
+
762
+ return self
763
+
764
+ async def _validate_diff_against_allowed_changes(
765
+ self, diff: Dict[str, Any], allowed_changes: List[Dict[str, Any]]
766
+ ):
767
+ """Validate a collected diff against allowed changes."""
335
768
 
336
769
  def _is_change_allowed(
337
770
  table: str, row_id: Any, field: Optional[str], after_value: Any
@@ -458,6 +891,20 @@ class AsyncSnapshotDiff:
458
891
 
459
892
  return self
460
893
 
894
+ async def expect_only(self, allowed_changes: List[Dict[str, Any]]):
895
+ """Ensure only specified changes occurred."""
896
+ # Special case: empty allowed_changes means no changes should have occurred
897
+ if not allowed_changes:
898
+ return await self._expect_no_changes()
899
+
900
+ # For expect_only, we can optimize by only checking the specific rows mentioned
901
+ if self._can_use_targeted_queries(allowed_changes):
902
+ return await self._expect_only_targeted(allowed_changes)
903
+
904
+ # Fall back to full diff for complex cases
905
+ diff = await self._collect()
906
+ return await self._validate_diff_against_allowed_changes(diff, allowed_changes)
907
+
461
908
 
462
909
  class AsyncQueryBuilder:
463
910
  """Async query builder that translates DSL to SQL and executes through the API."""
@@ -865,7 +1312,6 @@ class AsyncSQLiteResource(Resource):
865
1312
  async def snapshot(self, name: Optional[str] = None) -> AsyncDatabaseSnapshot:
866
1313
  """Create a snapshot of the current database state."""
867
1314
  snapshot = AsyncDatabaseSnapshot(self, name)
868
- await snapshot._ensure_fetched()
869
1315
  return snapshot
870
1316
 
871
1317
  async def diff(
fleet/resources/sqlite.py CHANGED
@@ -23,7 +23,7 @@ from fleet.verifiers.db import (
23
23
 
24
24
 
25
25
  class SyncDatabaseSnapshot:
26
- """Async database snapshot that fetches data through API and stores locally for diffing."""
26
+ """Lazy database snapshot that fetches data on-demand through API."""
27
27
 
28
28
  def __init__(self, resource: "SQLiteResource", name: Optional[str] = None):
29
29
  self.resource = resource
@@ -31,11 +31,12 @@ class SyncDatabaseSnapshot:
31
31
  self.created_at = datetime.utcnow()
32
32
  self._data: Dict[str, List[Dict[str, Any]]] = {}
33
33
  self._schemas: Dict[str, List[str]] = {}
34
- self._fetched = False
34
+ self._table_names: Optional[List[str]] = None
35
+ self._fetched_tables: set = set()
35
36
 
36
- def _ensure_fetched(self):
37
- """Fetch all data from remote database if not already fetched."""
38
- if self._fetched:
37
+ def _ensure_tables_list(self):
38
+ """Fetch just the list of table names if not already fetched."""
39
+ if self._table_names is not None:
39
40
  return
40
41
 
41
42
  # Get all tables
@@ -44,35 +45,36 @@ class SyncDatabaseSnapshot:
44
45
  )
45
46
 
46
47
  if not tables_response.rows:
47
- self._fetched = True
48
+ self._table_names = []
48
49
  return
49
50
 
50
- table_names = [row[0] for row in tables_response.rows]
51
-
52
- # Fetch data from each table
53
- for table in table_names:
54
- # Get table schema
55
- schema_response = self.resource.query(f"PRAGMA table_info({table})")
56
- if schema_response.rows:
57
- self._schemas[table] = [
58
- row[1] for row in schema_response.rows
59
- ] # Column names
60
-
61
- # Get all data
62
- data_response = self.resource.query(f"SELECT * FROM {table}")
63
- if data_response.rows and data_response.columns:
64
- self._data[table] = [
65
- dict(zip(data_response.columns, row)) for row in data_response.rows
66
- ]
67
- else:
68
- self._data[table] = []
51
+ self._table_names = [row[0] for row in tables_response.rows]
52
+
53
+ def _ensure_table_data(self, table: str):
54
+ """Fetch data for a specific table on demand."""
55
+ if table in self._fetched_tables:
56
+ return
69
57
 
70
- self._fetched = True
58
+ # Get table schema
59
+ schema_response = self.resource.query(f"PRAGMA table_info({table})")
60
+ if schema_response.rows:
61
+ self._schemas[table] = [row[1] for row in schema_response.rows] # Column names
62
+
63
+ # Get all data for this table
64
+ data_response = self.resource.query(f"SELECT * FROM {table}")
65
+ if data_response.rows and data_response.columns:
66
+ self._data[table] = [
67
+ dict(zip(data_response.columns, row)) for row in data_response.rows
68
+ ]
69
+ else:
70
+ self._data[table] = []
71
+
72
+ self._fetched_tables.add(table)
71
73
 
72
74
  def tables(self) -> List[str]:
73
75
  """Get list of all tables in the snapshot."""
74
- self._ensure_fetched()
75
- return list(self._data.keys())
76
+ self._ensure_tables_list()
77
+ return list(self._table_names) if self._table_names else []
76
78
 
77
79
  def table(self, table_name: str) -> "SyncSnapshotQueryBuilder":
78
80
  """Create a query builder for snapshot data."""
@@ -84,13 +86,12 @@ class SyncDatabaseSnapshot:
84
86
  ignore_config: Optional[IgnoreConfig] = None,
85
87
  ) -> "SyncSnapshotDiff":
86
88
  """Compare this snapshot with another."""
87
- self._ensure_fetched()
88
- other._ensure_fetched()
89
+ # No need to fetch all data upfront - diff will fetch on demand
89
90
  return SyncSnapshotDiff(self, other, ignore_config)
90
91
 
91
92
 
92
93
  class SyncSnapshotQueryBuilder:
93
- """Query builder that works on local snapshot data."""
94
+ """Query builder that works on snapshot data - can use targeted queries when possible."""
94
95
 
95
96
  def __init__(self, snapshot: SyncDatabaseSnapshot, table: str):
96
97
  self._snapshot = snapshot
@@ -100,10 +101,63 @@ class SyncSnapshotQueryBuilder:
100
101
  self._limit: Optional[int] = None
101
102
  self._order_by: Optional[str] = None
102
103
  self._order_desc: bool = False
104
+ self._use_targeted_query = True # Try to use targeted queries when possible
105
+
106
+ def _can_use_targeted_query(self) -> bool:
107
+ """Check if we can use a targeted query instead of loading all data."""
108
+ # We can use targeted query if:
109
+ # 1. We have simple equality conditions
110
+ # 2. No complex operations like joins
111
+ # 3. The query is selective (has conditions)
112
+ if not self._conditions:
113
+ return False
114
+ for col, op, val in self._conditions:
115
+ if op not in ["=", "IS", "IS NOT"]:
116
+ return False
117
+ return True
118
+
119
+ def _execute_targeted_query(self) -> List[Dict[str, Any]]:
120
+ """Execute a targeted query directly instead of loading all data."""
121
+ # Build WHERE clause
122
+ where_parts = []
123
+ for col, op, val in self._conditions:
124
+ if op == "=" and val is None:
125
+ where_parts.append(f"{col} IS NULL")
126
+ elif op == "IS":
127
+ where_parts.append(f"{col} IS NULL")
128
+ elif op == "IS NOT":
129
+ where_parts.append(f"{col} IS NOT NULL")
130
+ elif op == "=":
131
+ if isinstance(val, str):
132
+ escaped_val = val.replace("'", "''")
133
+ where_parts.append(f"{col} = '{escaped_val}'")
134
+ else:
135
+ where_parts.append(f"{col} = '{val}'")
136
+
137
+ where_clause = " AND ".join(where_parts)
138
+
139
+ # Build full query
140
+ cols = ", ".join(self._select_cols)
141
+ query = f"SELECT {cols} FROM {self._table} WHERE {where_clause}"
142
+
143
+ if self._order_by:
144
+ query += f" ORDER BY {self._order_by}"
145
+ if self._limit is not None:
146
+ query += f" LIMIT {self._limit}"
147
+
148
+ # Execute query
149
+ response = self._snapshot.resource.query(query)
150
+ if response.rows and response.columns:
151
+ return [dict(zip(response.columns, row)) for row in response.rows]
152
+ return []
103
153
 
104
154
  def _get_data(self) -> List[Dict[str, Any]]:
105
- """Get table data from snapshot."""
106
- self._snapshot._ensure_fetched()
155
+ """Get table data - use targeted query if possible, otherwise load all data."""
156
+ if self._use_targeted_query and self._can_use_targeted_query():
157
+ return self._execute_targeted_query()
158
+
159
+ # Fall back to loading all data
160
+ self._snapshot._ensure_table_data(self._table)
107
161
  return self._snapshot._data.get(self._table, [])
108
162
 
109
163
  def eq(self, column: str, value: Any) -> "SyncSnapshotQueryBuilder":
@@ -142,6 +196,11 @@ class SyncSnapshotQueryBuilder:
142
196
  return rows[0] if rows else None
143
197
 
144
198
  def all(self) -> List[Dict[str, Any]]:
199
+ # If we can use targeted query, _get_data already applies filters
200
+ if self._use_targeted_query and self._can_use_targeted_query():
201
+ return self._get_data()
202
+
203
+ # Otherwise, get all data and apply filters manually
145
204
  data = self._get_data()
146
205
 
147
206
  # Apply filters
@@ -206,6 +265,7 @@ class SyncSnapshotDiff:
206
265
  self.after = after
207
266
  self.ignore_config = ignore_config or IgnoreConfig()
208
267
  self._cached: Optional[Dict[str, Any]] = None
268
+ self._targeted_mode = False # Flag to use targeted queries
209
269
 
210
270
  def _get_primary_key_columns(self, table: str) -> List[str]:
211
271
  """Get primary key columns for a table."""
@@ -246,6 +306,10 @@ class SyncSnapshotDiff:
246
306
  # Get primary key columns
247
307
  pk_columns = self._get_primary_key_columns(tbl)
248
308
 
309
+ # Ensure data is fetched for this table
310
+ self.before._ensure_table_data(tbl)
311
+ self.after._ensure_table_data(tbl)
312
+
249
313
  # Get data from both snapshots
250
314
  before_data = self.before._data.get(tbl, [])
251
315
  after_data = self.after._data.get(tbl, [])
@@ -324,9 +388,405 @@ class SyncSnapshotDiff:
324
388
  """Expose the computed diff so callers can introspect like the legacy API."""
325
389
  return self._collect()
326
390
 
327
- def expect_only(self, allowed_changes: List[Dict[str, Any]]):
328
- """Ensure only specified changes occurred."""
329
- diff = self._collect()
391
+ def _can_use_targeted_queries(self, allowed_changes: List[Dict[str, Any]]) -> bool:
392
+ """Check if we can use targeted queries for optimization."""
393
+ # We can use targeted queries if all allowed changes specify table and pk
394
+ for change in allowed_changes:
395
+ if "table" not in change or "pk" not in change:
396
+ return False
397
+ return True
398
+
399
+ def _build_pk_where_clause(self, pk_columns: List[str], pk_value: Any) -> str:
400
+ """Build WHERE clause for primary key lookup."""
401
+ # Escape single quotes in values to prevent SQL injection
402
+ def escape_value(val: Any) -> str:
403
+ if val is None:
404
+ return "NULL"
405
+ elif isinstance(val, str):
406
+ escaped = str(val).replace("'", "''")
407
+ return f"'{escaped}'"
408
+ else:
409
+ return f"'{val}'"
410
+
411
+ if len(pk_columns) == 1:
412
+ return f"{pk_columns[0]} = {escape_value(pk_value)}"
413
+ else:
414
+ # Composite key
415
+ if isinstance(pk_value, tuple):
416
+ conditions = [
417
+ f"{col} = {escape_value(val)}"
418
+ for col, val in zip(pk_columns, pk_value)
419
+ ]
420
+ return " AND ".join(conditions)
421
+ else:
422
+ # Shouldn't happen if data is consistent
423
+ return f"{pk_columns[0]} = {escape_value(pk_value)}"
424
+
425
+ def _expect_no_changes(self):
426
+ """Efficiently verify that no changes occurred between snapshots using row counts."""
427
+ try:
428
+ import concurrent.futures
429
+ from threading import Lock
430
+
431
+ # Get all tables from both snapshots
432
+ before_tables = set(self.before.tables())
433
+ after_tables = set(self.after.tables())
434
+
435
+ # Check for added/removed tables (excluding ignored ones)
436
+ added_tables = after_tables - before_tables
437
+ removed_tables = before_tables - after_tables
438
+
439
+ for table in added_tables:
440
+ if not self.ignore_config.should_ignore_table(table):
441
+ raise AssertionError(f"Unexpected table added: {table}")
442
+
443
+ for table in removed_tables:
444
+ if not self.ignore_config.should_ignore_table(table):
445
+ raise AssertionError(f"Unexpected table removed: {table}")
446
+
447
+ # Prepare tables to check
448
+ tables_to_check = []
449
+ all_tables = before_tables | after_tables
450
+ for table in all_tables:
451
+ if not self.ignore_config.should_ignore_table(table):
452
+ tables_to_check.append(table)
453
+
454
+ # If no tables to check, we're done
455
+ if not tables_to_check:
456
+ return self
457
+
458
+ # Use ThreadPoolExecutor to parallelize count queries
459
+ errors = []
460
+ errors_lock = Lock()
461
+ tables_needing_verification = []
462
+ verification_lock = Lock()
463
+
464
+ def check_table_counts(table: str):
465
+ """Check row counts for a single table."""
466
+ try:
467
+ # Get row counts from both snapshots
468
+ before_count = 0
469
+ after_count = 0
470
+
471
+ if table in before_tables:
472
+ before_count_response = self.before.resource.query(
473
+ f"SELECT COUNT(*) FROM {table}"
474
+ )
475
+ before_count = (
476
+ before_count_response.rows[0][0]
477
+ if before_count_response.rows
478
+ else 0
479
+ )
480
+
481
+ if table in after_tables:
482
+ after_count_response = self.after.resource.query(
483
+ f"SELECT COUNT(*) FROM {table}"
484
+ )
485
+ after_count = (
486
+ after_count_response.rows[0][0]
487
+ if after_count_response.rows
488
+ else 0
489
+ )
490
+
491
+ if before_count != after_count:
492
+ error_msg = (
493
+ f"Unexpected change in table '{table}': "
494
+ f"row count changed from {before_count} to {after_count}"
495
+ )
496
+ with errors_lock:
497
+ errors.append(AssertionError(error_msg))
498
+ elif before_count > 0 and before_count <= 1000:
499
+ # Mark for detailed verification
500
+ with verification_lock:
501
+ tables_needing_verification.append(table)
502
+
503
+ except Exception as e:
504
+ with errors_lock:
505
+ errors.append(e)
506
+
507
+ # Execute count checks in parallel
508
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
509
+ futures = [
510
+ executor.submit(check_table_counts, table)
511
+ for table in tables_to_check
512
+ ]
513
+ concurrent.futures.wait(futures)
514
+
515
+ # Check if any errors occurred during count checking
516
+ if errors:
517
+ raise errors[0]
518
+
519
+ # Now verify small tables for data changes (also in parallel)
520
+ if tables_needing_verification:
521
+ verification_errors = []
522
+
523
+ def verify_table(table: str):
524
+ """Verify a single table's data hasn't changed."""
525
+ try:
526
+ self._verify_table_unchanged(table)
527
+ except AssertionError as e:
528
+ with errors_lock:
529
+ verification_errors.append(e)
530
+
531
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
532
+ futures = [
533
+ executor.submit(verify_table, table)
534
+ for table in tables_needing_verification
535
+ ]
536
+ concurrent.futures.wait(futures)
537
+
538
+ # Check if any errors occurred during verification
539
+ if verification_errors:
540
+ raise verification_errors[0]
541
+
542
+ return self
543
+
544
+ except AssertionError:
545
+ # Re-raise assertion errors (these are expected failures)
546
+ raise
547
+ except Exception as e:
548
+ # If the optimized check fails for other reasons, fall back to full diff
549
+ print(f"Warning: Optimized no-changes check failed: {e}")
550
+ print("Falling back to full diff...")
551
+ return self._validate_diff_against_allowed_changes(self._collect(), [])
552
+
553
+ def _verify_table_unchanged(self, table: str):
554
+ """Verify that a table's data hasn't changed (for small tables)."""
555
+ # Get primary key columns
556
+ pk_columns = self._get_primary_key_columns(table)
557
+
558
+ # Get sorted data from both snapshots
559
+ order_by = ", ".join(pk_columns) if pk_columns else "rowid"
560
+
561
+ before_response = self.before.resource.query(
562
+ f"SELECT * FROM {table} ORDER BY {order_by}"
563
+ )
564
+ after_response = self.after.resource.query(
565
+ f"SELECT * FROM {table} ORDER BY {order_by}"
566
+ )
567
+
568
+ # Quick check: if column counts differ, there's a schema change
569
+ if before_response.columns != after_response.columns:
570
+ raise AssertionError(f"Schema changed in table '{table}'")
571
+
572
+ # Compare row by row
573
+ if len(before_response.rows) != len(after_response.rows):
574
+ raise AssertionError(
575
+ f"Row count mismatch in table '{table}': "
576
+ f"{len(before_response.rows)} vs {len(after_response.rows)}"
577
+ )
578
+
579
+ for i, (before_row, after_row) in enumerate(
580
+ zip(before_response.rows, after_response.rows)
581
+ ):
582
+ before_dict = dict(zip(before_response.columns, before_row))
583
+ after_dict = dict(zip(after_response.columns, after_row))
584
+
585
+ # Compare fields, ignoring those in ignore config
586
+ for field in before_response.columns:
587
+ if self.ignore_config.should_ignore_field(table, field):
588
+ continue
589
+
590
+ if not _values_equivalent(
591
+ before_dict.get(field), after_dict.get(field)
592
+ ):
593
+ pk_val = before_dict.get(pk_columns[0]) if pk_columns else i
594
+ raise AssertionError(
595
+ f"Unexpected change in table '{table}', row {pk_val}, "
596
+ f"field '{field}': {repr(before_dict.get(field))} -> {repr(after_dict.get(field))}"
597
+ )
598
+
599
+ def _is_field_change_allowed(
600
+ self, table_changes: List[Dict[str, Any]], pk: Any, field: str, after_val: Any
601
+ ) -> bool:
602
+ """Check if a specific field change is allowed."""
603
+ for change in table_changes:
604
+ if (
605
+ str(change.get("pk")) == str(pk)
606
+ and change.get("field") == field
607
+ and _values_equivalent(change.get("after"), after_val)
608
+ ):
609
+ return True
610
+ return False
611
+
612
+ def _is_row_change_allowed(
613
+ self, table_changes: List[Dict[str, Any]], pk: Any, change_type: str
614
+ ) -> bool:
615
+ """Check if a row addition/deletion is allowed."""
616
+ for change in table_changes:
617
+ if str(change.get("pk")) == str(pk) and change.get("after") == change_type:
618
+ return True
619
+ return False
620
+
621
+ def _expect_only_targeted(self, allowed_changes: List[Dict[str, Any]]):
622
+ """Optimized version that only queries specific rows mentioned in allowed_changes."""
623
+ import concurrent.futures
624
+ from threading import Lock
625
+
626
+ # Group allowed changes by table
627
+ changes_by_table: Dict[str, List[Dict[str, Any]]] = {}
628
+ for change in allowed_changes:
629
+ table = change["table"]
630
+ if table not in changes_by_table:
631
+ changes_by_table[table] = []
632
+ changes_by_table[table].append(change)
633
+
634
+ errors = []
635
+ errors_lock = Lock()
636
+
637
+ # Function to check a single row
638
+ def check_row(
639
+ table: str,
640
+ pk: Any,
641
+ table_changes: List[Dict[str, Any]],
642
+ pk_columns: List[str],
643
+ ):
644
+ try:
645
+ # Build WHERE clause for this PK
646
+ where_sql = self._build_pk_where_clause(pk_columns, pk)
647
+
648
+ # Query before snapshot
649
+ before_query = f"SELECT * FROM {table} WHERE {where_sql}"
650
+ before_response = self.before.resource.query(before_query)
651
+ before_row = (
652
+ dict(zip(before_response.columns, before_response.rows[0]))
653
+ if before_response.rows
654
+ else None
655
+ )
656
+
657
+ # Query after snapshot
658
+ after_response = self.after.resource.query(before_query)
659
+ after_row = (
660
+ dict(zip(after_response.columns, after_response.rows[0]))
661
+ if after_response.rows
662
+ else None
663
+ )
664
+
665
+ # Check changes for this row
666
+ if before_row and after_row:
667
+ # Modified row - check fields
668
+ for field in set(before_row.keys()) | set(after_row.keys()):
669
+ if self.ignore_config.should_ignore_field(table, field):
670
+ continue
671
+ before_val = before_row.get(field)
672
+ after_val = after_row.get(field)
673
+ if not _values_equivalent(before_val, after_val):
674
+ # Check if this change is allowed
675
+ if not self._is_field_change_allowed(
676
+ table_changes, pk, field, after_val
677
+ ):
678
+ error_msg = (
679
+ f"Unexpected change in table '{table}', "
680
+ f"row {pk}, field '{field}': "
681
+ f"{repr(before_val)} -> {repr(after_val)}"
682
+ )
683
+ with errors_lock:
684
+ errors.append(AssertionError(error_msg))
685
+ return # Stop checking this row
686
+ elif not before_row and after_row:
687
+ # Added row
688
+ if not self._is_row_change_allowed(table_changes, pk, "__added__"):
689
+ error_msg = f"Unexpected row added in table '{table}': {pk}"
690
+ with errors_lock:
691
+ errors.append(AssertionError(error_msg))
692
+ elif before_row and not after_row:
693
+ # Removed row
694
+ if not self._is_row_change_allowed(table_changes, pk, "__removed__"):
695
+ error_msg = f"Unexpected row removed from table '{table}': {pk}"
696
+ with errors_lock:
697
+ errors.append(AssertionError(error_msg))
698
+ except Exception as e:
699
+ with errors_lock:
700
+ errors.append(e)
701
+
702
+ # Prepare all row checks
703
+ row_checks = []
704
+ for table, table_changes in changes_by_table.items():
705
+ if self.ignore_config.should_ignore_table(table):
706
+ continue
707
+
708
+ # Get primary key columns once per table
709
+ pk_columns = self._get_primary_key_columns(table)
710
+
711
+ # Extract unique PKs to check
712
+ pks_to_check = {change["pk"] for change in table_changes}
713
+
714
+ for pk in pks_to_check:
715
+ row_checks.append((table, pk, table_changes, pk_columns))
716
+
717
+ # Execute row checks in parallel
718
+ if row_checks:
719
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
720
+ futures = [
721
+ executor.submit(check_row, table, pk, table_changes, pk_columns)
722
+ for table, pk, table_changes, pk_columns in row_checks
723
+ ]
724
+ concurrent.futures.wait(futures)
725
+
726
+ # Check for errors from row checks
727
+ if errors:
728
+ raise errors[0]
729
+
730
+ # Now check tables not mentioned in allowed_changes to ensure no changes
731
+ all_tables = set(self.before.tables()) | set(self.after.tables())
732
+ tables_to_verify = []
733
+
734
+ for table in all_tables:
735
+ if (
736
+ table not in changes_by_table
737
+ and not self.ignore_config.should_ignore_table(table)
738
+ ):
739
+ tables_to_verify.append(table)
740
+
741
+ # Function to verify no changes in a table
742
+ def verify_no_changes(table: str):
743
+ try:
744
+ # For tables with no allowed changes, just check row counts
745
+ before_count_response = self.before.resource.query(
746
+ f"SELECT COUNT(*) FROM {table}"
747
+ )
748
+ before_count = (
749
+ before_count_response.rows[0][0]
750
+ if before_count_response.rows
751
+ else 0
752
+ )
753
+
754
+ after_count_response = self.after.resource.query(
755
+ f"SELECT COUNT(*) FROM {table}"
756
+ )
757
+ after_count = (
758
+ after_count_response.rows[0][0] if after_count_response.rows else 0
759
+ )
760
+
761
+ if before_count != after_count:
762
+ error_msg = (
763
+ f"Unexpected change in table '{table}': "
764
+ f"row count changed from {before_count} to {after_count}"
765
+ )
766
+ with errors_lock:
767
+ errors.append(AssertionError(error_msg))
768
+ except Exception as e:
769
+ with errors_lock:
770
+ errors.append(e)
771
+
772
+ # Execute table verification in parallel
773
+ if tables_to_verify:
774
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
775
+ futures = [
776
+ executor.submit(verify_no_changes, table) for table in tables_to_verify
777
+ ]
778
+ concurrent.futures.wait(futures)
779
+
780
+ # Final error check
781
+ if errors:
782
+ raise errors[0]
783
+
784
+ return self
785
+
786
+ def _validate_diff_against_allowed_changes(
787
+ self, diff: Dict[str, Any], allowed_changes: List[Dict[str, Any]]
788
+ ):
789
+ """Validate a collected diff against allowed changes."""
330
790
 
331
791
  def _is_change_allowed(
332
792
  table: str, row_id: Any, field: Optional[str], after_value: Any
@@ -453,6 +913,20 @@ class SyncSnapshotDiff:
453
913
 
454
914
  return self
455
915
 
916
+ def expect_only(self, allowed_changes: List[Dict[str, Any]]):
917
+ """Ensure only specified changes occurred."""
918
+ # Special case: empty allowed_changes means no changes should have occurred
919
+ if not allowed_changes:
920
+ return self._expect_no_changes()
921
+
922
+ # For expect_only, we can optimize by only checking the specific rows mentioned
923
+ if self._can_use_targeted_queries(allowed_changes):
924
+ return self._expect_only_targeted(allowed_changes)
925
+
926
+ # Fall back to full diff for complex cases
927
+ diff = self._collect()
928
+ return self._validate_diff_against_allowed_changes(diff, allowed_changes)
929
+
456
930
 
457
931
  class SyncQueryBuilder:
458
932
  """Async query builder that translates DSL to SQL and executes through the API."""
@@ -852,7 +1326,6 @@ class SQLiteResource(Resource):
852
1326
  def snapshot(self, name: Optional[str] = None) -> SyncDatabaseSnapshot:
853
1327
  """Create a snapshot of the current database state."""
854
1328
  snapshot = SyncDatabaseSnapshot(self, name)
855
- snapshot._ensure_fetched()
856
1329
  return snapshot
857
1330
 
858
1331
  def diff(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.72
3
+ Version: 0.2.72b2
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -46,7 +46,7 @@ fleet/_async/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
46
46
  fleet/_async/resources/base.py,sha256=UfrenxUqcpL8SgYGOo8o8HgRvv2-ZO5G2Cdo91ofEdg,664
47
47
  fleet/_async/resources/browser.py,sha256=oldoSiymJ1lJkADhpUG81ViOBDNyppX1jSoEwe9-W94,1369
48
48
  fleet/_async/resources/mcp.py,sha256=TLEsLiFhfVfZFs0Fu_uDPm-h4FPdvqgQblYqs-PTHhc,1720
49
- fleet/_async/resources/sqlite.py,sha256=zRTbqW0qLt8EO9UTIxtcbrmwPV3cGK3rCaX1oSFsd_E,33036
49
+ fleet/_async/resources/sqlite.py,sha256=smzDpSyjSswKnFEZhO-tbonMrItHTKl-9E-CRu2t7sM,51881
50
50
  fleet/_async/verifiers/__init__.py,sha256=1WTlCNq4tIFbbXaQu5Bf2WppZq0A8suhtZbxMTSOwxI,465
51
51
  fleet/_async/verifiers/bundler.py,sha256=9aWWXFsovBPcndE06IATn5jaeli5fRORAYeenF9heN0,26264
52
52
  fleet/_async/verifiers/verifier.py,sha256=Zvok2Mog09l885StW429Rg_8_4bd-gaYUGIgpILeb_I,14207
@@ -60,7 +60,7 @@ fleet/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  fleet/resources/base.py,sha256=AXZzT0_yWHkT497q3yekfr0xsD4cPGMCC6y7C43TIkk,663
61
61
  fleet/resources/browser.py,sha256=hRNM0YMsVQUAraZGNi_B-KXxLpuddy4ntoEDFSw7czU,1295
62
62
  fleet/resources/mcp.py,sha256=c6O4vVJnXANuHMGMe4IPxgp4zBEbFaGm6_d9e6j8Myc,1695
63
- fleet/resources/sqlite.py,sha256=BZE31mMOuRIT0vB4ppy6bO-QczmlYytHQkgo9nCZIew,31837
63
+ fleet/resources/sqlite.py,sha256=dkEkVaf0NHITRe9dsWRmevmsN6xDKZ_Y75l0ikzm20Q,51821
64
64
  fleet/verifiers/__init__.py,sha256=GntS8qc3xv8mm-cku1t3xjvOll5jcc5FuiVqQgR4Y6Q,458
65
65
  fleet/verifiers/bundler.py,sha256=9aWWXFsovBPcndE06IATn5jaeli5fRORAYeenF9heN0,26264
66
66
  fleet/verifiers/code.py,sha256=A1i_UabZspbyj1awzKVQ_HRxgMO3fU7NbkxYyTrp7So,48
@@ -69,7 +69,7 @@ fleet/verifiers/decorator.py,sha256=RuTjjDijbicNfMSjA7HcTpKueEki5dzNOdTuHS7UoZs,
69
69
  fleet/verifiers/parse.py,sha256=qz9AfJrTbjlg-LU-lE8Ciqi7Yt2a8-cs17FdpjTLhMk,8550
70
70
  fleet/verifiers/sql_differ.py,sha256=TqTLWyK3uOyLbitT6HYzYEzuSFC39wcyhgk3rcm__k8,6525
71
71
  fleet/verifiers/verifier.py,sha256=npnTBB-A1Cl66gNOGPR6UaybvcDy6C6_hWchyIJeDyc,14252
72
- fleet_python-0.2.72.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
72
+ fleet_python-0.2.72b2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
73
73
  scripts/fix_sync_imports.py,sha256=X9fWLTpiPGkSHsjyQUDepOJkxOqw1DPj7nd8wFlFqLQ,8368
74
74
  scripts/unasync.py,sha256=vWVQxRWX8SRZO5cmzEhpvnG_REhCWXpidIGIpWmEcvI,696
75
75
  tests/__init__.py,sha256=Re1SdyxH8NfyL1kjhi7SQkGP1mYeWB-D6UALqdIMd8I,35
@@ -78,7 +78,7 @@ tests/test_instance_dispatch.py,sha256=CvU4C3LBIqsYZdEsEFfontGjyxAZfVYyXnGwxyIvX
78
78
  tests/test_sqlite_resource_dual_mode.py,sha256=Mh8jBd-xsIGDYFsOACKKK_5DXMUYlFFS7W-jaY6AjG4,8734
79
79
  tests/test_sqlite_shared_memory_behavior.py,sha256=fKx_1BmLS3b8x-9pMgjMycpnaHWY8P-2ZuXEspx6Sbw,4082
80
80
  tests/test_verifier_from_string.py,sha256=Lxi3TpFHFb-hG4-UhLKZJkqo84ax9YJY8G6beO-1erM,13581
81
- fleet_python-0.2.72.dist-info/METADATA,sha256=kLMcRhlggIcI6dTHDsNLERjMO31GVTl8iIo4nC9vkBs,3304
82
- fleet_python-0.2.72.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
- fleet_python-0.2.72.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
84
- fleet_python-0.2.72.dist-info/RECORD,,
81
+ fleet_python-0.2.72b2.dist-info/METADATA,sha256=sfbqYC5oJI-MKlwTK_1qNgB7QheXKOUGDHnufWBdNR0,3306
82
+ fleet_python-0.2.72b2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
+ fleet_python-0.2.72b2.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
84
+ fleet_python-0.2.72b2.dist-info/RECORD,,