From f08fbc17a1df61e9a7e46591b92fb1e116f6ef75 Mon Sep 17 00:00:00 2001 From: Clemens Koza Date: Sat, 19 Jul 2025 18:08:04 +0200 Subject: [PATCH 1/3] update password for the demo site --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8fe3607..4ac5208 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ def domain() -> str: @fixture def moodle(domain: str) -> Moodle: username = "manager" - password = "moodle2024" + password = "moodle25" return Moodle.login(domain, username, password) From 418bad2ca84993222daf4be60f3fbc5c73487eb6 Mon Sep 17 00:00:00 2001 From: Clemens Koza Date: Fri, 25 Jul 2025 21:55:22 +0200 Subject: [PATCH 2/3] add file upload method --- moodle/exception.py | 5 +++++ moodle/mdl.py | 46 +++++++++++++++++++++++++++++++++++++++ moodle/moodle.py | 6 +++++ moodle/upload/__init__.py | 8 +++++++ moodle/upload/file.py | 32 +++++++++++++++++++++++++++ moodle/upload/upload.py | 23 ++++++++++++++++++++ tests/test_upload.py | 14 ++++++++++++ 7 files changed, 134 insertions(+) create mode 100644 moodle/upload/__init__.py create mode 100644 moodle/upload/file.py create mode 100644 moodle/upload/upload.py create mode 100644 tests/test_upload.py diff --git a/moodle/exception.py b/moodle/exception.py index be37a07..e9fde94 100644 --- a/moodle/exception.py +++ b/moodle/exception.py @@ -36,6 +36,11 @@ class InvalidCredentialException(BaseException): message: str = "Wrong username or password!" +@dataclass +class UploadUrlException(BaseException): + message: str = "File Upload URL can not be determined!" + + @dataclass class NetworkMoodleException(BaseException): """Moodle wrapper for network related network error""" diff --git a/moodle/mdl.py b/moodle/mdl.py index 35dcb00..e4e6590 100644 --- a/moodle/mdl.py +++ b/moodle/mdl.py @@ -99,6 +99,52 @@ def post(self, wsfunction: str, moodlewsrestformat="json", **kwargs: Any) -> Any return self.process_response(data) return res.text + def post_upload( + self, *files, + moodlewsrestformat="json", + itemid: int = 0, filepath: str = "/" + ) -> Any: + """Send post request to file upload endpoint. + + Args: + files (file-like-objects, multiple): The files to upload. + moodlewsrestformat (str, optional): Expected format. Defaults to "json". + itemid (int, optional): itemid to upload into. Defaults to 0. + filepath (str, optional): file path under which to upload. Defaults to '/'. + + Raises: + NetworkMoodleException: If request failed + EmptyResponseException: If the response empty + UploadUrlException: If the upload endpoint URL can't be inferred + MoodleException: Error from server + + Returns: + Any: Raw data (str) or dict + """ + if not self.url.endswith("/rest/server.php"): + raise UploadUrlException() + + params = { + "token": self.token, + "moodlewsrestformat": moodlewsrestformat, + "itemid": itemid, + "filepath": filepath, + } + try: + res = self.session.post( + self.url.replace("/rest/server.php", "/upload.php"), + params=params, + files={f"file_{i}": file for i, file in enumerate(files, start=1)}, + ) + except RequestException as e: + raise NetworkMoodleException(e) + if not res.ok or not res.text: + raise EmptyResponseException() + if res.ok and moodlewsrestformat == "json": + data = json.loads(res.text) + return self.process_response(data) + return res.text + def process_response(self, data: Any) -> Any: """Process data to handle exception or warnings diff --git a/moodle/moodle.py b/moodle/moodle.py index d64caa4..d1cc7e9 100644 --- a/moodle/moodle.py +++ b/moodle/moodle.py @@ -1,6 +1,7 @@ from typing import Any from moodle.mdl import Mdl +from moodle.upload import Upload from moodle.auth import Auth from moodle.block import Block @@ -20,6 +21,11 @@ def __init__(self, url: str, token: str): def __call__(self, wsfunction: str, moodlewsrestformat="json", **kwargs) -> Any: return self.post(wsfunction, moodlewsrestformat, **kwargs) + @property # type: ignore + @lazy + def upload(self) -> Upload: + return Upload(self) + @property # type: ignore @lazy def auth(self) -> Auth: diff --git a/moodle/upload/__init__.py b/moodle/upload/__init__.py new file mode 100644 index 0000000..8f5677d --- /dev/null +++ b/moodle/upload/__init__.py @@ -0,0 +1,8 @@ +from .file import File + +from .upload import Upload + +__all__ = [ + "File", + "Upload", +] diff --git a/moodle/upload/file.py b/moodle/upload/file.py new file mode 100644 index 0000000..3b35bd9 --- /dev/null +++ b/moodle/upload/file.py @@ -0,0 +1,32 @@ +from moodle.attr import dataclass + + +@dataclass +class File: + """File + + Args: + component (str): component + contextid (int): contextid + userid (str): userid + filearea (str): filearea + filename (str): filename + filepath (str): filepath + itemid (int): itemid + license (str): license + author (str): author + source (str): source + filesize (int): filesize + """ + + component: str + contextid: int + userid: str + filearea: str + filename: str + filepath: str + itemid: int + license: str + author: str + source: str + filesize: int diff --git a/moodle/upload/upload.py b/moodle/upload/upload.py new file mode 100644 index 0000000..c2126b0 --- /dev/null +++ b/moodle/upload/upload.py @@ -0,0 +1,23 @@ +from typing import List + +from moodle import BaseMoodle +from . import File + + +class Upload(BaseMoodle): + def __call__( + self, *files, itemid: int = 0, filepath: str = "/" + ) -> List[File]: + """Uploads files to moodle. + + Args: + files (file-like-objects, multiple): The files to upload. + moodlewsrestformat (str, optional): Expected format. Defaults to "json". + itemid (int, optional): itemid to upload into. Defaults to 0. + filepath (str, optional): file path under which to upload. Defaults to '/'. + + Returns: + List[File]: The details for the uploaded files + """ + data = self.moodle.post_upload(*files, itemid=itemid, filepath=filepath) + return self._trs(File, data) diff --git a/tests/test_upload.py b/tests/test_upload.py new file mode 100644 index 0000000..36ec87f --- /dev/null +++ b/tests/test_upload.py @@ -0,0 +1,14 @@ +from moodle import Moodle +from moodle.upload import File + + +class TestUpload: + def test_upload(self, moodle: Moodle): + files = moodle.upload( + ('LICENSE', open('LICENSE', 'rb')), + ) + assert type(files) == list + assert len(files) == 1 + for f in files: + assert isinstance(f, File) + assert f.filename == 'LICENSE' From 0f72f354ec483f485658295c37a052b33f1221a7 Mon Sep 17 00:00:00 2001 From: Clemens Koza Date: Fri, 25 Jul 2025 22:59:55 +0200 Subject: [PATCH 3/3] fix error handling logic --- moodle/mdl.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/moodle/mdl.py b/moodle/mdl.py index e4e6590..981d3cf 100644 --- a/moodle/mdl.py +++ b/moodle/mdl.py @@ -166,7 +166,14 @@ def process_response(self, data: Any) -> Any: elif isinstance(data["warnings"], dict): warning = MoodleWarning(**data["warnings"]) # type: ignore self.logger.warning(str(warning)) - if "exception" in data or "errorcode" in data: + if "error" in data: + # upload.php errors are sometimes structured a bit differently + error = data.pop("error") + data.pop("stacktrace") + data.pop("reproductionlink") + data["message"] = error + raise MoodleException(**data) # type: ignore + elif "exception" in data or "errorcode" in data: raise MoodleException(**data) # type: ignore return data