Skip to content

Commit 4887967

Browse files
committed
Merge remote-tracking branch 'upstream/main' into improving-ci-coverage-and-configuring-.coveragerc
2 parents dbca070 + c95adb1 commit 4887967

6 files changed

Lines changed: 489 additions & 73 deletions

File tree

src/ogx/core/storage/kvstore/kvstore.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import asyncio
1515
from collections import defaultdict
16-
from datetime import datetime
16+
from datetime import UTC, datetime
1717
from typing import cast
1818

1919
from ogx.core.storage.datatypes import (
@@ -43,26 +43,66 @@ def kvstore_dependencies() -> list[str]:
4343
class InmemoryKVStoreImpl(KVStore):
4444
"""In-memory key-value store implementation for testing and ephemeral usage."""
4545

46-
def __init__(self) -> None:
47-
self._store: dict[str, str] = {}
46+
def __init__(self, namespace: str | None = None) -> None:
47+
self._store: dict[str, tuple[str, datetime | None]] = {}
48+
self._namespace = namespace
49+
50+
def _namespaced_key(self, key: str) -> str:
51+
if not self._namespace:
52+
return key
53+
return f"{self._namespace}:{key}"
54+
55+
def _strip_namespace(self, key: str) -> str:
56+
if self._namespace and key.startswith(f"{self._namespace}:"):
57+
return key[len(self._namespace) + 1 :]
58+
return key
59+
60+
def _is_expired(self, expiration: datetime | None) -> bool:
61+
if expiration is None:
62+
return False
63+
return datetime.now(tz=UTC) >= expiration
4864

4965
async def initialize(self) -> None:
5066
pass
5167

5268
async def get(self, key: str) -> str | None:
53-
return self._store.get(key)
69+
key = self._namespaced_key(key)
70+
entry = self._store.get(key)
71+
if entry is None:
72+
return None
73+
value, expiration = entry
74+
if self._is_expired(expiration):
75+
return None
76+
return value
5477

5578
async def set(self, key: str, value: str, expiration: datetime | None = None) -> None:
56-
self._store[key] = value
79+
key = self._namespaced_key(key)
80+
self._store[key] = (value, expiration)
5781

5882
async def values_in_range(self, start_key: str, end_key: str) -> list[str]:
59-
return [self._store[key] for key in self._store.keys() if key >= start_key and key < end_key]
83+
start_key = self._namespaced_key(start_key)
84+
end_key = self._namespaced_key(end_key)
85+
result = []
86+
for key in sorted(self._store.keys()):
87+
if key >= start_key and key < end_key:
88+
value, expiration = self._store[key]
89+
if not self._is_expired(expiration):
90+
result.append(value)
91+
return result
6092

6193
async def keys_in_range(self, start_key: str, end_key: str) -> list[str]:
62-
"""Get all keys in the given range."""
63-
return [key for key in self._store.keys() if key >= start_key and key < end_key]
94+
start_key = self._namespaced_key(start_key)
95+
end_key = self._namespaced_key(end_key)
96+
result = []
97+
for key in sorted(self._store.keys()):
98+
if key >= start_key and key < end_key:
99+
_, expiration = self._store[key]
100+
if not self._is_expired(expiration):
101+
result.append(self._strip_namespace(key))
102+
return result
64103

65104
async def delete(self, key: str) -> None:
105+
key = self._namespaced_key(key)
66106
self._store.pop(key, None)
67107

68108
async def shutdown(self) -> None:

src/ogx/core/storage/kvstore/postgres/postgres.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ def _namespaced_key(self, key: str) -> str:
6464
return key
6565
return f"{self.config.namespace}:{key}"
6666

67+
def _strip_namespace(self, key: str) -> str:
68+
if self.config.namespace and key.startswith(f"{self.config.namespace}:"):
69+
return key[len(self.config.namespace) + 1 :]
70+
return key
71+
6772
async def set(self, key: str, value: str, expiration: datetime | None = None) -> None:
6873
key = self._namespaced_key(key)
6974
cursor = self._cursor_or_raise()
@@ -129,7 +134,7 @@ async def keys_in_range(self, start_key: str, end_key: str) -> list[str]:
129134
""",
130135
(start_key, end_key),
131136
)
132-
return [row[0] for row in cursor.fetchall()]
137+
return [self._strip_namespace(row[0]) for row in cursor.fetchall()]
133138

134139
async def shutdown(self) -> None:
135140
if self._cursor:

src/ogx/core/storage/kvstore/sqlite/sqlite.py

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# the root directory of this source tree.
66

77
import os
8-
from datetime import datetime
8+
from datetime import UTC, datetime
99

1010
import aiosqlite
1111

@@ -23,6 +23,7 @@ class SqliteKVStoreImpl(KVStore):
2323
def __init__(self, config: SqliteKVStoreConfig) -> None:
2424
self.db_path = config.db_path
2525
self.table_name = "kvstore"
26+
self._namespace = config.namespace
2627
self._conn: aiosqlite.Connection | None = None
2728

2829
def __str__(self) -> str:
@@ -32,6 +33,16 @@ def _is_memory_db(self) -> bool:
3233
"""Check if this is an in-memory database."""
3334
return self.db_path == ":memory:" or "mode=memory" in self.db_path
3435

36+
def _namespaced_key(self, key: str) -> str:
37+
if not self._namespace:
38+
return key
39+
return f"{self._namespace}:{key}"
40+
41+
def _strip_namespace(self, key: str) -> str:
42+
if self._namespace and key.startswith(f"{self._namespace}:"):
43+
return key[len(self._namespace) + 1 :]
44+
return key
45+
3546
async def initialize(self) -> None:
3647
# Skip directory creation for in-memory databases and file: URIs
3748
if not self._is_memory_db() and not self.db_path.startswith("file:"):
@@ -74,103 +85,90 @@ async def shutdown(self) -> None:
7485
self._conn = None
7586

7687
async def set(self, key: str, value: str, expiration: datetime | None = None) -> None:
88+
key = self._namespaced_key(key)
89+
exp_str = expiration.isoformat() if expiration else None
7790
if self._conn:
78-
# In-memory database with persistent connection
7991
await self._conn.execute(
8092
f"INSERT OR REPLACE INTO {self.table_name} (key, value, expiration) VALUES (?, ?, ?)",
81-
(key, value, expiration),
93+
(key, value, exp_str),
8294
)
8395
await self._conn.commit()
8496
else:
85-
# File-based database with connection per operation
8697
async with aiosqlite.connect(self.db_path) as db:
8798
await db.execute(
8899
f"INSERT OR REPLACE INTO {self.table_name} (key, value, expiration) VALUES (?, ?, ?)",
89-
(key, value, expiration),
100+
(key, value, exp_str),
90101
)
91102
await db.commit()
92103

93104
async def get(self, key: str) -> str | None:
105+
key = self._namespaced_key(key)
106+
now = datetime.now(tz=UTC).isoformat()
107+
query = f"SELECT value FROM {self.table_name} WHERE key = ? AND (expiration IS NULL OR expiration > ?)"
94108
if self._conn:
95-
# In-memory database with persistent connection
96-
async with self._conn.execute(
97-
f"SELECT value, expiration FROM {self.table_name} WHERE key = ?", (key,)
98-
) as cursor:
109+
async with self._conn.execute(query, (key, now)) as cursor:
99110
row = await cursor.fetchone()
100111
if row is None:
101112
return None
102-
value, expiration = row
113+
value = row[0]
103114
if not isinstance(value, str):
104115
logger.warning("Expected string value for key, returning None", key=key, value_type=type(value))
105116
return None
106117
return value
107118
else:
108-
# File-based database with connection per operation
109119
async with aiosqlite.connect(self.db_path) as db:
110-
async with db.execute(
111-
f"SELECT value, expiration FROM {self.table_name} WHERE key = ?", (key,)
112-
) as cursor:
120+
async with db.execute(query, (key, now)) as cursor:
113121
row = await cursor.fetchone()
114122
if row is None:
115123
return None
116-
value, expiration = row
124+
value = row[0]
117125
if not isinstance(value, str):
118126
logger.warning("Expected string value for key, returning None", key=key, value_type=type(value))
119127
return None
120128
return value
121129

122130
async def delete(self, key: str) -> None:
131+
key = self._namespaced_key(key)
123132
if self._conn:
124-
# In-memory database with persistent connection
125133
await self._conn.execute(f"DELETE FROM {self.table_name} WHERE key = ?", (key,))
126134
await self._conn.commit()
127135
else:
128-
# File-based database with connection per operation
129136
async with aiosqlite.connect(self.db_path) as db:
130137
await db.execute(f"DELETE FROM {self.table_name} WHERE key = ?", (key,))
131138
await db.commit()
132139

133140
async def values_in_range(self, start_key: str, end_key: str) -> list[str]:
141+
start_key = self._namespaced_key(start_key)
142+
end_key = self._namespaced_key(end_key)
143+
now = datetime.now(tz=UTC).isoformat()
144+
query = (
145+
f"SELECT value FROM {self.table_name} "
146+
f"WHERE key >= ? AND key < ? AND (expiration IS NULL OR expiration > ?) "
147+
f"ORDER BY key"
148+
)
134149
if self._conn:
135-
# In-memory database with persistent connection
136-
async with self._conn.execute(
137-
f"SELECT key, value, expiration FROM {self.table_name} WHERE key >= ? AND key <= ?",
138-
(start_key, end_key),
139-
) as cursor:
140-
result = []
141-
async for row in cursor:
142-
_, value, _ = row
143-
result.append(value)
144-
return result
150+
async with self._conn.execute(query, (start_key, end_key, now)) as cursor:
151+
return [row[0] async for row in cursor]
145152
else:
146-
# File-based database with connection per operation
147153
async with aiosqlite.connect(self.db_path) as db:
148-
async with db.execute(
149-
f"SELECT key, value, expiration FROM {self.table_name} WHERE key >= ? AND key <= ?",
150-
(start_key, end_key),
151-
) as cursor:
152-
result = []
153-
async for row in cursor:
154-
_, value, _ = row
155-
result.append(value)
156-
return result
154+
async with db.execute(query, (start_key, end_key, now)) as cursor:
155+
return [row[0] async for row in cursor]
157156

158157
async def keys_in_range(self, start_key: str, end_key: str) -> list[str]:
159-
"""Get all keys in the given range."""
158+
start_key = self._namespaced_key(start_key)
159+
end_key = self._namespaced_key(end_key)
160+
now = datetime.now(tz=UTC).isoformat()
161+
query = (
162+
f"SELECT key FROM {self.table_name} "
163+
f"WHERE key >= ? AND key < ? AND (expiration IS NULL OR expiration > ?) "
164+
f"ORDER BY key"
165+
)
160166
if self._conn:
161-
# In-memory database with persistent connection
162-
cursor = await self._conn.execute(
163-
f"SELECT key FROM {self.table_name} WHERE key >= ? AND key <= ?",
164-
(start_key, end_key),
165-
)
167+
cursor = await self._conn.execute(query, (start_key, end_key, now))
166168
rows = await cursor.fetchall()
167-
return [row[0] for row in rows]
169+
return [self._strip_namespace(row[0]) for row in rows]
168170
else:
169-
# File-based database with connection per operation
170171
async with aiosqlite.connect(self.db_path) as db:
171-
cursor = await db.execute(
172-
f"SELECT key FROM {self.table_name} WHERE key >= ? AND key <= ?",
173-
(start_key, end_key),
174-
)
172+
cursor = await db.execute(query, (start_key, end_key, now))
175173
rows = await cursor.fetchall()
176-
return [row[0] for row in rows]
174+
return [self._strip_namespace(row[0]) for row in rows]

tests/unit/registry/test_registry.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import pytest
99

1010
from ogx.core.datatypes import VectorStoreWithOwner
11-
from ogx.core.storage.datatypes import KVStoreReference, SqliteKVStoreConfig
12-
from ogx.core.storage.kvstore import kvstore_impl, register_kvstore_backends
11+
from ogx.core.storage.datatypes import SqliteKVStoreConfig
12+
from ogx.core.storage.kvstore.sqlite.sqlite import SqliteKVStoreImpl
1313
from ogx.core.store.registry import (
1414
KEY_FORMAT,
1515
CachedDiskDistributionRegistry,
@@ -69,14 +69,13 @@ async def test_cached_registry_initialization(sqlite_kvstore, sample_vector_stor
6969
await disk_registry.register(sample_vector_store)
7070
await disk_registry.register(sample_model)
7171

72-
# Test cached version loads from disk
72+
# Test cached version loads from disk via a fresh KVStore pointing at the same DB
7373
db_path = sqlite_kvstore.db_path
74-
backend_name = "kv_cached_test"
75-
register_kvstore_backends({backend_name: SqliteKVStoreConfig(db_path=db_path)})
74+
fresh_config = SqliteKVStoreConfig(db_path=db_path)
75+
fresh_kvstore = SqliteKVStoreImpl(fresh_config)
76+
await fresh_kvstore.initialize()
7677
# Use cache_ttl_seconds=0 for tests to ensure immediate synchronization
77-
cached_registry = CachedDiskDistributionRegistry(
78-
await kvstore_impl(KVStoreReference(backend=backend_name, namespace="registry")), cache_ttl_seconds=0
79-
)
78+
cached_registry = CachedDiskDistributionRegistry(fresh_kvstore, cache_ttl_seconds=0)
8079
await cached_registry.initialize()
8180

8281
result_vector_store = await cached_registry.get("vector_store", "test_vector_store")
@@ -103,13 +102,12 @@ async def test_cached_registry_updates(cached_disk_dist_registry):
103102
assert result_vector_store.identifier == new_vector_store.identifier
104103
assert result_vector_store.provider_id == new_vector_store.provider_id
105104

106-
# Verify persisted to disk
105+
# Verify persisted to disk via a fresh KVStore pointing at the same DB
107106
db_path = cached_disk_dist_registry.kvstore.db_path
108-
backend_name = "kv_cached_new"
109-
register_kvstore_backends({backend_name: SqliteKVStoreConfig(db_path=db_path)})
110-
new_registry = DiskDistributionRegistry(
111-
await kvstore_impl(KVStoreReference(backend=backend_name, namespace="registry"))
112-
)
107+
fresh_config = SqliteKVStoreConfig(db_path=db_path)
108+
fresh_kvstore = SqliteKVStoreImpl(fresh_config)
109+
await fresh_kvstore.initialize()
110+
new_registry = DiskDistributionRegistry(fresh_kvstore)
113111
await new_registry.initialize()
114112
result_vector_store = await new_registry.get("vector_store", "test_vector_store_2")
115113
assert result_vector_store is not None

0 commit comments

Comments
 (0)