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
3 changed files with 251 additions and 0 deletions
Showing only changes of commit db09a518d1 - Show all commits

21
core/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
"""Core domain models for JIGAIDO."""
from core.models import (
Bounty,
GroupBounty,
PersonalBounty,
TrackedBounty,
GroupData,
TrackingData,
UserData,
)
__all__ = [
"Bounty",
"GroupBounty",
"PersonalBounty",
"TrackedBounty",
"GroupData",
"TrackingData",
"UserData",
]

63
core/models.py Normal file
View File

@@ -0,0 +1,63 @@
"""Domain dataclasses for JIGAIDO bounty tracker."""
from dataclasses import dataclass
from typing import Optional
@dataclass
class Bounty:
"""Base bounty with common fields."""
id: int
text: Optional[str]
link: Optional[str]
due_date_ts: Optional[int]
created_at: int
@dataclass
class GroupBounty(Bounty):
"""Bounty created in a group context."""
created_by_user_id: int
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?
@dataclass
class PersonalBounty(Bounty):
"""Bounty created in DM/personal context."""
pass
@dataclass
class TrackedBounty:
"""A bounty that a user is tracking."""
bounty_id: int
created_at: int
@dataclass
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
class GroupData:
"""All data for a group."""
group_id: int
bounties: list[GroupBounty]
next_id: int
@dataclass
class TrackingData:
"""User tracking data within a group."""
user_id: int
tracked: list[TrackedBounty]
@dataclass
class UserData:
"""All personal bounties for a user (DM mode)."""
user_id: int
bounties: list[PersonalBounty]
next_id: int

167
tests/test_models.py Normal file
View File

@@ -0,0 +1,167 @@
"""Tests for core/models.py — domain dataclasses."""
import time
import pytest
from core.models import (
Bounty,
GroupBounty,
PersonalBounty,
TrackedBounty,
GroupData,
TrackingData,
UserData,
)
class TestBounty:
def test_create_bounty(self):
b = Bounty(
id=1,
text="Fix the bug",
link="https://github.com/example/repo/issues/1",
due_date_ts=1735689600,
created_at=1735603200,
)
assert b.id == 1
assert b.text == "Fix the bug"
assert b.link == "https://github.com/example/repo/issues/1"
assert b.due_date_ts == 1735689600
assert b.created_at == 1735603200
def test_bounty_optional_fields_can_be_none(self):
b = Bounty(
id=1,
text=None,
link=None,
due_date_ts=None,
created_at=0,
)
assert b.text is None
assert b.link is None
assert b.due_date_ts is None
def test_bounty_comparison(self):
b1 = Bounty(id=1, text="a", link=None, due_date_ts=None, created_at=0)
b2 = Bounty(id=1, text="a", link=None, due_date_ts=None, created_at=0)
assert b1 == b2
class TestGroupBounty:
def test_create_group_bounty(self):
gb = GroupBounty(
id=1,
text="Group task",
link=None,
due_date_ts=None,
created_at=0,
created_by_user_id=123456,
)
assert gb.id == 1
assert gb.text == "Group task"
assert gb.created_by_user_id == 123456
def test_group_bounty_inherits_from_bounty(self):
gb = GroupBounty(
id=1,
text="Task",
link="https://example.com",
due_date_ts=int(time.time()),
created_at=0,
created_by_user_id=999,
)
assert isinstance(gb, Bounty)
class TestPersonalBounty:
def test_create_personal_bounty(self):
pb = PersonalBounty(
id=1,
text="Personal task",
link=None,
due_date_ts=None,
created_at=0,
)
assert pb.id == 1
assert pb.text == "Personal task"
def test_personal_bounty_inherits_from_bounty(self):
pb = PersonalBounty(
id=1,
text="Task",
link=None,
due_date_ts=None,
created_at=0,
)
assert isinstance(pb, Bounty)
class TestTrackedBounty:
def test_create_tracked_bounty(self):
tb = TrackedBounty(bounty_id=5, created_at=1735600000)
assert tb.bounty_id == 5
assert tb.created_at == 1735600000
def test_tracked_bounty_comparison(self):
tb1 = TrackedBounty(bounty_id=1, created_at=0)
tb2 = TrackedBounty(bounty_id=1, created_at=0)
assert tb1 == tb2
class TestGroupData:
def test_create_group_data(self):
gd = GroupData(
group_id=-1001,
bounties=[],
next_id=1,
)
assert gd.group_id == -1001
assert gd.bounties == []
assert gd.next_id == 1
def test_group_data_with_bounties(self):
gb = GroupBounty(
id=1,
text="Task",
link=None,
due_date_ts=None,
created_at=0,
created_by_user_id=123,
)
gd = GroupData(group_id=-1001, bounties=[gb], next_id=2)
assert len(gd.bounties) == 1
assert gd.bounties[0].text == "Task"
class TestTrackingData:
def test_create_tracking_data(self):
td = TrackingData(user_id=123456, tracked=[])
assert td.user_id == 123456
assert td.tracked == []
def test_tracking_data_with_tracked(self):
tb = TrackedBounty(bounty_id=5, created_at=0)
td = TrackingData(user_id=123, tracked=[tb])
assert len(td.tracked) == 1
assert td.tracked[0].bounty_id == 5
class TestUserData:
def test_create_user_data(self):
ud = UserData(user_id=123456, bounties=[], next_id=1)
assert ud.user_id == 123456
assert ud.bounties == []
assert ud.next_id == 1
def test_user_data_with_bounties(self):
pb = PersonalBounty(
id=1,
text="My task",
link=None,
due_date_ts=None,
created_at=0,
)
ud = UserData(user_id=123, bounties=[pb], next_id=2)
assert len(ud.bounties) == 1
assert ud.bounties[0].text == "My task"