pyedookit 0.3.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 (33) hide show
  1. pyedookit-0.3.0/.github/workflows/ci.yml +16 -0
  2. pyedookit-0.3.0/.github/workflows/publish.yml +34 -0
  3. pyedookit-0.3.0/.gitignore +12 -0
  4. pyedookit-0.3.0/LICENSE +21 -0
  5. pyedookit-0.3.0/PKG-INFO +111 -0
  6. pyedookit-0.3.0/README.md +97 -0
  7. pyedookit-0.3.0/pyproject.toml +47 -0
  8. pyedookit-0.3.0/scripts/capture_fixtures.py +75 -0
  9. pyedookit-0.3.0/src/pyedookit/__init__.py +33 -0
  10. pyedookit-0.3.0/src/pyedookit/auth.py +385 -0
  11. pyedookit-0.3.0/src/pyedookit/client.py +107 -0
  12. pyedookit-0.3.0/src/pyedookit/exceptions.py +22 -0
  13. pyedookit-0.3.0/src/pyedookit/models.py +66 -0
  14. pyedookit-0.3.0/src/pyedookit/parsers/__init__.py +0 -0
  15. pyedookit-0.3.0/src/pyedookit/parsers/dates.py +29 -0
  16. pyedookit-0.3.0/src/pyedookit/parsers/events.py +84 -0
  17. pyedookit-0.3.0/src/pyedookit/parsers/fingerprint.py +26 -0
  18. pyedookit-0.3.0/src/pyedookit/parsers/payments.py +82 -0
  19. pyedookit-0.3.0/src/pyedookit/parsers/timetable.py +114 -0
  20. pyedookit-0.3.0/src/pyedookit/session.py +50 -0
  21. pyedookit-0.3.0/tests/__init__.py +0 -0
  22. pyedookit-0.3.0/tests/fakes.py +118 -0
  23. pyedookit-0.3.0/tests/fixtures/payments_structure.html +70 -0
  24. pyedookit-0.3.0/tests/fixtures/timetable_real_structure.html +57 -0
  25. pyedookit-0.3.0/tests/fixtures/upcoming_events_structure.html +62 -0
  26. pyedookit-0.3.0/tests/test_auth.py +322 -0
  27. pyedookit-0.3.0/tests/test_client.py +117 -0
  28. pyedookit-0.3.0/tests/test_dates.py +36 -0
  29. pyedookit-0.3.0/tests/test_events_parser.py +86 -0
  30. pyedookit-0.3.0/tests/test_fingerprint.py +27 -0
  31. pyedookit-0.3.0/tests/test_payments_parser.py +45 -0
  32. pyedookit-0.3.0/tests/test_timetable_parser.py +89 -0
  33. pyedookit-0.3.0/uv.lock +1113 -0
