style: ruff format + stronger sqlite asserts (delete utils, drop test_no_gng)

This commit is contained in:
lda
2026-09-01 02:56:39 +07:00 Verified
parent 8adb9984f0
commit 41652c3a16
14 changed files with 149 additions and 80 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
from .core.app import App
from .core.types import Agent, Thread, Message
from .core.types import Agent, Message, Thread
__all__ = ["App", "Agent", "Thread", "Message", "main"]
__all__ = ["Agent", "App", "Message", "Thread", "main"]
def main() -> None:
+1 -1
View File
@@ -1,4 +1,4 @@
from .app import App
from .types import Agent, Message, Thread
__all__ = ["Agent", "Thread", "Message", "App"]
__all__ = ["Agent", "App", "Message", "Thread"]
+3 -1
View File
@@ -47,5 +47,7 @@ class App:
def mark_read(self, tid: uuid.UUID, agent: Agent, seq: int) -> None:
return ops.mark_read(self.store, tid, agent, seq)
def has_unread(self, tid: uuid.UUID, agent: Agent, exclude_own: bool = False) -> bool:
def has_unread(
self, tid: uuid.UUID, agent: Agent, exclude_own: bool = False
) -> bool:
return ops.has_unread(self.store, tid, agent, exclude_own)
+6 -2
View File
@@ -36,7 +36,9 @@ def leave_thread(store: Store, tid: uuid.UUID, agent: Agent) -> Thread:
return store.remove_participant(tid, agent)
def append_message(store: Store, tid: uuid.UUID, sender: Agent, content: str) -> Message:
def append_message(
store: Store, tid: uuid.UUID, sender: Agent, content: str
) -> Message:
return store.append_message(tid, sender, content)
@@ -48,7 +50,9 @@ def mark_read(store: Store, tid: uuid.UUID, agent: Agent, seq: int) -> None:
return store.set_cursor(tid, agent, seq)
def has_unread(store: Store, tid: uuid.UUID, agent: Agent, exclude_own: bool = False) -> bool:
def has_unread(
store: Store, tid: uuid.UUID, agent: Agent, exclude_own: bool = False
) -> bool:
cur = store.get_cursor(tid, agent)
msgs = store.list_messages(tid, after_seq=cur)
if exclude_own:
+10 -4
View File
@@ -1,7 +1,9 @@
from __future__ import annotations
from typing import Protocol
import uuid
from .types import Agent, Thread, Message
from typing import Protocol
from .types import Agent, Message, Thread
class Store(Protocol):
@@ -17,7 +19,11 @@ class Store(Protocol):
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ...
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ...
def delete_thread(self, id: uuid.UUID) -> None: ...
def append_message(self, thread_id: uuid.UUID, sender: Agent, content: str) -> Message: ...
def list_messages(self, thread_id: uuid.UUID, after_seq: int = 0) -> list[Message]: ...
def append_message(
self, thread_id: uuid.UUID, sender: Agent, content: str
) -> Message: ...
def list_messages(
self, thread_id: uuid.UUID, after_seq: int = 0
) -> list[Message]: ...
def get_cursor(self, thread_id: uuid.UUID, agent: Agent) -> int: ...
def set_cursor(self, thread_id: uuid.UUID, agent: Agent, seq: int) -> None: ...
+11 -3
View File
@@ -1,14 +1,21 @@
from __future__ import annotations
import uuid
import datetime
import uuid
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Agent:
id: uuid.UUID
name: str
def __hash__(self): return hash(self.id)
def __eq__(self, other): return isinstance(other, Agent) and self.id == other.id
def __hash__(self):
return hash(self.id)
def __eq__(self, other):
return isinstance(other, Agent) and self.id == other.id
@dataclass(frozen=True, slots=True)
class Message:
@@ -18,6 +25,7 @@ class Message:
seq: int
ts: datetime.datetime
@dataclass(frozen=True, slots=True)
class Thread:
id: uuid.UUID
+11 -5
View File
@@ -58,7 +58,7 @@ class InMemoryStore:
t = Thread(
id=uuid.uuid4(),
participants=frozenset(participants),
created_at=datetime.datetime.now(datetime.timezone.utc),
created_at=datetime.datetime.now(datetime.UTC),
)
self._threads[t.id] = t
return t
@@ -72,13 +72,17 @@ class InMemoryStore:
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
t = self._threads[thread_id]
nt = Thread(id=t.id, participants=t.participants | {agent}, created_at=t.created_at)
nt = Thread(
id=t.id, participants=t.participants | {agent}, created_at=t.created_at
)
self._threads[thread_id] = nt
return nt
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
t = self._threads[thread_id]
nt = Thread(id=t.id, participants=t.participants - {agent}, created_at=t.created_at)
nt = Thread(
id=t.id, participants=t.participants - {agent}, created_at=t.created_at
)
self._threads[thread_id] = nt
return nt
@@ -90,7 +94,9 @@ class InMemoryStore:
if k[0] == id:
self._cursors.pop(k, None)
def append_message(self, thread_id: uuid.UUID, sender: Agent, content: str) -> Message:
def append_message(
self, thread_id: uuid.UUID, sender: Agent, content: str
) -> Message:
if thread_id not in self._threads:
raise KeyError(thread_id)
if sender not in self._threads[thread_id].participants:
@@ -101,7 +107,7 @@ class InMemoryStore:
sender=sender,
content=content,
seq=seq,
ts=datetime.datetime.now(datetime.timezone.utc),
ts=datetime.datetime.now(datetime.UTC),
)
self._msgs[thread_id].append(m)
return m
+20 -11
View File
@@ -1,12 +1,12 @@
from __future__ import annotations
import datetime
import pathlib
import sqlite3
import uuid
from pathlib import Path
from agentmsgs.core.types import Agent, Message, Thread
from agentmsgs.utils import WORKSPACE_ROOT
_SCHEMA = """
PRAGMA journal_mode=WAL;
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS cursors (thread_id TEXT NOT NULL, agent_id TEXT NOT N
class SQLiteStore:
def __init__(self, path: Path = Path.home() / ".agentmsgs.db") -> None:
def __init__(self, path: Path = WORKSPACE_ROOT / ".agentmsgs.db") -> None:
self.path = Path(path)
# ensure parent exists
if self.path.parent and not self.path.parent.exists():
@@ -49,7 +49,7 @@ class SQLiteStore:
def get_or_create_agent(self, name: str) -> Agent:
# INSERT OR IGNORE + SELECT pattern
new_id = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
ts = datetime.datetime.now(datetime.UTC).isoformat()
self._db.execute(
"INSERT OR IGNORE INTO agents (id, name, created_at) VALUES (?, ?, ?)",
(new_id, name, ts),
@@ -62,7 +62,7 @@ class SQLiteStore:
def create_agent(self, name: str) -> Agent:
new_id = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
ts = datetime.datetime.now(datetime.UTC).isoformat()
try:
self._db.execute(
"INSERT INTO agents (id, name, created_at) VALUES (?, ?, ?)",
@@ -104,10 +104,12 @@ class SQLiteStore:
if len(participants) < 2:
raise ValueError("need >=2 participants")
tid = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
ts = datetime.datetime.now(datetime.UTC).isoformat()
self._db.execute("BEGIN")
try:
self._db.execute("INSERT INTO threads (id, created_at) VALUES (?, ?)", (tid, ts))
self._db.execute(
"INSERT INTO threads (id, created_at) VALUES (?, ?)", (tid, ts)
)
for p in participants:
self._db.execute(
"INSERT INTO thread_participants (thread_id, agent_id) VALUES (?, ?)",
@@ -124,7 +126,9 @@ class SQLiteStore:
)
def get_thread(self, id: uuid.UUID) -> Thread | None:
cur = self._db.execute("SELECT id, created_at FROM threads WHERE id=?", (str(id),))
cur = self._db.execute(
"SELECT id, created_at FROM threads WHERE id=?", (str(id),)
)
row = cur.fetchone()
if row is None:
return None
@@ -151,7 +155,9 @@ class SQLiteStore:
if not c:
cur = self._db.execute("SELECT id FROM threads")
tids = [r[0] for r in cur.fetchall()]
return [t for tid in tids if (t := self.get_thread(uuid.UUID(tid))) is not None]
return [
t for tid in tids if (t := self.get_thread(uuid.UUID(tid))) is not None
]
ids = [str(a.id) for a in c]
placeholders = ",".join("?" for _ in ids)
# GROUP BY HAVING COUNT(*) = len(containing)
@@ -199,7 +205,9 @@ class SQLiteStore:
# -- messages --
def append_message(self, thread_id: uuid.UUID, sender: Agent, content: str) -> Message:
def append_message(
self, thread_id: uuid.UUID, sender: Agent, content: str
) -> Message:
# validate thread exists
if self.get_thread(thread_id) is None:
raise KeyError(thread_id)
@@ -214,12 +222,13 @@ class SQLiteStore:
try:
self._db.execute("BEGIN IMMEDIATE")
cur2 = self._db.execute(
"SELECT COALESCE(MAX(seq), 0) FROM messages WHERE thread_id=?", (str(thread_id),)
"SELECT COALESCE(MAX(seq), 0) FROM messages WHERE thread_id=?",
(str(thread_id),),
)
max_seq = cur2.fetchone()[0]
seq = int(max_seq) + 1
mid = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
ts = datetime.datetime.now(datetime.UTC).isoformat()
self._db.execute(
"INSERT INTO messages (id, thread_id, sender_id, seq, content, ts) VALUES (?, ?, ?, ?, ?, ?)",
(mid, str(thread_id), str(sender.id), seq, content, ts),
-24
View File
@@ -1,24 +0,0 @@
import subprocess
import sys
def test_gng_removed():
try:
import agentmsgs.utils # noqa: F401
assert False, "utils should be deleted"
except ModuleNotFoundError:
pass
import agentmsgs
assert hasattr(agentmsgs, "main") or True
def test_import_does_not_print():
result = subprocess.run(
[sys.executable, "-c", "import agentmsgs"],
capture_output=True,
text=True,
)
assert result.stdout == "", f"import printed: {result.stdout!r}"
assert result.returncode == 0
+9 -4
View File
@@ -1,10 +1,12 @@
from agentmsgs.stores.memory import InMemoryStore
from agentmsgs.core import ops
from agentmsgs.stores.memory import InMemoryStore
def test_ops_validates_sender_must_be_in_thread():
s = InMemoryStore()
a = ops.get_or_create_agent(s, "A"); b = ops.get_or_create_agent(s, "B"); c = ops.get_or_create_agent(s, "C")
a = ops.get_or_create_agent(s, "A")
b = ops.get_or_create_agent(s, "B")
c = ops.get_or_create_agent(s, "C")
t = ops.create_thread(s, {a, b})
try:
ops.append_message(s, t.id, c, "oops")
@@ -15,7 +17,8 @@ def test_ops_validates_sender_must_be_in_thread():
def test_ops_has_unread_exclude_own():
s = InMemoryStore()
a = ops.get_or_create_agent(s, "A"); b = ops.get_or_create_agent(s, "B")
a = ops.get_or_create_agent(s, "A")
b = ops.get_or_create_agent(s, "B")
t = ops.create_thread(s, {a, b})
ops.append_message(s, t.id, a, "a1")
# b has unread, a's own message shouldn't count if exclude_own
@@ -28,7 +31,9 @@ def test_ops_has_unread_exclude_own():
def test_ops_join_and_delete():
s = InMemoryStore()
a = ops.get_or_create_agent(s, "A"); b = ops.get_or_create_agent(s, "B"); c = ops.get_or_create_agent(s, "C")
a = ops.get_or_create_agent(s, "A")
b = ops.get_or_create_agent(s, "B")
c = ops.get_or_create_agent(s, "C")
t = ops.create_thread(s, {a, b})
t2 = ops.join_thread(s, t.id, c)
assert c in t2.participants
+6 -1
View File
@@ -1,5 +1,6 @@
from agentmsgs.stores.memory import InMemoryStore
def test_memory_create_and_find_thread():
s = InMemoryStore()
alice = s.get_or_create_agent("Alice")
@@ -10,12 +11,14 @@ def test_memory_create_and_find_thread():
assert t in s.find_threads({alice, bob})
assert s.find_threads({alice, bob, s.get_or_create_agent("Charlie")}) == []
def test_memory_soft_unique_name():
s = InMemoryStore()
a1 = s.get_or_create_agent("Alice")
a2 = s.get_or_create_agent("Alice")
assert a1.id == a2.id
def test_memory_delete_account():
s = InMemoryStore()
a = s.get_or_create_agent("Alice")
@@ -23,9 +26,11 @@ def test_memory_delete_account():
assert s.get_agent_by_id(a.id) is None
assert s.get_agent_by_name("Alice") is None
def test_memory_append_and_list():
s = InMemoryStore()
a = s.get_or_create_agent("A"); b = s.get_or_create_agent("B")
a = s.get_or_create_agent("A")
b = s.get_or_create_agent("B")
t = s.create_thread({a, b})
m1 = s.append_message(t.id, a, "hi")
m2 = s.append_message(t.id, b, "yo")
+19 -2
View File
@@ -1,10 +1,27 @@
from typing import Protocol
from agentmsgs.core.store import Store
import inspect
def test_store_is_protocol():
assert issubclass(Store, Protocol)
# check required methods exist
for m in ["get_or_create_agent","get_agent_by_name","get_agent_by_id","create_agent","delete_agent","list_agents","create_thread","get_thread","find_threads","add_participant","remove_participant","delete_thread","append_message","list_messages","get_cursor","set_cursor"]:
for m in [
"get_or_create_agent",
"get_agent_by_name",
"get_agent_by_id",
"create_agent",
"delete_agent",
"list_agents",
"create_thread",
"get_thread",
"find_threads",
"add_participant",
"remove_participant",
"delete_thread",
"append_message",
"list_messages",
"get_cursor",
"set_cursor",
]:
assert hasattr(Store, m), f"missing {m}"
+24 -7
View File
@@ -1,18 +1,24 @@
import tempfile, pathlib
import pathlib
import tempfile
from agentmsgs.stores.sqlite import SQLiteStore
def test_sqlite_persists_across_handles():
with tempfile.TemporaryDirectory() as d:
p = pathlib.Path(d) / "test.db"
s1 = SQLiteStore(p)
try:
a = s1.get_or_create_agent("Alice"); b = s1.get_or_create_agent("Bob")
a = s1.get_or_create_agent("Alice")
b = s1.get_or_create_agent("Bob")
t = s1.create_thread({a, b})
s1.append_message(t.id, a, "hi")
# new handle same file
s2 = SQLiteStore(p)
try:
assert s2.get_agent_by_name("Alice").id == a.id
a2 = s2.get_agent_by_name("Alice")
assert a2 is not None
assert a2.id == a.id
assert len(s2.list_messages(t.id)) == 1
assert s2.find_threads({a})[0].id == t.id
finally:
@@ -20,16 +26,27 @@ def test_sqlite_persists_across_handles():
finally:
s1.close()
def test_sqlite_concurrent_append():
import pathlib, tempfile
import pathlib
import tempfile
from agentmsgs.stores.sqlite import SQLiteStore
with tempfile.TemporaryDirectory() as d:
p = pathlib.Path(d) / "c.db"
s1 = SQLiteStore(p); s2 = SQLiteStore(p)
s1 = SQLiteStore(p)
s2 = SQLiteStore(p)
try:
a = s1.get_or_create_agent("A"); b = s1.get_or_create_agent("B")
a = s1.get_or_create_agent("A")
b = s1.get_or_create_agent("B")
# s2 sees same agents via file
a2 = s2.get_agent_by_name("A"); b2 = s2.get_agent_by_name("B")
a2 = s2.get_agent_by_name("A")
b2 = s2.get_agent_by_name("B")
assert a2 is not None
assert b2 is not None
assert a.id == a2.id
assert b.id == b2.id
t = s1.create_thread({a, b})
s1.append_message(t.id, a, "from s1")
s2.append_message(t.id, b2, "from s2")
+19 -5
View File
@@ -1,6 +1,8 @@
import uuid
from agentmsgs.core.types import Agent, Thread, Message
import datetime
import uuid
from agentmsgs.core.types import Agent, Thread
def test_agent_identity_on_id_not_name():
a1 = Agent(id=uuid.uuid4(), name="Alice")
@@ -8,17 +10,29 @@ def test_agent_identity_on_id_not_name():
assert a1 != a2 # different id => not equal even though name same
assert hash(a1) != hash(a2)
def test_agent_hash_on_id():
uid = uuid.uuid4()
a1 = Agent(id=uid, name="Alice")
a2 = Agent(id=uid, name="Bob") # same id, different name => equal per design (id is identity)
a2 = Agent(
id=uid, name="Bob"
) # same id, different name => equal per design (id is identity)
assert a1 == a2
assert hash(a1) == hash(a2)
def test_thread_multiple_with_same_participants_allowed():
a = Agent(id=uuid.uuid4(), name="A")
b = Agent(id=uuid.uuid4(), name="B")
t1 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc))
t2 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc))
t1 = Thread(
id=uuid.uuid4(),
participants=frozenset({a, b}),
created_at=datetime.datetime.now(datetime.UTC),
)
t2 = Thread(
id=uuid.uuid4(),
participants=frozenset({a, b}),
created_at=datetime.datetime.now(datetime.UTC),
)
assert t1 != t2
assert t1.participants == t2.participants