Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"extensions": [
"ms-python.python",
"ms-python.debugpy",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"ms-python.black-formatter",
"ms-python.autopep8"
],
"settings": {
"editor.formatOnSave": true,
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jobs:
pip install -r requirements.txt
pip install -r requirements-dev.txt

- name: Check formatting
run: |
black --check .
autopep8 --diff --exit-code --recursive .

- name: Run tests with pytest
run: |
pytest
21 changes: 18 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
{
// JSON
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.printWidth": 120
},
// Python
"python.analysis.typeCheckingMode": "standard",
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": [
"tests"
],

"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
},
"black-formatter.args": [
"--line-length",
"120"
],
"autopep8.args": [
"--max-line-length",
"120"
],
// VS Code
"editor.formatOnSave": true,
"files.exclude": {
"*.pytest_cache": true,
"*.egg-info": true,
"*.egg-info": true
}
}
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ from openproficiency import Topic
topic_arithmetic = Topic(
id="arithmetic",
description="Basic operations for numeric calculations",
subtopics=["addition", "subtraction"]
pretopics=['writing']
subtopics=["addition", "subtraction"],
pretopics=["numbers"]
)
```

Expand Down Expand Up @@ -72,7 +72,7 @@ t_arithmetic = Topic(
"addition",
"subtraction",
"multiplication",
"division
"division"
]
)
topic_list.add_topic(t_arithmetic)
Expand All @@ -86,7 +86,7 @@ t_algebra = Topic(
"single-variable-equations",
"multiple-variable-equations"
],
pretopics=[ "arithmetic" ]
pretopics=["arithmetic"]
)
```

