Skip to content

Commit f1d5965

Browse files
authored
feat: add transcripts (#2)
1 parent fe736f7 commit f1d5965

File tree

12 files changed

+919
-16
lines changed

12 files changed

+919
-16
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,53 @@ t_algebra = Topic(
9090
)
9191
```
9292

93+
### Create a Proficiency Score, by name
94+
95+
A **Proficiency Score** represents a person's level of proficiency in a specific topic.
96+
97+
```python
98+
proficiency_score = ProficiencyScore(
99+
topic_id="addition",
100+
score=ProficiencyScoreName.PROFICIENT
101+
)
102+
```
103+
104+
### Create a Proficiency Score, numerically
105+
106+
All score are internally numeric from 0.0 to 1.0, even if set using the score name (above).
107+
108+
```python
109+
proficiency_score = ProficiencyScore(
110+
topic_id="arithmetic",
111+
score=0.8 # Same as 'ProficiencyScoreName.PROFICIENT'
112+
)
113+
```
114+
115+
### Issue a Transcript Entry
116+
117+
A **Transcript Entry** associates a proficiency score to a user and is claimed by an issuer.
118+
119+
```python
120+
from openproficiency import TranscriptEntry
121+
122+
# Create a transcript entry
123+
entry = TranscriptEntry(
124+
user_id="john-doe",
125+
topic_id="arithmetic",
126+
score=0.9,
127+
issuer="university-of-example"
128+
)
129+
130+
# Access the transcript entry information
131+
print(entry.user_id) # john-doe
132+
print(entry.proficiency_score.score) # 0.9
133+
print(entry.issuer) # university-of-example
134+
print(entry.timestamp) # datetime object
135+
136+
# Convert to JSON for storage or transmission
137+
entry_json = entry.to_json()
138+
```
139+
93140
## How to Develop
94141

95142
This project is open to pull requests.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""ProficiencyScore module for OpenProficiency library."""
2+
3+
import json
4+
from enum import Enum
5+
from typing import Union
6+
7+
8+
class ProficiencyScoreName(Enum):
9+
"""Enum for proficiency score names."""
10+
11+
UNAWARE = 0.0
12+
AWARE = 0.1
13+
FAMILIAR = 0.5
14+
PROFICIENT = 0.8
15+
PROFICIENT_WITH_EVIDENCE = 1.0
16+
17+
18+
class ProficiencyScore:
19+
"""Class representing a proficiency score for a topic."""
20+
21+
# Initializers
22+
def __init__(
23+
self,
24+
# Required
25+
topic_id: str,
26+
score: Union[float, ProficiencyScoreName],
27+
):
28+
# Required
29+
self.topic_id = topic_id
30+
self.score = score
31+
32+
# Properties - Score
33+
@property
34+
def score(self) -> float:
35+
"""Get the score as a numeric value between 0.0 and 1.0."""
36+
return self._score
37+
38+
@score.setter
39+
def score(self, value: Union[float, ProficiencyScoreName]) -> None:
40+
"""Set the score numerically or using a ProficiencyScoreName enum."""
41+
# If numeric, directly store it.
42+
if isinstance(value, (int, float)):
43+
if not (0.0 <= value <= 1.0):
44+
raise ValueError(f"Score must be between 0.0 and 1.0, got {value}")
45+
self._score = float(value)
46+
47+
# If enum, store as numeric value.
48+
elif isinstance(value, ProficiencyScoreName):
49+
self._score = value.value
50+
51+
else:
52+
raise ValueError(f"Score must be numeric or ProficiencyScoreName enum. Got type: '{type(value)}'")
53+
54+
# Properties - Score
55+
@property
56+
def score_name(self) -> ProficiencyScoreName:
57+
"""Get the proficiency name as an enum value."""
58+
return ProficiencyScore.get_score_name(self._score)
59+
60+
# Methods
61+
def to_dict(self) -> dict[str, float]:
62+
"""Convert to a JSON-serializable dictionary."""
63+
return {
64+
"topic_id": self.topic_id,
65+
"score": self._score,
66+
}
67+
68+
def to_json(self) -> str:
69+
"""Convert to a JSON string."""
70+
return json.dumps(self.to_dict())
71+
72+
# Static Methods
73+
@staticmethod
74+
def get_score_name(score: float) -> ProficiencyScoreName:
75+
"""Internal method to determine proficiency name from numeric score."""
76+
if score == 1.0:
77+
return ProficiencyScoreName.PROFICIENT_WITH_EVIDENCE
78+
79+
elif score >= 0.8:
80+
return ProficiencyScoreName.PROFICIENT
81+
82+
elif score >= 0.5:
83+
return ProficiencyScoreName.FAMILIAR
84+
85+
elif score >= 0.1:
86+
return ProficiencyScoreName.AWARE
87+
88+
elif score >= 0.0:
89+
return ProficiencyScoreName.UNAWARE
90+
91+
else:
92+
raise ValueError(f"Invalid score value: {score}")
93+
94+
# Debugging
95+
def __repr__(self) -> str:
96+
"""String representation of ProficiencyScore."""
97+
return f"ProficiencyScore(topic_id='{self.topic_id}', score={self._score}, name={self.score_name.name})"

openproficiency/Topic.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Topic module for OpenProficiency library."""
22

3+
import json
34
from typing import List, Union
45

56

@@ -77,6 +78,19 @@ def add_pretopics(self, pretopics: List[Union[str, "Topic"]]) -> None:
7778
for pretopic in pretopics:
7879
self.add_pretopic(pretopic)
7980

81+
def to_dict(self) -> dict:
82+
"""Convert Topic to JSON-serializable dictionary."""
83+
return {
84+
"id": self.id,
85+
"description": self.description,
86+
"subtopics": self.subtopics,
87+
"pretopics": self.pretopics
88+
}
89+
90+
def to_json(self) -> str:
91+
"""Convert Topic to JSON string."""
92+
return json.dumps(self.to_dict())
93+
8094
# Debugging
8195
def __repr__(self) -> str:
8296
"""String representation of Topic."""

openproficiency/TopicList.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def __init__(
1515
owner: str,
1616
name: str,
1717
# Optional
18-
description: str = "",
18+
description: str = ""
1919
):
2020
# Required
2121
self.owner = owner
@@ -204,7 +204,7 @@ def _add_pretopics_recursive(
204204
topic_list.add_topic(pretopic)
205205
current_child.add_pretopic(pretopic)
206206

207-
def to_json(self) -> str:
207+
def to_dict(self) -> dict:
208208
"""
209209
Export the TopicList to a JSON string.
210210
"""
@@ -234,4 +234,8 @@ def to_json(self) -> str:
234234
# Store in data
235235
data["topics"][topic_id] = topic_data
236236

237-
return json.dumps(data, indent=2)
237+
return data
238+
239+
def to_json(self) -> str:
240+
"""Convert TopicList to JSON string."""
241+
return json.dumps(self.to_dict())

openproficiency/TranscriptEntry.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""TranscriptEntry module for OpenProficiency library."""
2+
3+
import json
4+
from datetime import datetime
5+
from typing import Optional, Union
6+
from .ProficiencyScore import ProficiencyScore
7+
8+
9+
class TranscriptEntry:
10+
"""A user's proficiency score, validated by a particular issuer."""
11+
12+
# Initializers
13+
def __init__(
14+
self,
15+
# Required
16+
user_id: str,
17+
topic_id: str,
18+
score: float,
19+
issuer: str,
20+
# Optional
21+
timestamp: Optional[datetime] = None,
22+
):
23+
# Required
24+
self.user_id = user_id
25+
self._proficiency_score = ProficiencyScore(
26+
topic_id=topic_id,
27+
score=score,
28+
)
29+
self.issuer = issuer
30+
31+
# Optional
32+
self.timestamp = timestamp or datetime.now()
33+
34+
# Properties
35+
@property
36+
def proficiency_score(self) -> ProficiencyScore:
37+
"""Get the topic ID from the proficiency score."""
38+
return self._proficiency_score
39+
40+
# Methods
41+
def to_dict(self) -> dict[str, Union[str, int, float]]:
42+
"""Convert TranscriptEntry to JSON-serializable dictionary."""
43+
return {
44+
"user_id": self.user_id,
45+
"topic_id": self._proficiency_score.topic_id,
46+
"score": self._proficiency_score.score,
47+
"issuer": self.issuer,
48+
"timestamp": self.timestamp.isoformat(),
49+
}
50+
51+
def to_json(self) -> str:
52+
"""Convert TranscriptEntry to JSON string."""
53+
return json.dumps(self.to_dict())
54+
55+
# Methods - Static
56+
@staticmethod
57+
def from_dict(data: dict[str, Union[str, int, float]]) -> "TranscriptEntry":
58+
"""Create a TranscriptEntry from a dictionary."""
59+
return TranscriptEntry(
60+
user_id=data["user_id"],
61+
topic_id=data["topic_id"],
62+
score=data["score"],
63+
issuer=data["issuer"],
64+
timestamp=datetime.fromisoformat(data["timestamp"]),
65+
)
66+
67+
@staticmethod
68+
def from_json(json_str: str) -> "TranscriptEntry":
69+
"""Create a TranscriptEntry from a JSON string."""
70+
return TranscriptEntry.from_dict(json.loads(json_str))
71+
72+
# Debugging
73+
def __repr__(self) -> str:
74+
"""String representation of TranscriptEntry."""
75+
return (
76+
f"TranscriptEntry(user_id='{self.user_id}', "
77+
f"topic_id='{self._proficiency_score.topic_id}', "
78+
f"score={self._proficiency_score.score}, "
79+
f"issuer='{self.issuer}'"
80+
)

openproficiency/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""OpenProficiency - Library to manage proficiency scores using topics and topic lists."""
22

33
from .Topic import Topic
4-
from .TopicList import TopicList
4+
from .TopicList import TopicList
5+
from .ProficiencyScore import ProficiencyScore, ProficiencyScoreName
6+
from .TranscriptEntry import TranscriptEntry

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "openproficiency"
7-
version = "0.1.0"
7+
version = "0.0.3"
88
description = "A simple library for managing proficiency topics and topic lists"
99
readme = "README.md"
1010
requires-python = ">=3.10"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name="openproficiency",
10-
version="0.1.0",
10+
version="0.0.3",
1111
author="OpenProficiency Contributors",
1212
description="A library for managing proficiency topics and topic lists",
1313
long_description=long_description,

0 commit comments

Comments
 (0)