Skip to content

Commit d6efced

Browse files
committed
refactor: ensure_package_and_question_state_exist
1 parent 3fd5485 commit d6efced

File tree

18 files changed

+372
-280
lines changed

18 files changed

+372
-280
lines changed

questionpy_server/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
__version__ = "0.1.0"
2-
31
# This file is part of the QuestionPy Server. (https://questionpy.org)
42
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
53
# (c) Technische Universität Berlin, innoCampus <[email protected]>
64

75
from questionpy_server.worker.pool import WorkerPool
86

9-
__all__ = ["WorkerPool"]
7+
__version__ = "0.1.0"
8+
9+
__all__ = ["WorkerPool", "__version__"]

questionpy_server/api/routes/_attempts.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@
88

99
from questionpy_common.environment import RequestUser
1010
from questionpy_server.api.models import AttemptScoreArguments, AttemptStartArguments, AttemptViewArguments
11-
from questionpy_server.decorators import ensure_package_and_question_state_exist
11+
from questionpy_server.app import QPyServer
12+
from questionpy_server.decorators import ensure_required_parts
1213
from questionpy_server.package import Package
1314
from questionpy_server.web import json_response
1415
from questionpy_server.worker.runtime.package_location import ZipPackageLocation
1516

1617
if TYPE_CHECKING:
17-
from questionpy_server.app import QPyServer
1818
from questionpy_server.worker.worker import Worker
1919

2020

2121
attempt_routes = web.RouteTableDef()
2222

2323

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