Expand All @@ -95,6 +95,8 @@ t_algebra = Topic(
A **Proficiency Score** represents a person's level of proficiency in a specific topic.

```python
from openproficiency import ProficiencyScore, ProficiencyScoreName

proficiency_score = ProficiencyScore(
topic_id="addition",
score=ProficiencyScoreName.PROFICIENT
Expand All @@ -106,6 +108,8 @@ proficiency_score = ProficiencyScore(
All score are internally numeric from 0.0 to 1.0, even if set using the score name (above).

```python
from openproficiency import ProficiencyScore

proficiency_score = ProficiencyScore(
topic_id="arithmetic",
score=0.8 # Same as 'ProficiencyScoreName.PROFICIENT'
Expand All @@ -121,10 +125,10 @@ from openproficiency import TranscriptEntry

# Create a transcript entry
entry = TranscriptEntry(
user_id="john-doe",
user_id="first.last@my-email.com",
topic_id="arithmetic",
score=0.9,
issuer="university-of-example"
issuer="example.com"
)

# Access the transcript entry information
Expand Down
12 changes: 5 additions & 7 deletions openproficiency/Topic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(
# Optional
description: str = "",
subtopics: List[Union[str, "Topic"]] = [],
pretopics: List[Union[str, "Topic"]] = []
pretopics: List[Union[str, "Topic"]] = [],
):
# Required
self.id = id
Expand Down Expand Up @@ -43,8 +43,7 @@ def add_subtopic(self, subtopic: Union[str, "Topic"]) -> None:
self.subtopics.append(subtopic.id)

else:
raise ValueError(
"Subtopic must be a string or a dictionary with an 'id' key.")
raise ValueError("Subtopic must be a string or a dictionary with an 'id' key.")

def add_subtopics(self, subtopics: List[Union[str, "Topic"]]) -> None:
"""
Expand All @@ -67,8 +66,7 @@ def add_pretopic(self, pretopic: Union[str, "Topic"]) -> None:
elif isinstance(pretopic, Topic):
self.pretopics.append(pretopic.id)
else:
raise ValueError(
"Pretopic must be a string or a dictionary with an 'id' key.")
raise ValueError("Pretopic must be a string or a dictionary with an 'id' key.")

def add_pretopics(self, pretopics: List[Union[str, "Topic"]]) -> None:
"""
Expand All @@ -78,13 +76,13 @@ def add_pretopics(self, pretopics: List[Union[str, "Topic"]]) -> None:
for pretopic in pretopics:
self.add_pretopic(pretopic)

def to_dict(self) -> dict:
def to_dict(self) -> dict[str, Union[str, List[str]]]:
"""Convert Topic to JSON-serializable dictionary."""
return {
"id": self.id,
"description": self.description,
"subtopics": self.subtopics,
"pretopics": self.pretopics
"pretopics": self.pretopics,
}

def to_json(self) -> str:
Expand Down
52 changes: 25 additions & 27 deletions openproficiency/TopicList.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""TopicList module for OpenProficiency library."""

import json
from datetime import datetime
from typing import Optional, Dict, Any, Union
from typing import Dict, Any, Union, List, cast
from .Topic import Topic


Expand All @@ -15,7 +14,7 @@ def __init__(
owner: str,
name: str,
# Optional
description: str = ""
description: str = "",
):
# Required
self.owner = owner
Expand Down Expand Up @@ -65,7 +64,7 @@ def from_json(cls, json_data: str) -> "TopicList":

# Verify input is json string
try:
data = json.loads(json_data)
data = cast(Dict[str, Any], json.loads(json_data))
except TypeError:
raise TypeError("Unable to import. 'json_data' must be a JSON string")
except Exception as e:
Expand All @@ -79,7 +78,7 @@ def from_json(cls, json_data: str) -> "TopicList":
)

# Add each topic
topics = data.get("topics", {})
topics = cast(Dict[str, Any], data.get("topics", {}))
for topic_id, topic_data in topics.items():

# Find or create Topic
Expand All @@ -88,20 +87,21 @@ def from_json(cls, json_data: str) -> "TopicList":
topic = topic_list.add_topic(Topic(id=topic_id))

if isinstance(topic_data, dict):
topic.description = topic_data.get("description", "")
topic_dict = cast(Dict[str, Any], topic_data)
topic.description = topic_dict.get("description", "")

# Add subtopics
cls._add_subtopics_recursive(
topic_list=topic_list,
parent_topic=topic,
subtopics=topic_data.get("subtopics", []),
subtopics=cast(List[Any], topic_dict.get("subtopics", [])),
)

# Add pretopics
cls._add_pretopics_recursive(
topic_list=topic_list,
child_topic=topic,
pretopics=topic_data.get("pretopics", []),
pretopics=cast(List[Any], topic_dict.get("pretopics", [])),
)

else:
Expand All @@ -113,13 +113,13 @@ def from_json(cls, json_data: str) -> "TopicList":
def _add_subtopics_recursive(
topic_list: "TopicList",
parent_topic: Topic,
subtopics: list,
subtopics: List[Any],
) -> None:
"""
Process subtopics and add them to the topic list.
Handles nested subtopics at any depth using an iterative approach.
"""
stack = [(subtopics, parent_topic)]
stack: List[tuple[List[Any], Topic]] = [(subtopics, parent_topic)]

while stack:
current_subtopics, current_parent = stack.pop()
Expand All @@ -136,16 +136,17 @@ def _add_subtopics_recursive(

# Handle dictionary with id and optional nested subtopics
elif isinstance(subtopic_object, dict) and "id" in subtopic_object:
subtopic_dict = cast(Dict[str, Any], subtopic_object)
# Check if the topic already exists
subtopic = topic_list.get_topic(subtopic_object["id"])
subtopic = topic_list.get_topic(subtopic_dict["id"])
if subtopic is None:
subtopic = Topic(
id=subtopic_object["id"],
description=subtopic_object.get("description", ""),
id=subtopic_dict["id"],
description=subtopic_dict.get("description", ""),
)

# Queue nested subtopics for processing
nested_subtopics = subtopic_object.get("subtopics", [])
nested_subtopics = cast(List[Any], subtopic_dict.get("subtopics", []))
if nested_subtopics:
stack.append((nested_subtopics, subtopic))

Expand All @@ -158,14 +159,14 @@ def _add_subtopics_recursive(
def _add_pretopics_recursive(
topic_list: "TopicList",
child_topic: Topic,
pretopics: list,
pretopics: List[Any],
) -> None:
"""
Process pretopics and add them to the topic list.
Handles nested pretopics at any depth using an iterative approach.
Pretopics inherit description from child topic if not explicitly set.
"""
stack = [(pretopics, child_topic)]
stack: List[tuple[List[Any], Topic]] = [(pretopics, child_topic)]

while stack:
current_pretopics, current_child = stack.pop()
Expand All @@ -178,24 +179,21 @@ def _add_pretopics_recursive(
# Check if the topic already exists
pretopic = topic_list.get_topic(pretopic_object)
if pretopic is None:
pretopic = Topic(
id=pretopic_object, description=current_child.description
)
pretopic = Topic(id=pretopic_object, description=current_child.description)

# Handle dictionary with id and optional nested pretopics
elif isinstance(pretopic_object, dict) and "id" in pretopic_object:
pretopic_dict = cast(Dict[str, Any], pretopic_object)
# Check if the topic already exists
pretopic = topic_list.get_topic(pretopic_object["id"])
pretopic = topic_list.get_topic(pretopic_dict["id"])
if pretopic is None:
pretopic = Topic(
id=pretopic_object["id"],
description=pretopic_object.get(
"description", current_child.description
),
id=pretopic_dict["id"],
description=pretopic_dict.get("description", current_child.description),
)

# Queue nested pretopics for processing
nested_pretopics = pretopic_object.get("pretopics", [])
nested_pretopics = cast(List[Any], pretopic_dict.get("pretopics", []))
if nested_pretopics:
stack.append((nested_pretopics, pretopic))

Expand All @@ -204,9 +202,9 @@ def _add_pretopics_recursive(
topic_list.add_topic(pretopic)
current_child.add_pretopic(pretopic)

def to_dict(self) -> dict:
def to_dict(self) -> Dict[str, Any]:
"""
Export the TopicList to a JSON string.
Export the TopicList to a dictionary.
"""

# Create dictionary
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,13 @@ Documentation = "https://github.com/openproficiency/model"
Repository = "https://github.com/openproficiency/python-sdk.git"
Issues = "https://github.com/openproficiency/python-sdk/issues"

[tool.black]
line-length = 120
target-version = ["py310"]

[tool.autopep8]
max_line_length = 120
aggressive = 1

[tool.setuptools]
packages = ["openproficiency"]
3 changes: 3 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typeCheckingMode": "strict"
}
4 changes: 4 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Local package
-e .

# Formatting
black>=26.1.0
autopep8>=2.0

# Testing
pytest>=6.0
pytest-cov>=2.0
Expand Down
Loading