fleet-python 0.2.68__py3-none-any.whl → 0.2.69b2__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.

Potentially problematic release.


This version of fleet-python might be problematic. Click here for more details.

@@ -0,0 +1,607 @@
1
+ """Unit tests for Fleet.instance() and AsyncFleet.instance() dispatch logic."""
2
+
3
+ import pytest
4
+ import tempfile
5
+ import sqlite3
6
+ import os
7
+ from unittest.mock import Mock, AsyncMock, patch
8
+ from fleet.client import Fleet
9
+ from fleet._async.client import AsyncFleet
10
+
11
+
12
+ class TestFleetInstanceDispatch:
13
+ """Test Fleet.instance() dispatching logic."""
14
+
15
+ @pytest.fixture
16
+ def fleet_client(self):
17
+ """Create a Fleet client with mocked HTTP client."""
18
+ with patch("fleet.client.default_httpx_client") as mock_client:
19
+ mock_client.return_value = Mock()
20
+ client = Fleet(api_key="test_key")
21
+ # Mock the internal client's request method
22
+ client.client.request = Mock()
23
+ return client
24
+
25
+ @pytest.fixture
26
+ def temp_db_files(self):
27
+ """Create temporary SQLite database files."""
28
+ files = {}
29
+ for name in ["current", "seed"]:
30
+ fd, path = tempfile.mkstemp(suffix=".db")
31
+ os.close(fd)
32
+
33
+ # Initialize with test table
34
+ conn = sqlite3.connect(path)
35
+ cursor = conn.cursor()
36
+ cursor.execute(f"""
37
+ CREATE TABLE test_{name} (
38
+ id INTEGER PRIMARY KEY,
39
+ value TEXT
40
+ )
41
+ """)
42
+ cursor.execute(f"INSERT INTO test_{name} (id, value) VALUES (1, '{name}_data')")
43
+ conn.commit()
44
+ conn.close()
45
+
46
+ files[name] = path
47
+
48
+ yield files
49
+
50
+ # Cleanup
51
+ for path in files.values():
52
+ if os.path.exists(path):
53
+ os.remove(path)
54
+
55
+ def test_dispatch_dict_local_mode(self, fleet_client, temp_db_files):
56
+ """Test that dict input dispatches to local mode."""
57
+ env = fleet_client.instance(temp_db_files)
58
+
59
+ assert env.instance_id == "local"
60
+ assert env.env_key == "local"
61
+ assert env.region == "local"
62
+
63
+ # Verify we can access databases
64
+ current_db = env.db("current")
65
+ assert current_db is not None
66
+ assert current_db.mode == "direct"
67
+
68
+ seed_db = env.db("seed")
69
+ assert seed_db is not None
70
+ assert seed_db.mode == "direct"
71
+
72
+ # Verify query works
73
+ result = current_db.query("SELECT * FROM test_current")
74
+ assert result.success is True
75
+ assert len(result.rows) == 1
76
+
77
+ def test_dispatch_url_localhost_mode(self, fleet_client):
78
+ """Test that http:// URL dispatches to localhost mode."""
79
+ env = fleet_client.instance("http://localhost:8080")
80
+
81
+ assert env.instance_id == "http://localhost:8080"
82
+ assert env.env_key == "localhost"
83
+ assert env.region == "localhost"
84
+
85
+ # Verify instance client is created with correct URL
86
+ assert env.instance.base_url == "http://localhost:8080"
87
+
88
+ def test_dispatch_https_url_localhost_mode(self, fleet_client):
89
+ """Test that https:// URL dispatches to localhost mode."""
90
+ env = fleet_client.instance("https://custom-server.local:9000/api")
91
+
92
+ assert env.instance_id == "https://custom-server.local:9000/api"
93
+ assert env.env_key == "localhost"
94
+
95
+ # Verify instance client is created with correct URL
96
+ assert env.instance.base_url == "https://custom-server.local:9000/api"
97
+
98
+ def test_dispatch_string_remote_mode(self, fleet_client):
99
+ """Test that regular string dispatches to remote mode."""
100
+ # Mock the HTTP response for remote mode
101
+ mock_response = Mock()
102
+ mock_response.json.return_value = {
103
+ "instance_id": "test-instance-123",
104
+ "env_key": "test_env",
105
+ "version": "v1.0.0",
106
+ "status": "running",
107
+ "subdomain": "test",
108
+ "created_at": "2025-01-01T00:00:00Z",
109
+ "updated_at": "2025-01-01T00:00:00Z",
110
+ "terminated_at": None,
111
+ "team_id": "team-123",
112
+ "region": "us-west-1",
113
+ "env_variables": None,
114
+ "data_key": None,
115
+ "data_version": None,
116
+ "urls": {
117
+ "root": "https://test.fleet.run/",
118
+ "app": ["https://test.fleet.run/app"],
119
+ "api": "https://test.fleet.run/api",
120
+ "health": "https://test.fleet.run/health",
121
+ "api_docs": "https://test.fleet.run/docs",
122
+ "manager": {
123
+ "api": "https://test.fleet.run/manager/api",
124
+ "docs": "https://test.fleet.run/manager/docs",
125
+ "reset": "https://test.fleet.run/manager/reset",
126
+ "diff": "https://test.fleet.run/manager/diff",
127
+ "snapshot": "https://test.fleet.run/manager/snapshot",
128
+ "execute_verifier_function": "https://test.fleet.run/manager/execute",
129
+ "execute_verifier_function_with_upload": "https://test.fleet.run/manager/execute_upload",
130
+ },
131
+ },
132
+ "health": True,
133
+ }
134
+ fleet_client.client.request.return_value = mock_response
135
+
136
+ env = fleet_client.instance("test-instance-123")
137
+
138
+ # Verify HTTP client was called for remote API
139
+ fleet_client.client.request.assert_called()
140
+ call_args = fleet_client.client.request.call_args
141
+ assert "/v1/env/instances/test-instance-123" in call_args[0][1]
142
+
143
+ # Verify we got a remote env back
144
+ assert env.instance_id == "test-instance-123"
145
+ assert env.env_key == "test_env"
146
+
147
+ def test_local_mode_query_functionality(self, fleet_client, temp_db_files):
148
+ """Test that local mode databases are fully functional."""
149
+ env = fleet_client.instance(temp_db_files)
150
+
151
+ # Test query on current db
152
+ current = env.db("current")
153
+ result = current.query("SELECT value FROM test_current WHERE id = 1")
154
+ assert result.success is True
155
+ assert result.rows[0][0] == "current_data"
156
+
157
+ # Test query on seed db
158
+ seed = env.db("seed")
159
+ result = seed.query("SELECT value FROM test_seed WHERE id = 1")
160
+ assert result.success is True
161
+ assert result.rows[0][0] == "seed_data"
162
+
163
+ # Test query builder
164
+ current_data = current.table("test_current").eq("id", 1).first()
165
+ assert current_data is not None
166
+ assert current_data["value"] == "current_data"
167
+
168
+ def test_local_mode_exec_functionality(self, fleet_client, temp_db_files):
169
+ """Test that local mode supports write operations."""
170
+ env = fleet_client.instance(temp_db_files)
171
+ db = env.db("current")
172
+
173
+ # Insert data
174
+ result = db.exec("INSERT INTO test_current (id, value) VALUES (2, 'new_data')")
175
+ assert result.success is True
176
+ assert result.rows_affected == 1
177
+
178
+ # Verify insert
179
+ check = db.query("SELECT value FROM test_current WHERE id = 2")
180
+ assert check.rows[0][0] == "new_data"
181
+
182
+ # Update data
183
+ result = db.exec("UPDATE test_current SET value = 'updated' WHERE id = 2")
184
+ assert result.success is True
185
+
186
+ # Verify update
187
+ check = db.query("SELECT value FROM test_current WHERE id = 2")
188
+ assert check.rows[0][0] == "updated"
189
+
190
+ def test_local_mode_creates_new_instances(self, fleet_client, temp_db_files):
191
+ """Test that db() creates new SQLiteResource instances each time (not cached)."""
192
+ env = fleet_client.instance(temp_db_files)
193
+
194
+ db1 = env.db("current")
195
+ db2 = env.db("current")
196
+
197
+ # Should be different objects (new wrapper each time)
198
+ assert db1 is not db2
199
+
200
+ # But both should work identically
201
+ result1 = db1.query("SELECT * FROM test_current")
202
+ result2 = db2.query("SELECT * FROM test_current")
203
+ assert result1.rows == result2.rows
204
+
205
+ def test_local_mode_no_state_leakage(self, fleet_client, temp_db_files):
206
+ """Test that monkey-patching one db() instance doesn't affect another."""
207
+ env = fleet_client.instance(temp_db_files)
208
+
209
+ db1 = env.db("current")
210
+ db1._custom_attribute = "test_value"
211
+
212
+ db2 = env.db("current")
213
+ # db2 should be a fresh instance without the custom attribute
214
+ assert not hasattr(db2, "_custom_attribute")
215
+
216
+ def test_memory_basic_syntax(self, fleet_client):
217
+ """Test that :memory: syntax works."""
218
+ env = fleet_client.instance({
219
+ "current": ":memory:"
220
+ })
221
+
222
+ db = env.db("current")
223
+ assert db.mode == "direct"
224
+
225
+ # Create table and insert data
226
+ result = db.exec("CREATE TABLE test (id INTEGER, value TEXT)")
227
+ assert result.success is True
228
+
229
+ result = db.exec("INSERT INTO test VALUES (1, 'hello')")
230
+ assert result.success is True
231
+
232
+ # Verify data exists
233
+ result = db.query("SELECT * FROM test")
234
+ assert result.success is True
235
+ assert result.rows == [[1, 'hello']]
236
+
237
+ def test_memory_namespace_syntax(self, fleet_client):
238
+ """Test that :memory:namespace syntax creates isolated databases."""
239
+ env = fleet_client.instance({
240
+ "current": ":memory:current",
241
+ "seed": ":memory:seed"
242
+ })
243
+
244
+ current = env.db("current")
245
+ seed = env.db("seed")
246
+
247
+ # Create table in current
248
+ current.exec("CREATE TABLE test_current (id INTEGER, value TEXT)")
249
+ current.exec("INSERT INTO test_current VALUES (1, 'current_data')")
250
+
251
+ # Create table in seed
252
+ seed.exec("CREATE TABLE test_seed (id INTEGER, value TEXT)")
253
+ seed.exec("INSERT INTO test_seed VALUES (2, 'seed_data')")
254
+
255
+ # Verify current has its data
256
+ result = current.query("SELECT * FROM test_current")
257
+ assert result.success is True
258
+ assert result.rows == [[1, 'current_data']]
259
+
260
+ # Verify seed has its data
261
+ result = seed.query("SELECT * FROM test_seed")
262
+ assert result.success is True
263
+ assert result.rows == [[2, 'seed_data']]
264
+
265
+ # Verify current doesn't have seed's table
266
+ result = current.query("SELECT name FROM sqlite_master WHERE type='table' AND name='test_seed'")
267
+ assert result.success is True
268
+ assert result.rows is None or len(result.rows) == 0
269
+
270
+ # Verify seed doesn't have current's table
271
+ result = seed.query("SELECT name FROM sqlite_master WHERE type='table' AND name='test_current'")
272
+ assert result.success is True
273
+ assert result.rows is None or len(result.rows) == 0
274
+
275
+ def test_memory_data_sharing(self, fleet_client):
276
+ """Test that multiple db() calls to same namespace share data."""
277
+ env = fleet_client.instance({
278
+ "current": ":memory:shared"
279
+ })
280
+
281
+ # Create table and insert data via first connection
282
+ db1 = env.db("current")
283
+ db1.exec("CREATE TABLE test (id INTEGER, value TEXT)")
284
+ db1.exec("INSERT INTO test VALUES (1, 'shared_data')")
285
+
286
+ # Access same database via second connection
287
+ db2 = env.db("current")
288
+ result = db2.query("SELECT * FROM test")
289
+ assert result.success is True
290
+ assert result.rows == [[1, 'shared_data']]
291
+
292
+ # Insert via second connection
293
+ db2.exec("INSERT INTO test VALUES (2, 'more_data')")
294
+
295
+ # Verify via first connection
296
+ result = db1.query("SELECT * FROM test ORDER BY id")
297
+ assert result.success is True
298
+ assert result.rows == [[1, 'shared_data'], [2, 'more_data']]
299
+
300
+ def test_memory_data_persists(self, fleet_client):
301
+ """Test that memory data persists while env is alive."""
302
+ env = fleet_client.instance({
303
+ "current": ":memory:persistent"
304
+ })
305
+
306
+ # Create and populate database
307
+ db = env.db("current")
308
+ db.exec("CREATE TABLE test (id INTEGER, value TEXT)")
309
+ db.exec("INSERT INTO test VALUES (1, 'persistent')")
310
+
311
+ # Delete the db reference
312
+ del db
313
+
314
+ # Create new connection - data should still be there
315
+ db_new = env.db("current")
316
+ result = db_new.query("SELECT * FROM test")
317
+ assert result.success is True
318
+ assert result.rows == [[1, 'persistent']]
319
+
320
+ def test_memory_anchor_connection_cleanup(self, fleet_client):
321
+ """Test that closing the instance cleans up anchor connections."""
322
+ env = fleet_client.instance({
323
+ "current": ":memory:cleanup_test",
324
+ "seed": ":memory:cleanup_test2"
325
+ })
326
+
327
+ # Verify anchor connections were created
328
+ assert hasattr(env._instance, '_memory_anchors')
329
+ assert 'current' in env._instance._memory_anchors
330
+ assert 'seed' in env._instance._memory_anchors
331
+
332
+ # Setup databases
333
+ current = env.db("current")
334
+ current.exec("CREATE TABLE test (id INTEGER)")
335
+
336
+ # Close the instance
337
+ env._instance.close()
338
+
339
+ # Verify anchors were cleaned up
340
+ assert len(env._instance._memory_anchors) == 0
341
+
342
+
343
+ @pytest.mark.asyncio
344
+ class TestAsyncFleetInstanceDispatch:
345
+ """Test AsyncFleet.instance() dispatching logic."""
346
+
347
+ @pytest.fixture
348
+ async def async_fleet_client(self):
349
+ """Create an AsyncFleet client with mocked HTTP client."""
350
+ with patch("fleet._async.client.default_httpx_client") as mock_client:
351
+ mock_client.return_value = AsyncMock()
352
+ client = AsyncFleet(api_key="test_key")
353
+ # Mock the internal client's request method
354
+ client.client.request = AsyncMock()
355
+ return client
356
+
357
+ @pytest.fixture
358
+ def temp_db_files(self):
359
+ """Create temporary SQLite database files."""
360
+ files = {}
361
+ for name in ["current", "seed"]:
362
+ fd, path = tempfile.mkstemp(suffix=".db")
363
+ os.close(fd)
364
+
365
+ # Initialize with test table
366
+ conn = sqlite3.connect(path)
367
+ cursor = conn.cursor()
368
+ cursor.execute(f"""
369
+ CREATE TABLE test_{name} (
370
+ id INTEGER PRIMARY KEY,
371
+ value TEXT
372
+ )
373
+ """)
374
+ cursor.execute(f"INSERT INTO test_{name} (id, value) VALUES (1, '{name}_data')")
375
+ conn.commit()
376
+ conn.close()
377
+
378
+ files[name] = path
379
+
380
+ yield files
381
+
382
+ # Cleanup
383
+ for path in files.values():
384
+ if os.path.exists(path):
385
+ os.remove(path)
386
+
387
+ async def test_dispatch_dict_local_mode(self, async_fleet_client, temp_db_files):
388
+ """Test that dict input dispatches to local mode in async."""
389
+ env = await async_fleet_client.instance(temp_db_files)
390
+
391
+ assert env.instance_id == "local"
392
+ assert env.env_key == "local"
393
+ assert env.region == "local"
394
+
395
+ # Verify we can access databases
396
+ current_db = env.db("current")
397
+ assert current_db is not None
398
+ assert current_db.mode == "direct"
399
+
400
+ # Verify async query works
401
+ result = await current_db.query("SELECT * FROM test_current")
402
+ assert result.success is True
403
+ assert len(result.rows) == 1
404
+
405
+ async def test_dispatch_url_localhost_mode(self, async_fleet_client):
406
+ """Test that http:// URL dispatches to localhost mode in async."""
407
+ env = await async_fleet_client.instance("http://localhost:8080")
408
+
409
+ assert env.instance_id == "http://localhost:8080"
410
+ assert env.env_key == "localhost"
411
+
412
+ # Verify instance client is created with correct URL
413
+ assert env.instance.base_url == "http://localhost:8080"
414
+
415
+ async def test_local_mode_async_query_functionality(self, async_fleet_client, temp_db_files):
416
+ """Test that local mode databases work with async queries."""
417
+ env = await async_fleet_client.instance(temp_db_files)
418
+
419
+ # Test async query
420
+ current = env.db("current")
421
+ result = await current.query("SELECT value FROM test_current WHERE id = 1")
422
+ assert result.success is True
423
+ assert result.rows[0][0] == "current_data"
424
+
425
+ # Test async query builder
426
+ current_data = await current.table("test_current").eq("id", 1).first()
427
+ assert current_data is not None
428
+ assert current_data["value"] == "current_data"
429
+
430
+ async def test_local_mode_async_exec_functionality(self, async_fleet_client, temp_db_files):
431
+ """Test that local mode supports async write operations."""
432
+ env = await async_fleet_client.instance(temp_db_files)
433
+ db = env.db("current")
434
+
435
+ # Insert data
436
+ result = await db.exec("INSERT INTO test_current (id, value) VALUES (2, 'async_data')")
437
+ assert result.success is True
438
+ assert result.rows_affected == 1
439
+
440
+ # Verify insert
441
+ check = await db.query("SELECT value FROM test_current WHERE id = 2")
442
+ assert check.rows[0][0] == "async_data"
443
+
444
+ async def test_local_mode_async_creates_new_instances(self, async_fleet_client, temp_db_files):
445
+ """Test that db() creates new AsyncSQLiteResource instances each time (not cached)."""
446
+ env = await async_fleet_client.instance(temp_db_files)
447
+
448
+ db1 = env.db("current")
449
+ db2 = env.db("current")
450
+
451
+ # Should be different objects (new wrapper each time)
452
+ assert db1 is not db2
453
+
454
+ # But both should work identically
455
+ result1 = await db1.query("SELECT * FROM test_current")
456
+ result2 = await db2.query("SELECT * FROM test_current")
457
+ assert result1.rows == result2.rows
458
+
459
+ async def test_local_mode_async_no_state_leakage(self, async_fleet_client, temp_db_files):
460
+ """Test that monkey-patching one db() instance doesn't affect another in async."""
461
+ env = await async_fleet_client.instance(temp_db_files)
462
+
463
+ db1 = env.db("current")
464
+ db1._custom_attribute = "test_value"
465
+
466
+ db2 = env.db("current")
467
+ # db2 should be a fresh instance without the custom attribute
468
+ assert not hasattr(db2, "_custom_attribute")
469
+
470
+
471
+ @pytest.mark.asyncio
472
+ class TestAsyncFleetInMemoryTests:
473
+ """Test AsyncFleet in-memory functionality."""
474
+
475
+ @pytest.fixture
476
+ async def async_fleet_client(self):
477
+ """Create an AsyncFleet client with mocked HTTP client."""
478
+ with patch("fleet._async.client.default_httpx_client") as mock_client:
479
+ mock_client.return_value = AsyncMock()
480
+ client = AsyncFleet(api_key="test_key")
481
+ client.client.request = AsyncMock()
482
+ return client
483
+
484
+ async def test_memory_basic_syntax(self, async_fleet_client):
485
+ """Test that :memory: syntax works in async."""
486
+ env = await async_fleet_client.instance({
487
+ "current": ":memory:"
488
+ })
489
+
490
+ db = env.db("current")
491
+ assert db.mode == "direct"
492
+
493
+ # Create table and insert data
494
+ result = await db.exec("CREATE TABLE test (id INTEGER, value TEXT)")
495
+ assert result.success is True
496
+
497
+ result = await db.exec("INSERT INTO test VALUES (1, 'hello')")
498
+ assert result.success is True
499
+
500
+ # Verify data exists
501
+ result = await db.query("SELECT * FROM test")
502
+ assert result.success is True
503
+ assert result.rows == [[1, 'hello']]
504
+
505
+ async def test_memory_namespace_syntax(self, async_fleet_client):
506
+ """Test that :memory:namespace syntax creates isolated databases in async."""
507
+ env = await async_fleet_client.instance({
508
+ "current": ":memory:current",
509
+ "seed": ":memory:seed"
510
+ })
511
+
512
+ current = env.db("current")
513
+ seed = env.db("seed")
514
+
515
+ # Create table in current
516
+ await current.exec("CREATE TABLE test_current (id INTEGER, value TEXT)")
517
+ await current.exec("INSERT INTO test_current VALUES (1, 'current_data')")
518
+
519
+ # Create table in seed
520
+ await seed.exec("CREATE TABLE test_seed (id INTEGER, value TEXT)")
521
+ await seed.exec("INSERT INTO test_seed VALUES (2, 'seed_data')")
522
+
523
+ # Verify current has its data
524
+ result = await current.query("SELECT * FROM test_current")
525
+ assert result.success is True
526
+ assert result.rows == [[1, 'current_data']]
527
+
528
+ # Verify seed has its data
529
+ result = await seed.query("SELECT * FROM test_seed")
530
+ assert result.success is True
531
+ assert result.rows == [[2, 'seed_data']]
532
+
533
+ # Verify namespaces are isolated
534
+ result = await current.query("SELECT name FROM sqlite_master WHERE type='table' AND name='test_seed'")
535
+ assert result.success is True
536
+ assert result.rows is None or len(result.rows) == 0
537
+
538
+ async def test_memory_data_sharing(self, async_fleet_client):
539
+ """Test that multiple db() calls to same namespace share data in async."""
540
+ env = await async_fleet_client.instance({
541
+ "current": ":memory:shared"
542
+ })
543
+
544
+ # Create table and insert data via first connection
545
+ db1 = env.db("current")
546
+ await db1.exec("CREATE TABLE test (id INTEGER, value TEXT)")
547
+ await db1.exec("INSERT INTO test VALUES (1, 'shared_data')")
548
+
549
+ # Access same database via second connection
550
+ db2 = env.db("current")
551
+ result = await db2.query("SELECT * FROM test")
552
+ assert result.success is True
553
+ assert result.rows == [[1, 'shared_data']]
554
+
555
+ # Insert via second connection
556
+ await db2.exec("INSERT INTO test VALUES (2, 'more_data')")
557
+
558
+ # Verify via first connection
559
+ result = await db1.query("SELECT * FROM test ORDER BY id")
560
+ assert result.success is True
561
+ assert result.rows == [[1, 'shared_data'], [2, 'more_data']]
562
+
563
+ async def test_memory_data_persists(self, async_fleet_client):
564
+ """Test that memory data persists while env is alive in async."""
565
+ env = await async_fleet_client.instance({
566
+ "current": ":memory:persistent"
567
+ })
568
+
569
+ # Create and populate database
570
+ db = env.db("current")
571
+ await db.exec("CREATE TABLE test (id INTEGER, value TEXT)")
572
+ await db.exec("INSERT INTO test VALUES (1, 'persistent')")
573
+
574
+ # Delete the db reference
575
+ del db
576
+
577
+ # Create new connection - data should still be there
578
+ db_new = env.db("current")
579
+ result = await db_new.query("SELECT * FROM test")
580
+ assert result.success is True
581
+ assert result.rows == [[1, 'persistent']]
582
+
583
+ async def test_memory_anchor_connection_cleanup(self, async_fleet_client):
584
+ """Test that closing the instance cleans up anchor connections in async."""
585
+ env = await async_fleet_client.instance({
586
+ "current": ":memory:cleanup_test",
587
+ "seed": ":memory:cleanup_test2"
588
+ })
589
+
590
+ # Verify anchor connections were created
591
+ assert hasattr(env._instance, '_memory_anchors')
592
+ assert 'current' in env._instance._memory_anchors
593
+ assert 'seed' in env._instance._memory_anchors
594
+
595
+ # Setup databases
596
+ current = env.db("current")
597
+ await current.exec("CREATE TABLE test (id INTEGER)")
598
+
599
+ # Close the instance
600
+ env._instance.close()
601
+
602
+ # Verify anchors were cleaned up
603
+ assert len(env._instance._memory_anchors) == 0
604
+
605
+
606
+ if __name__ == "__main__":
607
+ pytest.main([__file__, "-v"])