android-emu-agent 0.1.3__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.
- android_emu_agent/__init__.py +3 -0
- android_emu_agent/actions/__init__.py +1 -0
- android_emu_agent/actions/executor.py +288 -0
- android_emu_agent/actions/selector.py +122 -0
- android_emu_agent/actions/wait.py +193 -0
- android_emu_agent/artifacts/__init__.py +1 -0
- android_emu_agent/artifacts/manager.py +125 -0
- android_emu_agent/artifacts/py.typed +0 -0
- android_emu_agent/cli/__init__.py +1 -0
- android_emu_agent/cli/commands/__init__.py +1 -0
- android_emu_agent/cli/commands/action.py +158 -0
- android_emu_agent/cli/commands/app_cmd.py +95 -0
- android_emu_agent/cli/commands/artifact.py +81 -0
- android_emu_agent/cli/commands/daemon.py +62 -0
- android_emu_agent/cli/commands/device.py +122 -0
- android_emu_agent/cli/commands/emulator.py +46 -0
- android_emu_agent/cli/commands/file.py +139 -0
- android_emu_agent/cli/commands/reliability.py +310 -0
- android_emu_agent/cli/commands/session.py +65 -0
- android_emu_agent/cli/commands/ui.py +112 -0
- android_emu_agent/cli/commands/wait.py +132 -0
- android_emu_agent/cli/daemon_client.py +185 -0
- android_emu_agent/cli/main.py +52 -0
- android_emu_agent/cli/utils.py +171 -0
- android_emu_agent/daemon/__init__.py +1 -0
- android_emu_agent/daemon/core.py +62 -0
- android_emu_agent/daemon/health.py +177 -0
- android_emu_agent/daemon/models.py +244 -0
- android_emu_agent/daemon/server.py +1644 -0
- android_emu_agent/db/__init__.py +1 -0
- android_emu_agent/db/models.py +229 -0
- android_emu_agent/device/__init__.py +1 -0
- android_emu_agent/device/manager.py +522 -0
- android_emu_agent/device/session.py +129 -0
- android_emu_agent/errors.py +232 -0
- android_emu_agent/files/__init__.py +1 -0
- android_emu_agent/files/manager.py +311 -0
- android_emu_agent/py.typed +0 -0
- android_emu_agent/reliability/__init__.py +1 -0
- android_emu_agent/reliability/manager.py +244 -0
- android_emu_agent/ui/__init__.py +1 -0
- android_emu_agent/ui/context.py +169 -0
- android_emu_agent/ui/ref_resolver.py +149 -0
- android_emu_agent/ui/snapshotter.py +236 -0
- android_emu_agent/validation.py +59 -0
- android_emu_agent-0.1.3.dist-info/METADATA +375 -0
- android_emu_agent-0.1.3.dist-info/RECORD +50 -0
- android_emu_agent-0.1.3.dist-info/WHEEL +4 -0
- android_emu_agent-0.1.3.dist-info/entry_points.txt +2 -0
- android_emu_agent-0.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Database - SQLite persistence for sessions and refs."""
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Database models and connection management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
11
|
+
|
|
12
|
+
import aiosqlite
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger()
|
|
19
|
+
|
|
20
|
+
DEFAULT_DB_PATH = Path.home() / ".android-emu-agent" / "state.db"
|
|
21
|
+
|
|
22
|
+
SCHEMA = """
|
|
23
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
24
|
+
session_id TEXT PRIMARY KEY,
|
|
25
|
+
device_serial TEXT NOT NULL,
|
|
26
|
+
created_at TEXT NOT NULL,
|
|
27
|
+
last_activity TEXT NOT NULL,
|
|
28
|
+
generation INTEGER DEFAULT 0,
|
|
29
|
+
metadata TEXT DEFAULT '{}'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS ref_maps (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
session_id TEXT NOT NULL,
|
|
35
|
+
generation INTEGER NOT NULL,
|
|
36
|
+
ref TEXT NOT NULL,
|
|
37
|
+
locator_bundle TEXT NOT NULL,
|
|
38
|
+
created_at TEXT NOT NULL,
|
|
39
|
+
UNIQUE(session_id, generation, ref)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_device ON sessions(device_serial);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_refs_session_gen ON ref_maps(session_id, generation);
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Database:
|
|
48
|
+
"""Async SQLite database manager."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, db_path: Path | None = None) -> None:
|
|
51
|
+
self.db_path = db_path or DEFAULT_DB_PATH
|
|
52
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
self._connection: aiosqlite.Connection | None = None
|
|
54
|
+
|
|
55
|
+
async def connect(self) -> None:
|
|
56
|
+
"""Connect to database and initialize schema."""
|
|
57
|
+
self._connection = await aiosqlite.connect(self.db_path)
|
|
58
|
+
await self._connection.executescript(SCHEMA)
|
|
59
|
+
await self._connection.commit()
|
|
60
|
+
logger.info("database_connected", path=str(self.db_path))
|
|
61
|
+
|
|
62
|
+
async def disconnect(self) -> None:
|
|
63
|
+
"""Close database connection."""
|
|
64
|
+
if self._connection:
|
|
65
|
+
await self._connection.close()
|
|
66
|
+
self._connection = None
|
|
67
|
+
logger.info("database_disconnected")
|
|
68
|
+
|
|
69
|
+
@asynccontextmanager
|
|
70
|
+
async def transaction(self) -> AsyncGenerator[aiosqlite.Connection, None]:
|
|
71
|
+
"""Context manager for database transactions."""
|
|
72
|
+
if not self._connection:
|
|
73
|
+
raise RuntimeError("Database not connected")
|
|
74
|
+
try:
|
|
75
|
+
yield self._connection
|
|
76
|
+
await self._connection.commit()
|
|
77
|
+
except Exception:
|
|
78
|
+
await self._connection.rollback()
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
# Session operations
|
|
82
|
+
|
|
83
|
+
async def save_session(
|
|
84
|
+
self,
|
|
85
|
+
session_id: str,
|
|
86
|
+
device_serial: str,
|
|
87
|
+
generation: int = 0,
|
|
88
|
+
metadata: dict[str, Any] | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Save or update a session."""
|
|
91
|
+
now = datetime.now().isoformat()
|
|
92
|
+
async with self.transaction() as conn:
|
|
93
|
+
await conn.execute(
|
|
94
|
+
"""
|
|
95
|
+
INSERT INTO sessions (session_id, device_serial, created_at, last_activity, generation, metadata)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
97
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
98
|
+
last_activity = excluded.last_activity,
|
|
99
|
+
generation = excluded.generation,
|
|
100
|
+
metadata = excluded.metadata
|
|
101
|
+
""",
|
|
102
|
+
(session_id, device_serial, now, now, generation, json.dumps(metadata or {})),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def get_session(self, session_id: str) -> dict[str, Any] | None:
|
|
106
|
+
"""Get session by ID."""
|
|
107
|
+
if not self._connection:
|
|
108
|
+
return None
|
|
109
|
+
cursor = await self._connection.execute(
|
|
110
|
+
"SELECT * FROM sessions WHERE session_id = ?",
|
|
111
|
+
(session_id,),
|
|
112
|
+
)
|
|
113
|
+
row = await cursor.fetchone()
|
|
114
|
+
if row:
|
|
115
|
+
return {
|
|
116
|
+
"session_id": row[0],
|
|
117
|
+
"device_serial": row[1],
|
|
118
|
+
"created_at": row[2],
|
|
119
|
+
"last_activity": row[3],
|
|
120
|
+
"generation": row[4],
|
|
121
|
+
"metadata": json.loads(row[5]),
|
|
122
|
+
}
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
async def list_sessions(self) -> list[dict[str, Any]]:
|
|
126
|
+
"""List all sessions."""
|
|
127
|
+
if not self._connection:
|
|
128
|
+
return []
|
|
129
|
+
cursor = await self._connection.execute(
|
|
130
|
+
"SELECT * FROM sessions ORDER BY last_activity DESC"
|
|
131
|
+
)
|
|
132
|
+
rows = await cursor.fetchall()
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
"session_id": row[0],
|
|
136
|
+
"device_serial": row[1],
|
|
137
|
+
"created_at": row[2],
|
|
138
|
+
"last_activity": row[3],
|
|
139
|
+
"generation": row[4],
|
|
140
|
+
}
|
|
141
|
+
for row in rows
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
async def delete_session(self, session_id: str) -> None:
|
|
145
|
+
"""Delete a session and its refs."""
|
|
146
|
+
async with self.transaction() as conn:
|
|
147
|
+
await conn.execute("DELETE FROM ref_maps WHERE session_id = ?", (session_id,))
|
|
148
|
+
await conn.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
|
149
|
+
|
|
150
|
+
# Ref operations
|
|
151
|
+
|
|
152
|
+
async def save_refs(
|
|
153
|
+
self,
|
|
154
|
+
session_id: str,
|
|
155
|
+
generation: int,
|
|
156
|
+
refs: list[dict[str, Any]],
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Save refs for a snapshot generation."""
|
|
159
|
+
now = datetime.now().isoformat()
|
|
160
|
+
async with self.transaction() as conn:
|
|
161
|
+
await conn.executemany(
|
|
162
|
+
"""
|
|
163
|
+
INSERT INTO ref_maps (session_id, generation, ref, locator_bundle, created_at)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?)
|
|
165
|
+
ON CONFLICT(session_id, generation, ref) DO UPDATE SET
|
|
166
|
+
locator_bundle = excluded.locator_bundle
|
|
167
|
+
""",
|
|
168
|
+
[(session_id, generation, r["ref"], json.dumps(r), now) for r in refs],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def get_ref(
|
|
172
|
+
self,
|
|
173
|
+
session_id: str,
|
|
174
|
+
generation: int,
|
|
175
|
+
ref: str,
|
|
176
|
+
) -> dict[str, Any] | None:
|
|
177
|
+
"""Get a specific ref from a generation."""
|
|
178
|
+
if not self._connection:
|
|
179
|
+
return None
|
|
180
|
+
cursor = await self._connection.execute(
|
|
181
|
+
"SELECT locator_bundle FROM ref_maps WHERE session_id = ? AND generation = ? AND ref = ?",
|
|
182
|
+
(session_id, generation, ref),
|
|
183
|
+
)
|
|
184
|
+
row = await cursor.fetchone()
|
|
185
|
+
if row:
|
|
186
|
+
return cast(dict[str, Any], json.loads(row[0]))
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
async def get_ref_any_generation(
|
|
190
|
+
self,
|
|
191
|
+
session_id: str,
|
|
192
|
+
ref: str,
|
|
193
|
+
) -> tuple[int, dict[str, Any]] | None:
|
|
194
|
+
"""Get the most recent ref across generations."""
|
|
195
|
+
if not self._connection:
|
|
196
|
+
return None
|
|
197
|
+
cursor = await self._connection.execute(
|
|
198
|
+
"""
|
|
199
|
+
SELECT generation, locator_bundle
|
|
200
|
+
FROM ref_maps
|
|
201
|
+
WHERE session_id = ? AND ref = ?
|
|
202
|
+
ORDER BY generation DESC
|
|
203
|
+
LIMIT 1
|
|
204
|
+
""",
|
|
205
|
+
(session_id, ref),
|
|
206
|
+
)
|
|
207
|
+
row = await cursor.fetchone()
|
|
208
|
+
if row:
|
|
209
|
+
return row[0], cast(dict[str, Any], json.loads(row[1]))
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
async def cleanup_old_refs(self, session_id: str, keep_generations: int = 3) -> None:
|
|
213
|
+
"""Remove refs older than keep_generations."""
|
|
214
|
+
if not self._connection:
|
|
215
|
+
return
|
|
216
|
+
async with self.transaction() as conn:
|
|
217
|
+
# Get current max generation
|
|
218
|
+
cursor = await conn.execute(
|
|
219
|
+
"SELECT MAX(generation) FROM ref_maps WHERE session_id = ?",
|
|
220
|
+
(session_id,),
|
|
221
|
+
)
|
|
222
|
+
row = await cursor.fetchone()
|
|
223
|
+
if row and row[0]:
|
|
224
|
+
max_gen = row[0]
|
|
225
|
+
cutoff = max_gen - keep_generations
|
|
226
|
+
await conn.execute(
|
|
227
|
+
"DELETE FROM ref_maps WHERE session_id = ? AND generation < ?",
|
|
228
|
+
(session_id, cutoff),
|
|
229
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Device - ADB connectivity, device management, determinism controls."""
|