moodle-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ workflow_call:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ name: Test (Python ${{ matrix.python-version }})
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
20
+
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Set up uv
26
+ uses: astral-sh/setup-uv@v6
27
+
28
+ - name: Set up Python
29
+ uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+
33
+ - name: Sync dependencies
34
+ run: uv sync --locked
35
+
36
+ - name: Compile package
37
+ run: uv run python -m compileall moodle_cli
38
+
39
+ - name: Smoke test CLI
40
+ run: uv run moodle --help
41
+
42
+ build:
43
+ name: Build distribution
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - name: Checkout
47
+ uses: actions/checkout@v4
48
+
49
+ - name: Set up uv
50
+ uses: astral-sh/setup-uv@v6
51
+
52
+ - name: Set up Python
53
+ uses: actions/setup-python@v5
54
+ with:
55
+ python-version: "3.12"
56
+
57
+ - name: Build package
58
+ run: uv build
59
+
60
+ - name: Check distribution metadata
61
+ run: uvx twine check dist/*
@@ -0,0 +1,61 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ verify:
13
+ uses: ./.github/workflows/ci.yml
14
+
15
+ publish:
16
+ needs: verify
17
+ runs-on: ubuntu-latest
18
+ environment:
19
+ name: pypi
20
+ url: https://pypi.org/project/moodle-cli/
21
+ permissions:
22
+ id-token: write
23
+ contents: read
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+
32
+ - name: Setup uv
33
+ uses: astral-sh/setup-uv@v6
34
+
35
+ - name: Verify tag matches package version
36
+ run: |
37
+ python - <<'PY'
38
+ import os
39
+ import pathlib
40
+ import tomllib
41
+
42
+ project = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]
43
+ name = project["name"]
44
+ version = project["version"]
45
+ tag = os.environ["GITHUB_REF_NAME"]
46
+ expected = f"v{version}"
47
+ if name != "moodle-cli":
48
+ raise SystemExit(f"Refusing to publish unexpected package name: {name}")
49
+ if tag != expected:
50
+ raise SystemExit(f"Tag {tag} does not match package version {expected}")
51
+ print(f"Validated release tag {tag} for {name}")
52
+ PY
53
+
54
+ - name: Build package
55
+ run: uv build
56
+
57
+ - name: Check distribution metadata
58
+ run: uvx twine check dist/*
59
+
60
+ - name: Publish to PyPI
61
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,59 @@
1
+ name: Publish GitHub Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ verify:
13
+ uses: ./.github/workflows/ci.yml
14
+
15
+ release:
16
+ needs: verify
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: write
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ - name: Setup uv
29
+ uses: astral-sh/setup-uv@v6
30
+
31
+ - name: Verify tag matches package version
32
+ run: |
33
+ python - <<'PY'
34
+ import os
35
+ import pathlib
36
+ import tomllib
37
+
38
+ project = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]
39
+ name = project["name"]
40
+ version = project["version"]
41
+ tag = os.environ["GITHUB_REF_NAME"]
42
+ expected = f"v{version}"
43
+ if name != "moodle-cli":
44
+ raise SystemExit(f"Refusing to release unexpected package name: {name}")
45
+ if tag != expected:
46
+ raise SystemExit(f"Tag {tag} does not match package version {expected}")
47
+ print(f"Validated release tag {tag} for {name}")
48
+ PY
49
+
50
+ - name: Build package
51
+ run: uv build
52
+
53
+ - name: Publish GitHub release
54
+ uses: softprops/action-gh-release@v2
55
+ with:
56
+ generate_release_notes: true
57
+ files: |
58
+ dist/*.tar.gz
59
+ dist/*.whl
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ config.yaml
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ *.pyd
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ dist/
11
+ build/
12
+ *.egg-info/
@@ -0,0 +1 @@
1
+ CLAUDE.md
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Agent Flags
6
+
7
+ ```yaml
8
+ agent_flags:
9
+ token_economy: prioritize
10
+ response_style: terse
11
+ planning_style: minimal
12
+ ```
13
+
14
+ Interpret these as defaults:
15
+
16
+ - Prefer the shortest sufficient response.
17
+ - Avoid long preambles and repeated summaries.
18
+ - Ask questions only when a wrong assumption is likely to be costly.
19
+ - Make the smallest maintainable change that solves the task.
20
+
21
+ ## Build & Run
22
+
23
+ ```bash
24
+ uv sync # install dependencies
25
+ uv run moodle --help # run CLI
26
+ uv run moodle -v user # verbose mode (debug logging)
27
+ ```
28
+
29
+ Entry point: `moodle_cli.cli:main` (registered as `moodle` console script in pyproject.toml).
30
+
31
+ No tests exist yet. No linter/formatter is configured.
32
+
33
+ ## Architecture
34
+
35
+ Terminal CLI for Moodle LMS that piggybacks on the user's browser session — no API tokens needed.
36
+
37
+ ### API Strategy
38
+
39
+ Uses Moodle's **internal AJAX endpoint** (`/lib/ajax/service.php`), not the official Web Services token API. This endpoint accepts the `MoodleSession` browser cookie, same as the Moodle web UI. The client first loads an authenticated page to resolve `sesskey`, then tries AJAX APIs and falls back to page scraping when site-specific Moodle restrictions disable some services.
40
+
41
+ Request format: `POST /lib/ajax/service.php?sesskey={sesskey}&info={function_name}` with JSON body `[{"index": 0, "methodname": "...", "args": {...}}]`. Response: `[{"error": false, "data": ...}]`.
42
+
43
+ ### Data Flow
44
+
45
+ ```
46
+ auth.py (get cookie) → client.py (API calls) → parser.py (JSON→models) → formatter.py/output.py (display)
47
+ ↑ ↑
48
+ env var or sesskey auto-obtained
49
+ browser-cookie3 from get_site_info()
50
+ ```
51
+
52
+ - **cli.py**: Click command group. `main()` wraps `cli(standalone_mode=False)` to handle `AuthError`/`MoodleAPIError` cleanly. Client is lazily created via `ctx.obj["get_client"]()` closure — auth only happens when a command actually needs the API.
53
+ - **auth.py**: Priority: `MOODLE_SESSION` env var → browser-cookie3 extraction (Chrome, Firefox, Brave, Edge). Arc browser is not supported by browser-cookie3.
54
+ - **client.py**: `MoodleClient` — holds session cookie, auto-obtains `sesskey`+`userid` on first API call via `_ensure_session()`.
55
+ - **models.py**: Dataclasses (`UserInfo`, `Course`, `Section`, `Activity`) with `to_dict()` for serialization.
56
+ - **parser.py**: Pure functions transforming Moodle JSON dicts → model instances.
57
+ - **formatter.py**: Rich tables (courses) and trees (course sections→activities).
58
+ - **output.py**: `--json`/`--yaml` structured output to stdout.
59
+ - **config.py**: Loads `config.yaml` from CWD or `~/.config/moodle-cli/`. If no `base_url` is configured, it prompts the user, validates the root URL, probes the site, and saves the result. `MOODLE_BASE_URL` env var overrides.
60
+ - **constants.py**: API paths, function names, env var names.
61
+
62
+ ### Adding a New Command
63
+
64
+ 1. Add the Moodle AJAX function name to `constants.py`
65
+ 2. Add a method to `MoodleClient` in `client.py` (call `self._ensure_session()` first)
66
+ 3. Add model dataclass to `models.py`, parser function to `parser.py`
67
+ 4. Add Rich display function to `formatter.py`
68
+ 5. Add `@cli.command()` in `cli.py` with `--json`/`--yaml` options
69
+
70
+ ### Adding a New Moodle API Call
71
+
72
+ All API calls go through `MoodleClient._call(function_name, args)`. It handles the AJAX envelope format and error extraction. Just add a new public method that calls `self._call()` with the right function name.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bunizao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: moodle-cli
3
+ Version: 0.1.0
4
+ Summary: Terminal-first CLI for Moodle LMS
5
+ Project-URL: Homepage, https://github.com/bunizao/moodle-cli
6
+ Project-URL: Repository, https://github.com/bunizao/moodle-cli
7
+ Project-URL: Issues, https://github.com/bunizao/moodle-cli/issues
8
+ Author: bunizao
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 bunizao
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Requires-Python: >=3.10
32
+ Requires-Dist: beautifulsoup4>=4.12
33
+ Requires-Dist: browser-cookie3>=0.19
34
+ Requires-Dist: click>=8.0
35
+ Requires-Dist: packaging>=24.0
36
+ Requires-Dist: pyyaml>=6.0
37
+ Requires-Dist: requests>=2.28
38
+ Requires-Dist: rich>=13.0
39
+ Description-Content-Type: text/markdown
40
+
41
+ # moodle-cli
42
+
43
+ Terminal-first CLI for Moodle LMS that reuses an authenticated browser session.
44
+
45
+ ## Features
46
+
47
+ - No API token setup required
48
+ - Reads `MoodleSession` from your browser or `MOODLE_SESSION`
49
+ - Works with Moodle AJAX APIs and falls back to authenticated page scraping when needed
50
+ - Terminal output plus `--json` and `--yaml`
51
+
52
+ ## Requirements
53
+
54
+ - Python 3.10+
55
+ - `uv`
56
+ - An active Moodle browser session, or a `MOODLE_SESSION` environment variable
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ # Recommended: uv tool
62
+ uv tool install moodle-cli
63
+
64
+ # Alternative: pipx
65
+ pipx install moodle-cli
66
+ ```
67
+
68
+ Install from source:
69
+
70
+ ```bash
71
+ git clone https://github.com/bunizao/moodle-cli.git
72
+ cd moodle-cli
73
+ uv sync
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ ```bash
79
+ moodle --help
80
+ moodle user
81
+ moodle courses
82
+ moodle activities 34637
83
+ moodle update
84
+ ```
85
+
86
+ To upgrade after an update is available:
87
+
88
+ ```bash
89
+ uv tool upgrade moodle-cli
90
+ # or
91
+ pipx upgrade moodle-cli
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ On first run, if no `base_url` is configured, the CLI will prompt you and write it to `config.yaml` in the project directory or in `~/.config/moodle-cli/`:
97
+
98
+ ```yaml
99
+ base_url: https://school.example.edu
100
+ ```
101
+
102
+ Required format:
103
+
104
+ - Use a full root URL such as `https://school.example.edu`
105
+ - Do not include paths, query strings, or fragments
106
+ - Do not use URLs like `/login/index.php` or `/my/`
107
+ - The CLI validates the URL against Moodle's token endpoint and asks again if it does not look valid
108
+
109
+ You can also set `MOODLE_BASE_URL` instead of using the interactive prompt.
110
+ You can copy from `config.example.yaml`.
111
+
112
+ Environment overrides:
113
+
114
+ - `MOODLE_BASE_URL`
115
+ - `MOODLE_SESSION`
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ uv run python -m compileall moodle_cli
121
+ uv build
122
+ ```
@@ -0,0 +1,82 @@
1
+ # moodle-cli
2
+
3
+ Terminal-first CLI for Moodle LMS that reuses an authenticated browser session.
4
+
5
+ ## Features
6
+
7
+ - No API token setup required
8
+ - Reads `MoodleSession` from your browser or `MOODLE_SESSION`
9
+ - Works with Moodle AJAX APIs and falls back to authenticated page scraping when needed
10
+ - Terminal output plus `--json` and `--yaml`
11
+
12
+ ## Requirements
13
+
14
+ - Python 3.10+
15
+ - `uv`
16
+ - An active Moodle browser session, or a `MOODLE_SESSION` environment variable
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ # Recommended: uv tool
22
+ uv tool install moodle-cli
23
+
24
+ # Alternative: pipx
25
+ pipx install moodle-cli
26
+ ```
27
+
28
+ Install from source:
29
+
30
+ ```bash
31
+ git clone https://github.com/bunizao/moodle-cli.git
32
+ cd moodle-cli
33
+ uv sync
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ moodle --help
40
+ moodle user
41
+ moodle courses
42
+ moodle activities 34637
43
+ moodle update
44
+ ```
45
+
46
+ To upgrade after an update is available:
47
+
48
+ ```bash
49
+ uv tool upgrade moodle-cli
50
+ # or
51
+ pipx upgrade moodle-cli
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ On first run, if no `base_url` is configured, the CLI will prompt you and write it to `config.yaml` in the project directory or in `~/.config/moodle-cli/`:
57
+
58
+ ```yaml
59
+ base_url: https://school.example.edu
60
+ ```
61
+
62
+ Required format:
63
+
64
+ - Use a full root URL such as `https://school.example.edu`
65
+ - Do not include paths, query strings, or fragments
66
+ - Do not use URLs like `/login/index.php` or `/my/`
67
+ - The CLI validates the URL against Moodle's token endpoint and asks again if it does not look valid
68
+
69
+ You can also set `MOODLE_BASE_URL` instead of using the interactive prompt.
70
+ You can copy from `config.example.yaml`.
71
+
72
+ Environment overrides:
73
+
74
+ - `MOODLE_BASE_URL`
75
+ - `MOODLE_SESSION`
76
+
77
+ ## Development
78
+
79
+ ```bash
80
+ uv run python -m compileall moodle_cli
81
+ uv build
82
+ ```
@@ -0,0 +1 @@
1
+ base_url: https://school.example.edu
@@ -0,0 +1,3 @@
1
+ """Terminal-first CLI for Moodle LMS."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,74 @@
1
+ """Authentication: extract MoodleSession cookie from env or browser."""
2
+
3
+ import os
4
+ import logging
5
+ from urllib.parse import urlparse
6
+
7
+ from moodle_cli.constants import ENV_MOODLE_SESSION
8
+ from moodle_cli.exceptions import AuthError
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ def load_from_env() -> str | None:
14
+ """Check MOODLE_SESSION environment variable."""
15
+ value = os.environ.get(ENV_MOODLE_SESSION)
16
+ if value:
17
+ log.debug("Using MoodleSession from environment variable")
18
+ return value
19
+
20
+
21
+ def load_from_browser(domain: str) -> str | None:
22
+ """Extract MoodleSession cookie from browser cookie stores.
23
+
24
+ Tries Arc, Chrome, Brave, Edge, Firefox in order.
25
+ """
26
+ try:
27
+ import browser_cookie3 # noqa: F811
28
+ except ImportError:
29
+ log.warning("browser-cookie3 not installed; skipping browser cookie extraction")
30
+ return None
31
+
32
+ # browser-cookie3 loaders to try (in priority order)
33
+ loaders = [
34
+ ("Chrome", browser_cookie3.chrome),
35
+ ("Firefox", browser_cookie3.firefox),
36
+ ("Brave", browser_cookie3.brave),
37
+ ("Edge", browser_cookie3.edge),
38
+ ]
39
+
40
+ for name, loader in loaders:
41
+ try:
42
+ cj = loader(domain_name=domain)
43
+ for cookie in cj:
44
+ if cookie.name == "MoodleSession" and domain in (cookie.domain or ""):
45
+ log.debug("Found MoodleSession in %s", name)
46
+ return cookie.value
47
+ except Exception as exc:
48
+ log.debug("Could not read cookies from %s: %s", name, exc)
49
+
50
+ return None
51
+
52
+
53
+ def get_session(base_url: str) -> str:
54
+ """Get a valid MoodleSession cookie value.
55
+
56
+ Priority: env var → browser cookies.
57
+ Raises AuthError if no session is found.
58
+ """
59
+ # 1. Environment variable
60
+ session = load_from_env()
61
+ if session:
62
+ return session
63
+
64
+ # 2. Browser cookies
65
+ domain = urlparse(base_url).hostname or ""
66
+ session = load_from_browser(domain)
67
+ if session:
68
+ return session
69
+
70
+ raise AuthError(
71
+ "No MoodleSession found. Either:\n"
72
+ f" 1. Log in to {base_url} in your browser, or\n"
73
+ f" 2. Set the {ENV_MOODLE_SESSION} environment variable"
74
+ )