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

Statische Dateien ausliefern #114

Merged
merged 4 commits into from
Sep 3, 2024
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
52 changes: 52 additions & 0 deletions docs/qppe-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,58 @@ paths:
404:
description: Job not found.

/packages/{package_hash}/file/{namespace}/{short_name}/{path}:
parameters:
- $ref: '#/components/parameters/PackageHash'
- name: namespace
in: path
required: true
description: Namespace of the package from which to retrieve the file.
schema:
type: string
- name: short_name
in: path
required: true
description: Short name of the package from which to retrieve the file.
schema:
type: string
- name: path
in: path
required: true
description: Path within the package. Must start with either `static/` or `static-private/`.
schema:
type: string
pattern: "^(static|static-private)/.*$"
- $ref: '#/components/parameters/UserAgent'
- $ref: '#/components/parameters/AcceptLanguageOne'
post:
summary: Retrieve a static file from a package.
description: While the `package_hash` refers to the main package to load, that package may load other packages in
turn, in which case the path params `namespace` and `short_name` may identify one of those. Within the request
data, `lms_auth_token` should provide access to the AuthPackageAccess QPPE LMS Callback API security scheme.
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/RequestBaseData"
responses:
"200":
description: Static file data
content:
"*/*": {}
janbritz marked this conversation as resolved.
Show resolved Hide resolved
"404":
description: Package or its static file not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundStatus"
"500":
description: Error occurred.
content:
application/json:
schema:
$ref: "#/components/schemas/RequestError"

/package-extract-info:
parameters:
- $ref: '#/components/parameters/UserAgent'
Expand Down
172 changes: 0 additions & 172 deletions questionpy_server/api/routes.py

This file was deleted.

10 changes: 10 additions & 0 deletions questionpy_server/api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file is part of the QuestionPy Server. (https://questionpy.org)
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from ._attempts import attempt_routes
from ._files import file_routes
from ._packages import package_routes
from ._status import status_routes

routes = (*attempt_routes, *file_routes, *package_routes, *status_routes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Die Aufteilung macht es übersichtlicher, aber es wäre schöner gewesen, wenn du das Umkopieren in einem eigenen Commit erledigt hättest, damit die wesentlichen Änderungen in dem PR schneller ersichtlich sind. ;)

78 changes: 78 additions & 0 deletions questionpy_server/api/routes/_attempts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# This file is part of the QuestionPy Server. (https://questionpy.org)
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from typing import TYPE_CHECKING

from aiohttp import web

from questionpy_common.environment import RequestUser
from questionpy_server.api.models import AttemptScoreArguments, AttemptStartArguments, AttemptViewArguments
from questionpy_server.decorators import ensure_package_and_question_state_exist
from questionpy_server.package import Package
from questionpy_server.web import json_response
from questionpy_server.worker.runtime.package_location import ZipPackageLocation

if TYPE_CHECKING:
from questionpy_server.app import QPyServer
from questionpy_server.worker.worker import Worker


attempt_routes = web.RouteTableDef()


@attempt_routes.post(r"/packages/{package_hash:\w+}/attempt/start") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def post_attempt_start(
request: web.Request, package: Package, question_state: bytes, data: AttemptStartArguments
) -> web.Response:
qpyserver: "QPyServer" = request.app["qpy_server_app"]

package_path = await package.get_path()
worker: Worker
async with qpyserver.worker_pool.get_worker(ZipPackageLocation(package_path), 0, data.context) as worker:
attempt = await worker.start_attempt(RequestUser(["de", "en"]), question_state.decode(), data.variant)

return json_response(data=attempt, status=201)


@attempt_routes.post(r"/packages/{package_hash:\w+}/attempt/view") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def post_attempt_view(
request: web.Request, package: Package, question_state: bytes, data: AttemptViewArguments
) -> web.Response:
qpyserver: "QPyServer" = request.app["qpy_server_app"]

package_path = await package.get_path()
worker: Worker
async with qpyserver.worker_pool.get_worker(ZipPackageLocation(package_path), 0, data.context) as worker:
attempt = await worker.get_attempt(
request_user=RequestUser(["de", "en"]),
question_state=question_state.decode(),
attempt_state=data.attempt_state,
scoring_state=data.scoring_state,
response=data.response,
)

return json_response(data=attempt, status=201)


@attempt_routes.post(r"/packages/{package_hash:\w+}/attempt/score") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def post_attempt_score(
request: web.Request, package: Package, question_state: bytes, data: AttemptScoreArguments
) -> web.Response:
qpyserver: "QPyServer" = request.app["qpy_server_app"]

package_path = await package.get_path()
worker: Worker
async with qpyserver.worker_pool.get_worker(ZipPackageLocation(package_path), 0, data.context) as worker:
attempt_scored = await worker.score_attempt(
request_user=RequestUser(["de", "en"]),
question_state=question_state.decode(),
attempt_state=data.attempt_state,
scoring_state=data.scoring_state,
response=data.response,
)

return json_response(data=attempt_scored, status=201)
45 changes: 45 additions & 0 deletions questionpy_server/api/routes/_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This file is part of the QuestionPy Server. (https://questionpy.org)
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>
from typing import TYPE_CHECKING

from aiohttp import web
from aiohttp.web_exceptions import HTTPNotImplemented

from questionpy_server.decorators import ensure_package_and_question_state_exist
from questionpy_server.package import Package
from questionpy_server.worker.runtime.package_location import ZipPackageLocation

if TYPE_CHECKING:
from questionpy_server.app import QPyServer
from questionpy_server.worker.worker import Worker

file_routes = web.RouteTableDef()


@file_routes.post(r"/packages/{package_hash}/file/{namespace}/{short_name}/{path:static/.*}") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def serve_static_file(request: web.Request, package: Package) -> web.Response:
qpy_server: "QPyServer" = request.app["qpy_server_app"]
namespace = request.match_info["namespace"]
short_name = request.match_info["short_name"]
path = request.match_info["path"]

if package.manifest.namespace != namespace or package.manifest.short_name != short_name:
# TODO: Support static files in non-main packages by using namespace and short_name.
raise HTTPNotImplemented(reason="Static file retrieval from non-main packages is not supported yet.")

worker: Worker
async with qpy_server.worker_pool.get_worker(ZipPackageLocation(await package.get_path()), 0, None) as worker:
try:
file = await worker.get_static_file(path)
except FileNotFoundError as e:
raise web.HTTPNotFound(reason="File not found.") from e

return web.Response(
body=file.data,
content_type=file.mime_type,
# Set a lifetime of 1 year, i.e. effectively never expire. Since the package hash is part of the URL, cache
# busting is automatic.
headers={"Cache-Control": "public, immutable, max-age=31536000"},
)
Loading