diff --git a/README.md b/README.md index 824513d..dbd95cf 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,53 @@ t_algebra = Topic( ) ``` +### Create a Proficiency Score, by name + +A **Proficiency Score** represents a person's level of proficiency in a specific topic. + +```python +proficiency_score = ProficiencyScore( + topic_id="addition", + score=ProficiencyScoreName.PROFICIENT +) +``` + +### Create a Proficiency Score, numerically + +All score are internally numeric from 0.0 to 1.0, even if set using the score name (above). + +```python +proficiency_score = ProficiencyScore( + topic_id="arithmetic", + score=0.8 # Same as 'ProficiencyScoreName.PROFICIENT' +) +``` + +### Issue a Transcript Entry + +A **Transcript Entry** associates a proficiency score to a user and is claimed by an issuer. + +```python +from openproficiency import TranscriptEntry + +# Create a transcript entry +entry = TranscriptEntry( + user_id="john-doe", + topic_id="arithmetic", + score=0.9, + issuer="university-of-example" +) + +# Access the transcript entry information +print(entry.user_id) # john-doe +print(entry.proficiency_score.score) # 0.9 +print(entry.issuer) # university-of-example +print(entry.timestamp) # datetime object + +# Convert to JSON for storage or transmission +entry_json = entry.to_json() +``` + ## How to Develop This project is open to pull requests. diff --git a/openproficiency/ProficiencyScore.py b/openproficiency/ProficiencyScore.py new file mode 100644 index 0000000..d322104 --- /dev/null +++ b/openproficiency/ProficiencyScore.py @@ -0,0 +1,97 @@ +"""ProficiencyScore module for OpenProficiency library.""" + +import json +from enum import Enum +from typing import Union + + +class ProficiencyScoreName(Enum): + """Enum for proficiency score names.""" + + UNAWARE = 0.0 + AWARE = 0.1 + FAMILIAR = 0.5 + PROFICIENT = 0.8 + PROFICIENT_WITH_EVIDENCE = 1.0 + + +class ProficiencyScore: + """Class representing a proficiency score for a topic.""" + + # Initializers + def __init__( + self, + # Required + topic_id: str, + score: Union[float, ProficiencyScoreName], + ): + # Required + self.topic_id = topic_id + self.score = score + + # Properties - Score + @property + def score(self) -> float: + """Get the score as a numeric value between 0.0 and 1.0.""" + return self._score + + @score.setter + def score(self, value: Union[float, ProficiencyScoreName]) -> None: + """Set the score numerically or using a ProficiencyScoreName enum.""" + # If numeric, directly store it. + if isinstance(value, (int, float)): + if not (0.0 <= value <= 1.0): + raise ValueError(f"Score must be between 0.0 and 1.0, got {value}") + self._score = float(value) + + # If enum, store as numeric value. + elif isinstance(value, ProficiencyScoreName): + self._score = value.value + + else: + raise ValueError(f"Score must be numeric or ProficiencyScoreName enum. Got type: '{type(value)}'") + + # Properties - Score + @property + def score_name(self) -> ProficiencyScoreName: + """Get the proficiency name as an enum value.""" + return ProficiencyScore.get_score_name(self._score) + + # Methods + def to_dict(self) -> dict[str, float]: + """Convert to a JSON-serializable dictionary.""" + return { + "topic_id": self.topic_id, + "score": self._score, + } + + def to_json(self) -> str: + """Convert to a JSON string.""" + return json.dumps(self.to_dict()) + + # Static Methods + @staticmethod + def get_score_name(score: float) -> ProficiencyScoreName: + """Internal method to determine proficiency name from numeric score.""" + if score == 1.0: + return ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE + + elif score >= 0.8: + return ProficiencyScoreName.PROFICIENT + + elif score >= 0.5: + return ProficiencyScoreName.FAMILIAR + + elif score >= 0.1: + return ProficiencyScoreName.AWARE + + elif score >= 0.0: + return ProficiencyScoreName.UNAWARE + + else: + raise ValueError(f"Invalid score value: {score}") + + # Debugging + def __repr__(self) -> str: + """String representation of ProficiencyScore.""" + return f"ProficiencyScore(topic_id='{self.topic_id}', score={self._score}, name={self.score_name.name})" diff --git a/openproficiency/Topic.py b/openproficiency/Topic.py index 667ddcd..4646eab 100644 --- a/openproficiency/Topic.py +++ b/openproficiency/Topic.py @@ -1,5 +1,6 @@ """Topic module for OpenProficiency library.""" +import json from typing import List, Union @@ -77,6 +78,19 @@ def add_pretopics(self, pretopics: List[Union[str, "Topic"]]) -> None: for pretopic in pretopics: self.add_pretopic(pretopic) + def to_dict(self) -> dict: + """Convert Topic to JSON-serializable dictionary.""" + return { + "id": self.id, + "description": self.description, + "subtopics": self.subtopics, + "pretopics": self.pretopics + } + + def to_json(self) -> str: + """Convert Topic to JSON string.""" + return json.dumps(self.to_dict()) + # Debugging def __repr__(self) -> str: """String representation of Topic.""" diff --git a/openproficiency/TopicList.py b/openproficiency/TopicList.py index f6c86a7..f14ff67 100644 --- a/openproficiency/TopicList.py +++ b/openproficiency/TopicList.py @@ -15,7 +15,7 @@ def __init__( owner: str, name: str, # Optional - description: str = "", + description: str = "" ): # Required self.owner = owner @@ -204,7 +204,7 @@ def _add_pretopics_recursive( topic_list.add_topic(pretopic) current_child.add_pretopic(pretopic) - def to_json(self) -> str: + def to_dict(self) -> dict: """ Export the TopicList to a JSON string. """ @@ -234,4 +234,8 @@ def to_json(self) -> str: # Store in data data["topics"][topic_id] = topic_data - return json.dumps(data, indent=2) + return data + + def to_json(self) -> str: + """Convert TopicList to JSON string.""" + return json.dumps(self.to_dict()) diff --git a/openproficiency/TranscriptEntry.py b/openproficiency/TranscriptEntry.py new file mode 100644 index 0000000..95c5564 --- /dev/null +++ b/openproficiency/TranscriptEntry.py @@ -0,0 +1,80 @@ +"""TranscriptEntry module for OpenProficiency library.""" + +import json +from datetime import datetime +from typing import Optional, Union +from .ProficiencyScore import ProficiencyScore + + +class TranscriptEntry: + """A user's proficiency score, validated by a particular issuer.""" + + # Initializers + def __init__( + self, + # Required + user_id: str, + topic_id: str, + score: float, + issuer: str, + # Optional + timestamp: Optional[datetime] = None, + ): + # Required + self.user_id = user_id + self._proficiency_score = ProficiencyScore( + topic_id=topic_id, + score=score, + ) + self.issuer = issuer + + # Optional + self.timestamp = timestamp or datetime.now() + + # Properties + @property + def proficiency_score(self) -> ProficiencyScore: + """Get the topic ID from the proficiency score.""" + return self._proficiency_score + + # Methods + def to_dict(self) -> dict[str, Union[str, int, float]]: + """Convert TranscriptEntry to JSON-serializable dictionary.""" + return { + "user_id": self.user_id, + "topic_id": self._proficiency_score.topic_id, + "score": self._proficiency_score.score, + "issuer": self.issuer, + "timestamp": self.timestamp.isoformat(), + } + + def to_json(self) -> str: + """Convert TranscriptEntry to JSON string.""" + return json.dumps(self.to_dict()) + + # Methods - Static + @staticmethod + def from_dict(data: dict[str, Union[str, int, float]]) -> "TranscriptEntry": + """Create a TranscriptEntry from a dictionary.""" + return TranscriptEntry( + user_id=data["user_id"], + topic_id=data["topic_id"], + score=data["score"], + issuer=data["issuer"], + timestamp=datetime.fromisoformat(data["timestamp"]), + ) + + @staticmethod + def from_json(json_str: str) -> "TranscriptEntry": + """Create a TranscriptEntry from a JSON string.""" + return TranscriptEntry.from_dict(json.loads(json_str)) + + # Debugging + def __repr__(self) -> str: + """String representation of TranscriptEntry.""" + return ( + f"TranscriptEntry(user_id='{self.user_id}', " + f"topic_id='{self._proficiency_score.topic_id}', " + f"score={self._proficiency_score.score}, " + f"issuer='{self.issuer}'" + ) diff --git a/openproficiency/__init__.py b/openproficiency/__init__.py index 62a0752..8a52fbd 100644 --- a/openproficiency/__init__.py +++ b/openproficiency/__init__.py @@ -1,4 +1,6 @@ """OpenProficiency - Library to manage proficiency scores using topics and topic lists.""" from .Topic import Topic -from .TopicList import TopicList \ No newline at end of file +from .TopicList import TopicList +from .ProficiencyScore import ProficiencyScore, ProficiencyScoreName +from .TranscriptEntry import TranscriptEntry diff --git a/pyproject.toml b/pyproject.toml index a546ebe..8c80190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openproficiency" -version = "0.1.0" +version = "0.0.3" description = "A simple library for managing proficiency topics and topic lists" readme = "README.md" requires-python = ">=3.10" diff --git a/setup.py b/setup.py index c3a529f..559432d 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="openproficiency", - version="0.1.0", + version="0.0.3", author="OpenProficiency Contributors", description="A library for managing proficiency topics and topic lists", long_description=long_description, diff --git a/tests/ProficiencyScore_test.py b/tests/ProficiencyScore_test.py new file mode 100644 index 0000000..46b49de --- /dev/null +++ b/tests/ProficiencyScore_test.py @@ -0,0 +1,357 @@ +"""Tests for the ProficiencyScore class.""" + +from openproficiency import ProficiencyScore, ProficiencyScoreName + + +class TestProficiencyScore: + + # Initializers + def test_init_with_numeric_score(self): + """Create a proficiency score with numeric value.""" + + # Arrange + topic_id = "git-commit" + score = 0.8 + + # Act + ps = ProficiencyScore( + topic_id=topic_id, + score=score, + ) + + # Assert + assert ps.topic_id == topic_id + assert ps.score == score + + def test_init_with_enum_score(self): + """Create a proficiency score with ProficiencyScoreName enum.""" + + # Arrange + topic_id = "git-commit" + score_name = ProficiencyScoreName.PROFICIENT + + # Act + ps = ProficiencyScore( + topic_id=topic_id, + score=score_name, + ) + + # Assert + assert ps.topic_id == topic_id + assert ps.score == 0.8 + + def test_init_invalid_score_too_low(self): + """Test that score below 0.0 raises ValueError.""" + + # Act + result = None + try: + ProficiencyScore( + topic_id="test", + score=-0.1, + ) + except Exception as e: + result = e + + # Assert + assert isinstance(result, ValueError) + assert "0.0" in str(result) + assert "1.0" in str(result) + + def test_init_invalid_score_too_high(self): + """Test that score above 1.0 raises ValueError.""" + + # Act + result = None + try: + ProficiencyScore( + topic_id="test", + score=1.1, + ) + except Exception as e: + result = e + + # Assert + assert isinstance(result, ValueError) + assert "0.0" in str(result) + assert "1.0" in str(result) + + def test_init_invalid_score_type(self): + """Test that invalid score type raises ValueError.""" + + # Act + result = None + try: + ProficiencyScore( + topic_id="test", + score="invalid", + ) + except Exception as e: + result = e + + # Assert + assert isinstance(result, ValueError) + assert "ProficiencyScoreName" in str(result) + + # Properties + def test_score_numeric(self): + """Test setting score with numeric value.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=0.1, + ) + + # Act + ps.score = 0.9 + + # Assert + assert ps.score == 0.9 + assert ps.score_name == ProficiencyScoreName.PROFICIENT + + def test_score_numeric_invalid(self): + """Test setting invalid numeric score raises ValueError.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=0.1, + ) + + # Act + result = None + try: + ps.score = 1.5 + except Exception as e: + result = e + + # Assert + # Check that result is a ValueError and contains the expected message + assert isinstance(result, ValueError) + assert "0.0" in str(result) + assert "1.0" in str(result) + + def test_score_enum(self): + """Test setting score with enum value.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=0.1, + ) + + # Act + ps.score = ProficiencyScoreName.FAMILIAR + + # Assert + assert ps.score == 0.5 + assert ps.score_name == ProficiencyScoreName.FAMILIAR + + def test_score_name_unaware(self): + """Test score_name property for UNAWARE level.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=0.0, + ) + + # Act + result = ps.score_name + + # Assert + assert result == ProficiencyScoreName.UNAWARE + + def test_score_name_aware(self): + """Test score_name property for AWARE level.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=0.1, + ) + + # Act + result = ps.score_name + + # Assert + assert result == ProficiencyScoreName.AWARE + + def test_score_name_familiar(self): + """Test score_name property for FAMILIAR level.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=0.5, + ) + + # Act + result = ps.score_name + + # Assert + assert result == ProficiencyScoreName.FAMILIAR + + def test_score_name_proficient(self): + """Test score_name property for PROFICIENT level.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=0.8, + ) + + # Act + result = ps.score_name + + # Assert + assert result == ProficiencyScoreName.PROFICIENT + + def test_score_name_proficient_with_evidence(self): + """Test score_name property for PROFICIENT_WITH_EVIDENCE level.""" + + # Arrange + ps = ProficiencyScore( + topic_id="test", + score=1.0, + ) + + # Act + result = ps.score_name + + # Assert + assert result == ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE + + # Static Methods + def test_get_score_name_unaware(self): + """Test get_score_name returns UNAWARE for score 0.0.""" + + # Arrange + score = 0.0 + + # Act + result = ProficiencyScore.get_score_name(score) + + # Assert + assert result == ProficiencyScoreName.UNAWARE + + def test_get_score_name_aware(self): + """Test get_score_name returns AWARE for score 0.1.""" + + # Arrange + score = 0.1 + + # Act + result = ProficiencyScore.get_score_name(score) + + # Assert + assert result == ProficiencyScoreName.AWARE + + def test_get_score_name_familiar(self): + """Test get_score_name returns FAMILIAR for score 0.5.""" + + # Arrange + score = 0.5 + + # Act + result = ProficiencyScore.get_score_name(score) + + # Assert + assert result == ProficiencyScoreName.FAMILIAR + + def test_get_score_name_proficient(self): + """Test get_score_name returns PROFICIENT for score 0.8.""" + + # Arrange + score = 0.8 + + # Act + result = ProficiencyScore.get_score_name(score) + + # Assert + assert result == ProficiencyScoreName.PROFICIENT + + def test_get_score_name_proficient_with_evidence(self): + """Test get_score_name returns PROFICIENT_WITH_EVIDENCE for score 1.0.""" + + # Arrange + score = 1.0 + + # Act + result = ProficiencyScore.get_score_name(score) + + # Assert + assert result == ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE + + def test_get_score_name_invalid(self): + """Test get_score_name static method with invalid score.""" + + # Act + result = None + try: + ProficiencyScore.get_score_name(-0.1) + except Exception as e: + result = e + + # Assert + assert isinstance(result, ValueError) + assert "Invalid" in str(result) + + # Methods + def test_to_dict(self): + """Test conversion to JSON-serializable dictionary.""" + + # Arrange + topic_id = "git-commit" + score = 0.8 + ps = ProficiencyScore( + topic_id=topic_id, + score=score, + ) + + # Act + json_dict = ps.to_dict() + + # Assert + assert json_dict == { + "topic_id": topic_id, + "score": score, + } + + def test_to_json(self): + """Test conversion to JSON string.""" + + # Arrange + topic_id = "git-commit" + score = 0.8 + ps = ProficiencyScore( + topic_id=topic_id, + score=score, + ) + + # Act + json_str = ps.to_json() + + # Assert + expected_json = '{"topic_id": "git-commit", "score": 0.8}' + assert json_str == expected_json + + # Debugging + def test_repr(self): + """Test string representation of ProficiencyScore.""" + + # Arrange + ps = ProficiencyScore( + topic_id="git-commit", + score=0.8, + ) + + # Act + repr_str = repr(ps) + + # Assert + assert "ProficiencyScore" in repr_str + assert "git-commit" in repr_str + assert "0.8" in repr_str + assert "PROFICIENT" in repr_str diff --git a/tests/TopicList_test.py b/tests/TopicList_test.py index ee7895a..267b18f 100644 --- a/tests/TopicList_test.py +++ b/tests/TopicList_test.py @@ -374,8 +374,8 @@ def test_load_from_json_prepretopics(self): assert "git-pull" in topic_list.topics assert topic_list.topics["git-pull"].description == "Versioning code with Git repositories" - def test_to_json_simple(self): - """Exporting a simple TopicList to JSON.""" + def test_to_dict_simple(self): + """Exporting a simple TopicList to dictionary.""" # Arrange topic_list = TopicList( @@ -383,16 +383,60 @@ def test_to_json_simple(self): name="github-features", description="Features of the GitHub platform", ) - topic1 = Topic(id="actions", description="Storing changes to the Git history") - topic1.add_subtopic("automation") - topic1.add_pretopic("yaml") + topic_list.add_topic(Topic( + id="actions", + description="Storing changes to the Git history", + subtopics=["automation"], + pretopics=["yaml"] + )) + topic_list.add_topic(Topic( + id="repositories", + description="Versioning code with Git repositories", + subtopics=["git-clone"], + pretopics=["git-push"] + )) - topic2 = Topic(id="repositories", description="Versioning code with Git repositories") - topic2.add_subtopic("git-clone") - topic2.add_pretopic("git-push") + # Act + data = topic_list.to_dict() - topic_list.add_topic(topic1) - topic_list.add_topic(topic2) + # Assert - List Info + assert data["owner"] == "github" + assert data["name"] == "github-features" + assert data["description"] == "Features of the GitHub platform" + + # Assert - Topic 1 + assert "actions" in data["topics"] + assert data["topics"]["actions"]["description"] == "Storing changes to the Git history" + assert "automation" in data["topics"]["actions"]["subtopics"] + assert "yaml" in data["topics"]["actions"]["pretopics"] + + # Assert - Topic 2 + assert "repositories" in data["topics"] + assert data["topics"]["repositories"]["description"] == "Versioning code with Git repositories" + assert "git-clone" in data["topics"]["repositories"]["subtopics"] + assert "git-push" in data["topics"]["repositories"]["pretopics"] + + def test_to_json_simple(self): + """Exporting a simple TopicList to JSON string.""" + + # Arrange + topic_list = TopicList( + owner="github", + name="github-features", + description="Features of the GitHub platform", + ) + topic_list.add_topic(Topic( + id="actions", + description="Storing changes to the Git history", + subtopics=["automation"], + pretopics=["yaml"] + )) + topic_list.add_topic(Topic( + id="repositories", + description="Versioning code with Git repositories", + subtopics=["git-clone"], + pretopics=["git-push"] + )) # Act json_data = topic_list.to_json() diff --git a/tests/Topic_test.py b/tests/Topic_test.py index 81f1287..a073ec1 100644 --- a/tests/Topic_test.py +++ b/tests/Topic_test.py @@ -1,3 +1,4 @@ +import json from openproficiency import Topic @@ -141,6 +142,49 @@ def test_add_pretopics_mixed(self): assert pretopic1 in topic.pretopics assert pretopic2.id in topic.pretopics + def test_to_dict(self): + """Test converting a Topic to JSON.""" + + # Arrange + topic = Topic( + id="git-merge", + description="Combining branches in Git", + subtopics=["git-branch", "git-commit"], + pretopics=["cli"] + ) + + # Act + topic_json = topic.to_dict() + + # Assert + assert topic_json["id"] == "git-merge" + assert topic_json["description"] == "Combining branches in Git" + assert "git-branch" in topic_json["subtopics"] + assert "git-commit" in topic_json["subtopics"] + assert "cli" in topic_json["pretopics"] + + def test_to_json(self): + """Test converting a Topic to JSON string.""" + + # Arrange + topic = Topic( + id="git-merge", + description="Combining branches in Git", + subtopics=["git-branch", "git-commit"], + pretopics=["cli"] + ) + + # Act + topic_json_str = topic.to_json() + topic_json = json.loads(topic_json_str) + + # Assert + assert topic_json["id"] == "git-merge" + assert topic_json["description"] == "Combining branches in Git" + assert "git-branch" in topic_json["subtopics"] + assert "git-commit" in topic_json["subtopics"] + assert "cli" in topic_json["pretopics"] + # Debugging def test_topic_repr(self): """Check string representation of a Topic""" diff --git a/tests/TranscriptEntry_test.py b/tests/TranscriptEntry_test.py new file mode 100644 index 0000000..5a13954 --- /dev/null +++ b/tests/TranscriptEntry_test.py @@ -0,0 +1,214 @@ +"""Tests for the TranscriptEntry class.""" + +from datetime import datetime +from openproficiency import TranscriptEntry + + +class TestTranscriptEntry: + + # Initializers + def test_init_required_params(self): + """Create a transcript entry with required fields.""" + + # Arrange + user_id = "user-123" + topic_id = "git-commit" + score = 0.8 + issuer = "github-learn" + + # Act + entry = TranscriptEntry( + user_id=user_id, + topic_id=topic_id, + score=score, + issuer=issuer, + ) + + # Assert + assert entry.user_id == user_id + assert entry.proficiency_score.topic_id == topic_id + assert entry.proficiency_score.score == score + assert entry.issuer == issuer + assert entry.timestamp is not None + + def test_init_with_optional_params(self): + """Create a transcript entry with optional parameters.""" + + # Arrange + user_id = "user-123" + topic_id = "git-commit" + score = 0.8 + issuer = "github-learn" + timestamp = datetime(2024, 1, 15, 10, 30, 0) + + # Act + entry = TranscriptEntry( + user_id=user_id, + topic_id=topic_id, + score=score, + issuer=issuer, + timestamp=timestamp, + ) + + # Assert + assert entry.timestamp == timestamp + + def test_init_default_timestamp(self): + """Test that timestamp defaults to current time.""" + + # Arrange + user_id = "user-123" + topic_id = "git-commit" + score = 0.8 + issuer = "github-learn" + + # Act + before = datetime.now() + entry = TranscriptEntry( + user_id=user_id, + topic_id=topic_id, + score=score, + issuer=issuer, + ) + after = datetime.now() + + # Assert + assert entry.timestamp >= before + assert entry.timestamp <= after + + # Properties + def test_proficiency_score(self): + """Test that proficiency_score topic and score.""" + + # Arrange + entry = TranscriptEntry( + user_id="user-123", + topic_id="git-commit", + score=0.8, + issuer="github-learn", + ) + + # Act + topic_id = entry.proficiency_score.topic_id + score = entry.proficiency_score.score + + # Assert + assert topic_id == "git-commit" + assert score == 0.8 + + # Methods + def test_to_dict(self): + """Test conversion of TranscriptEntry to dictionary.""" + + # Arrange + entry = TranscriptEntry( + user_id="user-123", + topic_id="git-commit", + score=0.8, + issuer="github-learn", + timestamp=datetime(2024, 1, 15, 10, 30, 0), + ) + + # Act + entry_dict = entry.to_dict() + + # Assert + assert entry_dict == { + "user_id": "user-123", + "topic_id": "git-commit", + "score": 0.8, + "issuer": "github-learn", + "timestamp": "2024-01-15T10:30:00", + } + + def test_to_json(self): + """Test conversion of TranscriptEntry to JSON string.""" + + # Arrange + entry = TranscriptEntry( + user_id="user-123", + topic_id="git-commit", + score=0.8, + issuer="github-learn", + timestamp=datetime(2024, 1, 15, 10, 30, 0), + ) + + # Act + json_str = entry.to_json() + + # Assert + expected_json = ( + '{"user_id": "user-123", "topic_id": "git-commit", ' + '"score": 0.8, "issuer": "github-learn", ' + '"timestamp": "2024-01-15T10:30:00"}' + ) + assert json_str == expected_json + + # Methods - Static + def test_from_dict(self): + """Test creating TranscriptEntry from dictionary.""" + + # Arrange + data = { + "user_id": "user-456", + "topic_id": "git-branch", + "score": 0.6, + "issuer": "test-issuer", + "timestamp": "2024-02-20T15:45:30", + } + + # Act + entry = TranscriptEntry.from_dict(data) + + # Assert + assert entry.user_id == "user-456" + assert entry.proficiency_score.topic_id == "git-branch" + assert entry.proficiency_score.score == 0.6 + assert entry.issuer == "test-issuer" + assert entry.timestamp.year == 2024 + assert entry.timestamp.month == 2 + assert entry.timestamp.day == 20 + + def test_from_json(self): + """Test creating TranscriptEntry from JSON string.""" + + # Arrange + json_str = ( + '{"user_id": "user-789", "topic_id": "git-merge", ' + '"score": 0.9, "issuer": "test-system", ' + '"timestamp": "2024-03-10T08:20:15"}' + ) + + # Act + entry = TranscriptEntry.from_json(json_str) + + # Assert + assert entry.user_id == "user-789" + assert entry.proficiency_score.topic_id == "git-merge" + assert entry.proficiency_score.score == 0.9 + assert entry.issuer == "test-system" + assert entry.timestamp.year == 2024 + assert entry.timestamp.month == 3 + assert entry.timestamp.day == 10 + + # Debugging + def test_repr(self): + """Test string representation of TranscriptEntry.""" + + # Arrange + entry = TranscriptEntry( + user_id="user-123", + topic_id="git-commit", + score=0.8, + issuer="github-learn", + ) + + # Act + repr_str = repr(entry) + + # Assert + assert "TranscriptEntry" in repr_str + assert "user-123" in repr_str + assert "git-commit" in repr_str + assert "0.8" in repr_str + assert "github-learn" in repr_str