agentmesh-platform 1.0.0a1__py3-none-any.whl → 1.0.0a2__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.
@@ -0,0 +1,232 @@
1
+ """
2
+ In-Memory Storage Provider.
3
+
4
+ Simple in-memory implementation for development and testing.
5
+ """
6
+
7
+ from typing import Optional
8
+ import asyncio
9
+ from collections import defaultdict
10
+
11
+ from .provider import AbstractStorageProvider, StorageConfig
12
+
13
+
14
+ class MemoryStorageProvider(AbstractStorageProvider):
15
+ """
16
+ In-memory storage provider.
17
+
18
+ Uses Python dictionaries for storage. Data is lost on restart.
19
+ Suitable for development and testing only.
20
+ """
21
+
22
+ def __init__(self, config: StorageConfig):
23
+ """Initialize in-memory storage."""
24
+ super().__init__(config)
25
+ self._data: dict[str, str] = {}
26
+ self._hashes: dict[str, dict[str, str]] = defaultdict(dict)
27
+ self._lists: dict[str, list[str]] = defaultdict(list)
28
+ self._sorted_sets: dict[str, dict[str, float]] = defaultdict(dict)
29
+ self._ttls: dict[str, float] = {}
30
+ self._connected = False
31
+
32
+ async def connect(self) -> None:
33
+ """Establish connection (no-op for memory)."""
34
+ self._connected = True
35
+
36
+ async def disconnect(self) -> None:
37
+ """Close connection (no-op for memory)."""
38
+ self._connected = False
39
+
40
+ async def health_check(self) -> bool:
41
+ """Check if storage is healthy."""
42
+ return self._connected
43
+
44
+ # Key-Value Operations
45
+
46
+ async def get(self, key: str) -> Optional[str]:
47
+ """Get value by key."""
48
+ return self._data.get(key)
49
+
50
+ async def set(
51
+ self,
52
+ key: str,
53
+ value: str,
54
+ ttl_seconds: Optional[int] = None,
55
+ ) -> bool:
56
+ """Set value with optional TTL."""
57
+ self._data[key] = value
58
+ if ttl_seconds is not None:
59
+ import time
60
+ self._ttls[key] = time.time() + ttl_seconds
61
+ return True
62
+
63
+ async def delete(self, key: str) -> bool:
64
+ """Delete key."""
65
+ if key in self._data:
66
+ del self._data[key]
67
+ return True
68
+ return False
69
+
70
+ async def exists(self, key: str) -> bool:
71
+ """Check if key exists."""
72
+ return key in self._data
73
+
74
+ # Hash Operations
75
+
76
+ async def hget(self, key: str, field: str) -> Optional[str]:
77
+ """Get hash field value."""
78
+ return self._hashes.get(key, {}).get(field)
79
+
80
+ async def hset(self, key: str, field: str, value: str) -> bool:
81
+ """Set hash field value."""
82
+ self._hashes[key][field] = value
83
+ return True
84
+
85
+ async def hgetall(self, key: str) -> dict[str, str]:
86
+ """Get all hash fields."""
87
+ return dict(self._hashes.get(key, {}))
88
+
89
+ async def hdel(self, key: str, field: str) -> bool:
90
+ """Delete hash field."""
91
+ if key in self._hashes and field in self._hashes[key]:
92
+ del self._hashes[key][field]
93
+ return True
94
+ return False
95
+
96
+ async def hkeys(self, key: str) -> list[str]:
97
+ """Get all hash field names."""
98
+ return list(self._hashes.get(key, {}).keys())
99
+
100
+ # List Operations
101
+
102
+ async def lpush(self, key: str, value: str) -> int:
103
+ """Push value to head of list."""
104
+ self._lists[key].insert(0, value)
105
+ return len(self._lists[key])
106
+
107
+ async def rpush(self, key: str, value: str) -> int:
108
+ """Push value to tail of list."""
109
+ self._lists[key].append(value)
110
+ return len(self._lists[key])
111
+
112
+ async def lrange(self, key: str, start: int, stop: int) -> list[str]:
113
+ """Get list range [start, stop]."""
114
+ lst = self._lists.get(key, [])
115
+ if stop == -1:
116
+ return lst[start:]
117
+ return lst[start:stop + 1]
118
+
119
+ async def llen(self, key: str) -> int:
120
+ """Get list length."""
121
+ return len(self._lists.get(key, []))
122
+
123
+ # Sorted Set Operations
124
+
125
+ async def zadd(self, key: str, score: float, member: str) -> bool:
126
+ """Add member to sorted set with score."""
127
+ self._sorted_sets[key][member] = score
128
+ return True
129
+
130
+ async def zscore(self, key: str, member: str) -> Optional[float]:
131
+ """Get score of member in sorted set."""
132
+ return self._sorted_sets.get(key, {}).get(member)
133
+
134
+ async def zrange(
135
+ self,
136
+ key: str,
137
+ start: int,
138
+ stop: int,
139
+ with_scores: bool = False,
140
+ ) -> list[str] | list[tuple[str, float]]:
141
+ """Get sorted set range."""
142
+ sorted_set = self._sorted_sets.get(key, {})
143
+ sorted_items = sorted(sorted_set.items(), key=lambda x: x[1])
144
+
145
+ if stop == -1:
146
+ items = sorted_items[start:]
147
+ else:
148
+ items = sorted_items[start:stop + 1]
149
+
150
+ if with_scores:
151
+ return items
152
+ return [member for member, _ in items]
153
+
154
+ async def zrangebyscore(
155
+ self,
156
+ key: str,
157
+ min_score: float,
158
+ max_score: float,
159
+ with_scores: bool = False,
160
+ ) -> list[str] | list[tuple[str, float]]:
161
+ """Get sorted set range by score."""
162
+ sorted_set = self._sorted_sets.get(key, {})
163
+ items = [
164
+ (member, score)
165
+ for member, score in sorted(sorted_set.items(), key=lambda x: x[1])
166
+ if min_score <= score <= max_score
167
+ ]
168
+
169
+ if with_scores:
170
+ return items
171
+ return [member for member, _ in items]
172
+
173
+ # Atomic Operations
174
+
175
+ async def incr(self, key: str) -> int:
176
+ """Increment value atomically."""
177
+ current = int(self._data.get(key, "0"))
178
+ new_value = current + 1
179
+ self._data[key] = str(new_value)
180
+ return new_value
181
+
182
+ async def decr(self, key: str) -> int:
183
+ """Decrement value atomically."""
184
+ current = int(self._data.get(key, "0"))
185
+ new_value = current - 1
186
+ self._data[key] = str(new_value)
187
+ return new_value
188
+
189
+ async def incrby(self, key: str, amount: int) -> int:
190
+ """Increment value by amount."""
191
+ current = int(self._data.get(key, "0"))
192
+ new_value = current + amount
193
+ self._data[key] = str(new_value)
194
+ return new_value
195
+
196
+ # Batch Operations
197
+
198
+ async def mget(self, keys: list[str]) -> list[Optional[str]]:
199
+ """Get multiple values."""
200
+ return [self._data.get(key) for key in keys]
201
+
202
+ async def mset(self, mapping: dict[str, str]) -> bool:
203
+ """Set multiple key-value pairs."""
204
+ self._data.update(mapping)
205
+ return True
206
+
207
+ # Pattern Operations
208
+
209
+ async def keys(self, pattern: str) -> list[str]:
210
+ """Get keys matching pattern."""
211
+ import fnmatch
212
+ return [key for key in self._data.keys() if fnmatch.fnmatch(key, pattern)]
213
+
214
+ async def scan(
215
+ self,
216
+ cursor: int = 0,
217
+ match: Optional[str] = None,
218
+ count: int = 100,
219
+ ) -> tuple[int, list[str]]:
220
+ """Scan keys with cursor."""
221
+ all_keys = list(self._data.keys())
222
+
223
+ if match:
224
+ import fnmatch
225
+ all_keys = [key for key in all_keys if fnmatch.fnmatch(key, match)]
226
+
227
+ start = cursor
228
+ end = cursor + count
229
+ keys = all_keys[start:end]
230
+
231
+ new_cursor = end if end < len(all_keys) else 0
232
+ return new_cursor, keys
@@ -0,0 +1,463 @@
1
+ """
2
+ PostgreSQL Storage Provider.
3
+
4
+ Enterprise-grade PostgreSQL backend with async SQLAlchemy ORM.
5
+ """
6
+
7
+ from typing import Optional
8
+ import json
9
+
10
+ from .provider import AbstractStorageProvider, StorageConfig
11
+
12
+
13
+ class PostgresStorageProvider(AbstractStorageProvider):
14
+ """
15
+ PostgreSQL storage provider.
16
+
17
+ Features:
18
+ - Async SQLAlchemy ORM
19
+ - Connection pooling
20
+ - JSONB support for structured data
21
+ - Full ACID compliance
22
+
23
+ Requires: sqlalchemy[asyncio], asyncpg packages
24
+ """
25
+
26
+ def __init__(self, config: StorageConfig):
27
+ """Initialize PostgreSQL storage."""
28
+ super().__init__(config)
29
+ self._engine = None
30
+ self._session_factory = None
31
+
32
+ async def connect(self) -> None:
33
+ """Establish connection to PostgreSQL."""
34
+ try:
35
+ from sqlalchemy.ext.asyncio import (
36
+ create_async_engine,
37
+ async_sessionmaker,
38
+ )
39
+ except ImportError:
40
+ raise ImportError(
41
+ "sqlalchemy[asyncio] and asyncpg packages are required for PostgresStorageProvider. "
42
+ "Install with: pip install sqlalchemy[asyncio] asyncpg"
43
+ )
44
+
45
+ # Build connection string
46
+ if self.config.connection_string:
47
+ conn_str = self.config.connection_string
48
+ else:
49
+ password_part = (
50
+ f":{self.config.postgres_password}"
51
+ if self.config.postgres_password
52
+ else ""
53
+ )
54
+ conn_str = (
55
+ f"postgresql+asyncpg://{self.config.postgres_user}"
56
+ f"{password_part}@{self.config.postgres_host}"
57
+ f":{self.config.postgres_port}/{self.config.postgres_database}"
58
+ )
59
+
60
+ if self.config.postgres_ssl_mode != "disable":
61
+ conn_str += f"?ssl={self.config.postgres_ssl_mode}"
62
+
63
+ # Create engine
64
+ self._engine = create_async_engine(
65
+ conn_str,
66
+ pool_size=self.config.pool_size,
67
+ max_overflow=20,
68
+ pool_pre_ping=True,
69
+ echo=False,
70
+ )
71
+
72
+ self._session_factory = async_sessionmaker(
73
+ self._engine,
74
+ expire_on_commit=False,
75
+ )
76
+
77
+ # Initialize schema
78
+ await self._init_schema()
79
+
80
+ async def _init_schema(self) -> None:
81
+ """Initialize database schema."""
82
+ from sqlalchemy import text
83
+
84
+ # Create tables for key-value, hashes, lists, etc.
85
+ async with self._engine.begin() as conn:
86
+ await conn.execute(text(
87
+ """
88
+ CREATE TABLE IF NOT EXISTS agentmesh_kv (
89
+ key VARCHAR(512) PRIMARY KEY,
90
+ value TEXT NOT NULL,
91
+ expires_at TIMESTAMP
92
+ );
93
+ CREATE INDEX IF NOT EXISTS idx_kv_expires ON agentmesh_kv(expires_at);
94
+
95
+ CREATE TABLE IF NOT EXISTS agentmesh_hash (
96
+ key VARCHAR(512) NOT NULL,
97
+ field VARCHAR(512) NOT NULL,
98
+ value TEXT NOT NULL,
99
+ PRIMARY KEY (key, field)
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS agentmesh_list (
103
+ key VARCHAR(512) NOT NULL,
104
+ idx INTEGER NOT NULL,
105
+ value TEXT NOT NULL,
106
+ PRIMARY KEY (key, idx)
107
+ );
108
+ CREATE INDEX IF NOT EXISTS idx_list_key ON agentmesh_list(key, idx);
109
+
110
+ CREATE TABLE IF NOT EXISTS agentmesh_zset (
111
+ key VARCHAR(512) NOT NULL,
112
+ member VARCHAR(512) NOT NULL,
113
+ score DOUBLE PRECISION NOT NULL,
114
+ PRIMARY KEY (key, member)
115
+ );
116
+ CREATE INDEX IF NOT EXISTS idx_zset_score ON agentmesh_zset(key, score);
117
+ """
118
+ ))
119
+
120
+ async def disconnect(self) -> None:
121
+ """Close connection to PostgreSQL."""
122
+ if self._engine:
123
+ await self._engine.dispose()
124
+
125
+ async def health_check(self) -> bool:
126
+ """Check if PostgreSQL is healthy."""
127
+ try:
128
+ if self._engine:
129
+ from sqlalchemy import text
130
+ async with self._engine.begin() as conn:
131
+ await conn.execute(text("SELECT 1"))
132
+ return True
133
+ except Exception:
134
+ pass
135
+ return False
136
+
137
+ # Key-Value Operations
138
+
139
+ async def get(self, key: str) -> Optional[str]:
140
+ """Get value by key."""
141
+ async with self._session_factory() as session:
142
+ result = await session.execute(
143
+ "SELECT value FROM agentmesh_kv WHERE key = :key "
144
+ "AND (expires_at IS NULL OR expires_at > NOW())",
145
+ {"key": key},
146
+ )
147
+ row = result.fetchone()
148
+ return row[0] if row else None
149
+
150
+ async def set(
151
+ self,
152
+ key: str,
153
+ value: str,
154
+ ttl_seconds: Optional[int] = None,
155
+ ) -> bool:
156
+ """Set value with optional TTL."""
157
+ async with self._session_factory() as session:
158
+ if ttl_seconds:
159
+ await session.execute(
160
+ "INSERT INTO agentmesh_kv (key, value, expires_at) "
161
+ "VALUES (:key, :value, NOW() + INTERVAL '1 second' * :ttl) "
162
+ "ON CONFLICT (key) DO UPDATE SET value = :value, expires_at = NOW() + INTERVAL '1 second' * :ttl",
163
+ {"key": key, "value": value, "ttl": ttl_seconds},
164
+ )
165
+ else:
166
+ await session.execute(
167
+ "INSERT INTO agentmesh_kv (key, value, expires_at) "
168
+ "VALUES (:key, :value, NULL) "
169
+ "ON CONFLICT (key) DO UPDATE SET value = :value, expires_at = NULL",
170
+ {"key": key, "value": value},
171
+ )
172
+ await session.commit()
173
+ return True
174
+
175
+ async def delete(self, key: str) -> bool:
176
+ """Delete key."""
177
+ async with self._session_factory() as session:
178
+ result = await session.execute(
179
+ "DELETE FROM agentmesh_kv WHERE key = :key",
180
+ {"key": key},
181
+ )
182
+ await session.commit()
183
+ return result.rowcount > 0
184
+
185
+ async def exists(self, key: str) -> bool:
186
+ """Check if key exists."""
187
+ result = await self.get(key)
188
+ return result is not None
189
+
190
+ # Hash Operations
191
+
192
+ async def hget(self, key: str, field: str) -> Optional[str]:
193
+ """Get hash field value."""
194
+ async with self._session_factory() as session:
195
+ result = await session.execute(
196
+ "SELECT value FROM agentmesh_hash WHERE key = :key AND field = :field",
197
+ {"key": key, "field": field},
198
+ )
199
+ row = result.fetchone()
200
+ return row[0] if row else None
201
+
202
+ async def hset(self, key: str, field: str, value: str) -> bool:
203
+ """Set hash field value."""
204
+ async with self._session_factory() as session:
205
+ await session.execute(
206
+ "INSERT INTO agentmesh_hash (key, field, value) "
207
+ "VALUES (:key, :field, :value) "
208
+ "ON CONFLICT (key, field) DO UPDATE SET value = :value",
209
+ {"key": key, "field": field, "value": value},
210
+ )
211
+ await session.commit()
212
+ return True
213
+
214
+ async def hgetall(self, key: str) -> dict[str, str]:
215
+ """Get all hash fields."""
216
+ async with self._session_factory() as session:
217
+ result = await session.execute(
218
+ "SELECT field, value FROM agentmesh_hash WHERE key = :key",
219
+ {"key": key},
220
+ )
221
+ return {row[0]: row[1] for row in result.fetchall()}
222
+
223
+ async def hdel(self, key: str, field: str) -> bool:
224
+ """Delete hash field."""
225
+ async with self._session_factory() as session:
226
+ result = await session.execute(
227
+ "DELETE FROM agentmesh_hash WHERE key = :key AND field = :field",
228
+ {"key": key, "field": field},
229
+ )
230
+ await session.commit()
231
+ return result.rowcount > 0
232
+
233
+ async def hkeys(self, key: str) -> list[str]:
234
+ """Get all hash field names."""
235
+ async with self._session_factory() as session:
236
+ result = await session.execute(
237
+ "SELECT field FROM agentmesh_hash WHERE key = :key",
238
+ {"key": key},
239
+ )
240
+ return [row[0] for row in result.fetchall()]
241
+
242
+ # List Operations
243
+
244
+ async def lpush(self, key: str, value: str) -> int:
245
+ """Push value to head of list."""
246
+ async with self._session_factory() as session:
247
+ # Shift all indices up
248
+ await session.execute(
249
+ "UPDATE agentmesh_list SET idx = idx + 1 WHERE key = :key",
250
+ {"key": key},
251
+ )
252
+ # Insert at position 0
253
+ await session.execute(
254
+ "INSERT INTO agentmesh_list (key, idx, value) VALUES (:key, 0, :value)",
255
+ {"key": key, "value": value},
256
+ )
257
+ await session.commit()
258
+ # Get new length
259
+ result = await session.execute(
260
+ "SELECT COUNT(*) FROM agentmesh_list WHERE key = :key",
261
+ {"key": key},
262
+ )
263
+ return result.scalar()
264
+
265
+ async def rpush(self, key: str, value: str) -> int:
266
+ """Push value to tail of list."""
267
+ async with self._session_factory() as session:
268
+ # Get max index
269
+ result = await session.execute(
270
+ "SELECT COALESCE(MAX(idx), -1) FROM agentmesh_list WHERE key = :key",
271
+ {"key": key},
272
+ )
273
+ max_idx = result.scalar()
274
+ # Insert at end
275
+ await session.execute(
276
+ "INSERT INTO agentmesh_list (key, idx, value) VALUES (:key, :idx, :value)",
277
+ {"key": key, "idx": max_idx + 1, "value": value},
278
+ )
279
+ await session.commit()
280
+ return max_idx + 2
281
+
282
+ async def lrange(self, key: str, start: int, stop: int) -> list[str]:
283
+ """Get list range [start, stop]."""
284
+ async with self._session_factory() as session:
285
+ if stop == -1:
286
+ result = await session.execute(
287
+ "SELECT value FROM agentmesh_list WHERE key = :key AND idx >= :start ORDER BY idx",
288
+ {"key": key, "start": start},
289
+ )
290
+ else:
291
+ result = await session.execute(
292
+ "SELECT value FROM agentmesh_list WHERE key = :key AND idx >= :start AND idx <= :stop ORDER BY idx",
293
+ {"key": key, "start": start, "stop": stop},
294
+ )
295
+ return [row[0] for row in result.fetchall()]
296
+
297
+ async def llen(self, key: str) -> int:
298
+ """Get list length."""
299
+ async with self._session_factory() as session:
300
+ result = await session.execute(
301
+ "SELECT COUNT(*) FROM agentmesh_list WHERE key = :key",
302
+ {"key": key},
303
+ )
304
+ return result.scalar()
305
+
306
+ # Sorted Set Operations
307
+
308
+ async def zadd(self, key: str, score: float, member: str) -> bool:
309
+ """Add member to sorted set with score."""
310
+ async with self._session_factory() as session:
311
+ await session.execute(
312
+ "INSERT INTO agentmesh_zset (key, member, score) "
313
+ "VALUES (:key, :member, :score) "
314
+ "ON CONFLICT (key, member) DO UPDATE SET score = :score",
315
+ {"key": key, "member": member, "score": score},
316
+ )
317
+ await session.commit()
318
+ return True
319
+
320
+ async def zscore(self, key: str, member: str) -> Optional[float]:
321
+ """Get score of member in sorted set."""
322
+ async with self._session_factory() as session:
323
+ result = await session.execute(
324
+ "SELECT score FROM agentmesh_zset WHERE key = :key AND member = :member",
325
+ {"key": key, "member": member},
326
+ )
327
+ row = result.fetchone()
328
+ return row[0] if row else None
329
+
330
+ async def zrange(
331
+ self,
332
+ key: str,
333
+ start: int,
334
+ stop: int,
335
+ with_scores: bool = False,
336
+ ) -> list[str] | list[tuple[str, float]]:
337
+ """Get sorted set range."""
338
+ async with self._session_factory() as session:
339
+ if stop == -1:
340
+ result = await session.execute(
341
+ "SELECT member, score FROM agentmesh_zset WHERE key = :key "
342
+ "ORDER BY score OFFSET :start",
343
+ {"key": key, "start": start},
344
+ )
345
+ else:
346
+ result = await session.execute(
347
+ "SELECT member, score FROM agentmesh_zset WHERE key = :key "
348
+ "ORDER BY score LIMIT :limit OFFSET :start",
349
+ {"key": key, "start": start, "limit": stop - start + 1},
350
+ )
351
+ rows = result.fetchall()
352
+ if with_scores:
353
+ return [(row[0], row[1]) for row in rows]
354
+ return [row[0] for row in rows]
355
+
356
+ async def zrangebyscore(
357
+ self,
358
+ key: str,
359
+ min_score: float,
360
+ max_score: float,
361
+ with_scores: bool = False,
362
+ ) -> list[str] | list[tuple[str, float]]:
363
+ """Get sorted set range by score."""
364
+ async with self._session_factory() as session:
365
+ result = await session.execute(
366
+ "SELECT member, score FROM agentmesh_zset "
367
+ "WHERE key = :key AND score >= :min AND score <= :max ORDER BY score",
368
+ {"key": key, "min": min_score, "max": max_score},
369
+ )
370
+ rows = result.fetchall()
371
+ if with_scores:
372
+ return [(row[0], row[1]) for row in rows]
373
+ return [row[0] for row in rows]
374
+
375
+ # Atomic Operations
376
+
377
+ async def incr(self, key: str) -> int:
378
+ """Increment value atomically."""
379
+ return await self.incrby(key, 1)
380
+
381
+ async def decr(self, key: str) -> int:
382
+ """Decrement value atomically."""
383
+ return await self.incrby(key, -1)
384
+
385
+ async def incrby(self, key: str, amount: int) -> int:
386
+ """Increment value by amount."""
387
+ async with self._session_factory() as session:
388
+ # Use PostgreSQL's atomic UPDATE ... RETURNING
389
+ result = await session.execute(
390
+ "INSERT INTO agentmesh_kv (key, value) VALUES (:key, :amount) "
391
+ "ON CONFLICT (key) DO UPDATE SET value = "
392
+ "(CAST(agentmesh_kv.value AS INTEGER) + :amount)::TEXT "
393
+ "RETURNING CAST(value AS INTEGER)",
394
+ {"key": key, "amount": str(amount)},
395
+ )
396
+ await session.commit()
397
+ return result.scalar()
398
+
399
+ # Batch Operations
400
+
401
+ async def mget(self, keys: list[str]) -> list[Optional[str]]:
402
+ """Get multiple values."""
403
+ async with self._session_factory() as session:
404
+ result = await session.execute(
405
+ "SELECT key, value FROM agentmesh_kv WHERE key = ANY(:keys) "
406
+ "AND (expires_at IS NULL OR expires_at > NOW())",
407
+ {"keys": keys},
408
+ )
409
+ values_dict = {row[0]: row[1] for row in result.fetchall()}
410
+ return [values_dict.get(key) for key in keys]
411
+
412
+ async def mset(self, mapping: dict[str, str]) -> bool:
413
+ """Set multiple key-value pairs."""
414
+ async with self._session_factory() as session:
415
+ for key, value in mapping.items():
416
+ await session.execute(
417
+ "INSERT INTO agentmesh_kv (key, value) VALUES (:key, :value) "
418
+ "ON CONFLICT (key) DO UPDATE SET value = :value",
419
+ {"key": key, "value": value},
420
+ )
421
+ await session.commit()
422
+ return True
423
+
424
+ # Pattern Operations
425
+
426
+ async def keys(self, pattern: str) -> list[str]:
427
+ """Get keys matching pattern."""
428
+ # Convert glob pattern to SQL LIKE pattern
429
+ sql_pattern = pattern.replace("*", "%").replace("?", "_")
430
+ async with self._session_factory() as session:
431
+ result = await session.execute(
432
+ "SELECT key FROM agentmesh_kv WHERE key LIKE :pattern "
433
+ "AND (expires_at IS NULL OR expires_at > NOW())",
434
+ {"pattern": sql_pattern},
435
+ )
436
+ return [row[0] for row in result.fetchall()]
437
+
438
+ async def scan(
439
+ self,
440
+ cursor: int = 0,
441
+ match: Optional[str] = None,
442
+ count: int = 100,
443
+ ) -> tuple[int, list[str]]:
444
+ """Scan keys with cursor."""
445
+ async with self._session_factory() as session:
446
+ if match:
447
+ sql_pattern = match.replace("*", "%").replace("?", "_")
448
+ result = await session.execute(
449
+ "SELECT key FROM agentmesh_kv WHERE key LIKE :pattern "
450
+ "AND (expires_at IS NULL OR expires_at > NOW()) "
451
+ "ORDER BY key LIMIT :count OFFSET :cursor",
452
+ {"pattern": sql_pattern, "count": count, "cursor": cursor},
453
+ )
454
+ else:
455
+ result = await session.execute(
456
+ "SELECT key FROM agentmesh_kv "
457
+ "WHERE expires_at IS NULL OR expires_at > NOW() "
458
+ "ORDER BY key LIMIT :count OFFSET :cursor",
459
+ {"count": count, "cursor": cursor},
460
+ )
461
+ keys = [row[0] for row in result.fetchall()]
462
+ new_cursor = cursor + count if len(keys) == count else 0
463
+ return new_cursor, keys