Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

init()-Entrypoint und Environment #63

Merged
merged 2 commits into from
Nov 27, 2023
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
6 changes: 6 additions & 0 deletions example/python/local/example/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# This file is part of the QuestionPy SDK. (https://questionpy.org)
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from .question_type import ExampleQuestionType


def init() -> ExampleQuestionType:
return ExampleQuestionType()
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
# This file is part of the QuestionPy SDK. (https://questionpy.org)
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from importlib.resources import files

from questionpy_common.models import QuestionModel, ScoringMethod, AttemptModel, AttemptUi

from questionpy import QuestionType, Attempt, Question, BaseQuestionState, BaseAttemptState
from questionpy.form import *
from questionpy.form import FormModel, text_input, OptionEnum, option, select, group, static_text, is_checked,\
checkbox, radio_group, hidden, repeat, is_not_checked


class NameGroup(FormModel):
Expand Down Expand Up @@ -39,19 +31,3 @@ class MyModel(FormModel):

has_name = checkbox(None, "I have a name.")
name_group = group("Name", NameGroup, disable_if=[is_not_checked("has_name")])


class ExampleAttempt(Attempt["ExampleQuestion", BaseAttemptState]):
def export(self) -> AttemptModel:
return AttemptModel(variant=1, ui=AttemptUi(
content=(files(__package__) / "multiple-choice.xhtml").read_text()
))


class ExampleQuestion(Question[BaseQuestionState[MyModel], ExampleAttempt]):
def export(self) -> QuestionModel:
return QuestionModel(scoring_method=ScoringMethod.AUTOMATICALLY_SCORABLE)


class ExampleQuestionType(QuestionType[MyModel, ExampleQuestion]):
pass
22 changes: 22 additions & 0 deletions example/python/local/example/question_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from importlib.resources import files

from questionpy_common.models import AttemptModel, AttemptUi, QuestionModel, ScoringMethod

from questionpy import Attempt, BaseAttemptState, Question, BaseQuestionState, QuestionType
from .form import MyModel


class ExampleAttempt(Attempt["ExampleQuestion", BaseAttemptState]):
def export(self) -> AttemptModel:
return AttemptModel(variant=1, ui=AttemptUi(
content=(files(__package__) / "multiple-choice.xhtml").read_text()
))


class ExampleQuestion(Question[BaseQuestionState[MyModel], ExampleAttempt]):
def export(self) -> QuestionModel:
return QuestionModel(scoring_method=ScoringMethod.AUTOMATICALLY_SCORABLE)


class ExampleQuestionType(QuestionType[MyModel, ExampleQuestion]):
pass
1 change: 0 additions & 1 deletion example/qpy_manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ author: Maximilian Haye <[email protected]>
name:
de: Beispiel
en: Example
entrypoint: main
languages: [de, en]
794 changes: 310 additions & 484 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ python = "^3.9"
aiohttp = "^3.8.1"
pydantic = "^2.4"
PyYAML = "^6.0"
questionpy-common = { git = "https://github.com/questionpy-org/questionpy-common.git", rev = "9c6540e2b06bcfc6643a6b6c0331787d400920c2" }
questionpy-server = { git = "https://github.com/questionpy-org/questionpy-server.git", rev = "2bd02e9c7f17e5b3329b64f6c3008acd4f6ce302" }
questionpy-common = { git = "https://github.com/questionpy-org/questionpy-common.git", rev = "885f7a2b3333eb3c7143c270873b79810eb410d6" }
questionpy-server = { git = "https://github.com/questionpy-org/questionpy-server.git", rev = "35fa36b04a6a2a81fafb33ceacd673e15784561e" }
jinja2 = "^3.1.2"
aiohttp-jinja2 = "^1.5"

Expand Down
2 changes: 2 additions & 0 deletions questionpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from questionpy_common.manifest import Manifest, PackageType # noqa
from questionpy_common.environment import RequestUser, WorkerResourceLimits, Package, OnRequestCallback, Environment, \
PackageInitFunction, get_qpy_environment, NoEnvironmentError # noqa
from questionpy._qtype import QuestionType, Question, Attempt, BaseQuestionState, BaseAttemptState # noqa
14 changes: 5 additions & 9 deletions questionpy/_qtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from abc import ABC
from typing import Optional, Type, Generic, TypeVar, ClassVar, get_args, get_origin, Literal, Union, cast
from typing import Optional, Type, Generic, TypeVar, get_args, get_origin, Literal, Union, cast

from pydantic import BaseModel, ValidationError
from questionpy_common.qtype import OptionsFormDefinition, BaseQuestionType, BaseQuestion, OptionsFormValidationError, \
BaseAttempt

from questionpy import get_qpy_environment
from questionpy.form import FormModel

_T = TypeVar("_T")
Expand Down Expand Up @@ -139,9 +140,6 @@ class QuestionType(BaseQuestionType, Generic[_F, _Q]):
options_class: Type[FormModel] = FormModel
question_class: Type["Question"]

# TODO: questionpy-server#67: Replace with global init() function.
implementation: ClassVar[Optional[Type["QuestionType"]]] = None

