moodle-scraper 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.
Files changed (32) hide show
  1. moodle_scraper-0.1.0/.env.example +30 -0
  2. moodle_scraper-0.1.0/.github/workflows/ci.yml +36 -0
  3. moodle_scraper-0.1.0/.github/workflows/release.yml +47 -0
  4. moodle_scraper-0.1.0/.gitignore +30 -0
  5. moodle_scraper-0.1.0/LICENSE +21 -0
  6. moodle_scraper-0.1.0/PKG-INFO +335 -0
  7. moodle_scraper-0.1.0/README.md +298 -0
  8. moodle_scraper-0.1.0/pyproject.toml +85 -0
  9. moodle_scraper-0.1.0/src/moodle_scraper/__init__.py +7 -0
  10. moodle_scraper-0.1.0/src/moodle_scraper/__main__.py +8 -0
  11. moodle_scraper-0.1.0/src/moodle_scraper/cli.py +435 -0
  12. moodle_scraper-0.1.0/src/moodle_scraper/config.py +130 -0
  13. moodle_scraper-0.1.0/src/moodle_scraper/discovery.py +552 -0
  14. moodle_scraper-0.1.0/src/moodle_scraper/downloader.py +500 -0
  15. moodle_scraper-0.1.0/src/moodle_scraper/files.py +272 -0
  16. moodle_scraper-0.1.0/src/moodle_scraper/manifest.py +225 -0
  17. moodle_scraper-0.1.0/src/moodle_scraper/models.py +110 -0
  18. moodle_scraper-0.1.0/src/moodle_scraper/session.py +234 -0
  19. moodle_scraper-0.1.0/tests/conftest.py +45 -0
  20. moodle_scraper-0.1.0/tests/fixtures/course_named_page.html +14 -0
  21. moodle_scraper-0.1.0/tests/fixtures/course_page.html +59 -0
  22. moodle_scraper-0.1.0/tests/fixtures/folder_page.html +27 -0
  23. moodle_scraper-0.1.0/tests/fixtures/login_page.html +22 -0
  24. moodle_scraper-0.1.0/tests/fixtures/my_courses.html +25 -0
  25. moodle_scraper-0.1.0/tests/fixtures/resource_page.html +16 -0
  26. moodle_scraper-0.1.0/tests/fixtures/url_page.html +11 -0
  27. moodle_scraper-0.1.0/tests/test_cli.py +32 -0
  28. moodle_scraper-0.1.0/tests/test_discovery.py +395 -0
  29. moodle_scraper-0.1.0/tests/test_downloader.py +370 -0
  30. moodle_scraper-0.1.0/tests/test_files.py +272 -0
  31. moodle_scraper-0.1.0/tests/test_manifest.py +153 -0
  32. moodle_scraper-0.1.0/tests/test_session.py +169 -0
