From db09a518d197a8e1b7ba941e9fd037c8252778c2 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:15:41 +0000 Subject: [PATCH] feat(core): implement domain dataclasses for issue #5 - Create core/__init__.py - Create core/models.py with all domain dataclasses: - Bounty (base class) - GroupBounty (extends Bounty) - PersonalBounty (extends Bounty) - TrackedBounty - GroupData - TrackingData - UserData - Create tests/test_models.py with 15 passing tests Fixes #5 --- core/__init__.py | 21 ++++++ core/models.py | 63 ++++++++++++++++ tests/test_models.py | 167 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 core/__init__.py create mode 100644 core/models.py create mode 100644 tests/test_models.py diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..3004cfa --- /dev/null +++ b/core/__init__.py @@ -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", +] diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..9c55720 --- /dev/null +++ b/core/models.py @@ -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 + + +@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 +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 diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..2afe0e9 --- /dev/null +++ b/tests/test_models.py @@ -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"