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.
- moodle_cli-0.1.0/.github/workflows/ci.yml +61 -0
- moodle_cli-0.1.0/.github/workflows/publish.yml +61 -0
- moodle_cli-0.1.0/.github/workflows/release.yml +59 -0
- moodle_cli-0.1.0/.gitignore +12 -0
- moodle_cli-0.1.0/AGENTS.md +1 -0
- moodle_cli-0.1.0/CLAUDE.md +72 -0
- moodle_cli-0.1.0/LICENSE +21 -0
- moodle_cli-0.1.0/PKG-INFO +122 -0
- moodle_cli-0.1.0/README.md +82 -0
- moodle_cli-0.1.0/config.example.yaml +1 -0
- moodle_cli-0.1.0/moodle_cli/__init__.py +3 -0
- moodle_cli-0.1.0/moodle_cli/auth.py +74 -0
- moodle_cli-0.1.0/moodle_cli/cli.py +198 -0
- moodle_cli-0.1.0/moodle_cli/client.py +206 -0
- moodle_cli-0.1.0/moodle_cli/config.py +161 -0
- moodle_cli-0.1.0/moodle_cli/constants.py +22 -0
- moodle_cli-0.1.0/moodle_cli/exceptions.py +17 -0
- moodle_cli-0.1.0/moodle_cli/formatter.py +90 -0
- moodle_cli-0.1.0/moodle_cli/models.py +85 -0
- moodle_cli-0.1.0/moodle_cli/output.py +17 -0
- moodle_cli-0.1.0/moodle_cli/parser.py +57 -0
- moodle_cli-0.1.0/moodle_cli/scraper.py +186 -0
- moodle_cli-0.1.0/moodle_cli/update_check.py +67 -0
- moodle_cli-0.1.0/pyproject.toml +39 -0
- moodle_cli-0.1.0/tests/test_cli_e2e.py +580 -0
- moodle_cli-0.1.0/uv.lock +598 -0
|
@@ -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 @@
|
|
|
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.
|
moodle_cli-0.1.0/LICENSE
ADDED
|
@@ -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,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
|
+
)
|