feat(core): implement domain dataclasses for issue #5 #19

Merged
shoko merged 4 commits from feat/issue-5-core-models into main 2026-04-03 00:37:30 +02:00
2 changed files with 38 additions and 42 deletions
Showing only changes of commit 330203e6ef - Show all commits

View File

@@ -1,53 +1,63 @@
"""Domain dataclasses for JIGAIDO bounty tracker.""" """Domain dataclasses for JIGAIDO bounty tracker."""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
@dataclass @dataclass
class Bounty: class Bounty:
"""Bounty - used for both group and personal bounties. """A bounty created by a user.
Use created_by_user_id to distinguish: if set, it's a group bounty The created_by_user_id field always refers to the user who created the bounty.
created by that user. If None, it's a personal/DM bounty. It does NOT indicate whether the bounty is a group or personal bounty.
""" """
id: int id: int
text: Optional[str] text: str | None
link: Optional[str] link: str | None
due_date_ts: Optional[int] due_date_ts: int | None
created_at: int created_at: int
created_by_user_id: Optional[int] = None created_by_user_id: int
@dataclass @dataclass
han marked this conversation as resolved
Review

can we simplify the Bounty by not having both GroupBounty and PersonalBounty? can we utilize Bounty by adding optional created_by_user_id so in this case we can use Bounty for Group and Personal Bounty. what do you think?

can we simplify the Bounty by not having both GroupBounty and PersonalBounty? can we utilize Bounty by adding optional created_by_user_id so in this case we can use Bounty for Group and Personal Bounty. what do you think?
class TrackedBounty: class TrackedBounty:
"""A bounty that a user is tracking.""" """A bounty that a user is tracking.
Use TrackedBounty when you need to record that a user is tracking a specific
bounty from a specific room. The room_id indicates which room the bounty
was tracked from (useful when users can track bounties across multiple rooms).
For simple tracking lists, just use bounty_id and created_at.
"""
bounty_id: int bounty_id: int
created_at: int created_at: int
room_id: int
@dataclass @dataclass
class RoomData: class RoomData:
"""All data for a room (group or DM). """All data for a room (group or DM).
han marked this conversation as resolved
Review

since room_id can be negative in RoomData, I don't think we need is_group anymore. if its negative, its group, that should be clear enough

since room_id can be negative in RoomData, I don't think we need is_group anymore. if its negative, its group, that should be clear enough
For groups: is_group=True, room_id is negative (Telegram group ID) The room_id can be negative for Telegram groups or positive for DMs.
For DMs: is_group=False, room_id is the user_id (positive) The next_id field is used to generate unique bounty IDs within this room.
next_id is used to generate unique bounty IDs within this room.
""" """
room_id: int room_id: int
is_group: bool
bounties: list[Bounty] bounties: list[Bounty]
next_id: int next_id: int
@dataclass @dataclass
class TrackingData: class TrackingData:
"""User tracking data within a group. """User tracking state within a group.
group_id is the Telegram group ID (always negative). TrackingData vs TrackedBounty:
- Use TrackingData to store ALL tracked bounties for a user in a specific group.
It contains the group_id, user_id, and a list of TrackedBounty entries.
- Use TrackedBounty to represent a single tracked bounty entry within that list.
TrackingData is the container, TrackedBounty is the item.
""" """
group_id: int group_id: int

View File

