-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: implement server-package API changes
- Loading branch information
Showing
14 changed files
with
219 additions
and
180 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ description = "QuestionPy application server" | |
authors = ["Technische Universität Berlin, innoCampus <[email protected]>"] | ||
license = "MIT" | ||
homepage = "https://questionpy.org" | ||
version = "0.2.4" | ||
version = "0.3.0" | ||
packages = [ | ||
{ include = "questionpy_common" }, | ||
{ include = "questionpy_server" } | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,15 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
from collections.abc import Mapping, Sequence | ||
from typing import TypeAlias | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class Localized(BaseModel): | ||
lang: str | ||
|
||
|
||
PlainValue: TypeAlias = None | int | str | bool | Sequence["PlainValue"] | "PlainMapping" | ||
PlainMapping: TypeAlias = Mapping[str, "PlainValue"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,16 +3,18 @@ | |
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
from abc import ABC, abstractmethod | ||
Check failure on line 5 in questionpy_common/api/attempt.py GitHub Actions / ci / ruff-lintRuff (F401)
|
||
from collections.abc import Sequence | ||
from enum import Enum | ||
from typing import Annotated | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from . import Localized | ||
|
||
__all__ = [ | ||
"AttemptFile", | ||
"AttemptModel", | ||
"AttemptScoredModel", | ||
"AttemptStartedModel", | ||
"AttemptUi", | ||
"BaseAttempt", | ||
"CacheControl", | ||
|
@@ -51,11 +53,15 @@ class AttemptUi(BaseModel): | |
cache_control: CacheControl = CacheControl.PRIVATE_CACHE | ||
|
||
|
||
class AttemptModel(BaseModel): | ||
class AttemptModel(Localized): | ||
variant: int | ||
ui: AttemptUi | ||
|
||
|
||
class AttemptStartedModel(AttemptModel): | ||
attempt_state: str | ||
|
||
|
||
class ScoringCode(Enum): | ||
AUTOMATICALLY_SCORED = "AUTOMATICALLY_SCORED" | ||
NEEDS_MANUAL_SCORING = "NEEDS_MANUAL_SCORING" | ||
|
@@ -70,31 +76,40 @@ class ClassifiedResponse(BaseModel): | |
score: float | ||
|
||
|
||
class ScoredInputState(Enum): | ||
CORRECT = "CORRECT" | ||
CONSEQUENTIAL_SCORE = "CONSEQUENTIAL_SCORE" | ||
PARTIALLY_CORRECT = "PARTIALLY_CORRECT" | ||
WRONG = "WRONG" | ||
|
||
|
||
class ScoredInputModel(BaseModel): | ||
state: ScoredInputState | ||
score: float | None = None | ||
score_max: float | None = None | ||
specific_feedback: str | None = None | ||
right_answer: str | None = None | ||
|
||
|
||
class ScoredSubquestionModel(BaseModel): | ||
score: float | None = None | ||
score_final: float | None = None | ||
scoring_code: ScoringCode | None = None | ||
response_summary: str | ||
response_class: str | ||
|
||
|
||
class ScoreModel(BaseModel): | ||
scoring_state: str = "{}" | ||
scoring_state: str | ||
scoring_code: ScoringCode | ||
score: float | None | ||
"""The total score for this question attempt, as a fraction of the default mark set by the LMS.""" | ||
classification: Sequence[ClassifiedResponse] | None = None | ||
score_final: float | None | ||
scored_inputs: dict[str, ScoredInputModel] = {} | ||
"""Maps input names to their individual scores.""" | ||
scored_subquestions: dict[str, ScoredSubquestionModel] = {} | ||
"""Maps subquestion IDs to their individual scores.""" | ||
|
||
|
||
class AttemptScoredModel(AttemptModel, ScoreModel): | ||
pass | ||
|
||
|
||
class BaseAttempt(ABC): | ||
@abstractmethod | ||
def export_attempt_state(self) -> str: | ||
"""Serialize this attempt's relevant data. | ||
A future call to :meth:`BaseQuestion.view_attempt` should result in an attempt object identical to the one which | ||
exported the state. | ||
""" | ||
|
||
@abstractmethod | ||
def export(self) -> AttemptModel: | ||
"""Get metadata about this attempt.""" | ||
|
||
@abstractmethod | ||
def export_scored_attempt(self) -> AttemptScoredModel: | ||
"""Score this attempt.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
from typing import Protocol | ||
|
||
from questionpy_common.manifest import PackageFile | ||
|
||
|
||
class QPyPackageInterface(Protocol): | ||
def get_static_files(self) -> dict[str, PackageFile]: | ||
pass | ||
|
||
|
||
class QPyLibraryInterface(Protocol, QPyPackageInterface): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,18 +2,19 @@ | |
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
from abc import ABC, abstractmethod | ||
from abc import abstractmethod | ||
from typing import Protocol | ||
|
||
from questionpy_common.elements import OptionsFormDefinition | ||
|
||
from .question import BaseQuestion | ||
from .question import QuestionInterface | ||
|
||
__all__ = ["BaseQuestionType", "InvalidQuestionStateError", "OptionsFormValidationError"] | ||
__all__ = ["InvalidQuestionStateError", "OptionsFormValidationError", "QuestionTypeInterface"] | ||
|
||
|
||
class BaseQuestionType(ABC): | ||
class QuestionTypeInterface(Protocol): | ||
@abstractmethod | ||
def get_options_form(self, question_state: str | None) -> tuple[OptionsFormDefinition, dict[str, object]]: | ||
def get_options_form(self, question_state: str | None) -> tuple[OptionsFormDefinition, PlainMapping]: | ||
"""Get the form used to create a new or edit an existing question. | ||
Args: | ||
|
@@ -24,7 +25,7 @@ def get_options_form(self, question_state: str | None) -> tuple[OptionsFormDefin | |
""" | ||
|
||
@abstractmethod | ||
def create_question_from_options(self, old_state: str | None, form_data: dict[str, object]) -> BaseQuestion: | ||
def create_question_from_options(self, old_state: str | None, form_data: PlainMapping) -> QuestionInterface: | ||
"""Create or update the question (state) with the form data from a submitted question edit form. | ||
Args: | ||
|
@@ -39,7 +40,7 @@ def create_question_from_options(self, old_state: str | None, form_data: dict[st | |
""" | ||
|
||
@abstractmethod | ||
def create_question_from_state(self, question_state: str) -> BaseQuestion: | ||
def create_question_from_state(self, question_state: str) -> QuestionInterface: | ||
"""Deserialize the given question state, returning a question object equivalent to the one which exported it. | ||
Raises: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,16 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
from abc import ABC, abstractmethod | ||
from abc import abstractmethod | ||
from enum import Enum | ||
from typing import Annotated | ||
from typing import Annotated, Protocol | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from .attempt import BaseAttempt | ||
from . import Localized | ||
from .attempt import AttemptModel, AttemptScoredModel, AttemptStartedModel | ||
|
||
__all__ = ["BaseQuestion", "PossibleResponse", "QuestionModel", "ScoringMethod", "SubquestionModel"] | ||
__all__ = ["PossibleResponse", "QuestionInterface", "QuestionModel", "ScoringMethod", "SubquestionModel"] | ||
|
||
|
||
class ScoringMethod(Enum): | ||
|
@@ -29,7 +30,7 @@ class SubquestionModel(BaseModel): | |
response_classes: list[PossibleResponse] | None | ||
|
||
|
||
class QuestionModel(BaseModel): | ||
class QuestionModel(Localized): | ||
num_variants: Annotated[int, Field(ge=1, strict=True)] = 1 | ||
score_min: float = 0 | ||
"""Lowest score used by this question, as a fraction of the default mark set by the LMS.""" | ||
|
@@ -43,9 +44,9 @@ class QuestionModel(BaseModel): | |
subquestions: list[SubquestionModel] | None = None | ||
|
||
|
||
class BaseQuestion(ABC): | ||
class QuestionInterface(Protocol): | ||
@abstractmethod | ||
def start_attempt(self, variant: int) -> BaseAttempt: | ||
def start_attempt(self, variant: int) -> AttemptStartedModel: | ||
"""Start an attempt at this question with the given variant. | ||
Args: | ||
|
@@ -57,23 +58,39 @@ def start_attempt(self, variant: int) -> BaseAttempt: | |
|
||
@abstractmethod | ||
def get_attempt( | ||
self, attempt_state: str, scoring_state: str | None = None, response: dict | None = None | ||
) -> AttemptModel: | ||
"""Create an attempt object for a previously started attempt. | ||
Args: | ||
attempt_state: The `attempt_state` attribute of an attempt which was previously returned by | ||
:meth:`start_attempt`. | ||
scoring_state: Not implemented. | ||
response: The response currently entered by the student. | ||
Returns: | ||
A :class:`BaseAttempt` object which should be identical to the one which generated the given state(s). | ||
""" | ||
|
||
@abstractmethod | ||
def score_attempt( | ||
self, | ||
attempt_state: str, | ||
scoring_state: str | None = None, | ||
response: dict | None = None, | ||
*, | ||
compute_score: bool = False, | ||
generate_hint: bool = False, | ||
) -> BaseAttempt: | ||
try_scoring_with_countback: bool = False, | ||
try_giving_hint: bool = False, | ||
) -> AttemptScoredModel: | ||
"""Create an attempt object for a previously started attempt. | ||
Args: | ||
attempt_state: The `attempt_state` attribute of an attempt which was previously returned by | ||
:meth:`start_attempt`. | ||
scoring_state: Not implemented. | ||
response: The response currently entered by the student. | ||
compute_score: Whether the attempt is retrieved to be scored. | ||
generate_hint: Whether the package should generate a hint for the student. | ||
try_scoring_with_countback: TBD | ||
try_giving_hint: TBD | ||
Returns: | ||
A :class:`BaseAttempt` object which should be identical to the one which generated the given state(s). | ||
|
@@ -83,7 +100,7 @@ def get_attempt( | |
def export_question_state(self) -> str: | ||
"""Serialize this question's relevant data. | ||
A future call to :meth:`BaseQuestionType.create_question_from_state` should result in a question object | ||
A future call to :meth:`QuestionTypeInterface.create_question_from_state` should result in a question object | ||
identical to the one which exported the state. | ||
""" | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.