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
14 changes: 13 additions & 1 deletion openproficiency/ProficiencyScore.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
from enum import Enum
from typing import Union
from .validators import validate_kebab_case


class ProficiencyScoreName(Enum):
Expand All @@ -29,7 +30,18 @@ def __init__(
self.topic_id = topic_id
self.score = score

# Properties - Score
# Properties
@property
def topic_id(self) -> str:
"""Get the topic ID."""
return self._topic_id

@topic_id.setter
def topic_id(self, value: str) -> None:
"""Set the topic ID. kebab-case"""
validate_kebab_case(value)
self._topic_id = value

@property
def score(self) -> float:
"""Get the score as a numeric value between 0.0 and 1.0."""
Expand Down
13 changes: 13 additions & 0 deletions openproficiency/Topic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
from typing import List, Union
from .validators import validate_kebab_case


class Topic:
Expand All @@ -28,6 +29,18 @@ def __init__(
self.add_subtopics(subtopics)
self.add_pretopics(pretopics)

# Properties
@property
def id(self) -> str:
"""Get the topic ID."""
return self._id

@id.setter
def id(self, value: str) -> None:
"""Set the topic ID. kebab-case"""
validate_kebab_case(value)
self._id = value

# Methods
def add_subtopic(self, subtopic: Union[str, "Topic"]) -> None:
"""
Expand Down
23 changes: 23 additions & 0 deletions openproficiency/TopicList.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime, timezone
from typing import Dict, Any, Union, List, cast
from .Topic import Topic
from .validators import validate_kebab_case, validate_hostname


class TopicList:
Expand Down Expand Up @@ -61,6 +62,28 @@ def get_topic(self, topic_id: str) -> Union[Topic, None]:
return self.topics.get(topic_id, None)

# Properties
@property
def owner(self) -> str:
"""Get the owner name."""
return self._owner

@owner.setter
def owner(self, value: str) -> None:
"""Set the owner with hostname validation. Format: hostname, Ex: `example.com`"""
validate_hostname(value)
self._owner = value

@property
def name(self) -> str:
"""Get the TopicList name. Format: kebab-case"""
return self._name

@name.setter
def name(self, value: str) -> None:
"""Set the TopicList name with kebab-case validation."""
validate_kebab_case(value)
self._name = value

@property
def version(self) -> Union[str, None]:
"""Get the semantic version of the TopicList."""
Expand Down
57 changes: 57 additions & 0 deletions openproficiency/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Validation utilities for OpenProficiency library."""

import re


def validate_kebab_case(value: str) -> None:
"""
Validate that a string follows kebab-case format.

Pattern: lowercase alphanumeric characters with hyphens as separators.
Examples: "topic", "topic-id", "math-level-1"

Args:
value: The string to validate

Raises:
ValueError: If the value is not in valid kebab-case format
"""
if not value:
raise ValueError("Value cannot be empty")

# Pattern: starts and ends with alphanumeric, can have hyphens between
pattern = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"

if not re.match(pattern, value):
raise ValueError(
f"Value must be in kebab-case format (lowercase alphanumeric with hyphens). "
f"Got: '{value}'. Examples: 'topic', 'topic-id', 'math-level-1'"
)


def validate_hostname(value: str) -> None:
"""
Validate that a string is a valid hostname or domain name.

Pattern: lowercase alphanumeric characters with hyphens and dots as separators.
Examples: "example.com", "sub.example.com"

Args:
value: The string to validate

Raises:
ValueError: If the value is not in valid hostname format
"""
if not value:
raise ValueError("Value cannot be empty")

# Pattern: hostname components separated by dots (requires at least 2 components)
# Each component: starts and ends with alphanumeric, can have hyphens between
component_pattern = r"[a-z0-9]+(?:-[a-z0-9]+)*"
pattern = f"^{component_pattern}(?:\\.{component_pattern})+$"

if not re.match(pattern, value):
raise ValueError(
f"Value must be a valid hostname (lowercase alphanumeric with hyphens and dots). "
f"Got: '{value}'. Examples: 'example.com','sub.example.com'"
)
16 changes: 16 additions & 0 deletions tests/ProficiencyScore_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ def test_init_invalid_score_type(self):
assert "ProficiencyScoreName" in str(result)

# Properties
def test_topic_id(self):
"""Update the topic id and read it back."""

# Arrange
ps = ProficiencyScore(
topic_id="git-commit",
score=0.1,
)
updated_topic_id = "git-merge"

# Act
ps.topic_id = updated_topic_id

# Assert
assert ps.topic_id == updated_topic_id

def test_score_numeric(self):
"""Test setting score with numeric value."""

Expand Down
Loading