Skip to content
Open
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
1 change: 1 addition & 0 deletions frontend/src/types/Manifest.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface Manifest {
type: PackageType
license: string | null
permissions: PartialPackagePermissions | null
environment_variables: string[] | null
tags: string[]
requirements: string | string[] | null
static_files: StaticFiles
Expand Down
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies = [
"aiohttp >=3.11.18, <4.0.0",
"pydantic >=2.11.4, <3.0.0",
"PyYAML >=6.0.2, <7.0.0",
"questionpy-server @ git+https://github.com/questionpy-org/questionpy-server.git@a9f052d552e68268684792e028279feabae2c2e2",
"questionpy-server @ git+https://github.com/questionpy-org/questionpy-server.git@5e1681cbc19ebdbfe04a4500024f1cd6d9128ca9",
"jinja2 >=3.1.6, <4.0.0",
"aiohttp-jinja2 >=1.6, <2.0",
"lxml[html-clean] >=5.4.0, <5.5.0",
Expand Down
7 changes: 6 additions & 1 deletion questionpy_sdk/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from questionpy_sdk.constants import DEFAULT_STATE_STORAGE_PATH
from questionpy_sdk.watcher import Watcher
from questionpy_sdk.webserver import WebServer
from questionpy_sdk.webserver.errors import EnvironmentVariablesMissingError
from questionpy_sdk.webserver.server import WebServerArgs
from questionpy_server.worker.impl.subprocess import SubprocessWorker
from questionpy_server.worker.impl.thread import ThreadWorker
Expand Down Expand Up @@ -94,4 +95,8 @@ def run(
else:
coro = async_run(webserver_args)

asyncio.run(coro)
try:
asyncio.run(coro)
except EnvironmentVariablesMissingError as e:
msg = f"The following environment variables are required to run the package: {', '.join(e.missing)}"
raise click.ClickException(msg) from e
8 changes: 6 additions & 2 deletions questionpy_sdk/webserver/controllers/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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 <info@isis.tu-berlin.de>

import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING
Expand All @@ -26,7 +26,11 @@ def generate_api_url(self, name: str, **kwargs: str) -> URL:
@asynccontextmanager
async def get_worker(self) -> AsyncIterator[Worker]:
async with self._webserver.worker_pool.get_worker(
self._webserver.package_location, "sdk", "sdk", self._webserver.package_permissions
self._webserver.package_location,
"sdk",
"sdk",
self._webserver.package_permissions,
os.environ.copy(),
) as worker:
yield worker

Expand Down
5 changes: 5 additions & 0 deletions questionpy_sdk/webserver/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class MissingAttemptDataError(MissingStateError):
message = "The attempt data is missing."


class EnvironmentVariablesMissingError(Exception):
def __init__(self, missing: set[str]) -> None:
self.missing = missing


@dataclass(config=ConfigDict(use_attribute_docstrings=True))
class DetailedServerError:
"""Represents a server-side error serialized for client display."""
Expand Down
7 changes: 7 additions & 0 deletions questionpy_sdk/webserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# (c) Technische Universität Berlin, innoCampus <info@isis.tu-berlin.de>

import logging
import os
from pathlib import Path
from types import TracebackType
from typing import ClassVar, NotRequired, Self, TypedDict, Unpack
Expand All @@ -23,6 +24,7 @@
from questionpy_server.worker.runtime.package_location import PackageLocation

from .constants import API_PATH_PREFIX, USE_VITE_DEV_SERVER, WEBSERVER_KEY
from .errors import EnvironmentVariablesMissingError
from .manifest import read_manifest

log = logging.getLogger("questionpy-sdk:web-server")
Expand Down Expand Up @@ -59,6 +61,11 @@ async def __aenter__(self) -> Self:
# Read manifest
self._manifest = await read_manifest(self.package_location)

# Check if required environment variables are set
requested_environment_variables = self._manifest.environment_variables or set()
if missing_environment_variables := requested_environment_variables - os.environ.keys():
raise EnvironmentVariablesMissingError(missing=missing_environment_variables)

# Assemble worker permissions
permissions = CompletePackagePermissions()
if self._manifest.permissions:
Expand Down