diff --git a/openproficiency/TopicList.py b/openproficiency/TopicList.py index aca92b4..c8d427e 100644 --- a/openproficiency/TopicList.py +++ b/openproficiency/TopicList.py @@ -113,8 +113,11 @@ def timestamp(self, value: Union[str, datetime, None]) -> None: @property def full_name(self) -> str: - """Get the full name of the TopicList in 'owner/name' format.""" - return f"{self.owner}/{self.name}" + """Get the full name of the TopicList in 'owner/name@version' format.""" + full_name = f"{self.owner}/{self.name}" + if self.version: + full_name += f"@{self.version}" + return full_name # Debugging def __repr__(self) -> str: diff --git a/openproficiency/TranscriptEntry.py b/openproficiency/TranscriptEntry.py index 95c5564..17cae9a 100644 --- a/openproficiency/TranscriptEntry.py +++ b/openproficiency/TranscriptEntry.py @@ -2,7 +2,7 @@ import json from datetime import datetime -from typing import Optional, Union +from typing import Any, Dict, Optional, Union from .ProficiencyScore import ProficiencyScore @@ -15,10 +15,12 @@ def __init__( # Required user_id: str, topic_id: str, + topic_list: str, score: float, + timestamp: Union[datetime, str], issuer: str, # Optional - timestamp: Optional[datetime] = None, + certificate: Optional[str] = None, ): # Required self.user_id = user_id @@ -26,10 +28,11 @@ def __init__( topic_id=topic_id, score=score, ) + self.topic_list = topic_list + self.timestamp = timestamp self.issuer = issuer - # Optional - self.timestamp = timestamp or datetime.now() + self.certificate = certificate # Properties @property @@ -37,37 +40,95 @@ def proficiency_score(self) -> ProficiencyScore: """Get the topic ID from the proficiency score.""" return self._proficiency_score + @property + def topic_list(self) -> str: + """Get the topic list reference.""" + return self._topic_list + + @topic_list.setter + def topic_list(self, value: str) -> None: + """Set the topic list reference from TopicList instance""" + self._topic_list = value + + @property + def certificate(self) -> Optional[str]: + """Get the certificate text.""" + return self._certificate + + @certificate.setter + def certificate(self, value: Optional[str]) -> None: + """Set the certificate text.""" + self._certificate = value + + @property + def timestamp(self) -> datetime: + """Get the time this entry was created""" + return self._timestamp + + @timestamp.setter + def timestamp(self, value: Union[datetime, str]) -> None: + """Set the time this entry was created""" + # Directly store datetime object. + if isinstance(value, datetime): + self._timestamp = value + + # Convert ISO 8601 string to datetime object. + elif isinstance(value, str): + self._timestamp = datetime.fromisoformat(value) + + else: + raise ValueError("Timestamp must be a datetime object or ISO 8601 string") + # Methods - def to_dict(self) -> dict[str, Union[str, int, float]]: + def to_dict(self) -> Dict[str, Union[str, float]]: """Convert TranscriptEntry to JSON-serializable dictionary.""" - return { + data = { "user_id": self.user_id, - "topic_id": self._proficiency_score.topic_id, + "topic": self._proficiency_score.topic_id, + "topic_list": self.topic_list, "score": self._proficiency_score.score, - "issuer": self.issuer, "timestamp": self.timestamp.isoformat(), + "issuer": self.issuer, } + # Attach certificate if it exists + if self.certificate is not None: + data["certificate"] = self.certificate + + return data + def to_json(self) -> str: """Convert TranscriptEntry to JSON string.""" - return json.dumps(self.to_dict()) + data = self.to_dict() + # Change keys to camelCase for JSON output + data["userID"] = data.pop("user_id") + data["topicList"] = data.pop("topic_list") + return json.dumps(data) # Methods - Static @staticmethod - def from_dict(data: dict[str, Union[str, int, float]]) -> "TranscriptEntry": + def from_dict(data: Dict[str, Any]) -> "TranscriptEntry": """Create a TranscriptEntry from a dictionary.""" return TranscriptEntry( + # Required user_id=data["user_id"], - topic_id=data["topic_id"], + topic_id=data["topic"], + topic_list=data["topic_list"], score=data["score"], + timestamp=data["timestamp"], issuer=data["issuer"], - timestamp=datetime.fromisoformat(data["timestamp"]), + # Optional + certificate=data.get("certificate"), ) @staticmethod def from_json(json_str: str) -> "TranscriptEntry": """Create a TranscriptEntry from a JSON string.""" - return TranscriptEntry.from_dict(json.loads(json_str)) + # Load JSON string. Map camelCase to snake_case if needed + data = json.loads(json_str) + data["user_id"] = data.pop("userID") + data["topic_list"] = data.pop("topicList") + return TranscriptEntry.from_dict(data) # Debugging def __repr__(self) -> str: @@ -75,6 +136,7 @@ def __repr__(self) -> str: return ( f"TranscriptEntry(user_id='{self.user_id}', " f"topic_id='{self._proficiency_score.topic_id}', " + f"topic_list='{self.topic_list}', " f"score={self._proficiency_score.score}, " f"issuer='{self.issuer}'" ) diff --git a/tests/TopicList_test.py b/tests/TopicList_test.py index 0772c5a..c696391 100644 --- a/tests/TopicList_test.py +++ b/tests/TopicList_test.py @@ -220,6 +220,24 @@ def test_full_name(self): # Assert assert full_name == "example.com/example-topic-list" + def test_full_name_with_version(self): + """Test getting the full name of the topic list, including version.""" + + # Arrange + owner = "example.com" + name = "example-topic-list" + topic_list = TopicList( + owner=owner, + name=name, + version="1.2.3", + ) + + # Act + full_name = topic_list.full_name + + # Assert + assert full_name == "example.com/example-topic-list@1.2.3" + def test_version_setter_valid_format(self): """Test setting version with valid semantic versioning.""" diff --git a/tests/TranscriptEntry_test.py b/tests/TranscriptEntry_test.py index 5a13954..3196ced 100644 --- a/tests/TranscriptEntry_test.py +++ b/tests/TranscriptEntry_test.py @@ -1,7 +1,8 @@ """Tests for the TranscriptEntry class.""" +import json from datetime import datetime -from openproficiency import TranscriptEntry +from openproficiency import TranscriptEntry, TopicList class TestTranscriptEntry: @@ -13,23 +14,29 @@ def test_init_required_params(self): # Arrange user_id = "user-123" topic_id = "git-commit" + topic_list = "example.com/math@0.0.1" 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, + topic_list=topic_list, score=score, issuer=issuer, + timestamp=timestamp, ) # Assert assert entry.user_id == user_id assert entry.proficiency_score.topic_id == topic_id assert entry.proficiency_score.score == score + assert entry.topic_list == topic_list + assert entry.timestamp == timestamp assert entry.issuer == issuer - assert entry.timestamp is not None + assert entry.certificate is None def test_init_with_optional_params(self): """Create a transcript entry with optional parameters.""" @@ -39,42 +46,24 @@ def test_init_with_optional_params(self): topic_id = "git-commit" score = 0.8 issuer = "github-learn" + topic_list = "example.com/math@0.0.1" timestamp = datetime(2024, 1, 15, 10, 30, 0) + certificate = "test-certificate" # Act entry = TranscriptEntry( user_id=user_id, topic_id=topic_id, + topic_list=topic_list, score=score, issuer=issuer, + certificate=certificate, 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 + assert entry.certificate == certificate # Properties def test_proficiency_score(self): @@ -84,8 +73,10 @@ def test_proficiency_score(self): entry = TranscriptEntry( user_id="user-123", topic_id="git-commit", + topic_list="example.com/math@0.0.1", score=0.8, issuer="github-learn", + timestamp=datetime(2024, 1, 15, 10, 30, 0), ) # Act @@ -104,8 +95,10 @@ def test_to_dict(self): entry = TranscriptEntry( user_id="user-123", topic_id="git-commit", + topic_list="example.com/math@0.0.1", score=0.8, issuer="github-learn", + certificate="cert-data", timestamp=datetime(2024, 1, 15, 10, 30, 0), ) @@ -115,10 +108,12 @@ def test_to_dict(self): # Assert assert entry_dict == { "user_id": "user-123", - "topic_id": "git-commit", + "topic": "git-commit", + "topic_list": "example.com/math@0.0.1", "score": 0.8, - "issuer": "github-learn", "timestamp": "2024-01-15T10:30:00", + "issuer": "github-learn", + "certificate": "cert-data", } def test_to_json(self): @@ -128,8 +123,10 @@ def test_to_json(self): entry = TranscriptEntry( user_id="user-123", topic_id="git-commit", + topic_list="example.com/math@0.0.1", score=0.8, issuer="github-learn", + certificate="cert-data", timestamp=datetime(2024, 1, 15, 10, 30, 0), ) @@ -137,12 +134,14 @@ def test_to_json(self): 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 + data = json.loads(json_str) + assert data["userID"] == "user-123" + assert data["topic"] == "git-commit" + assert data["topicList"] == "example.com/math@0.0.1" + assert data["score"] == 0.8 + assert data["timestamp"] == "2024-01-15T10:30:00" + assert data["issuer"] == "github-learn" + assert data["certificate"] == "cert-data" # Methods - Static def test_from_dict(self): @@ -151,10 +150,12 @@ def test_from_dict(self): # Arrange data = { "user_id": "user-456", - "topic_id": "git-branch", + "topic": "git-branch", + "topic_list": "example.com/math@0.0.1", "score": 0.6, - "issuer": "test-issuer", "timestamp": "2024-02-20T15:45:30", + "issuer": "test-issuer", + "certificate": "cert-abc", } # Act @@ -164,20 +165,25 @@ def test_from_dict(self): assert entry.user_id == "user-456" assert entry.proficiency_score.topic_id == "git-branch" assert entry.proficiency_score.score == 0.6 + assert entry.topic_list == "example.com/math@0.0.1" + assert entry.timestamp.isoformat() == "2024-02-20T15:45:30" assert entry.issuer == "test-issuer" - assert entry.timestamp.year == 2024 - assert entry.timestamp.month == 2 - assert entry.timestamp.day == 20 + assert entry.certificate == "cert-abc" 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"}' + entry_orig = TranscriptEntry( + user_id="user-789", + topic_id="git-merge", + topic_list="example.com/math@0.0.1", + score=0.9, + timestamp=datetime(2024, 3, 10, 8, 20, 15), + issuer="test-system", + certificate="cert-xyz", ) + json_str = entry_orig.to_json() # Act entry = TranscriptEntry.from_json(json_str) @@ -187,9 +193,36 @@ def test_from_json(self): 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 + assert entry.timestamp.isoformat() == "2024-03-10T08:20:15" + assert entry.topic_list == "example.com/math@0.0.1" + assert entry.certificate == "cert-xyz" + + def test_json_round_trip_preserves_fields(self): + """Round-trip JSON preserves topic list fields and certificate.""" + + # Arrange + entry = TranscriptEntry( + user_id="user-123", + topic_id="git-commit", + topic_list="example.com/math@0.0.1", + score=0.8, + timestamp=datetime(2024, 1, 15, 10, 30, 0), + issuer="github-learn", + certificate="cert-data", + ) + + # Act + json_str = entry.to_json() + round_trip = TranscriptEntry.from_json(json_str) + + # Assert + assert round_trip.user_id == "user-123" + assert round_trip.proficiency_score.topic_id == "git-commit" + assert round_trip.topic_list == "example.com/math@0.0.1" + assert round_trip.proficiency_score.score == 0.8 + assert round_trip.timestamp.isoformat() == "2024-01-15T10:30:00" + assert round_trip.issuer == "github-learn" + assert round_trip.certificate == "cert-data" # Debugging def test_repr(self): @@ -199,7 +232,9 @@ def test_repr(self): entry = TranscriptEntry( user_id="user-123", topic_id="git-commit", + topic_list="example.com/math@0.0.1", score=0.8, + timestamp=datetime(2024, 1, 15, 10, 30, 0), issuer="github-learn", ) @@ -211,4 +246,3 @@ def test_repr(self): assert "user-123" in repr_str assert "git-commit" in repr_str assert "0.8" in repr_str - assert "github-learn" in repr_str