diff --git a/src/py_moodle/cli/app.py b/src/py_moodle/cli/app.py index 855ad5a..c0a3164 100644 --- a/src/py_moodle/cli/app.py +++ b/src/py_moodle/cli/app.py @@ -17,6 +17,7 @@ pages, resources, sections, + site, urls, users, ) @@ -61,6 +62,7 @@ def main( app.add_typer(pages.app, name="pages") app.add_typer(resources.app, name="resources") app.add_typer(urls.app, name="urls") +app.add_typer(site.app, name="site") # ...and so on for each new command group you create. diff --git a/src/py_moodle/cli/site.py b/src/py_moodle/cli/site.py new file mode 100644 index 0000000..74bb0fa --- /dev/null +++ b/src/py_moodle/cli/site.py @@ -0,0 +1,54 @@ +"""Site commands for ``py-moodle``.""" + +import json + +import typer +from rich.console import Console +from rich.table import Table + +from py_moodle.session import MoodleSession +from py_moodle.site import get_site_info + +app = typer.Typer(help="Get site information.") +console = Console() + + +@app.command("info") +def info( + ctx: typer.Context, + as_json: bool = typer.Option(False, "--json", help="Output as JSON."), +): + """Get site info.""" + ms = MoodleSession.get(ctx.obj["env"]) + site_info = get_site_info(ms) + + if as_json: + # We need to handle the dataclasses in the site_info object + # to make them serializable. + class DataclassEncoder(json.JSONEncoder): + def default(self, o): + from dataclasses import asdict, is_dataclass + + if is_dataclass(o): + return asdict(o) + return super().default(o) + + console.print( + json.dumps(site_info, indent=4, cls=DataclassEncoder, ensure_ascii=False) + ) + return + + table = Table(title="Site Info") + table.add_column("Property", style="cyan") + table.add_column("Value", style="magenta") + + for key, value in site_info.__dict__.items(): + if isinstance(value, list): + table.add_row(key, str(len(value)) + " items") + else: + table.add_row(key, str(value)) + + console.print(table) + + +__all__ = ["app"] diff --git a/src/py_moodle/session.py b/src/py_moodle/session.py index 1aee3ae..d38b7c5 100644 --- a/src/py_moodle/session.py +++ b/src/py_moodle/session.py @@ -12,7 +12,9 @@ if TYPE_CHECKING: from .settings import Settings -from .auth import login +from typing import Any, Dict, Optional + +from .auth import LoginError, login class MoodleSessionError(RuntimeError): @@ -96,6 +98,45 @@ def token(self) -> str | None: self._login() return self._token + def call( + self, + wsfunction: str, + params: Optional[Dict[str, Any]] = None, + ) -> Any: + """Makes a call to the Moodle webservice API.""" + if not self.token: + raise LoginError( + "Cannot make a webservice call without a token. " + "Did you login correctly?" + ) + + if params is None: + params = {} + + request_params = { + "moodlewsrestformat": "json", + "wsfunction": wsfunction, + "wstoken": self.token, + **params, + } + + response = self.session.post( + f"{self.settings.url}/webservice/rest/server.php", + params=request_params, + timeout=30, + ) + response.raise_for_status() + data = response.json() + # Check for Moodle-specific error response + if isinstance(data, dict) and ( + "exception" in data or "errorcode" in data or "message" in data + ): + raise MoodleSessionError( + f"Moodle API error: {data.get('message', 'Unknown error')} " + f"(errorcode: {data.get('errorcode', 'N/A')}, exception: {data.get('exception', 'N/A')})" + ) + return data + # ------------- factory ------------- @classmethod def get(cls, env: str | None = None) -> "MoodleSession": diff --git a/src/py_moodle/site.py b/src/py_moodle/site.py new file mode 100644 index 0000000..de42949 --- /dev/null +++ b/src/py_moodle/site.py @@ -0,0 +1,75 @@ +"""Site information.""" + +from dataclasses import dataclass +from typing import List + +from py_moodle.session import MoodleSession + + +@dataclass +class SiteFunction: + """A dataclass to represent a function available in the Moodle site.""" + + name: str + version: str + + +@dataclass +class AdvancedFeature: + """A dataclass to represent an advanced feature available in the Moodle site.""" + + name: str + value: int + + +@dataclass +class SiteInfo: + """A dataclass to represent the site information.""" + + sitename: str + username: str + firstname: str + lastname: str + fullname: str + lang: str + userid: int + siteurl: str + userpictureurl: str + functions: List[SiteFunction] + downloadfiles: int + uploadfiles: int + release: str + version: str + mobilecssurl: str + advancedfeatures: List[AdvancedFeature] + usercanmanageownfiles: bool + userquota: int + usermaxuploadfilesize: int + userhomepage: int + userprivateaccesskey: str + siteid: int + sitecalendartype: str + usercalendartype: str + userissiteadmin: bool + theme: str + limitconcurrentlogins: int + policyagreed: int + + +def get_site_info(session: MoodleSession) -> SiteInfo: + """Get site info. + + Args: + session (MoodleSession): The Moodle session. + + Returns: + SiteInfo: The site information. + """ + response = session.call("core_webservice_get_site_info") + response["functions"] = [ + SiteFunction(**function) for function in response["functions"] + ] + response["advancedfeatures"] = [ + AdvancedFeature(**feature) for feature in response["advancedfeatures"] + ] + return SiteInfo(**response) diff --git a/tests/test_site.py b/tests/test_site.py new file mode 100644 index 0000000..f1b2cd0 --- /dev/null +++ b/tests/test_site.py @@ -0,0 +1,41 @@ +"""Tests for the site module.""" + +from py_moodle.session import MoodleSession +from py_moodle.site import SiteInfo, get_site_info + + +def test_get_site_info_real(request): + """Test get_site_info in a real environment.""" + env = request.config.moodle_target.name + ms = MoodleSession.get(env) + site_info = get_site_info(ms) + + assert isinstance(site_info, SiteInfo) + assert site_info.sitename is not None + assert site_info.username is not None + assert site_info.firstname is not None + assert site_info.lastname is not None + assert site_info.fullname is not None + assert site_info.lang is not None + assert site_info.userid is not None + assert site_info.siteurl is not None + assert site_info.userpictureurl is not None + assert site_info.functions is not None + assert site_info.downloadfiles is not None + assert site_info.uploadfiles is not None + assert site_info.release is not None + assert site_info.version is not None + assert site_info.mobilecssurl is not None + assert site_info.advancedfeatures is not None + assert site_info.usercanmanageownfiles is not None + assert site_info.userquota is not None + assert site_info.usermaxuploadfilesize is not None + assert site_info.userhomepage is not None + assert site_info.userprivateaccesskey is not None + assert site_info.siteid is not None + assert site_info.sitecalendartype is not None + assert site_info.usercalendartype is not None + assert site_info.userissiteadmin is not None + assert site_info.theme is not None + assert site_info.limitconcurrentlogins is not None + assert site_info.policyagreed is not None