@@ -0,0 +1,30 @@
1
+ # Copy to `.env` and fill in. `.env` is git-ignored — never commit a real cookie.
2
+ #
3
+ # The MoodleSession cookie is a secret: anyone holding it can act as you until the
4
+ # session times out (8 hours on learn.haw-kiel.de). Treat it like a password.
5
+
6
+ # Base URL of the Moodle instance.
7
+ MOODLE_BASE_URL=https://learn.haw-kiel.de
8
+
9
+ # Option A: paste the cookie value (or a full "Cookie:" header) directly.
10
+ # MOODLE_COOKIE=MoodleSession=replace-me
11
+
12
+ # Option B: point at an exported Netscape cookies.txt file.
13
+ # MOODLE_COOKIE_FILE=./cookies.txt
14
+
15
+ # Where mirrored course files and manifest.json files are written.
16
+ MOODLE_OUT=data
17
+
18
+ # Politeness: seconds between requests, and parallel downloads (1 = sequential).
19
+ MOODLE_DELAY=0.5
20
+ MOODLE_CONCURRENCY=1
21
+
22
+ # Per-request timeout in seconds. Raise this if the server answers slowly
23
+ # (learn.haw-kiel.de dashboard pages can take well over 30 seconds).
24
+ MOODLE_TIMEOUT=30
25
+
26
+ # Optional byte cap per file; unset/0 means unlimited.
27
+ # MOODLE_MAX_FILE_SIZE=0
28
+
29
+ # Set to 1 only behind an intercepting campus proxy that breaks TLS verification.
30
+ MOODLE_INSECURE=0
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Test (Python ${{ matrix.python-version }})
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install package with dev dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Lint (ruff)
28
+ run: |
29
+ ruff check .
30
+ ruff format --check .
31
+
32
+ - name: Type check (mypy)
33
+ run: mypy src
34
+
35
+ - name: Test (pytest)
36
+ run: pytest -q
@@ -0,0 +1,47 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build distribution
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.13"
18
+
19
+ - name: Build sdist and wheel
20
+ run: |
21
+ pip install build twine
22
+ python -m build
23
+ twine check dist/*
24
+
25
+ - name: Upload distribution artifact
26
+ uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ name: Publish to PyPI
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ # Required for PyPI trusted publishing (OIDC) — no API token stored.
38
+ id-token: write
39
+ steps:
40
+ - name: Download distribution artifact
41
+ uses: actions/download-artifact@v4
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+
46
+ - name: Publish
47
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,30 @@
1
+ .venv/
2
+ venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ .eggs/
7
+
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ dist/
15
+ build/
16
+
17
+ # Secrets and scraped content must never be committed
18
+ .env
19
+ *.session
20
+ *.part
21
+ data/
22
+
23
+ # Kilo-managed worktrees
24
+ .kilo/
25
+
26
+ # Editors / OS
27
+ .vscode/
28
+ .idea/
29
+ Thumbs.db
30
+ desktop.ini
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simon Schwärzler
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,335 @@
1
+ Metadata-Version: 2.5
2
+ Name: moodle-scraper
3
+ Version: 0.1.0
4
+ Summary: Mirror the course files of your learn.haw-kiel.de Moodle courses into a local, resumable folder tree.
5
+ Project-URL: Homepage, https://github.com/RTXC01/moodle_scrape
6
+ Project-URL: Repository, https://github.com/RTXC01/moodle_scrape
7
+ Project-URL: Issues, https://github.com/RTXC01/moodle_scrape/issues
8
+ Author-email: RTXC1 <audiundlambofan@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backup,course-files,e-learning,haw-kiel,moodle,scraper
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Education
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: beautifulsoup4>=4.12
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: lxml>=5.2
26
+ Requires-Dist: python-dotenv>=1.0
27
+ Requires-Dist: rich>=13.7
28
+ Requires-Dist: typer>=0.12
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2; extra == 'dev'
31
+ Requires-Dist: mypy>=1.11; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
33
+ Requires-Dist: pytest>=8.2; extra == 'dev'
34
+ Requires-Dist: ruff>=0.6; extra == 'dev'
35
+ Requires-Dist: twine>=5.1; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # moodle-scraper
39
+
40
+ [![CI](https://github.com/RTXC01/moodle_scrape/actions/workflows/ci.yml/badge.svg)](https://github.com/RTXC01/moodle_scrape/actions/workflows/ci.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/moodle-scraper)](https://pypi.org/project/moodle-scraper/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/moodle-scraper)](https://pypi.org/project/moodle-scraper/)
43
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
44
+
45
+ Mirror the **course files** of your enrolled Moodle courses on
46
+ [`learn.haw-kiel.de`](https://learn.haw-kiel.de) into a local folder tree, with a per-course
47
+ `manifest.json` that makes repeat runs incremental and resumable.
48
+
49
+ It is a personal, read-only backup tool: it downloads the files you already have access to, and
50
+ never touches grades, submissions, forums, quizzes, or messages.
51
+
52
+ Built for HAW Kiel's Moodle instance, but any Moodle base URL works (`MOODLE_BASE_URL`).
53
+
54
+ ## What it does
55
+
56
+ - Discovers your courses (dashboard where available, or directly by course id/URL).
57
+ - Walks each course's sections and activities and resolves the file links behind
58
+ `mod_resource`, `mod_folder`, `mod_url`, `mod_page`, `mod_book` and `mod_imscp`.
59
+ - Mirrors those files under `data/<shortname>-<id>/<NN>-<section>/<activity>/...`.
60
+ - Records size / `Last-Modified` / ETag / optional SHA-1 in `manifest.json`.
61
+ - On later runs it skips unchanged files, resumes interrupted `.part` downloads, and can prune
62
+ files that were removed upstream.
63
+
64
+ ## What it does *not* do
65
+
66
+ Forums, grades, assignment submissions, quizzes, calendar, messages, streaming video/HLS capture,
67
+ HTML→Markdown conversion, scheduling, or any upload/POST — all out of scope. It also does not
68
+ automate SSO login.
69
+
70
+ ## Legal and ethical use
71
+
72
+ - Only use this for content you are **already enrolled in**, as a **personal backup**. Do not
73
+ redistribute any downloaded material.
74
+ - The tool only issues `GET`/`HEAD` requests. It never posts, submits, or grades anything.
75
+ - Defaults are deliberately polite: sequential requests, a `0.5s` delay, one download at a time,
76
+ exponential backoff on `429`/`5xx`, and an identifying `User-Agent`. Please keep the delay unless
77
+ you have a reason not to.
78
+ - Your `MoodleSession` cookie is a secret. It is read from `.env`/environment/a cookie file, never
79
+ logged, and never written into a manifest. `.env` and `data/` are git-ignored.
80
+
81
+ ## Requirements
82
+
83
+ - Python **3.11+**
84
+ - A browser where you are logged in to your Moodle instance.
85
+
86
+ ## Install
87
+
88
+ From PyPI:
89
+
90
+ ```powershell
91
+ pip install moodle-scraper
92
+ ```
93
+
94
+ From source:
95
+
96
+ ```powershell
97
+ git clone https://github.com/RTXC01/moodle_scrape.git moodle_scrape
98
+ cd moodle_scrape
99
+ python -m venv .venv
100
+ .\.venv\Scripts\Activate.ps1
101
+ pip install -e ".[dev]"
102
+ ```
103
+
104
+ Then copy the example config and fill in your cookie:
105
+
106
+ ```powershell
107
+ Copy-Item .env.example .env
108
+ ```
109
+
110
+ ## Cookie export
111
+
112
+ The tool reuses the browser session you already have; it never asks for your password and does not
113
+ drive SSO. Export the `MoodleSession` cookie and put it in `.env` as `MOODLE_COOKIE` (or save a
114
+ Netscape `cookies.txt` and point `MOODLE_COOKIE_FILE` at it).
115
+
116
+ ### Chrome / Edge
117
+
118
+ 1. Log in at <https://learn.haw-kiel.de>.
119
+ 2. Open DevTools (`F12`) → **Application** → **Storage** → **Cookies** →
120
+ `https://learn.haw-kiel.de`.
121
+ 3. Copy the **Value** of the `MoodleSession` cookie.
122
+ 4. Put it in `.env`:
123
+
124
+ ```dotenv
125
+ MOODLE_COOKIE=MoodleSession=<paste-value>
126
+ ```
127
+
128
+ A bare value also works (`MOODLE_COOKIE=<paste-value>`), as does a full header
129
+ (`MOODLE_COOKIE=Cookie: MoodleSession=...`).
130
+
131
+ ### Firefox
132
+
133
+ 1. Log in at <https://learn.haw-kiel.de>.
134
+ 2. DevTools (`F12`) → **Storage** → **Cookies** → `https://learn.haw-kiel.de` → copy the
135
+ `MoodleSession` **Value**.
136
+ 3. Paste it into `.env` as above. Alternatively, use a "cookies.txt" export extension, save the
137
+ Netscape file, and set `MOODLE_COOKIE_FILE=./cookies.txt`.
138
+
139
+ > The Moodle session times out after ~8 hours (`sessiontimeout: 28800`). When it expires, commands
140
+ > exit with code `3` and tell you to re-export the cookie.
141
+
142
+ ## Commands
143
+
144
+ ```text
145
+ moodle-scrape doctor
146
+ moodle-scrape courses
147
+ moodle-scrape files <course>
148
+ moodle-scrape sync [--course ID]... [options]
149
+ ```
150
+
151
+ ### `doctor`
152
+
153
+ Validates the cookie, prints the logged-in account and how many courses are visible.
154
+ Exit code `0` when the session is good, `3` when it is expired or invalid.
155
+
156
+ ```powershell
157
+ moodle-scrape doctor
158
+ ```
159
+
160
+ ### `courses`
161
+
162
+ Prints a table of enrolled courses (id, shortname, fullname, discovered file count).
163
+
164
+ ```powershell
165
+ moodle-scrape courses
166
+ ```
167
+
168
+ ### `files`
169
+
170
+ Dry discovery for one course: lists the files that *would* be downloaded, without downloading.
171
+
172
+ ```powershell
173
+ moodle-scrape files 12345
174
+ moodle-scrape files https://learn.haw-kiel.de/course/view.php?id=12345
175
+ ```
176
+
177
+ ### `sync`
178
+
179
+ The main command.
180
+
181
+ ```text
182
+ --course ID|URL Repeatable. Defaults to all enrolled courses.
183
+ --out DIR Output directory (default: data).
184
+ --delay SECONDS Delay between requests (default: 0.5).
185
+ --timeout SECONDS Per-request timeout (default: 30; raise it for slow servers).
186
+ --concurrency N Parallel downloads (default: 1 = sequential).
187
+ --dry-run Resolve and report everything, download nothing.
188
+ --prune Delete local files that no longer exist upstream.
189
+ --checksum Compute SHA-1 while downloading (slower).
190
+ --max-file-size N Skip files larger than N bytes (default: unlimited).
191
+ --insecure Disable TLS verification (only behind a broken campus proxy).
192
+ --verbose Verbose logging; the cookie is always redacted.
193
+ ```
194
+
195
+ Examples:
196
+
197
+ ```powershell
198
+ # Everything, politely
199
+ moodle-scrape sync
200
+
201
+ # One course, preview only
202
+ moodle-scrape sync --course 12345 --dry-run
203
+
204
+ # Two courses, checksums, prune removed files
205
+ moodle-scrape sync --course 12345 --course 67890 --checksum --prune
206
+ ```
207
+
208
+ > **Slow dashboard?** Some Moodle instances answer very slowly under load. The tool retries
209
+ > transient login bounces and transport errors automatically; for this site a timeout of
210
+ > `MOODLE_TIMEOUT=180` in `.env` is a good idea. Single-course runs fetch the course page directly
211
+ > and skip the dashboard entirely.
212
+
213
+ ## Output layout
214
+
215
+ ```text
216
+ data/
217
+ Analysis-12345/ # <shortname>-<id>
218
+ 00-General/
219
+ Slides.pdf
220
+ Scripts/
221
+ chapter1.pdf # nested folder structure preserved
222
+ 01-Week 1/
223
+ Exercises/
224
+ Notes.txt
225
+ _external/ # recorded links, not downloaded
226
+ manifest.json
227
+ Physics-67890/
228
+ ...
229
+ manifest.json
230
+ ```
231
+
232
+ Filenames and directories are sanitised for Windows (`<>:"/\|?*`, control characters, trailing
233
+ dots/spaces, reserved device names such as `CON`/`PRN`, over-long components, and path-length
234
+ limits). If two different URLs would map to the same path, a short deterministic hash
235
+ (`__<8 hex chars>`) is inserted before the extension.
236
+
237
+ ## Manifest format
238
+
239
+ One `manifest.json` per course, written atomically after the course finishes (and rewritten after
240
+ partial failures so a rerun resumes instead of restarting):
241
+
242
+ ```json
243
+ {
244
+ "schema_version": 1,
245
+ "course": { "id": 12345, "shortname": "Analysis", "fullname": "Analysis I" },
246
+ "generated_at": "2026-09-15T19:30:00+00:00",
247
+ "files": [
248
+ {
249
+ "rel_path": "Analysis-12345/00-General/Slides.pdf",
250
+ "url": "https://learn.haw-kiel.de/pluginfile.php/1/mod_resource/content/0/Slides.pdf",
251
+ "section": "General",
252
+ "activity": "",
253
+ "filename": "Slides.pdf",
254
+ "size": 1048576,
255
+ "last_modified": "Wed, 03 Sep 2026 10:00:00 GMT",
256
+ "etag": "\"abc123\"",
257
+ "sha1": null,
258
+ "downloaded": true,
259
+ "external": false,
260
+ "status": "downloaded",
261
+ "fetched_at": "2026-09-15T19:31:00+00:00"
262
+ }
263
+ ]
264
+ }
265
+ ```
266
+
267
+ `status` is one of `pending`, `downloaded`, `skipped`, `missing`, `failed`, `external`,
268
+ `unsupported`. `external: true` entries are links pointing outside the Moodle host; they are
269
+ recorded, never fetched.
270
+
271
+ ## Exit codes
272
+
273
+ | Code | Meaning |
274
+ | --- | --- |
275
+ | `0` | Success |
276
+ | `1` | Unexpected error |
277
+ | `2` | Bad usage or configuration (no cookie, bad values, missing cookie file) |
278
+ | `3` | Session expired or invalid |
279
+
280
+ ## Incremental behaviour
281
+
282
+ 1. If the manifest entry for a URL matches the local file's size (and, when available,
283
+ `Last-Modified`/ETag via a conditional request), the file is **skipped**.
284
+ 2. Otherwise the file is downloaded to `<name>.part`, flushed, then atomically renamed. If the run
285
+ is interrupted, the `.part` file stays and the next run resumes it with a `Range` request when
286
+ the server supports it.
287
+ 3. `404` is recorded as `missing` and the run continues. `401`/`403` triggers a session check and
288
+ aborts the run.
289
+
290
+ ## Development
291
+
292
+ ```powershell
293
+ pip install -e ".[dev]"
294
+ ruff check .
295
+ ruff format --check .
296
+ mypy src
297
+ pytest -q
298
+ ```
299
+
300
+ Tests are fully offline: HTML fixtures under `tests/fixtures/` plus `httpx.MockTransport` cover
301
+ discovery, path sanitisation, manifest diffing/pruning, cookie parsing, session-expiry detection
302
+ and retry/backoff behaviour.
303
+
304
+ ### Releasing (maintainers)
305
+
306
+ 1. Bump `__version__` in `src/moodle_scraper/__init__.py`.
307
+ 2. Commit and tag: `git tag v0.1.0 && git push origin v0.1.0` — the
308
+ [release workflow](.github/workflows/release.yml) builds the sdist/wheel, runs `twine check`,
309
+ and publishes to PyPI via [trusted publishing](https://docs.pypi.org/trusted-publishers/)
310
+ (configure the PyPI project once: owner `RTXC01`, repository `moodle_scrape`, workflow
311
+ `release.yml`, environment `pypi`).
312
+ 3. Manual alternative: `python -m build && twine upload dist/*`.
313
+
314
+ ## Troubleshooting
315
+
316
+ - **Exit code 3 / "session expired"** — log in again in your browser and re-export the cookie.
317
+ - **A course page returns no files** — Moodle markup varies by version/theme. Run
318
+ `moodle-scrape files <id> --verbose` and check whether the activity selectors in
319
+ `discovery.py` match. All selectors live in that one module.
320
+ - **A single activity is skipped** — some activities bounce to login even with a valid session
321
+ (insufficient rights); they are skipped individually instead of aborting the run. Run with
322
+ `--verbose` to see which.
323
+ - **Files look larger than expected / slow** — use `--max-file-size` to skip big video files, and
324
+ keep `--concurrency 1`.
325
+ - **TLS errors behind a campus proxy** — `--insecure` disables verification for that run only.
326
+
327
+ ## Roadmap
328
+
329
+ - `browser_cookie3` browser-store extraction as an optional extra.
330
+ - Optionally record `mod_page`/`mod_book` text content in the manifest (today only their
331
+ attachments are collected).
332
+
333
+ ## License
334
+
335
+ [MIT](LICENSE)