3131
package_path = await package.get_path()
3232
worker: Worker
@@ -37,11 +37,11 @@ async def post_attempt_start(
3737

3838

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

4646
package_path = await package.get_path()
4747
worker: Worker
@@ -58,11 +58,11 @@ async def post_attempt_view(
5858

5959

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

6767
package_path = await package.get_path()
6868
worker: Worker

questionpy_server/api/routes/_files.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,35 @@
66
from aiohttp import web
77
from aiohttp.web_exceptions import HTTPNotImplemented
88

9-
from questionpy_server.decorators import ensure_package_and_question_state_exist
9+
from questionpy_server.app import QPyServer
10+
from questionpy_server.decorators import ensure_package
1011
from questionpy_server.package import Package
1112
from questionpy_server.worker.runtime.package_location import ZipPackageLocation
1213

1314
if TYPE_CHECKING:
14-
from questionpy_server.app import QPyServer
1515
from questionpy_server.worker.worker import Worker
1616

1717
file_routes = web.RouteTableDef()
1818

1919

2020
@file_routes.post(r"/packages/{package_hash}/file/{namespace}/{short_name}/{path:static/.*}") # type: ignore[arg-type]
21-
@ensure_package_and_question_state_exist
21+
@ensure_package
2222
async def post_attempt_start(request: web.Request, package: Package) -> web.Response:
23-
qpy_server: QPyServer = request.app["qpy_server_app"]
23+
qpy_server = request.app[QPyServer.APP_KEY]
2424
namespace = request.match_info["namespace"]
2525
short_name = request.match_info["short_name"]
2626
path = request.match_info["path"]
2727

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

3232
worker: Worker
3333
async with qpy_server.worker_pool.get_worker(ZipPackageLocation(await package.get_path()), 0, None) as worker:
3434
try:
3535
file = await worker.get_static_file(path)
3636
except FileNotFoundError as e:
37-
raise web.HTTPNotFound(reason="File not found.") from e
37+
raise web.HTTPNotFound(text="File not found.") from e
3838

3939
return web.Response(
4040
body=file.data,

questionpy_server/api/routes/_packages.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@
99

1010
from questionpy_common.environment import RequestUser
1111
from questionpy_server.api.models import QuestionCreateArguments, QuestionEditFormResponse, RequestBaseData
12-
from questionpy_server.decorators import ensure_package_and_question_state_exist
12+
from questionpy_server.app import QPyServer
13+
from questionpy_server.decorators import ensure_package, ensure_required_parts
1314
from questionpy_server.package import Package
1415
from questionpy_server.web import json_response
1516
from questionpy_server.worker.runtime.package_location import ZipPackageLocation
1617

1718
if TYPE_CHECKING:
18-
from questionpy_server.app import QPyServer
1919
from questionpy_server.worker.worker import Worker
2020

2121
package_routes = web.RouteTableDef()
2222

2323

2424
@package_routes.get("/packages")
2525
async def get_packages(request: web.Request) -> web.Response:
26-
qpyserver: QPyServer = request.app["qpy_server_app"]
26+
qpyserver = request.app[QPyServer.APP_KEY]
2727

2828
packages = qpyserver.package_collection.get_packages()
2929
data = [package.get_info() for package in packages]
@@ -33,22 +33,22 @@ async def get_packages(request: web.Request) -> web.Response:
3333

3434
@package_routes.get(r"/packages/{package_hash:\w+}")
3535
async def get_package(request: web.Request) -> web.Response:
36-
qpyserver: QPyServer = request.app["qpy_server_app"]
36+
qpyserver = request.app[QPyServer.APP_KEY]
3737

38-
try:
39-
package = qpyserver.package_collection.get(request.match_info["package_hash"])
40-
return json_response(data=package.get_info())
41-
except FileNotFoundError as error:
42-
raise HTTPNotFound from error
38+
package = qpyserver.package_collection.get(request.match_info["package_hash"])
39+
if not package:
40+
raise HTTPNotFound
41+
42+
return json_response(data=package.get_info())
4343

4444

4545
@package_routes.post(r"/packages/{package_hash:\w+}/options") # type: ignore[arg-type]
46-
@ensure_package_and_question_state_exist
46+
@ensure_required_parts
4747
async def post_options(
48-
request: web.Request, package: Package, question_state: bytes | None, data: RequestBaseData
48+
request: web.Request, package: Package, data: RequestBaseData, question_state: bytes | None = None
4949
) -> web.Response:
5050
"""Get the options form definition that allow a question creator to customize a question."""
51-
qpyserver: QPyServer = request.app["qpy_server_app"]
51+
qpyserver = request.app[QPyServer.APP_KEY]
5252

5353
package_path = await package.get_path()
5454
worker: Worker
@@ -61,11 +61,11 @@ async def post_options(
6161

6262

6363
@package_routes.post(r"/packages/{package_hash:\w+}/question") # type: ignore[arg-type]
64-
@ensure_package_and_question_state_exist
64+
@ensure_required_parts
6565
async def post_question(
6666
request: web.Request, data: QuestionCreateArguments, package: Package, question_state: bytes | None = None
6767
) -> web.Response:
68-
qpyserver: QPyServer = request.app["qpy_server_app"]
68+
qpyserver = request.app[QPyServer.APP_KEY]
6969

7070
package_path = await package.get_path()
7171
worker: Worker
@@ -84,7 +84,7 @@ async def post_question_migrate(_request: web.Request) -> web.Response:
8484

8585

8686
@package_routes.post(r"/package-extract-info") # type: ignore[arg-type]
87-
@ensure_package_and_question_state_exist
87+
@ensure_package
8888
async def package_extract_info(_request: web.Request, package: Package) -> web.Response:
8989
"""Get package information."""
9090
return json_response(data=package.get_info(), status=201)

questionpy_server/api/routes/_status.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,20 @@
22
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
33
# (c) Technische Universität Berlin, innoCampus <[email protected]>
44

5-
from typing import TYPE_CHECKING
6-
75
from aiohttp import web
86

97
from questionpy_server import __version__
108
from questionpy_server.api.models import ServerStatus, Usage
9+
from questionpy_server.app import QPyServer
1110
from questionpy_server.web import json_response
1211

13-
if TYPE_CHECKING:
14-
from questionpy_server.app import QPyServer
15-
16-
1712
status_routes = web.RouteTableDef()
1813

1914

2015
@status_routes.get(r"/status")
2116
async def get_server_status(request: web.Request) -> web.Response:
2217
"""Get server status."""
23-
qpyserver: QPyServer = request.app["qpy_server_app"]
18+
qpyserver = request.app[QPyServer.APP_KEY]
2419
status = ServerStatus(
2520
version=__version__,
2621
allow_lms_packages=qpyserver.settings.webservice.allow_lms_packages,

questionpy_server/app.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,28 @@
33
# (c) Technische Universität Berlin, innoCampus <[email protected]>
44

55
from asyncio import create_task
6-
from typing import Any
6+
from typing import Any, ClassVar
77

88
from aiohttp import web
99

1010
from . import __version__
11-
from .api.routes import routes
1211
from .cache import FileLimitLRU
1312
from .collector import PackageCollection
1413
from .settings import Settings
1514
from .worker.pool import WorkerPool
1615

1716

18-
class QPyServer:
17+
class QPyServer(web.AppKey["QPyServer"]):
18+
APP_KEY: ClassVar[web.AppKey["QPyServer"]] = web.AppKey("qpy_server_app")
19+
1920
def __init__(self, settings: Settings):
21+
# We import here, so we don't have to work around circular imports.
22+
from .api.routes import routes # noqa: PLC0415
23+
2024
self.settings: Settings = settings
2125
self.web_app = web.Application(client_max_size=settings.webservice.max_main_size)
2226
self.web_app.add_routes(routes)
23-
self.web_app["qpy_server_app"] = self
27+
self.web_app[self.APP_KEY] = self
2428

2529
self.worker_pool = WorkerPool(
2630
settings.worker.max_workers, settings.worker.max_memory, worker_type=settings.worker.type

questionpy_server/cache.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ class File(NamedTuple):
1919
size: int
2020

2121

22-
class SizeError(Exception):
23-
def __init__(self, message: str = "", max_size: int = 0, actual_size: int = 0):
24-
super().__init__(message)
22+
class CacheItemTooLargeError(Exception):
23+
def __init__(self, key: str, actual_size: int, max_size: int):
24+
readable_actual = ByteSize(actual_size).human_readable()
25+
readable_max = ByteSize(max_size).human_readable()
26+
super().__init__(
27+
f"Unable to cache item '{key}' with size '{readable_actual}' because it exceeds the maximum "
28+
f"allowed size of '{readable_max}'"
29+
)
2530

2631
self.max_size = max_size
2732
self.actual_size = actual_size
@@ -146,12 +151,7 @@ async def put(self, key: str, value: bytes) -> Path:
146151
if size > self.max_size:
147152
# If we allowed this, the loop at the end would remove all items from the dictionary,
148153
# so we raise an error to allow exceptions for this case.
149-
msg = f"Item itself exceeds maximum allowed size of {ByteSize(self.max_size).human_readable()}"
150-
raise SizeError(
151-
msg,
152-
max_size=self.max_size,
153-
actual_size=size,
154-
)
154+
raise CacheItemTooLargeError(key, size, self.max_size)
155155

156156
async with self._lock:
157157
# Save the bytes on filesystem.

questionpy_server/collector/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
33
# (c) Technische Universität Berlin, innoCampus <[email protected]>
44

5-
from questionpy_server.collector.package_collection import PackageCollection
5+
from questionpy_server.collector._package_collection import PackageCollection
66

77
__all__ = [
88
"PackageCollection",

questionpy_server/collector/package_collection.py renamed to questionpy_server/collector/_package_collection.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,13 @@ async def put(self, package_container: "HashContainer") -> "Package":
8080
"""
8181
return await self._lms_collector.put(package_container)
8282

83-
def get(self, package_hash: str) -> "Package":
83+
def get(self, package_hash: str) -> "Package | None":
8484
"""Returns a package if it exists.
8585
8686
Args:
87-
package_hash (str): hash value of the package
88-
89-
Returns:
90-
path to the package
87+
package_hash: hash value of the package
9188
"""
92-
# Check if package was indexed
93-
if package := self._indexer.get_by_hash(package_hash):
94-
return package
95-
96-
raise FileNotFoundError
89+
return self._indexer.get_by_hash(package_hash)
9790

9891
def get_by_identifier(self, identifier: str) -> dict[SemVer, "Package"]:
9992
"""Returns a dict of packages with the given identifier and available versions.
@@ -106,20 +99,14 @@ def get_by_identifier(self, identifier: str) -> dict[SemVer, "Package"]:
10699
"""
107100
return self._indexer.get_by_identifier(identifier)
108101

109-
def get_by_identifier_and_version(self, identifier: str, version: SemVer) -> "Package":
102+
def get_by_identifier_and_version(self, identifier: str, version: SemVer) -> "Package | None":
110103
"""Returns a package with the given identifier and version.
111104
112105
Args:
113-
identifier (str): identifier of the package
114-
version (str): version of the package
115-
116-
Returns:
117-
package
106+
identifier: identifier of the package
107+
version: version of the package
118108
"""
119-
if package := self._indexer.get_by_identifier_and_version(identifier, version):
120-
return package
121-
122-
raise FileNotFoundError
109+
return self._indexer.get_by_identifier_and_version(identifier, version)
123110

124111
def get_packages(self) -> set["Package"]:
125112
"""Returns a set of all available packages.

0 commit comments

Comments
 (0)