def __init__(self, options_class: Optional[Type[_F]] = None, question_class: Optional[Type[_Q]] = None) -> None:
"""Initializes a new question.

Expand All @@ -163,8 +161,6 @@ def __init_subclass__(cls, **kwargs: object) -> None:
raise TypeError(
f"{cls.__name__} must have the same FormModel as {cls.question_class.state_class.__name__}.")

QuestionType.implementation = cls

def get_options_form(self, question_state: Optional[str]) -> tuple[OptionsFormDefinition, dict[str, object]]:
if question_state:
question = self.create_question_from_state(question_state)
Expand All @@ -186,10 +182,10 @@ def create_question_from_options(self, old_state: Optional[str], form_data: dict
# TBD: Should we also update package_name and package_version here? Or check that they match?
state.options = parsed_form_data
else:
env = get_qpy_environment()
state = self.question_class.state_class(
# TODO: Replace these placeholders with values from the environment.
package_name="TODO",
package_version="1.2.3",
package_name=f"{env.main_package.manifest.namespace}.{env.main_package.manifest.short_name}",
package_version=env.main_package.manifest.version,
options=parsed_form_data,
)

Expand Down
1 change: 1 addition & 0 deletions questionpy_sdk/commands/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def package(source: Path, manifest_path: Optional[Path], out_path: Optional[Path
_copy_package(out_file, questionpy)
_install_dependencies(out_file, manifest_path, manifest)
out_file.write_glob(source, "python/**/*")
out_file.write_glob(source, "static/**/*")
out_file.write_manifest(manifest)
except subprocess.CalledProcessError as e:
out_path.unlink(missing_ok=True)
Expand Down
11 changes: 7 additions & 4 deletions questionpy_sdk/webserver/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from jinja2 import FileSystemLoader
from questionpy_common.constants import MiB
from questionpy_common.elements import OptionsFormDefinition
from questionpy_common.environment import RequestUser
from questionpy_server import WorkerPool
from questionpy_server.worker.worker import Worker
from questionpy_server.worker.exception import WorkerUnknownError
Expand Down Expand Up @@ -54,7 +55,7 @@ async def render_options(request: web.Request) -> web.Response:
worker: Worker
async with webserver.worker_pool.get_worker(webserver.package, 0, None) as worker:
manifest = await worker.get_manifest()
form_definition, form_data = await worker.get_options_form(old_state)
form_definition, form_data = await worker.get_options_form(RequestUser(["de", "en"]), old_state)

context = {
'manifest': manifest,
Expand All @@ -77,7 +78,8 @@ async def submit_form(request: web.Request) -> web.Response:
worker: Worker
async with webserver.worker_pool.get_worker(webserver.package, 0, None) as worker:
try:
question = await worker.create_question_from_options(old_state, form_data=parsed_form_data)
question = await worker.create_question_from_options(RequestUser(["de", "en"]), old_state,
form_data=parsed_form_data)
except WorkerUnknownError:
return HTTPBadRequest()

Expand All @@ -102,14 +104,15 @@ async def repeat_element(request: web.Request) -> web.Response:
async with webserver.worker_pool.get_worker(webserver.package, 0, None) as worker:
manifest = await worker.get_manifest()
try:
question = await worker.create_question_from_options(old_state, form_data=old_form_data)
question = await worker.create_question_from_options(RequestUser(["de", "en"]), old_state,
form_data=old_form_data)
except WorkerUnknownError:
return HTTPBadRequest()

new_state = question.question_state
webserver.state_storage.insert(webserver.package, json.loads(new_state))
form_definition: OptionsFormDefinition
form_definition, form_data = await worker.get_options_form(new_state.encode())
form_definition, form_data = await worker.get_options_form(RequestUser(["de", "en"]), new_state.encode())

context = {
'manifest': manifest,
Expand Down
31 changes: 28 additions & 3 deletions tests/test_qtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,40 @@
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>
import json
from typing import Optional
from collections.abc import Generator
from types import SimpleNamespace
from typing import Optional, cast

import pytest
from questionpy_common.environment import set_qpy_environment
from questionpy_common.models import QuestionModel, ScoringMethod, AttemptModel, AttemptUi
from questionpy_server.worker.runtime.manager import EnvironmentImpl
from questionpy_server.worker.runtime.package import QPyMainPackage

from questionpy import QuestionType, Question, BaseQuestionState, Attempt, BaseAttemptState
from questionpy import QuestionType, Question, BaseQuestionState, Attempt, BaseAttemptState, Environment, RequestUser
from questionpy.form import FormModel, text_input


@pytest.fixture(autouse=True)
def environment() -> Generator[Environment, None, None]:
env = EnvironmentImpl(
type="test",
limits=None,
request_user=RequestUser(["en"]),
main_package=cast(QPyMainPackage, SimpleNamespace(manifest=SimpleNamespace(
namespace="test_ns", short_name="test_package",
version="1.2.3"
))),
packages={},
_on_request_callbacks=[]
)
set_qpy_environment(env)
try:
yield env
finally:
set_qpy_environment(None)


class SomeModel(FormModel):
input: Optional[str] = text_input("Some Label")

Expand Down Expand Up @@ -112,7 +137,7 @@ def export(self) -> QuestionModel:


QUESTION_STATE_DICT = {
"package_name": "TODO",
"package_name": "test_ns.test_package",
"package_version": "1.2.3",
"options": {
"input": "something"
Expand Down