@@ -0,0 +1,16 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v6
13
+ - run: uv sync --group dev
14
+ - run: uv run pytest
15
+ - run: uv run ruff check .
16
+ - run: uv run mypy src
@@ -0,0 +1,34 @@
1
+ name: Publish to PyPI
2
+
3
+ # Uses PyPI trusted publishing: register this repo + workflow + the "pypi"
4
+ # environment as a trusted publisher on pypi.org first, no API token needed.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v6
15
+ - run: uv sync --group dev
16
+ - run: uv run pytest
17
+ - run: uv build
18
+ - uses: actions/upload-artifact@v4
19
+ with:
20
+ name: dist
21
+ path: dist/
22
+
23
+ publish:
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ permissions:
28
+ id-token: write
29
+ steps:
30
+ - uses: actions/download-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ *.egg-info/
9
+ .env
10
+ cookies.json
11
+ captures/
12
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tomas Liska
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,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyedookit
3
+ Version: 0.3.0
4
+ Summary: Async client for the Edookit parent portal (edookit.net)
5
+ Project-URL: Homepage, https://github.com/TFoxik/pyedookit
6
+ Author: tomas.liska
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: aiohttp>=3.9
11
+ Requires-Dist: beautifulsoup4>=4.12
12
+ Requires-Dist: lxml>=5.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # pyedookit
16
+
17
+ Unofficial async Python client for the Edookit parent portal
18
+ (`https://<school>.edookit.net`), used by Czech schools. Built for the
19
+ `ha-edookit` Home Assistant integration but usable standalone.
20
+
21
+ Edookit's official REST API exists but is only enabled per-school by an
22
+ administrator, so this library works the way a parent actually does: it logs
23
+ in through Plus4U (OIDC) and scrapes the portal's server-rendered pages.
24
+ There is no vendor support for this - selectors may break when Edookit
25
+ changes its markup.
26
+
27
+ ## Status
28
+
29
+ v0.3.0 - timetable, upcoming school events and payments. Verified end-to-end against a real portal
30
+ (login, session-settling after the OIDC callback, and the timetable parser
31
+ all confirmed working on real data).
32
+
33
+ **Only `week_offset=0` (the current week) is confirmed working.** The
34
+ portal accepts `week_offset=1` ("next week") without error, but returns the
35
+ same week's data back rather than actually advancing - the real
36
+ week-navigation parameter hasn't been identified yet. Since a school's
37
+ weekly timetable is typically the same all year, current-week-only is
38
+ usually enough in practice. See `scripts/capture_fixtures.py` for how to
39
+ capture and inspect real portal HTML if you want to chase this further (a
40
+ browser HAR trace of clicking the forward-week arrow is what's needed).
41
+
42
+ Cancelled/substituted lessons are also unverified: no real capture has had
43
+ one yet, so `Lesson.is_changed`/`is_cancelled` are always `False` for now.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install pyedookit
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ```python
54
+ import asyncio
55
+ from pyedookit import login, EdookitClient
56
+
57
+ async def main():
58
+ session = await login("zs-example", "parent@example.com", "hunter2")
59
+ client = EdookitClient(session)
60
+ for lesson in await client.get_timetable(week_offset=0):
61
+ print(lesson.start, lesson.subject, lesson.room)
62
+
63
+ asyncio.run(main())
64
+ ```
65
+
66
+ `client.get_events()` returns upcoming school events (`SchoolEvent`, all-day
67
+ events have `date` start/end with an inclusive end), and `client.get_payments()`
68
+ returns the family's payment totals and requests (`Payments`, amounts as
69
+ `Decimal`). Only paid requests have been seen on a real portal so far; a request
70
+ without the "accepted" marker is reported as unpaid.
71
+
72
+ `client.fetch_page(name)` returns the raw HTML of a page that has no parser yet
73
+ (`CAPTURE_PAGES`: assignments, exams, inbox, consents), and
74
+ `content_fingerprint(html)` tells you when such a page's content changed.
75
+
76
+ Persist `session.to_json()` / `Session.from_json()` between runs instead of
77
+ storing the password; use `pyedookit.refresh(session)` to renew the
78
+ short-lived edookit session from the long-lived Plus4U cookie, and fall back
79
+ to `login()` only when `refresh()` raises `AuthError`.
80
+
81
+ Lesson times are timezone-aware (`pyedookit.PORTAL_TZ`, Europe/Prague).
82
+
83
+ Errors all derive from `EdookitError`:
84
+
85
+ | Exception | Meaning | What to do |
86
+ |---|---|---|
87
+ | `SessionExpired` | short-lived edookit session aged out | `refresh()` and retry |
88
+ | `AuthError` | bad credentials, or the Plus4U cookie expired | full `login()` |
89
+ | `EdookitConnectionError` | network error or 30 s timeout | retry later |
90
+ | `ParseError` | portal markup changed | parser needs updating |
91
+
92
+ `login()`, `refresh()` and `EdookitClient` all accept an optional
93
+ `http: aiohttp.ClientSession` to reuse. Cookies are handled by hand, so
94
+ create that session with `cookie_jar=aiohttp.DummyCookieJar()`; a session
95
+ with a real cookie jar would also store and resend the portal's cookies.
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ uv sync --group dev
101
+ uv run pytest
102
+ uv run ruff check .
103
+ uv run mypy src
104
+ ```
105
+
106
+ ## Releasing
107
+
108
+ Bump `version` in `pyproject.toml`, then push a `vX.Y.Z` tag. The
109
+ `Publish to PyPI` workflow tests, builds and uploads using PyPI trusted
110
+ publishing, which has to be configured once on pypi.org for this repository
111
+ and the `pypi` environment.
@@ -0,0 +1,97 @@
1
+ # pyedookit
2
+
3
+ Unofficial async Python client for the Edookit parent portal
4
+ (`https://<school>.edookit.net`), used by Czech schools. Built for the
5
+ `ha-edookit` Home Assistant integration but usable standalone.
6
+
7
+ Edookit's official REST API exists but is only enabled per-school by an
8
+ administrator, so this library works the way a parent actually does: it logs
9
+ in through Plus4U (OIDC) and scrapes the portal's server-rendered pages.
10
+ There is no vendor support for this - selectors may break when Edookit
11
+ changes its markup.
12
+
13
+ ## Status
14
+
15
+ v0.3.0 - timetable, upcoming school events and payments. Verified end-to-end against a real portal
16
+ (login, session-settling after the OIDC callback, and the timetable parser
17
+ all confirmed working on real data).
18
+
19
+ **Only `week_offset=0` (the current week) is confirmed working.** The
20
+ portal accepts `week_offset=1` ("next week") without error, but returns the
21
+ same week's data back rather than actually advancing - the real
22
+ week-navigation parameter hasn't been identified yet. Since a school's
23
+ weekly timetable is typically the same all year, current-week-only is
24
+ usually enough in practice. See `scripts/capture_fixtures.py` for how to
25
+ capture and inspect real portal HTML if you want to chase this further (a
26
+ browser HAR trace of clicking the forward-week arrow is what's needed).
27
+
28
+ Cancelled/substituted lessons are also unverified: no real capture has had
29
+ one yet, so `Lesson.is_changed`/`is_cancelled` are always `False` for now.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install pyedookit
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ import asyncio
41
+ from pyedookit import login, EdookitClient
42
+
43
+ async def main():
44
+ session = await login("zs-example", "parent@example.com", "hunter2")
45
+ client = EdookitClient(session)
46
+ for lesson in await client.get_timetable(week_offset=0):
47
+ print(lesson.start, lesson.subject, lesson.room)
48
+
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ `client.get_events()` returns upcoming school events (`SchoolEvent`, all-day
53
+ events have `date` start/end with an inclusive end), and `client.get_payments()`
54
+ returns the family's payment totals and requests (`Payments`, amounts as
55
+ `Decimal`). Only paid requests have been seen on a real portal so far; a request
56
+ without the "accepted" marker is reported as unpaid.
57
+
58
+ `client.fetch_page(name)` returns the raw HTML of a page that has no parser yet
59
+ (`CAPTURE_PAGES`: assignments, exams, inbox, consents), and
60
+ `content_fingerprint(html)` tells you when such a page's content changed.
61
+
62
+ Persist `session.to_json()` / `Session.from_json()` between runs instead of
63
+ storing the password; use `pyedookit.refresh(session)` to renew the
64
+ short-lived edookit session from the long-lived Plus4U cookie, and fall back
65
+ to `login()` only when `refresh()` raises `AuthError`.
66
+
67
+ Lesson times are timezone-aware (`pyedookit.PORTAL_TZ`, Europe/Prague).
68
+
69
+ Errors all derive from `EdookitError`:
70
+
71
+ | Exception | Meaning | What to do |
72
+ |---|---|---|
73
+ | `SessionExpired` | short-lived edookit session aged out | `refresh()` and retry |
74
+ | `AuthError` | bad credentials, or the Plus4U cookie expired | full `login()` |
75
+ | `EdookitConnectionError` | network error or 30 s timeout | retry later |
76
+ | `ParseError` | portal markup changed | parser needs updating |
77
+
78
+ `login()`, `refresh()` and `EdookitClient` all accept an optional
79
+ `http: aiohttp.ClientSession` to reuse. Cookies are handled by hand, so
80
+ create that session with `cookie_jar=aiohttp.DummyCookieJar()`; a session
81
+ with a real cookie jar would also store and resend the portal's cookies.
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ uv sync --group dev
87
+ uv run pytest
88
+ uv run ruff check .
89
+ uv run mypy src
90
+ ```
91
+
92
+ ## Releasing
93
+
94
+ Bump `version` in `pyproject.toml`, then push a `vX.Y.Z` tag. The
95
+ `Publish to PyPI` workflow tests, builds and uploads using PyPI trusted
96
+ publishing, which has to be configured once on pypi.org for this repository
97
+ and the `pypi` environment.
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyedookit"
7
+ version = "0.3.0"
8
+ description = "Async client for the Edookit parent portal (edookit.net)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "tomas.liska" }]
13
+ dependencies = [
14
+ "aiohttp>=3.9",
15
+ "beautifulsoup4>=4.12",
16
+ "lxml>=5.0",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/TFoxik/pyedookit"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/pyedookit"]
24
+
25
+ [tool.pytest.ini_options]
26
+ asyncio_mode = "auto"
27
+ testpaths = ["tests"]
28
+
29
+ [tool.ruff]
30
+ line-length = 100
31
+ target-version = "py312"
32
+
33
+ [tool.ruff.lint]
34
+ select = ["E", "F", "I", "UP", "B"]
35
+
36
+ [tool.mypy]
37
+ strict = true
38
+ python_version = "3.12"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8.0",
43
+ "pytest-asyncio>=0.24",
44
+ "multidict>=6.0",
45
+ "ruff>=0.6",
46
+ "mypy>=1.11",
47
+ ]
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """Log in to a real Edookit portal and save timetable HTML for inspection.
3
+
4
+ Run this yourself so credentials never enter the assistant's session:
5
+
6
+ ! python scripts/capture_fixtures.py <school-subdomain>
7
+
8
+ It first discovers this school's Plus4U client_id (scraped from its own
9
+ /user/login page - it's per-school, not shared) without needing a password,
10
+ then prompts for username/password interactively (not via argv, so they
11
+ don't end up in shell history), logs in, and saves:
12
+
13
+ captures/timetable_current_week.html
14
+ captures/timetable_next_week.html
15
+
16
+ `captures/` is gitignored. These files come from an authenticated session
17
+ and can contain more than student/teacher names - a CSRF token, session
18
+ identifiers, or the parent's e-mail may be echoed into the page. Read the
19
+ printed warning before turning any of this into a committed test fixture.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import getpass
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
30
+
31
+ from pyedookit import auth # noqa: E402
32
+ from pyedookit.client import EdookitClient # noqa: E402
33
+
34
+ CAPTURES_DIR = Path(__file__).resolve().parent.parent / "captures"
35
+
36
+
37
+ async def main() -> None:
38
+ if len(sys.argv) != 2:
39
+ print("usage: capture_fixtures.py <school-subdomain>", file=sys.stderr)
40
+ raise SystemExit(2)
41
+ school = sys.argv[1]
42
+
43
+ client_id = await auth.probe_client_id(school)
44
+ print(f"Discovered Plus4U client_id for {school}: {client_id!r}")
45
+
46
+ username = input("Plus4U username/email: ")
47
+ password = getpass.getpass("Plus4U password: ")
48
+
49
+ print(f"Logging in to https://{school}.edookit.net ...")
50
+ session = await auth.login(school, username, password)
51
+ print("Login succeeded.")
52
+
53
+ client = EdookitClient(session)
54
+ CAPTURES_DIR.mkdir(parents=True, exist_ok=True)
55
+
56
+ weeks = ((0, "timetable_current_week.html"), (1, "timetable_next_week.html"))
57
+ for week_offset, filename in weeks:
58
+ html = await client.fetch_timetable_html(week_offset)
59
+ (CAPTURES_DIR / filename).write_text(html)
60
+ print(f"Saved {filename}")
61
+
62
+ print()
63
+ print(f"Files were saved to {CAPTURES_DIR}/ (gitignored) rather than tests/fixtures/,")
64
+ print("because an authenticated page can contain more than names. Before turning any")
65
+ print("of this into a committed test fixture:")
66
+ print(" 1. Replace real student/teacher/parent names with placeholders.")
67
+ print(" 2. Scrub any CSRF token, session id, or e-mail address you find in the HTML.")
68
+ print(" 3. Confirm the day-column and lesson-entry markup matches the selectors in")
69
+ print(" src/pyedookit/parsers/timetable.py, and adjust them if the real markup")
70
+ print(" differs (view page source in a browser to compare class names).")
71
+ print(" 4. Only then copy the relevant snippet into tests/fixtures/ and `git add` it.")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ asyncio.run(main())
@@ -0,0 +1,33 @@
1
+ from .auth import login, refresh
2
+ from .client import CAPTURE_PAGES, EdookitClient
3
+ from .exceptions import (
4
+ AuthError,
5
+ EdookitConnectionError,
6
+ EdookitError,
7
+ ParseError,
8
+ SessionExpired,
9
+ )
10
+ from .models import PORTAL_TZ, Lesson, PaymentRequest, Payments, SchoolEvent, Student, Term
11
+ from .parsers.fingerprint import content_fingerprint
12
+ from .session import Session
13
+
14
+ __all__ = [
15
+ "CAPTURE_PAGES",
16
+ "PORTAL_TZ",
17
+ "AuthError",
18
+ "EdookitClient",
19
+ "EdookitConnectionError",
20
+ "EdookitError",
21
+ "Lesson",
22
+ "ParseError",
23
+ "PaymentRequest",
24
+ "Payments",
25
+ "SchoolEvent",
26
+ "Session",
27
+ "SessionExpired",
28
+ "Student",
29
+ "Term",
30
+ "content_fingerprint",
31
+ "login",
32
+ "refresh",
33
+ ]