From 5a4a0ce9f2124656b54bc9f36a24dff3e30f9118 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Sun, 1 Feb 2026 22:32:54 +0000 Subject: [PATCH 1/9] feat: add method to export Topic as json object --- openproficiency/Topic.py | 9 +++++++++ tests/Topic_test.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/openproficiency/Topic.py b/openproficiency/Topic.py index 667ddcd..41ae18f 100644 --- a/openproficiency/Topic.py +++ b/openproficiency/Topic.py @@ -77,6 +77,15 @@ def add_pretopics(self, pretopics: List[Union[str, "Topic"]]) -> None: for pretopic in pretopics: self.add_pretopic(pretopic) + def to_json(self) -> dict: + """Convert Topic to JSON-serializable dictionary.""" + return { + "id": self.id, + "description": self.description, + "subtopics": self.subtopics, + "pretopics": self.pretopics + } + # Debugging def __repr__(self) -> str: """String representation of Topic.""" diff --git a/tests/Topic_test.py b/tests/Topic_test.py index 81f1287..8a102f2 100644 --- a/tests/Topic_test.py +++ b/tests/Topic_test.py @@ -141,6 +141,27 @@ def test_add_pretopics_mixed(self): assert pretopic1 in topic.pretopics assert pretopic2.id in topic.pretopics + def test_to_json(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_json() + + # 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""" From ce2d299cf7e263da8bdfd416de43f68af7e33d20 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Sun, 1 Feb 2026 22:39:37 +0000 Subject: [PATCH 2/9] feat: add class ProficencyScore --- openproficiency/ProficiencyScore.py | 98 +++++++++++++ openproficiency/__init__.py | 3 +- tests/ProficiencyScore_test.py | 218 ++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 openproficiency/ProficiencyScore.py create mode 100644 tests/ProficiencyScore_test.py diff --git a/openproficiency/ProficiencyScore.py b/openproficiency/ProficiencyScore.py new file mode 100644 index 0000000..c8515b7 --- /dev/null +++ b/openproficiency/ProficiencyScore.py @@ -0,0 +1,98 @@ +"""ProficiencyScore module for OpenProficiency library.""" + +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._set_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.""" + self._set_score(value) + + # Properties - Score + @property + def score_name(self) -> ProficiencyScoreName: + """Get the proficiency name as an enum value.""" + return self._get_name_from_score(self._score) + + @score_name.setter + def score_name(self, value: ProficiencyScoreName) -> None: + """Set the proficiency name using a ProficiencyScoreName enum.""" + if not isinstance(value, ProficiencyScoreName): + raise ValueError( + f"Name must be a ProficiencyScoreName enum, got {type(value)}") + self._score = value.value + + # Methods + def _set_score(self, value: Union[float, ProficiencyScoreName]) -> None: + """Internal method to set score from numeric or enum value.""" + if isinstance(value, ProficiencyScoreName): + self._score = value.value + + elif isinstance(value, (int, float)): + # Validate score is between 0.0 and 1.0 + 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) + + else: + raise ValueError( + f"Score must be numeric or ProficiencyScoreName enum. Got type: '{type(value)}'") + + def _get_name_from_score(self, score: float) -> ProficiencyScoreName: + """Internal method to determine proficiency name from numeric score.""" + if score <= 0.0: + return ProficiencyScoreName.UNAWARE + elif score <= 0.1: + return ProficiencyScoreName.AWARE + elif score <= 0.5: + return ProficiencyScoreName.FAMILIAR + elif score <= 0.8: + return ProficiencyScoreName.PROFICIENT + elif score <= 1.0: + return ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE + else: + raise ValueError(f"Invalid score value: {score}") + + def to_json(self) -> dict: + """Convert to a JSON-serializable dictionary.""" + return { + "topic_id": self.topic_id, + "score": self._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/__init__.py b/openproficiency/__init__.py index 62a0752..15e9266 100644 --- a/openproficiency/__init__.py +++ b/openproficiency/__init__.py @@ -1,4 +1,5 @@ """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 diff --git a/tests/ProficiencyScore_test.py b/tests/ProficiencyScore_test.py new file mode 100644 index 0000000..498acce --- /dev/null +++ b/tests/ProficiencyScore_test.py @@ -0,0 +1,218 @@ +"""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 & Assert + try: + ProficiencyScore(topic_id="test", score=-0.1) + assert False, "Should have raised ValueError" + except ValueError as e: + assert "between 0.0 and 1.0" in str(e) + + def test_init_invalid_score_too_high(self): + """Test that score above 1.0 raises ValueError.""" + + # Act & Assert + try: + ProficiencyScore(topic_id="test", score=1.1) + assert False, "Should have raised ValueError" + except ValueError as e: + assert "between 0.0 and 1.0" in str(e) + + def test_init_invalid_score_type(self): + """Test that invalid score type raises ValueError.""" + + # Act & Assert + try: + ProficiencyScore(topic_id="test", score="invalid") + assert False, "Should have raised ValueError" + except ValueError as e: + assert "numeric or ProficiencyScoreName" in str(e) + + # Properties - Score + def test_score_getter(self): + """Test getting score property.""" + + # Arrange + ps = ProficiencyScore(topic_id="test", score=0.5) + + # Act & Assert + assert ps.score == 0.5 + + def test_score_setter_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 + + def test_score_setter_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 + + def test_score_setter_invalid(self): + """Test setting invalid score raises ValueError.""" + + # Arrange + ps = ProficiencyScore(topic_id="test", score=0.1) + + # Act & Assert + try: + ps.score = 1.5 + assert False, "Should have raised ValueError" + except ValueError: + pass + + # Properties - Score Name + def test_score_name_unaware(self): + """Test score_name property for UNAWARE level.""" + + # Arrange + ps = ProficiencyScore(topic_id="test", score=0.0) + + # Act & Assert + assert ps.score_name == ProficiencyScoreName.UNAWARE + + def test_score_name_aware(self): + """Test score_name property for AWARE level.""" + + # Arrange + ps = ProficiencyScore(topic_id="test", score=0.05) + + # Act & Assert + assert ps.score_name == ProficiencyScoreName.AWARE + + def test_score_name_familiar(self): + """Test score_name property for FAMILIAR level.""" + + # Arrange + ps = ProficiencyScore(topic_id="test", score=0.3) + + # Act & Assert + assert ps.score_name == 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 & Assert + assert ps.score_name == 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 & Assert + assert ps.score_name == ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE + + def test_score_name_setter(self): + """Test setting score_name property.""" + + # Arrange + ps = ProficiencyScore(topic_id="test", score=0.1) + + # Act + ps.score_name = ProficiencyScoreName.PROFICIENT + + # Assert + assert ps.score == 0.8 + + def test_score_name_setter_invalid_type(self): + """Test setting score_name with invalid type raises ValueError.""" + + # Arrange + ps = ProficiencyScore(topic_id="test", score=0.1) + + # Act & Assert + try: + ps.score_name = 0.5 + assert False, "Should have raised ValueError" + except ValueError as e: + assert "ProficiencyScoreName enum" in str(e) + + # Methods + + def test_to_json(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_json() + + # Assert + assert json_dict == { + "topic_id": topic_id, + "score": score + } + + # 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 From 20f64e4bf78b62faced53b8ad2bba29197685aba Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Sun, 1 Feb 2026 23:07:53 +0000 Subject: [PATCH 3/9] feat: add class TranscriptEntry --- openproficiency/TranscriptEntry.py | 54 +++++++++++++ openproficiency/__init__.py | 1 + tests/TranscriptEntry_test.py | 119 +++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 openproficiency/TranscriptEntry.py create mode 100644 tests/TranscriptEntry_test.py diff --git a/openproficiency/TranscriptEntry.py b/openproficiency/TranscriptEntry.py new file mode 100644 index 0000000..7e85451 --- /dev/null +++ b/openproficiency/TranscriptEntry.py @@ -0,0 +1,54 @@ +"""TranscriptEntry module for OpenProficiency library.""" + +from datetime import datetime +from typing import Optional +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_json(self) -> dict: + """Convert Topic 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() + } + + # Debugging + def __repr__(self) -> str: + """String representation of TranscriptEntry.""" + return f"TranscriptEntry(user_id='{self.user_id}', topic_id='{self._proficiency_score.topic_id}', score={self._proficiency_score.score}, issuer='{self.issuer}')" diff --git a/openproficiency/__init__.py b/openproficiency/__init__.py index 15e9266..8a52fbd 100644 --- a/openproficiency/__init__.py +++ b/openproficiency/__init__.py @@ -3,3 +3,4 @@ from .Topic import Topic from .TopicList import TopicList from .ProficiencyScore import ProficiencyScore, ProficiencyScoreName +from .TranscriptEntry import TranscriptEntry diff --git a/tests/TranscriptEntry_test.py b/tests/TranscriptEntry_test.py new file mode 100644 index 0000000..376ef16 --- /dev/null +++ b/tests/TranscriptEntry_test.py @@ -0,0 +1,119 @@ +"""Tests for the TranscriptEntry class.""" + +from datetime import datetime +from openproficiency import TranscriptEntry, ProficiencyScore, ProficiencyScoreName + + +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 + + # 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 From 0623ff02f53cb4d5c3e42647b210507cf8096982 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Sun, 1 Feb 2026 23:24:19 +0000 Subject: [PATCH 4/9] docs: add example usage of ProfiencyScore and TranscriptEntry --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) 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. From 2d459b95855bd9a356c7fddfb0457a650ef19bd5 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Sun, 1 Feb 2026 23:27:15 +0000 Subject: [PATCH 5/9] chore: set version to 0.0.2 --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a546ebe..236057a 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.2" 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..6fa0f3f 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="openproficiency", - version="0.1.0", + version="0.0.2", author="OpenProficiency Contributors", description="A library for managing proficiency topics and topic lists", long_description=long_description, From 7f5feb0bea5061e8d99dc52d76aa6f40a69e0006 Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Mon, 2 Feb 2026 02:53:57 +0000 Subject: [PATCH 6/9] fix: inconsistent use of to_dict and to_json --- openproficiency/ProficiencyScore.py | 7 +++- openproficiency/Topic.py | 7 +++- openproficiency/TopicList.py | 10 +++-- openproficiency/TranscriptEntry.py | 7 +++- tests/ProficiencyScore_test.py | 20 +++++++-- tests/TopicList_test.py | 64 ++++++++++++++++++++++++----- tests/Topic_test.py | 27 +++++++++++- tests/TranscriptEntry_test.py | 48 ++++++++++++++++++++++ 8 files changed, 169 insertions(+), 21 deletions(-) diff --git a/openproficiency/ProficiencyScore.py b/openproficiency/ProficiencyScore.py index c8515b7..16112ae 100644 --- a/openproficiency/ProficiencyScore.py +++ b/openproficiency/ProficiencyScore.py @@ -1,5 +1,6 @@ """ProficiencyScore module for OpenProficiency library.""" +import json from enum import Enum from typing import Union @@ -84,13 +85,17 @@ def _get_name_from_score(self, score: float) -> ProficiencyScoreName: else: raise ValueError(f"Invalid score value: {score}") - def to_json(self) -> dict: + def to_dict(self) -> dict: """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()) + # Debugging def __repr__(self) -> str: diff --git a/openproficiency/Topic.py b/openproficiency/Topic.py index 41ae18f..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,7 +78,7 @@ def add_pretopics(self, pretopics: List[Union[str, "Topic"]]) -> None: for pretopic in pretopics: self.add_pretopic(pretopic) - def to_json(self) -> dict: + def to_dict(self) -> dict: """Convert Topic to JSON-serializable dictionary.""" return { "id": self.id, @@ -86,6 +87,10 @@ def to_json(self) -> dict: "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 index 7e85451..97d003a 100644 --- a/openproficiency/TranscriptEntry.py +++ b/openproficiency/TranscriptEntry.py @@ -1,5 +1,6 @@ """TranscriptEntry module for OpenProficiency library.""" +import json from datetime import datetime from typing import Optional from .ProficiencyScore import ProficiencyScore @@ -38,7 +39,7 @@ def proficiency_score(self) -> ProficiencyScore: return self._proficiency_score # Methods - def to_json(self) -> dict: + def to_dict(self) -> dict: """Convert Topic to JSON-serializable dictionary.""" return { "user_id": self.user_id, @@ -48,6 +49,10 @@ def to_json(self) -> dict: "timestamp": self.timestamp.isoformat() } + def to_json(self) -> str: + """Convert Topic to JSON string.""" + return json.dumps(self.to_dict()) + # Debugging def __repr__(self) -> str: """String representation of TranscriptEntry.""" diff --git a/tests/ProficiencyScore_test.py b/tests/ProficiencyScore_test.py index 498acce..88a2314 100644 --- a/tests/ProficiencyScore_test.py +++ b/tests/ProficiencyScore_test.py @@ -183,8 +183,7 @@ def test_score_name_setter_invalid_type(self): assert "ProficiencyScoreName enum" in str(e) # Methods - - def test_to_json(self): + def test_to_dict(self): """Test conversion to JSON-serializable dictionary.""" # Arrange @@ -193,7 +192,7 @@ def test_to_json(self): ps = ProficiencyScore(topic_id=topic_id, score=score) # Act - json_dict = ps.to_json() + json_dict = ps.to_dict() # Assert assert json_dict == { @@ -201,6 +200,21 @@ def test_to_json(self): "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.""" 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 8a102f2..a073ec1 100644 --- a/tests/Topic_test.py +++ b/tests/Topic_test.py @@ -1,3 +1,4 @@ +import json from openproficiency import Topic @@ -141,7 +142,7 @@ def test_add_pretopics_mixed(self): assert pretopic1 in topic.pretopics assert pretopic2.id in topic.pretopics - def test_to_json(self): + def test_to_dict(self): """Test converting a Topic to JSON.""" # Arrange @@ -153,7 +154,29 @@ def test_to_json(self): ) # Act - topic_json = topic.to_json() + 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" diff --git a/tests/TranscriptEntry_test.py b/tests/TranscriptEntry_test.py index 376ef16..dd51e6d 100644 --- a/tests/TranscriptEntry_test.py +++ b/tests/TranscriptEntry_test.py @@ -96,6 +96,54 @@ def test_proficiency_score(self): 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 + # Debugging def test_repr(self): """Test string representation of TranscriptEntry.""" From ad1186b3bfe5acbafd7cd34be56fccdba55bc3ae Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Mon, 2 Feb 2026 02:54:31 +0000 Subject: [PATCH 7/9] chore: set version to 0.0.3 --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 236057a..8c80190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openproficiency" -version = "0.0.2" +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 6fa0f3f..559432d 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="openproficiency", - version="0.0.2", + version="0.0.3", author="OpenProficiency Contributors", description="A library for managing proficiency topics and topic lists", long_description=long_description, From 0fcecf2becd2a173b30a657b3d370fb35cd7382a Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Sun, 15 Feb 2026 23:10:09 +0000 Subject: [PATCH 8/9] feat: add from_dict, to_json, and general cleanup --- openproficiency/TranscriptEntry.py | 37 +++++++++++++++++++++++------- tests/TranscriptEntry_test.py | 22 +++++++++--------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/openproficiency/TranscriptEntry.py b/openproficiency/TranscriptEntry.py index 97d003a..f1cf9af 100644 --- a/openproficiency/TranscriptEntry.py +++ b/openproficiency/TranscriptEntry.py @@ -2,7 +2,7 @@ import json from datetime import datetime -from typing import Optional +from typing import Optional, Union from .ProficiencyScore import ProficiencyScore @@ -17,7 +17,6 @@ def __init__( topic_id: str, score: float, issuer: str, - # Optional timestamp: Optional[datetime] = None, ): @@ -25,7 +24,7 @@ def __init__( self.user_id = user_id self._proficiency_score = ProficiencyScore( topic_id=topic_id, - score=score + score=score, ) self.issuer = issuer @@ -39,21 +38,43 @@ def proficiency_score(self) -> ProficiencyScore: return self._proficiency_score # Methods - def to_dict(self) -> dict: - """Convert Topic to JSON-serializable dictionary.""" + 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() + "timestamp": self.timestamp.isoformat(), } def to_json(self) -> str: - """Convert Topic to JSON string.""" + """Convert TranscriptEntry to JSON string.""" return json.dumps(self.to_dict()) + # Methods - Class + @classmethod + def from_dict(cls, data: dict[str, Union[str, int, float]]) -> "TranscriptEntry": + """Create a TranscriptEntry from a dictionary.""" + return cls( + user_id=data["user_id"], + topic_id=data["topic_id"], + score=data["score"], + issuer=data["issuer"], + timestamp=datetime.fromisoformat(data["timestamp"]), + ) + + @classmethod + def from_json(cls, json_str: str) -> "TranscriptEntry": + """Create a TranscriptEntry from a JSON string.""" + return cls.from_dict(json.loads(json_str)) + # Debugging def __repr__(self) -> str: """String representation of TranscriptEntry.""" - return f"TranscriptEntry(user_id='{self.user_id}', topic_id='{self._proficiency_score.topic_id}', score={self._proficiency_score.score}, issuer='{self.issuer}')" + 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/tests/TranscriptEntry_test.py b/tests/TranscriptEntry_test.py index dd51e6d..ac318e8 100644 --- a/tests/TranscriptEntry_test.py +++ b/tests/TranscriptEntry_test.py @@ -1,7 +1,7 @@ """Tests for the TranscriptEntry class.""" from datetime import datetime -from openproficiency import TranscriptEntry, ProficiencyScore, ProficiencyScoreName +from openproficiency import TranscriptEntry class TestTranscriptEntry: @@ -21,13 +21,13 @@ def test_init_required_params(self): user_id=user_id, topic_id=topic_id, score=score, - issuer=issuer + 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.proficiency_score.topic_id == topic_id + assert entry.proficiency_score.score == score assert entry.issuer == issuer assert entry.timestamp is not None @@ -47,7 +47,7 @@ def test_init_with_optional_params(self): topic_id=topic_id, score=score, issuer=issuer, - timestamp=timestamp + timestamp=timestamp, ) # Assert @@ -68,7 +68,7 @@ def test_init_default_timestamp(self): user_id=user_id, topic_id=topic_id, score=score, - issuer=issuer + issuer=issuer, ) after = datetime.now() @@ -85,7 +85,7 @@ def test_proficiency_score(self): user_id="user-123", topic_id="git-commit", score=0.8, - issuer="github-learn" + issuer="github-learn", ) # Act @@ -106,7 +106,7 @@ def test_to_dict(self): topic_id="git-commit", score=0.8, issuer="github-learn", - timestamp=datetime(2024, 1, 15, 10, 30, 0) + timestamp=datetime(2024, 1, 15, 10, 30, 0), ) # Act @@ -118,7 +118,7 @@ def test_to_dict(self): "topic_id": "git-commit", "score": 0.8, "issuer": "github-learn", - "timestamp": "2024-01-15T10:30:00" + "timestamp": "2024-01-15T10:30:00", } def test_to_json(self): @@ -130,7 +130,7 @@ def test_to_json(self): topic_id="git-commit", score=0.8, issuer="github-learn", - timestamp=datetime(2024, 1, 15, 10, 30, 0) + timestamp=datetime(2024, 1, 15, 10, 30, 0), ) # Act @@ -153,7 +153,7 @@ def test_repr(self): user_id="user-123", topic_id="git-commit", score=0.8, - issuer="github-learn" + issuer="github-learn", ) # Act From 6dc047c20093675c1eaf9f4f039a238b14da93fc Mon Sep 17 00:00:00 2001 From: "Christopher W. Blake" Date: Mon, 16 Feb 2026 00:30:55 +0000 Subject: [PATCH 9/9] chore: cleanup --- openproficiency/ProficiencyScore.py | 86 ++++----- openproficiency/TranscriptEntry.py | 14 +- tests/ProficiencyScore_test.py | 275 ++++++++++++++++++++-------- tests/TranscriptEntry_test.py | 47 +++++ 4 files changed, 294 insertions(+), 128 deletions(-) diff --git a/openproficiency/ProficiencyScore.py b/openproficiency/ProficiencyScore.py index 16112ae..d322104 100644 --- a/openproficiency/ProficiencyScore.py +++ b/openproficiency/ProficiencyScore.py @@ -7,6 +7,7 @@ class ProficiencyScoreName(Enum): """Enum for proficiency score names.""" + UNAWARE = 0.0 AWARE = 0.1 FAMILIAR = 0.5 @@ -22,11 +23,11 @@ def __init__( self, # Required topic_id: str, - score: Union[float, ProficiencyScoreName] + score: Union[float, ProficiencyScoreName], ): # Required self.topic_id = topic_id - self._set_score(score) + self.score = score # Properties - Score @property @@ -37,67 +38,60 @@ def score(self) -> float: @score.setter def score(self, value: Union[float, ProficiencyScoreName]) -> None: """Set the score numerically or using a ProficiencyScoreName enum.""" - self._set_score(value) + # 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 self._get_name_from_score(self._score) - - @score_name.setter - def score_name(self, value: ProficiencyScoreName) -> None: - """Set the proficiency name using a ProficiencyScoreName enum.""" - if not isinstance(value, ProficiencyScoreName): - raise ValueError( - f"Name must be a ProficiencyScoreName enum, got {type(value)}") - self._score = value.value + return ProficiencyScore.get_score_name(self._score) # Methods - def _set_score(self, value: Union[float, ProficiencyScoreName]) -> None: - """Internal method to set score from numeric or enum value.""" - if isinstance(value, ProficiencyScoreName): - self._score = value.value - - elif isinstance(value, (int, float)): - # Validate score is between 0.0 and 1.0 - 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) - - else: - raise ValueError( - f"Score must be numeric or ProficiencyScoreName enum. Got type: '{type(value)}'") - - def _get_name_from_score(self, score: float) -> ProficiencyScoreName: - """Internal method to determine proficiency name from numeric score.""" - if score <= 0.0: - return ProficiencyScoreName.UNAWARE - elif score <= 0.1: - return ProficiencyScoreName.AWARE - elif score <= 0.5: - return ProficiencyScoreName.FAMILIAR - elif score <= 0.8: - return ProficiencyScoreName.PROFICIENT - elif score <= 1.0: - return ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE - else: - raise ValueError(f"Invalid score value: {score}") - - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, float]: """Convert to a JSON-serializable dictionary.""" return { "topic_id": self.topic_id, - "score": self._score + "score": self._score, } def to_json(self) -> str: """Convert to a JSON string.""" return json.dumps(self.to_dict()) - # Debugging + # 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/TranscriptEntry.py b/openproficiency/TranscriptEntry.py index f1cf9af..95c5564 100644 --- a/openproficiency/TranscriptEntry.py +++ b/openproficiency/TranscriptEntry.py @@ -52,11 +52,11 @@ def to_json(self) -> str: """Convert TranscriptEntry to JSON string.""" return json.dumps(self.to_dict()) - # Methods - Class - @classmethod - def from_dict(cls, data: dict[str, Union[str, int, float]]) -> "TranscriptEntry": + # Methods - Static + @staticmethod + def from_dict(data: dict[str, Union[str, int, float]]) -> "TranscriptEntry": """Create a TranscriptEntry from a dictionary.""" - return cls( + return TranscriptEntry( user_id=data["user_id"], topic_id=data["topic_id"], score=data["score"], @@ -64,10 +64,10 @@ def from_dict(cls, data: dict[str, Union[str, int, float]]) -> "TranscriptEntry" timestamp=datetime.fromisoformat(data["timestamp"]), ) - @classmethod - def from_json(cls, json_str: str) -> "TranscriptEntry": + @staticmethod + def from_json(json_str: str) -> "TranscriptEntry": """Create a TranscriptEntry from a JSON string.""" - return cls.from_dict(json.loads(json_str)) + return TranscriptEntry.from_dict(json.loads(json_str)) # Debugging def __repr__(self) -> str: diff --git a/tests/ProficiencyScore_test.py b/tests/ProficiencyScore_test.py index 88a2314..46b49de 100644 --- a/tests/ProficiencyScore_test.py +++ b/tests/ProficiencyScore_test.py @@ -14,7 +14,10 @@ def test_init_with_numeric_score(self): score = 0.8 # Act - ps = ProficiencyScore(topic_id=topic_id, score=score) + ps = ProficiencyScore( + topic_id=topic_id, + score=score, + ) # Assert assert ps.topic_id == topic_id @@ -28,7 +31,10 @@ def test_init_with_enum_score(self): score_name = ProficiencyScoreName.PROFICIENT # Act - ps = ProficiencyScore(topic_id=topic_id, score=score_name) + ps = ProficiencyScore( + topic_id=topic_id, + score=score_name, + ) # Assert assert ps.topic_id == topic_id @@ -37,150 +43,260 @@ def test_init_with_enum_score(self): def test_init_invalid_score_too_low(self): """Test that score below 0.0 raises ValueError.""" - # Act & Assert + # Act + result = None try: - ProficiencyScore(topic_id="test", score=-0.1) - assert False, "Should have raised ValueError" - except ValueError as e: - assert "between 0.0 and 1.0" in str(e) + 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 & Assert + # Act + result = None try: - ProficiencyScore(topic_id="test", score=1.1) - assert False, "Should have raised ValueError" - except ValueError as e: - assert "between 0.0 and 1.0" in str(e) + 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 & Assert + # Act + result = None try: - ProficiencyScore(topic_id="test", score="invalid") - assert False, "Should have raised ValueError" - except ValueError as e: - assert "numeric or ProficiencyScoreName" in str(e) - - # Properties - Score - def test_score_getter(self): - """Test getting score property.""" - - # Arrange - ps = ProficiencyScore(topic_id="test", score=0.5) + ProficiencyScore( + topic_id="test", + score="invalid", + ) + except Exception as e: + result = e - # Act & Assert - assert ps.score == 0.5 + # Assert + assert isinstance(result, ValueError) + assert "ProficiencyScoreName" in str(result) - def test_score_setter_numeric(self): + # Properties + def test_score_numeric(self): """Test setting score with numeric value.""" # Arrange - ps = ProficiencyScore(topic_id="test", score=0.1) + 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_setter_enum(self): - """Test setting score with enum value.""" + def test_score_numeric_invalid(self): + """Test setting invalid numeric score raises ValueError.""" # Arrange - ps = ProficiencyScore(topic_id="test", score=0.1) + ps = ProficiencyScore( + topic_id="test", + score=0.1, + ) # Act - ps.score = ProficiencyScoreName.FAMILIAR + result = None + try: + ps.score = 1.5 + except Exception as e: + result = e # Assert - assert ps.score == 0.5 + # 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_setter_invalid(self): - """Test setting invalid score raises ValueError.""" + def test_score_enum(self): + """Test setting score with enum value.""" # Arrange - ps = ProficiencyScore(topic_id="test", score=0.1) + ps = ProficiencyScore( + topic_id="test", + score=0.1, + ) - # Act & Assert - try: - ps.score = 1.5 - assert False, "Should have raised ValueError" - except ValueError: - pass + # Act + ps.score = ProficiencyScoreName.FAMILIAR + + # Assert + assert ps.score == 0.5 + assert ps.score_name == ProficiencyScoreName.FAMILIAR - # Properties - Score Name def test_score_name_unaware(self): """Test score_name property for UNAWARE level.""" # Arrange - ps = ProficiencyScore(topic_id="test", score=0.0) + ps = ProficiencyScore( + topic_id="test", + score=0.0, + ) - # Act & Assert - assert ps.score_name == ProficiencyScoreName.UNAWARE + # 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.05) + ps = ProficiencyScore( + topic_id="test", + score=0.1, + ) - # Act & Assert - assert ps.score_name == ProficiencyScoreName.AWARE + # 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.3) + ps = ProficiencyScore( + topic_id="test", + score=0.5, + ) - # Act & Assert - assert ps.score_name == ProficiencyScoreName.FAMILIAR + # 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) + ps = ProficiencyScore( + topic_id="test", + score=0.8, + ) - # Act & Assert - assert ps.score_name == ProficiencyScoreName.PROFICIENT + # 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) + ps = ProficiencyScore( + topic_id="test", + score=1.0, + ) + + # Act + result = ps.score_name - # Act & Assert - assert ps.score_name == ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE + # Assert + assert result == ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE - def test_score_name_setter(self): - """Test setting score_name property.""" + # Static Methods + def test_get_score_name_unaware(self): + """Test get_score_name returns UNAWARE for score 0.0.""" # Arrange - ps = ProficiencyScore(topic_id="test", score=0.1) + score = 0.0 # Act - ps.score_name = ProficiencyScoreName.PROFICIENT + result = ProficiencyScore.get_score_name(score) # Assert - assert ps.score == 0.8 + assert result == ProficiencyScoreName.UNAWARE - def test_score_name_setter_invalid_type(self): - """Test setting score_name with invalid type raises ValueError.""" + def test_get_score_name_aware(self): + """Test get_score_name returns AWARE for score 0.1.""" # Arrange - ps = ProficiencyScore(topic_id="test", score=0.1) + 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.""" - # Act & Assert + # 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: - ps.score_name = 0.5 - assert False, "Should have raised ValueError" - except ValueError as e: - assert "ProficiencyScoreName enum" in str(e) + 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): @@ -189,7 +305,10 @@ def test_to_dict(self): # Arrange topic_id = "git-commit" score = 0.8 - ps = ProficiencyScore(topic_id=topic_id, score=score) + ps = ProficiencyScore( + topic_id=topic_id, + score=score, + ) # Act json_dict = ps.to_dict() @@ -197,7 +316,7 @@ def test_to_dict(self): # Assert assert json_dict == { "topic_id": topic_id, - "score": score + "score": score, } def test_to_json(self): @@ -206,7 +325,10 @@ def test_to_json(self): # Arrange topic_id = "git-commit" score = 0.8 - ps = ProficiencyScore(topic_id=topic_id, score=score) + ps = ProficiencyScore( + topic_id=topic_id, + score=score, + ) # Act json_str = ps.to_json() @@ -220,7 +342,10 @@ def test_repr(self): """Test string representation of ProficiencyScore.""" # Arrange - ps = ProficiencyScore(topic_id="git-commit", score=0.8) + ps = ProficiencyScore( + topic_id="git-commit", + score=0.8, + ) # Act repr_str = repr(ps) diff --git a/tests/TranscriptEntry_test.py b/tests/TranscriptEntry_test.py index ac318e8..5a13954 100644 --- a/tests/TranscriptEntry_test.py +++ b/tests/TranscriptEntry_test.py @@ -144,6 +144,53 @@ def test_to_json(self): ) 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."""