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.
- fleet/_async/client.py +163 -6
- fleet/_async/instance/client.py +19 -4
- fleet/_async/resources/sqlite.py +150 -1
- fleet/_async/tasks.py +5 -2
- fleet/client.py +163 -6
- fleet/instance/client.py +20 -5
- fleet/resources/sqlite.py +143 -1
- fleet/tasks.py +5 -2
- {fleet_python-0.2.68.dist-info → fleet_python-0.2.69b2.dist-info}/METADATA +1 -1
- {fleet_python-0.2.68.dist-info → fleet_python-0.2.69b2.dist-info}/RECORD +16 -13
- tests/test_instance_dispatch.py +607 -0
- tests/test_sqlite_resource_dual_mode.py +263 -0
- tests/test_sqlite_shared_memory_behavior.py +117 -0
- {fleet_python-0.2.68.dist-info → fleet_python-0.2.69b2.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.68.dist-info → fleet_python-0.2.69b2.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.68.dist-info → fleet_python-0.2.69b2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Unit tests for SQLiteResource dual-mode functionality."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import tempfile
|
|
5
|
+
import sqlite3
|
|
6
|
+
import os
|
|
7
|
+
from fleet.resources.sqlite import SQLiteResource
|
|
8
|
+
from fleet.instance.models import (
|
|
9
|
+
Resource as ResourceModel,
|
|
10
|
+
ResourceType,
|
|
11
|
+
ResourceMode,
|
|
12
|
+
QueryResponse,
|
|
13
|
+
DescribeResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestSQLiteResourceDirectMode:
|
|
18
|
+
"""Test SQLiteResource in direct (local file) mode."""
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def temp_db(self):
|
|
22
|
+
"""Create a temporary SQLite database for testing."""
|
|
23
|
+
fd, path = tempfile.mkstemp(suffix=".db")
|
|
24
|
+
os.close(fd)
|
|
25
|
+
|
|
26
|
+
# Initialize with test data
|
|
27
|
+
conn = sqlite3.connect(path)
|
|
28
|
+
cursor = conn.cursor()
|
|
29
|
+
cursor.execute("""
|
|
30
|
+
CREATE TABLE users (
|
|
31
|
+
id INTEGER PRIMARY KEY,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
email TEXT,
|
|
34
|
+
age INTEGER
|
|
35
|
+
)
|
|
36
|
+
""")
|
|
37
|
+
cursor.execute(
|
|
38
|
+
"INSERT INTO users (id, name, email, age) VALUES (?, ?, ?, ?)",
|
|
39
|
+
(1, "Alice", "alice@example.com", 30),
|
|
40
|
+
)
|
|
41
|
+
cursor.execute(
|
|
42
|
+
"INSERT INTO users (id, name, email, age) VALUES (?, ?, ?, ?)",
|
|
43
|
+
(2, "Bob", "bob@example.com", 25),
|
|
44
|
+
)
|
|
45
|
+
conn.commit()
|
|
46
|
+
conn.close()
|
|
47
|
+
|
|
48
|
+
yield path
|
|
49
|
+
|
|
50
|
+
# Cleanup
|
|
51
|
+
if os.path.exists(path):
|
|
52
|
+
os.remove(path)
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def resource(self, temp_db):
|
|
56
|
+
"""Create a SQLiteResource in direct mode."""
|
|
57
|
+
resource_model = ResourceModel(
|
|
58
|
+
name="test_db",
|
|
59
|
+
type=ResourceType.db,
|
|
60
|
+
mode=ResourceMode.rw,
|
|
61
|
+
)
|
|
62
|
+
return SQLiteResource(resource_model, client=None, db_path=temp_db)
|
|
63
|
+
|
|
64
|
+
def test_mode_property(self, resource):
|
|
65
|
+
"""Test that mode property returns 'direct'."""
|
|
66
|
+
assert resource.mode == "direct"
|
|
67
|
+
|
|
68
|
+
def test_query_select(self, resource):
|
|
69
|
+
"""Test SELECT query in direct mode."""
|
|
70
|
+
response = resource.query("SELECT * FROM users ORDER BY id")
|
|
71
|
+
|
|
72
|
+
assert response.success is True
|
|
73
|
+
assert response.columns == ["id", "name", "email", "age"]
|
|
74
|
+
assert len(response.rows) == 2
|
|
75
|
+
# Rows can be either tuples or lists depending on the implementation
|
|
76
|
+
assert list(response.rows[0]) == [1, "Alice", "alice@example.com", 30]
|
|
77
|
+
assert list(response.rows[1]) == [2, "Bob", "bob@example.com", 25]
|
|
78
|
+
|
|
79
|
+
def test_query_with_params(self, resource):
|
|
80
|
+
"""Test query with parameters."""
|
|
81
|
+
response = resource.query("SELECT * FROM users WHERE id = ?", [1])
|
|
82
|
+
|
|
83
|
+
assert response.success is True
|
|
84
|
+
assert len(response.rows) == 1
|
|
85
|
+
assert response.rows[0][1] == "Alice"
|
|
86
|
+
|
|
87
|
+
def test_exec_insert(self, resource):
|
|
88
|
+
"""Test INSERT operation in direct mode."""
|
|
89
|
+
response = resource.exec(
|
|
90
|
+
"INSERT INTO users (id, name, email, age) VALUES (?, ?, ?, ?)",
|
|
91
|
+
[3, "Charlie", "charlie@example.com", 35],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert response.success is True
|
|
95
|
+
assert response.rows_affected == 1
|
|
96
|
+
assert response.last_insert_id == 3
|
|
97
|
+
|
|
98
|
+
# Verify the insert
|
|
99
|
+
check = resource.query("SELECT * FROM users WHERE id = 3")
|
|
100
|
+
assert len(check.rows) == 1
|
|
101
|
+
assert check.rows[0][1] == "Charlie"
|
|
102
|
+
|
|
103
|
+
def test_exec_update(self, resource):
|
|
104
|
+
"""Test UPDATE operation in direct mode."""
|
|
105
|
+
response = resource.exec("UPDATE users SET age = ? WHERE id = ?", [31, 1])
|
|
106
|
+
|
|
107
|
+
assert response.success is True
|
|
108
|
+
assert response.rows_affected == 1
|
|
109
|
+
|
|
110
|
+
# Verify the update
|
|
111
|
+
check = resource.query("SELECT age FROM users WHERE id = 1")
|
|
112
|
+
assert check.rows[0][0] == 31
|
|
113
|
+
|
|
114
|
+
def test_exec_delete(self, resource):
|
|
115
|
+
"""Test DELETE operation in direct mode."""
|
|
116
|
+
response = resource.exec("DELETE FROM users WHERE id = ?", [2])
|
|
117
|
+
|
|
118
|
+
assert response.success is True
|
|
119
|
+
assert response.rows_affected == 1
|
|
120
|
+
|
|
121
|
+
# Verify the delete
|
|
122
|
+
check = resource.query("SELECT * FROM users")
|
|
123
|
+
assert len(check.rows) == 1
|
|
124
|
+
|
|
125
|
+
def test_describe(self, resource):
|
|
126
|
+
"""Test describe() in direct mode."""
|
|
127
|
+
response = resource.describe()
|
|
128
|
+
|
|
129
|
+
assert response.success is True
|
|
130
|
+
assert response.resource_name == "test_db"
|
|
131
|
+
assert len(response.tables) == 1
|
|
132
|
+
|
|
133
|
+
table = response.tables[0]
|
|
134
|
+
assert table.name == "users"
|
|
135
|
+
assert table.sql is not None
|
|
136
|
+
assert len(table.columns) == 4
|
|
137
|
+
|
|
138
|
+
# Check column details
|
|
139
|
+
columns = {col["name"]: col for col in table.columns}
|
|
140
|
+
assert "id" in columns
|
|
141
|
+
assert columns["id"]["primary_key"] is True
|
|
142
|
+
assert "name" in columns
|
|
143
|
+
assert columns["name"]["notnull"] is True
|
|
144
|
+
|
|
145
|
+
def test_table_query_builder(self, resource):
|
|
146
|
+
"""Test table() query builder in direct mode."""
|
|
147
|
+
users = resource.table("users").all()
|
|
148
|
+
|
|
149
|
+
assert len(users) == 2
|
|
150
|
+
assert users[0]["name"] == "Alice"
|
|
151
|
+
assert users[1]["name"] == "Bob"
|
|
152
|
+
|
|
153
|
+
def test_query_builder_eq(self, resource):
|
|
154
|
+
"""Test query builder eq() filter."""
|
|
155
|
+
user = resource.table("users").eq("name", "Alice").first()
|
|
156
|
+
|
|
157
|
+
assert user is not None
|
|
158
|
+
assert user["name"] == "Alice"
|
|
159
|
+
assert user["email"] == "alice@example.com"
|
|
160
|
+
|
|
161
|
+
def test_query_builder_count(self, resource):
|
|
162
|
+
"""Test query builder count()."""
|
|
163
|
+
count = resource.table("users").count()
|
|
164
|
+
assert count == 2
|
|
165
|
+
|
|
166
|
+
count_filtered = resource.table("users").eq("age", 30).count()
|
|
167
|
+
assert count_filtered == 1
|
|
168
|
+
|
|
169
|
+
def test_query_builder_where(self, resource):
|
|
170
|
+
"""Test query builder where() with multiple conditions."""
|
|
171
|
+
users = resource.table("users").where(age=25).all()
|
|
172
|
+
|
|
173
|
+
assert len(users) == 1
|
|
174
|
+
assert users[0]["name"] == "Bob"
|
|
175
|
+
|
|
176
|
+
def test_query_builder_limit(self, resource):
|
|
177
|
+
"""Test query builder limit()."""
|
|
178
|
+
users = resource.table("users").limit(1).all()
|
|
179
|
+
|
|
180
|
+
assert len(users) == 1
|
|
181
|
+
|
|
182
|
+
def test_query_error_handling(self, resource):
|
|
183
|
+
"""Test error handling for invalid queries."""
|
|
184
|
+
response = resource.query("SELECT * FROM nonexistent_table")
|
|
185
|
+
|
|
186
|
+
assert response.success is False
|
|
187
|
+
assert response.error is not None
|
|
188
|
+
assert "nonexistent_table" in response.error.lower() or "no such table" in response.error.lower()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestSQLiteResourceHTTPMode:
|
|
192
|
+
"""Test SQLiteResource in HTTP (remote) mode."""
|
|
193
|
+
|
|
194
|
+
@pytest.fixture
|
|
195
|
+
def mock_client(self, mocker):
|
|
196
|
+
"""Create a mock HTTP client."""
|
|
197
|
+
return mocker.Mock()
|
|
198
|
+
|
|
199
|
+
@pytest.fixture
|
|
200
|
+
def resource(self, mock_client):
|
|
201
|
+
"""Create a SQLiteResource in HTTP mode."""
|
|
202
|
+
resource_model = ResourceModel(
|
|
203
|
+
name="remote_db",
|
|
204
|
+
type=ResourceType.db,
|
|
205
|
+
mode=ResourceMode.rw,
|
|
206
|
+
)
|
|
207
|
+
return SQLiteResource(resource_model, client=mock_client, db_path=None)
|
|
208
|
+
|
|
209
|
+
def test_mode_property(self, resource):
|
|
210
|
+
"""Test that mode property returns 'http'."""
|
|
211
|
+
assert resource.mode == "http"
|
|
212
|
+
|
|
213
|
+
def test_query_http(self, resource, mock_client, mocker):
|
|
214
|
+
"""Test that query() calls HTTP client."""
|
|
215
|
+
# Mock the HTTP response
|
|
216
|
+
mock_response = mocker.Mock()
|
|
217
|
+
mock_response.json.return_value = {
|
|
218
|
+
"success": True,
|
|
219
|
+
"columns": ["id", "name"],
|
|
220
|
+
"rows": [[1, "Alice"]],
|
|
221
|
+
"message": "Query successful",
|
|
222
|
+
}
|
|
223
|
+
mock_client.request.return_value = mock_response
|
|
224
|
+
|
|
225
|
+
response = resource.query("SELECT * FROM users")
|
|
226
|
+
|
|
227
|
+
# Verify HTTP client was called
|
|
228
|
+
mock_client.request.assert_called_once()
|
|
229
|
+
call_args = mock_client.request.call_args
|
|
230
|
+
assert call_args[0][0] == "POST"
|
|
231
|
+
assert "/query" in call_args[0][1]
|
|
232
|
+
|
|
233
|
+
# Verify response
|
|
234
|
+
assert response.success is True
|
|
235
|
+
assert response.columns == ["id", "name"]
|
|
236
|
+
|
|
237
|
+
def test_describe_http(self, resource, mock_client, mocker):
|
|
238
|
+
"""Test that describe() calls HTTP client."""
|
|
239
|
+
# Mock the HTTP response
|
|
240
|
+
mock_response = mocker.Mock()
|
|
241
|
+
mock_response.json.return_value = {
|
|
242
|
+
"success": True,
|
|
243
|
+
"resource_name": "remote_db",
|
|
244
|
+
"tables": [],
|
|
245
|
+
"message": "Schema retrieved",
|
|
246
|
+
}
|
|
247
|
+
mock_client.request.return_value = mock_response
|
|
248
|
+
|
|
249
|
+
response = resource.describe()
|
|
250
|
+
|
|
251
|
+
# Verify HTTP client was called
|
|
252
|
+
mock_client.request.assert_called_once()
|
|
253
|
+
call_args = mock_client.request.call_args
|
|
254
|
+
assert call_args[0][0] == "GET"
|
|
255
|
+
assert "/describe" in call_args[0][1]
|
|
256
|
+
|
|
257
|
+
# Verify response
|
|
258
|
+
assert response.success is True
|
|
259
|
+
assert response.resource_name == "remote_db"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Verification tests for SQLite shared memory behavior.
|
|
2
|
+
|
|
3
|
+
These tests verify how SQLite's shared memory databases work to ensure
|
|
4
|
+
our implementation assumptions are correct.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlite3
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_plain_memory_no_sharing():
|
|
12
|
+
"""Verify that plain :memory: databases don't share data."""
|
|
13
|
+
conn1 = sqlite3.connect(':memory:')
|
|
14
|
+
conn1.execute("CREATE TABLE test (id INT)")
|
|
15
|
+
conn1.execute("INSERT INTO test VALUES (1)")
|
|
16
|
+
|
|
17
|
+
# Second connection to :memory: creates a SEPARATE database
|
|
18
|
+
conn2 = sqlite3.connect(':memory:')
|
|
19
|
+
|
|
20
|
+
with pytest.raises(sqlite3.OperationalError, match="no such table"):
|
|
21
|
+
conn2.execute("SELECT * FROM test")
|
|
22
|
+
|
|
23
|
+
conn1.close()
|
|
24
|
+
conn2.close()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_shared_memory_uri_sharing():
|
|
28
|
+
"""Verify that shared memory URIs DO share data."""
|
|
29
|
+
conn1 = sqlite3.connect('file:testdb?mode=memory&cache=shared', uri=True)
|
|
30
|
+
conn1.execute("CREATE TABLE test (id INT)")
|
|
31
|
+
conn1.execute("INSERT INTO test VALUES (1)")
|
|
32
|
+
conn1.commit() # Commit so other connections can see changes
|
|
33
|
+
|
|
34
|
+
# Second connection to same URI shares the database
|
|
35
|
+
conn2 = sqlite3.connect('file:testdb?mode=memory&cache=shared', uri=True)
|
|
36
|
+
result = conn2.execute("SELECT * FROM test").fetchall()
|
|
37
|
+
|
|
38
|
+
assert result == [(1,)]
|
|
39
|
+
|
|
40
|
+
conn1.close()
|
|
41
|
+
conn2.close()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_different_namespaces_isolated():
|
|
45
|
+
"""Verify that different shared memory namespaces are isolated."""
|
|
46
|
+
conn1 = sqlite3.connect('file:db1?mode=memory&cache=shared', uri=True)
|
|
47
|
+
conn1.execute("CREATE TABLE test (id INT)")
|
|
48
|
+
conn1.execute("INSERT INTO test VALUES (1)")
|
|
49
|
+
|
|
50
|
+
conn2 = sqlite3.connect('file:db2?mode=memory&cache=shared', uri=True)
|
|
51
|
+
|
|
52
|
+
# db2 should not have the test table from db1
|
|
53
|
+
with pytest.raises(sqlite3.OperationalError, match="no such table"):
|
|
54
|
+
conn2.execute("SELECT * FROM test")
|
|
55
|
+
|
|
56
|
+
conn1.close()
|
|
57
|
+
conn2.close()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_data_lost_when_all_connections_close():
|
|
61
|
+
"""Verify that shared memory data is lost when all connections close."""
|
|
62
|
+
conn1 = sqlite3.connect('file:tempdb?mode=memory&cache=shared', uri=True)
|
|
63
|
+
conn1.execute("CREATE TABLE test (id INT)")
|
|
64
|
+
conn1.execute("INSERT INTO test VALUES (1)")
|
|
65
|
+
conn1.commit() # Commit so other connections can see changes
|
|
66
|
+
|
|
67
|
+
conn2 = sqlite3.connect('file:tempdb?mode=memory&cache=shared', uri=True)
|
|
68
|
+
result = conn2.execute("SELECT * FROM test").fetchall()
|
|
69
|
+
assert result == [(1,)]
|
|
70
|
+
|
|
71
|
+
# Close all connections
|
|
72
|
+
conn1.close()
|
|
73
|
+
conn2.close()
|
|
74
|
+
|
|
75
|
+
# Open new connection - database is recreated empty
|
|
76
|
+
conn3 = sqlite3.connect('file:tempdb?mode=memory&cache=shared', uri=True)
|
|
77
|
+
|
|
78
|
+
with pytest.raises(sqlite3.OperationalError, match="no such table"):
|
|
79
|
+
conn3.execute("SELECT * FROM test")
|
|
80
|
+
|
|
81
|
+
conn3.close()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_anchor_connection_keeps_data_alive():
|
|
85
|
+
"""Verify that keeping one connection open preserves the data."""
|
|
86
|
+
# Create anchor connection
|
|
87
|
+
anchor = sqlite3.connect('file:persistent?mode=memory&cache=shared', uri=True)
|
|
88
|
+
# Drop table if it exists from a previous test run
|
|
89
|
+
anchor.execute("DROP TABLE IF EXISTS test")
|
|
90
|
+
anchor.execute("CREATE TABLE test (id INT)")
|
|
91
|
+
anchor.execute("INSERT INTO test VALUES (1)")
|
|
92
|
+
anchor.commit() # Commit so other connections can see changes
|
|
93
|
+
|
|
94
|
+
# Open and close other connections
|
|
95
|
+
conn1 = sqlite3.connect('file:persistent?mode=memory&cache=shared', uri=True)
|
|
96
|
+
result = conn1.execute("SELECT * FROM test").fetchall()
|
|
97
|
+
assert result == [(1,)]
|
|
98
|
+
conn1.close()
|
|
99
|
+
|
|
100
|
+
# Even after conn1 closes, data is still there because anchor is open
|
|
101
|
+
conn2 = sqlite3.connect('file:persistent?mode=memory&cache=shared', uri=True)
|
|
102
|
+
result = conn2.execute("SELECT * FROM test").fetchall()
|
|
103
|
+
assert result == [(1,)]
|
|
104
|
+
conn2.close()
|
|
105
|
+
|
|
106
|
+
# Close anchor
|
|
107
|
+
anchor.close()
|
|
108
|
+
|
|
109
|
+
# Now data is gone
|
|
110
|
+
conn3 = sqlite3.connect('file:persistent?mode=memory&cache=shared', uri=True)
|
|
111
|
+
with pytest.raises(sqlite3.OperationalError, match="no such table"):
|
|
112
|
+
conn3.execute("SELECT * FROM test")
|
|
113
|
+
conn3.close()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
pytest.main([__file__, "-v"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|