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.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: def main() -> None:
+1 -1
View File
@@ -1,4 +1,4 @@
from .app import App from .app import App
from .types import Agent, Message, Thread 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: def mark_read(self, tid: uuid.UUID, agent: Agent, seq: int) -> None:
return ops.mark_read(self.store, tid, agent, seq) 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) 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) 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) 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) 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) cur = store.get_cursor(tid, agent)
msgs = store.list_messages(tid, after_seq=cur) msgs = store.list_messages(tid, after_seq=cur)
if exclude_own: if exclude_own:
+10 -4
View File
@@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
from typing import Protocol
import uuid import uuid
from .types import Agent, Thread, Message from typing import Protocol
from .types import Agent, Message, Thread
class Store(Protocol): class Store(Protocol):
@@ -17,7 +19,11 @@ class Store(Protocol):
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ... def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ...
def remove_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 delete_thread(self, id: uuid.UUID) -> None: ...
def append_message(self, thread_id: uuid.UUID, sender: Agent, content: str) -> Message: ... def append_message(
def list_messages(self, thread_id: uuid.UUID, after_seq: int = 0) -> list[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 get_cursor(self, thread_id: uuid.UUID, agent: Agent) -> int: ...
def set_cursor(self, thread_id: uuid.UUID, agent: Agent, seq: int) -> None: ... 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 from __future__ import annotations
import uuid
import datetime import datetime
import uuid
from dataclasses import dataclass from dataclasses import dataclass
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class Agent: class Agent:
id: uuid.UUID id: uuid.UUID
name: str 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) @dataclass(frozen=True, slots=True)
class Message: class Message:
@@ -18,6 +25,7 @@ class Message:
seq: int seq: int
ts: datetime.datetime ts: datetime.datetime
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class Thread: class Thread:
id: uuid.UUID id: uuid.UUID
+11 -5
View File
@@ -58,7 +58,7 @@ class InMemoryStore:
t = Thread( t = Thread(
id=uuid.uuid4(), id=uuid.uuid4(),
participants=frozenset(participants), participants=frozenset(participants),
created_at=datetime.datetime.now(datetime.timezone.utc), created_at=datetime.datetime.now(datetime.UTC),
) )
self._threads[t.id] = t self._threads[t.id] = t
return t return t
@@ -72,13 +72,17 @@ class InMemoryStore:
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
t = self._threads[thread_id] 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 self._threads[thread_id] = nt
return nt return nt
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
t = self._threads[thread_id] 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 self._threads[thread_id] = nt
return nt return nt
@@ -90,7 +94,9 @@ class InMemoryStore:
if k[0] == id: if k[0] == id:
self._cursors.pop(k, None) 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: if thread_id not in self._threads:
raise KeyError(thread_id) raise KeyError(thread_id)
if sender not in self._threads[thread_id].participants: if sender not in self._threads[thread_id].participants:
@@ -101,7 +107,7 @@ class InMemoryStore:
sender=sender, sender=sender,
content=content, content=content,
seq=seq, seq=seq,
ts=datetime.datetime.now(datetime.timezone.utc), ts=datetime.datetime.now(datetime.UTC),
) )
self._msgs[thread_id].append(m) self._msgs[thread_id].append(m)
return m return m
+20 -11
View File
@@ -1,12 +1,12 @@
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import pathlib
import sqlite3 import sqlite3
import uuid import uuid
from pathlib import Path from pathlib import Path
from agentmsgs.core.types import Agent, Message, Thread from agentmsgs.core.types import Agent, Message, Thread
from agentmsgs.utils import WORKSPACE_ROOT
_SCHEMA = """ _SCHEMA = """
PRAGMA journal_mode=WAL; 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: 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) self.path = Path(path)
# ensure parent exists # ensure parent exists
if self.path.parent and not self.path.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: def get_or_create_agent(self, name: str) -> Agent:
# INSERT OR IGNORE + SELECT pattern # INSERT OR IGNORE + SELECT pattern
new_id = str(uuid.uuid4()) new_id = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat() ts = datetime.datetime.now(datetime.UTC).isoformat()
self._db.execute( self._db.execute(
"INSERT OR IGNORE INTO agents (id, name, created_at) VALUES (?, ?, ?)", "INSERT OR IGNORE INTO agents (id, name, created_at) VALUES (?, ?, ?)",
(new_id, name, ts), (new_id, name, ts),
@@ -62,7 +62,7 @@ class SQLiteStore:
def create_agent(self, name: str) -> Agent: def create_agent(self, name: str) -> Agent:
new_id = str(uuid.uuid4()) new_id = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat() ts = datetime.datetime.now(datetime.UTC).isoformat()
try: try:
self._db.execute( self._db.execute(
"INSERT INTO agents (id, name, created_at) VALUES (?, ?, ?)", "INSERT INTO agents (id, name, created_at) VALUES (?, ?, ?)",
@@ -104,10 +104,12 @@ class SQLiteStore:
if len(participants) < 2: if len(participants) < 2:
raise ValueError("need >=2 participants") raise ValueError("need >=2 participants")
tid = str(uuid.uuid4()) tid = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat() ts = datetime.datetime.now(datetime.UTC).isoformat()
self._db.execute("BEGIN") self._db.execute("BEGIN")
try: 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: for p in participants:
self._db.execute( self._db.execute(
"INSERT INTO thread_participants (thread_id, agent_id) VALUES (?, ?)", "INSERT INTO thread_participants (thread_id, agent_id) VALUES (?, ?)",
@@ -124,7 +126,9 @@ class SQLiteStore:
) )
def get_thread(self, id: uuid.UUID) -> Thread | None: 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() row = cur.fetchone()
if row is None: if row is None:
return None return None
@@ -151,7 +155,9 @@ class SQLiteStore:
if not c: if not c:
cur = self._db.execute("SELECT id FROM threads") cur = self._db.execute("SELECT id FROM threads")
tids = [r[0] for r in cur.fetchall()] 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] ids = [str(a.id) for a in c]
placeholders = ",".join("?" for _ in ids) placeholders = ",".join("?" for _ in ids)
# GROUP BY HAVING COUNT(*) = len(containing) # GROUP BY HAVING COUNT(*) = len(containing)
@@ -199,7 +205,9 @@ class SQLiteStore:
# -- messages -- # -- 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 # validate thread exists
if self.get_thread(thread_id) is None: if self.get_thread(thread_id) is None:
raise KeyError(thread_id) raise KeyError(thread_id)
@@ -214,12 +222,13 @@ class SQLiteStore:
try: try:
self._db.execute("BEGIN IMMEDIATE") self._db.execute("BEGIN IMMEDIATE")
cur2 = self._db.execute( 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] max_seq = cur2.fetchone()[0]
seq = int(max_seq) + 1 seq = int(max_seq) + 1
mid = str(uuid.uuid4()) mid = str(uuid.uuid4())
ts = datetime.datetime.now(datetime.timezone.utc).isoformat() ts = datetime.datetime.now(datetime.UTC).isoformat()
self._db.execute( self._db.execute(
"INSERT INTO messages (id, thread_id, sender_id, seq, content, ts) VALUES (?, ?, ?, ?, ?, ?)", "INSERT INTO messages (id, thread_id, sender_id, seq, content, ts) VALUES (?, ?, ?, ?, ?, ?)",
(mid, str(thread_id), str(sender.id), seq, content, ts), (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
+12 -7
View File
@@ -1,11 +1,13 @@
from agentmsgs.stores.memory import InMemoryStore
from agentmsgs.core import ops from agentmsgs.core import ops
from agentmsgs.stores.memory import InMemoryStore
def test_ops_validates_sender_must_be_in_thread(): def test_ops_validates_sender_must_be_in_thread():
s = InMemoryStore() 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")
t = ops.create_thread(s, {a,b}) b = ops.get_or_create_agent(s, "B")
c = ops.get_or_create_agent(s, "C")
t = ops.create_thread(s, {a, b})
try: try:
ops.append_message(s, t.id, c, "oops") ops.append_message(s, t.id, c, "oops")
assert False, "should raise" assert False, "should raise"
@@ -15,8 +17,9 @@ def test_ops_validates_sender_must_be_in_thread():
def test_ops_has_unread_exclude_own(): def test_ops_has_unread_exclude_own():
s = InMemoryStore() 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")
t = ops.create_thread(s, {a,b}) b = ops.get_or_create_agent(s, "B")
t = ops.create_thread(s, {a, b})
ops.append_message(s, t.id, a, "a1") ops.append_message(s, t.id, a, "a1")
# b has unread, a's own message shouldn't count if exclude_own # b has unread, a's own message shouldn't count if exclude_own
assert ops.has_unread(s, t.id, b) is True assert ops.has_unread(s, t.id, b) is True
@@ -28,8 +31,10 @@ def test_ops_has_unread_exclude_own():
def test_ops_join_and_delete(): def test_ops_join_and_delete():
s = InMemoryStore() 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")
t = ops.create_thread(s, {a,b}) 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) t2 = ops.join_thread(s, t.id, c)
assert c in t2.participants assert c in t2.participants
ops.delete_agent(s, c.id) ops.delete_agent(s, c.id)
+7 -2
View File
@@ -1,5 +1,6 @@
from agentmsgs.stores.memory import InMemoryStore from agentmsgs.stores.memory import InMemoryStore
def test_memory_create_and_find_thread(): def test_memory_create_and_find_thread():
s = InMemoryStore() s = InMemoryStore()
alice = s.get_or_create_agent("Alice") 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 t in s.find_threads({alice, bob})
assert s.find_threads({alice, bob, s.get_or_create_agent("Charlie")}) == [] assert s.find_threads({alice, bob, s.get_or_create_agent("Charlie")}) == []
def test_memory_soft_unique_name(): def test_memory_soft_unique_name():
s = InMemoryStore() s = InMemoryStore()
a1 = s.get_or_create_agent("Alice") a1 = s.get_or_create_agent("Alice")
a2 = s.get_or_create_agent("Alice") a2 = s.get_or_create_agent("Alice")
assert a1.id == a2.id assert a1.id == a2.id
def test_memory_delete_account(): def test_memory_delete_account():
s = InMemoryStore() s = InMemoryStore()
a = s.get_or_create_agent("Alice") a = s.get_or_create_agent("Alice")
@@ -23,10 +26,12 @@ def test_memory_delete_account():
assert s.get_agent_by_id(a.id) is None assert s.get_agent_by_id(a.id) is None
assert s.get_agent_by_name("Alice") is None assert s.get_agent_by_name("Alice") is None
def test_memory_append_and_list(): def test_memory_append_and_list():
s = InMemoryStore() s = InMemoryStore()
a = s.get_or_create_agent("A"); b = s.get_or_create_agent("B") a = s.get_or_create_agent("A")
t = s.create_thread({a,b}) b = s.get_or_create_agent("B")
t = s.create_thread({a, b})
m1 = s.append_message(t.id, a, "hi") m1 = s.append_message(t.id, a, "hi")
m2 = s.append_message(t.id, b, "yo") m2 = s.append_message(t.id, b, "yo")
assert m1.seq == 1 and m2.seq == 2 assert m1.seq == 1 and m2.seq == 2
+19 -2
View File
@@ -1,10 +1,27 @@
from typing import Protocol from typing import Protocol
from agentmsgs.core.store import Store from agentmsgs.core.store import Store
import inspect
def test_store_is_protocol(): def test_store_is_protocol():
assert issubclass(Store, Protocol) assert issubclass(Store, Protocol)
# check required methods exist # 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}" assert hasattr(Store, m), f"missing {m}"
+28 -11
View File
@@ -1,18 +1,24 @@
import tempfile, pathlib import pathlib
import tempfile
from agentmsgs.stores.sqlite import SQLiteStore from agentmsgs.stores.sqlite import SQLiteStore
def test_sqlite_persists_across_handles(): def test_sqlite_persists_across_handles():
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
p = pathlib.Path(d)/"test.db" p = pathlib.Path(d) / "test.db"
s1 = SQLiteStore(p) s1 = SQLiteStore(p)
try: try:
a = s1.get_or_create_agent("Alice"); b = s1.get_or_create_agent("Bob") a = s1.get_or_create_agent("Alice")
t = s1.create_thread({a,b}) b = s1.get_or_create_agent("Bob")
t = s1.create_thread({a, b})
s1.append_message(t.id, a, "hi") s1.append_message(t.id, a, "hi")
# new handle same file # new handle same file
s2 = SQLiteStore(p) s2 = SQLiteStore(p)
try: 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 len(s2.list_messages(t.id)) == 1
assert s2.find_threads({a})[0].id == t.id assert s2.find_threads({a})[0].id == t.id
finally: finally:
@@ -20,17 +26,28 @@ def test_sqlite_persists_across_handles():
finally: finally:
s1.close() s1.close()
def test_sqlite_concurrent_append(): def test_sqlite_concurrent_append():
import pathlib, tempfile import pathlib
import tempfile
from agentmsgs.stores.sqlite import SQLiteStore from agentmsgs.stores.sqlite import SQLiteStore
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
p = pathlib.Path(d)/"c.db" p = pathlib.Path(d) / "c.db"
s1 = SQLiteStore(p); s2 = SQLiteStore(p) s1 = SQLiteStore(p)
s2 = SQLiteStore(p)
try: 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 # 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")
t = s1.create_thread({a,b}) 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") s1.append_message(t.id, a, "from s1")
s2.append_message(t.id, b2, "from s2") s2.append_message(t.id, b2, "from s2")
assert len(s1.list_messages(t.id)) == 2 assert len(s1.list_messages(t.id)) == 2
+19 -5
View File
@@ -1,6 +1,8 @@
import uuid
from agentmsgs.core.types import Agent, Thread, Message
import datetime import datetime
import uuid
from agentmsgs.core.types import Agent, Thread
def test_agent_identity_on_id_not_name(): def test_agent_identity_on_id_not_name():
a1 = Agent(id=uuid.uuid4(), name="Alice") 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 a1 != a2 # different id => not equal even though name same
assert hash(a1) != hash(a2) assert hash(a1) != hash(a2)
def test_agent_hash_on_id(): def test_agent_hash_on_id():
uid = uuid.uuid4() uid = uuid.uuid4()
a1 = Agent(id=uid, name="Alice") 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 a1 == a2
assert hash(a1) == hash(a2) assert hash(a1) == hash(a2)
def test_thread_multiple_with_same_participants_allowed(): def test_thread_multiple_with_same_participants_allowed():
a = Agent(id=uuid.uuid4(), name="A") a = Agent(id=uuid.uuid4(), name="A")
b = Agent(id=uuid.uuid4(), name="B") b = Agent(id=uuid.uuid4(), name="B")
t1 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc)) t1 = Thread(
t2 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc)) 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 != t2
assert t1.participants == t2.participants assert t1.participants == t2.participants