@@ -20,14 +20,14 @@ class TestBounty:
link="https://github.com/example/repo/issues/1", link="https://github.com/example/repo/issues/1",
due_date_ts=1735689600, due_date_ts=1735689600,
created_at=1735603200, created_at=1735603200,
created_by_user_id=None, created_by_user_id=123,
) )
assert b.id == 1 assert b.id == 1
assert b.text == "Fix the bug" assert b.text == "Fix the bug"
assert b.link == "https://github.com/example/repo/issues/1" assert b.link == "https://github.com/example/repo/issues/1"
assert b.due_date_ts == 1735689600 assert b.due_date_ts == 1735689600
assert b.created_at == 1735603200 assert b.created_at == 1735603200
assert b.created_by_user_id is None assert b.created_by_user_id == 123
def test_bounty_optional_fields_can_be_none(self): def test_bounty_optional_fields_can_be_none(self):
b = Bounty( b = Bounty(
@@ -36,7 +36,7 @@ class TestBounty:
link=None, link=None,
due_date_ts=None, due_date_ts=None,
created_at=0, created_at=0,
created_by_user_id=None, created_by_user_id=123,
) )
assert b.text is None assert b.text is None
assert b.link is None assert b.link is None
@@ -49,7 +49,7 @@ class TestBounty:
link=None, link=None,
due_date_ts=None, due_date_ts=None,
created_at=0, created_at=0,
created_by_user_id=None, created_by_user_id=123,
) )
b2 = Bounty( b2 = Bounty(
id=1, id=1,
@@ -57,7 +57,7 @@ class TestBounty:
link=None, link=None,
due_date_ts=None, due_date_ts=None,
created_at=0, created_at=0,
created_by_user_id=None, created_by_user_id=123,
) )
assert b1 == b2 assert b1 == b2
@@ -68,7 +68,7 @@ class TestBounty:
link=None, link=None,
due_date_ts=None, due_date_ts=None,
created_at=0, created_at=0,
created_by_user_id=None, created_by_user_id=123,
) )
b2 = Bounty( b2 = Bounty(
id=2, id=2,
@@ -76,31 +76,21 @@ class TestBounty:
link=None, link=None,
due_date_ts=None, due_date_ts=None,
created_at=0, created_at=0,
created_by_user_id=None, created_by_user_id=456,
) )
assert b1 != b2 assert b1 != b2
def test_bounty_with_created_by_user_id(self):
b = Bounty(
id=1,
text="Group task",
link=None,
due_date_ts=None,
created_at=0,
created_by_user_id=123456,
)
assert b.created_by_user_id == 123456
class TestTrackedBounty: class TestTrackedBounty:
def test_create_tracked_bounty(self): def test_create_tracked_bounty(self):
tb = TrackedBounty(bounty_id=5, created_at=1735600000) tb = TrackedBounty(bounty_id=5, created_at=1735600000, room_id=-1001)
assert tb.bounty_id == 5 assert tb.bounty_id == 5
assert tb.created_at == 1735600000 assert tb.created_at == 1735600000
assert tb.room_id == -1001
def test_tracked_bounty_comparison(self): def test_tracked_bounty_comparison(self):
tb1 = TrackedBounty(bounty_id=1, created_at=0) tb1 = TrackedBounty(bounty_id=1, created_at=0, room_id=-1001)
tb2 = TrackedBounty(bounty_id=1, created_at=0) tb2 = TrackedBounty(bounty_id=1, created_at=0, room_id=-1001)
assert tb1 == tb2 assert tb1 == tb2
@@ -108,24 +98,20 @@ class TestRoomData:
def test_create_group_room_data(self): def test_create_group_room_data(self):
rd = RoomData( rd = RoomData(
room_id=-1001, room_id=-1001,
is_group=True,
bounties=[], bounties=[],
next_id=1, next_id=1,
) )
assert rd.room_id == -1001 assert rd.room_id == -1001
assert rd.is_group is True
assert rd.bounties == [] assert rd.bounties == []
assert rd.next_id == 1 assert rd.next_id == 1
def test_create_dm_room_data(self): def test_create_dm_room_data(self):
rd = RoomData( rd = RoomData(
room_id=123456, room_id=123456,
is_group=False,
bounties=[], bounties=[],
next_id=1, next_id=1,
) )
assert rd.room_id == 123456 assert rd.room_id == 123456
assert rd.is_group is False
assert rd.bounties == [] assert rd.bounties == []
assert rd.next_id == 1 assert rd.next_id == 1
@@ -138,7 +124,7 @@ class TestRoomData:
created_at=0, created_at=0,
created_by_user_id=123, created_by_user_id=123,
) )
rd = RoomData(room_id=-1001, is_group=True, bounties=[b], next_id=2) rd = RoomData(room_id=-1001, bounties=[b], next_id=2)
assert len(rd.bounties) == 1 assert len(rd.bounties) == 1
assert rd.bounties[0].text == "Task" assert rd.bounties[0].text == "Task"
assert rd.bounties[0].created_by_user_id == 123 assert rd.bounties[0].created_by_user_id == 123
@@ -152,7 +138,7 @@ class TestTrackingData:
assert td.tracked == [] assert td.tracked == []
def test_tracking_data_with_tracked(self): def test_tracking_data_with_tracked(self):
tb = TrackedBounty(bounty_id=5, created_at=0) tb = TrackedBounty(bounty_id=5, created_at=0, room_id=-1001)
td = TrackingData(group_id=-1001, user_id=123, tracked=[tb]) td = TrackingData(group_id=-1001, user_id=123, tracked=[tb])
assert len(td.tracked) == 1 assert len(td.tracked) == 1
assert td.tracked[0].bounty_id == 5 assert td.tracked[0].bounty_id == 5