bakalari 1.0.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 (123) hide show
  1. bakalari-1.0.0/CHANGELOG.md +227 -0
  2. bakalari-1.0.0/CODE_OF_CONDUCT.md +36 -0
  3. bakalari-1.0.0/CONTRIBUTING.md +197 -0
  4. bakalari-1.0.0/LICENSE +21 -0
  5. bakalari-1.0.0/MANIFEST.in +19 -0
  6. bakalari-1.0.0/PKG-INFO +247 -0
  7. bakalari-1.0.0/README.md +199 -0
  8. bakalari-1.0.0/SECURITY.md +53 -0
  9. bakalari-1.0.0/bakalari/__init__.py +205 -0
  10. bakalari-1.0.0/bakalari/__main__.py +5 -0
  11. bakalari-1.0.0/bakalari/_core.py +579 -0
  12. bakalari-1.0.0/bakalari/_mcp_tools.py +237 -0
  13. bakalari-1.0.0/bakalari/_parse.py +289 -0
  14. bakalari-1.0.0/bakalari/_version.py +1 -0
  15. bakalari-1.0.0/bakalari/absence.py +181 -0
  16. bakalari-1.0.0/bakalari/cli.py +1993 -0
  17. bakalari-1.0.0/bakalari/client.py +86 -0
  18. bakalari-1.0.0/bakalari/errors.py +88 -0
  19. bakalari-1.0.0/bakalari/events.py +185 -0
  20. bakalari-1.0.0/bakalari/gdpr.py +160 -0
  21. bakalari-1.0.0/bakalari/homeworks.py +132 -0
  22. bakalari-1.0.0/bakalari/http.py +153 -0
  23. bakalari-1.0.0/bakalari/komens.py +974 -0
  24. bakalari-1.0.0/bakalari/marks.py +625 -0
  25. bakalari-1.0.0/bakalari/mcp.py +282 -0
  26. bakalari-1.0.0/bakalari/models.py +71 -0
  27. bakalari-1.0.0/bakalari/payments.py +307 -0
  28. bakalari-1.0.0/bakalari/public_timetable.py +1024 -0
  29. bakalari-1.0.0/bakalari/py.typed +0 -0
  30. bakalari-1.0.0/bakalari/schools.py +259 -0
  31. bakalari-1.0.0/bakalari/subjects.py +201 -0
  32. bakalari-1.0.0/bakalari/substitutions.py +76 -0
  33. bakalari-1.0.0/bakalari/teacher.py +580 -0
  34. bakalari-1.0.0/bakalari/timetable.py +660 -0
  35. bakalari-1.0.0/bakalari/user.py +374 -0
  36. bakalari-1.0.0/bakalari.egg-info/PKG-INFO +247 -0
  37. bakalari-1.0.0/bakalari.egg-info/SOURCES.txt +121 -0
  38. bakalari-1.0.0/bakalari.egg-info/dependency_links.txt +1 -0
  39. bakalari-1.0.0/bakalari.egg-info/entry_points.txt +3 -0
  40. bakalari-1.0.0/bakalari.egg-info/requires.txt +15 -0
  41. bakalari-1.0.0/bakalari.egg-info/top_level.txt +1 -0
  42. bakalari-1.0.0/docs/cli.md +1500 -0
  43. bakalari-1.0.0/docs/endpoints.md +267 -0
  44. bakalari-1.0.0/docs/guide.md +1158 -0
  45. bakalari-1.0.0/docs/library.md +917 -0
  46. bakalari-1.0.0/docs/mcp.md +786 -0
  47. bakalari-1.0.0/examples/README.md +38 -0
  48. bakalari-1.0.0/examples/class_fund_qr.py +75 -0
  49. bakalari-1.0.0/examples/find_school.py +67 -0
  50. bakalari-1.0.0/examples/homeworks_ics.py +119 -0
  51. bakalari-1.0.0/examples/new_marks.py +65 -0
  52. bakalari-1.0.0/examples/persistent_session.py +134 -0
  53. bakalari-1.0.0/examples/unread_messages.py +79 -0
  54. bakalari-1.0.0/examples/weekly_timetable.py +77 -0
  55. bakalari-1.0.0/pyproject.toml +133 -0
  56. bakalari-1.0.0/setup.cfg +4 -0
  57. bakalari-1.0.0/tests/__init__.py +0 -0
  58. bakalari-1.0.0/tests/test_live.py +651 -0
  59. bakalari-1.0.0/tests/test_mcp_sdk.py +54 -0
  60. bakalari-1.0.0/unittests/__init__.py +0 -0
  61. bakalari-1.0.0/unittests/fixtures/absence.json +29 -0
  62. bakalari-1.0.0/unittests/fixtures/actual.json +73 -0
  63. bakalari-1.0.0/unittests/fixtures/events.json +208 -0
  64. bakalari-1.0.0/unittests/fixtures/events_after_break.json +108 -0
  65. bakalari-1.0.0/unittests/fixtures/gdpr_child_consents.json +64 -0
  66. bakalari-1.0.0/unittests/fixtures/gdpr_commissioners.json +13 -0
  67. bakalari-1.0.0/unittests/fixtures/gdpr_consents.json +24 -0
  68. bakalari-1.0.0/unittests/fixtures/homeworks.json +38 -0
  69. bakalari-1.0.0/unittests/fixtures/komens_message_types.json +135 -0
  70. bakalari-1.0.0/unittests/fixtures/komens_message_types_reply.json +25 -0
  71. bakalari-1.0.0/unittests/fixtures/komens_received.json +101 -0
  72. bakalari-1.0.0/unittests/fixtures/komens_received_detail.json +29 -0
  73. bakalari-1.0.0/unittests/fixtures/komens_sent.json +69 -0
  74. bakalari-1.0.0/unittests/fixtures/komens_sent_detail.json +81 -0
  75. bakalari-1.0.0/unittests/fixtures/marks.json +234 -0
  76. bakalari-1.0.0/unittests/fixtures/marks_final.json +114 -0
  77. bakalari-1.0.0/unittests/fixtures/marks_measures.json +20 -0
  78. bakalari-1.0.0/unittests/fixtures/marks_what_if.json +91 -0
  79. bakalari-1.0.0/unittests/fixtures/payments_classfund.json +41 -0
  80. bakalari-1.0.0/unittests/fixtures/payments_info.json +8 -0
  81. bakalari-1.0.0/unittests/fixtures/payments_summary.json +5 -0
  82. bakalari-1.0.0/unittests/fixtures/permanent.json +60 -0
  83. bakalari-1.0.0/unittests/fixtures/public_timetable_actual.html +259 -0
  84. bakalari-1.0.0/unittests/fixtures/public_timetable_actual.json +653 -0
  85. bakalari-1.0.0/unittests/fixtures/public_timetable_errinfo.html +43 -0
  86. bakalari-1.0.0/unittests/fixtures/public_timetable_index.html +205 -0
  87. bakalari-1.0.0/unittests/fixtures/public_timetable_legacy.html +121 -0
  88. bakalari-1.0.0/unittests/fixtures/public_timetable_permanent.html +258 -0
  89. bakalari-1.0.0/unittests/fixtures/schools_kocourkov.json +35 -0
  90. bakalari-1.0.0/unittests/fixtures/schools_municipalities.json +46 -0
  91. bakalari-1.0.0/unittests/fixtures/subjects.json +20 -0
  92. bakalari-1.0.0/unittests/fixtures/subjects_themes.json +17 -0
  93. bakalari-1.0.0/unittests/fixtures/substitutions.json +54 -0
  94. bakalari-1.0.0/unittests/fixtures/teacher_classbook.json +82 -0
  95. bakalari-1.0.0/unittests/fixtures/teacher_classbook_saved.json +28 -0
  96. bakalari-1.0.0/unittests/fixtures/teacher_lesson.json +60 -0
  97. bakalari-1.0.0/unittests/fixtures/teacher_marking.json +86 -0
  98. bakalari-1.0.0/unittests/fixtures/timetable_class.json +118 -0
  99. bakalari-1.0.0/unittests/fixtures/timetable_holiday.json +35 -0
  100. bakalari-1.0.0/unittests/fixtures/user_parent.json +245 -0
  101. bakalari-1.0.0/unittests/fixtures/user_student.json +49 -0
  102. bakalari-1.0.0/unittests/fixtures/webmodule.json +22 -0
  103. bakalari-1.0.0/unittests/support.py +147 -0
  104. bakalari-1.0.0/unittests/test_absence.py +229 -0
  105. bakalari-1.0.0/unittests/test_cli.py +1849 -0
  106. bakalari-1.0.0/unittests/test_core.py +505 -0
  107. bakalari-1.0.0/unittests/test_events.py +293 -0
  108. bakalari-1.0.0/unittests/test_gdpr.py +183 -0
  109. bakalari-1.0.0/unittests/test_homeworks.py +280 -0
  110. bakalari-1.0.0/unittests/test_http.py +149 -0
  111. bakalari-1.0.0/unittests/test_komens.py +1027 -0
  112. bakalari-1.0.0/unittests/test_marks.py +696 -0
  113. bakalari-1.0.0/unittests/test_mcp.py +386 -0
  114. bakalari-1.0.0/unittests/test_package.py +400 -0
  115. bakalari-1.0.0/unittests/test_parse.py +184 -0
  116. bakalari-1.0.0/unittests/test_payments.py +354 -0
  117. bakalari-1.0.0/unittests/test_public_timetable.py +507 -0
  118. bakalari-1.0.0/unittests/test_schools.py +380 -0
  119. bakalari-1.0.0/unittests/test_subjects.py +302 -0
  120. bakalari-1.0.0/unittests/test_substitutions.py +219 -0
  121. bakalari-1.0.0/unittests/test_teacher.py +457 -0
  122. bakalari-1.0.0/unittests/test_timetable.py +618 -0
  123. bakalari-1.0.0/unittests/test_user.py +376 -0
@@ -0,0 +1,227 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-10-26
9
+
10
+ Initial release of the `bakalari` distribution: an unofficial Python client, CLI and MCP server for
11
+ Bakaláři REST API v3, using only the standard library and supporting Python 3.9 through 3.14.
12
+
13
+ ### Added
14
+
15
+ - MCP server mode through `bakalari mcp`, `bakalari-mcp` and `python -m bakalari.mcp`, with no new runtime
16
+ dependencies and Python 3.9 support. The stdio server provides 50 read tools across the API modules and school
17
+ directory, plus 11 opt-in write tools with `--allow-writes`.
18
+ - Explicit tool schemas and validation, protocol/version negotiation, structured results, redacted errors,
19
+ bounded stdio frames, base64 attachment transfers, and automatic CLI session renewal/persistence.
20
+ - MCP setup and tool reference in `docs/mcp.md`, including macOS/Linux and Windows setup, multiple accounts,
21
+ read/write workflows, attachment examples, troubleshooting, an offline connection check and Python integration.
22
+ README, CLI and library guides link to the full usage guide.
23
+ - Offline MCP protocol/integration tests and a CI interoperability check using the official MCP Python client.
24
+
25
+ #### Client
26
+
27
+ - `BakalariClient` logs in like the official mobile app (the OAuth2 password grant on `POST /api/login`, client id
28
+ `ANDR`) on the first request that needs a token, renews the access token with the refresh token `renew_margin`
29
+ seconds (60) before it expires, renews and retries once after HTTP 401, and logs in again with the password when
30
+ the refresh token is rejected. `login()`, `refresh()`, `ensure_token()`, `logout()`, `token_info` (`TokenInfo`:
31
+ user id, scope, API version, expiry; never a token) and `is_authenticated` give explicit control, and a `with`
32
+ block logs out at its end.
33
+ - `BakalariClient.from_env()` reads the address from `BAKALARI_URL` and the login from `BAKALARI_USERNAME` and
34
+ `BAKALARI_PASSWORD`, or from other variables with `username_env=` and `password_env=` (e.g. another child's login).
35
+ - Sessions: `export_session()` and `import_session()` with `SessionState` (`to_dict()` and `from_dict()`), a client
36
+ started from a `refresh_token` alone, and the `on_session_update` callback after every login and renewal, so that
37
+ an application can keep the newest refresh token and continue without the password.
38
+ - Retries: `retries` (0 by default) repeats `GET`, `PUT` and `DELETE` requests after a connection error or HTTP 502,
39
+ 503 or 504, waiting `retry_backoff` (0.5 s) and twice as long before each further retry; `POST` requests, logins
40
+ included, are never repeated. `timeout` (30 s) applies to every request.
41
+ - Transports: the default `urllib_transport()` (no cookie jar, the usual proxy variables, an optional `ssl_context`)
42
+ or any function with the `Transport` signature that returns an `HttpResponse`, e.g. canned answers in tests.
43
+ - `normalize_base_url()` accepts an address copied from the browser: it adds `https://` and drops a page path, a
44
+ query and a trailing slash. Every request sends `Accept-Language: cs`, without which some installations answer
45
+ HTTP 500.
46
+ - Raw access with the same token handling: `request()` (any method, query parameters, a form, JSON or raw body,
47
+ extra headers, an expected status or a set of them), `get_json()`, `post_json()`, `api_version()` and
48
+ `available_api_versions()`.
49
+ - One client can be shared between threads: a lock guards the tokens.
50
+ - Logging through the `bakalari.client` and `bakalari.schools` loggers: the method, path and status of every
51
+ request, the logins, renewals and retries.
52
+
53
+ #### Typed models and contract checks
54
+
55
+ - The responses become dataclasses based on `Model` (frozen, except `Timetable`), with `to_dict()` (JSON-ready data)
56
+ and `raw` (the server's object, for the keys a model does not map). Ids are kept exactly as sent, space padding included, and
57
+ percent-encoded in paths; display strings are trimmed; timestamps are timezone-aware `datetime` objects and
58
+ calendar days `date` objects; decimal commas are understood; missing and `null` fields get defaults.
59
+ - `Item` for the `{Id, Abbrev, Name}` objects, `AttachmentInfo`, `format()` methods for readable one-line output, and
60
+ `from_json()` class methods and `parse_...()` functions for offline parsing.
61
+ - Contract checks: the methods with a `validate` parameter (true by default) check the essential shape of each
62
+ payload and raise `BakalariContractError` listing every problem (`TimetableContractError` for timetables). A body
63
+ that is not JSON, such as an HTML error page with HTTP 200, is a contract error too, and counters must be numbers.
64
+ `validate=False` parses leniently.
65
+ - Errors: `BakalariError` and its subclasses `BakalariConfigError`, `BakalariConnectionError`, `BakalariContractError`
66
+ and `BakalariHTTPError`, which has `method`, `path`, `status`, `expected`, `body` and the server's `message`, and
67
+ the subclasses `BakalariAuthError`, `BakalariPermissionError` (HTTP 403: a module or right is missing, or the
68
+ endpoint is for teachers) and `BakalariNotFoundError` (HTTP 404). Wrong arguments raise `ValueError` or `TypeError`
69
+ before any request.
70
+
71
+ #### Endpoints
72
+
73
+ Typed methods for 71 endpoints, one module per area. docs/endpoints.md lists every known endpoint with its
74
+ verification status: 41 of the 71 were verified against a live installation and 23 more by route and method.
75
+
76
+ - Account (`bakalari.user`): `user_profile()`, which returns a `UserProfile` (user type, names, class, school, school
77
+ year and semester, the students of a parent account, the modules and rights with `has_module()` and
78
+ `has_right()`), `enabled_modules()`, `student_at_school()`, `web_modules()`, `login_token()`, `web_login_url()` (a
79
+ one-time link that opens the Bakaláři web logged in) and `register_notification()`.
80
+ - Timetable (`bakalari.timetable`): `actual_timetable()`, `permanent_timetable()`, `class_timetable()` (a whole
81
+ class with all its groups) and `timetable_json()`, with the lessons' homeworks, notices, week themes and times.
82
+ - Substitutions (`bakalari.substitutions`): `substitutions()`, the changes of 14 days grouped by day.
83
+ - Marks (`bakalari.marks`): `marks()` (optionally only `since` a date), `new_marks_count()`, `final_marks()`,
84
+ `pedagogical_measures()`, the what-if prediction of an average with `predict_marks()` and `what_if()`, and
85
+ `confirm_marks()` for parents.
86
+ - Absence (`bakalari.absence`): `absence()` per day and per subject with the school's percentage threshold.
87
+ - Subjects (`bakalari.subjects`): `subjects()` with the teachers and their contacts, `subject_themes()` (with the
88
+ alternative route for ids a URL path cannot carry) and `find_subject()`.
89
+ - Homeworks (`bakalari.homeworks`): `homeworks()`, `active_homeworks_count()` and `set_homework_done()`.
90
+ - Komens messages (`bakalari.komens`): `messages()` of the received, sent, noticeboard, apology and rating folders
91
+ (optionally only `since` a date), `message()`, `unread_messages_count()`, `mark_message_read()`,
92
+ `confirm_message()`, `hide_message()`, `unhide_message()`, `delete_message()`, `message_types()` (also for a
93
+ reply), `send_message()`, `reply_to_message()`, `upload_attachment()`, `download_attachment()` with
94
+ `AttachmentFile.save()`, and `rating_templates()`. `Message.plain_text` turns the HTML body into readable text.
95
+ - Events (`bakalari.events`): `events()` of the whole school, the user's own or the public ones.
96
+ - GDPR (`bakalari.gdpr`): `consents()`, `child_consents()`, `new_consents_count()` and `gdpr_commissioners()`.
97
+ - Payments (`bakalari.payments`): `class_fund_summary()`, `class_fund_payments()`, `class_fund_payment_info()` with
98
+ the IBAN and the Czech QR payment (SPAYD) string, `regular_payments_summary()`, `account_to_iban()` and
99
+ `format_iban()`.
100
+ - Teacher accounts (`bakalari.teacher`): `classbook()`, `save_classbook_record()`, `lesson_tags()`,
101
+ `lesson_absence()`, `lesson_past()`, `lesson_students_timetable()`, `marking_atoms()`, `marking_marks()` and
102
+ `emergency_configuration()`.
103
+ - Public timetables (`bakalari.public_timetable`, no login): `public_timetable_targets()`, `public_timetable()` and
104
+ `public_timetable_url()` for classes, teachers and rooms, from the newer and the older page layouts, and
105
+ `parse_public_targets()` and `parse_public_timetable()` for saved pages.
106
+ - School directory (`bakalari.schools`, no login): `SchoolDirectory` with `municipalities()`, `schools()` and
107
+ `find()`, which ignores case and diacritics, to find a school's Bakaláři address.
108
+
109
+ #### Command line
110
+
111
+ - The `bakalari` command (also `python -m bakalari`) with 38 commands, grouped by area in `bakalari --help`: public
112
+ data (`version`, `schools`, `public-timetable`), the account and the session (`login`, `logout`, `profile` with the
113
+ alias `whoami`, `modules`, `web-login`, `web-modules`, `at-school`), `timetable`, `substitutions` and `events`,
114
+ marks (`marks`, `final-marks`, `measures`, `predict`, `confirm-marks`), `absence`, `subjects`, `homeworks` and
115
+ `homework-done`, Komens messages (`messages`, `message`, `message-types`, `send-message`, `reply`,
116
+ `confirm-message`, `hide-message`, `delete-message`, `attachment`), `consents`, `commissioners` and `class-fund`,
117
+ teacher accounts (`classbook`, `lesson-tags`, `marking`), and raw requests to any endpoint (`api`).
118
+ - Global options `--base-url`, `--username-env`, `--password-env`, `--session-file` (or `BAKALARI_SESSION_FILE`),
119
+ `--timeout` (30 s), `--retries` (2), `-v` and `--version`. Most commands accept `--json` and then print JSON
120
+ instead of text, and dates are `YYYY-MM-DD`, `today`, `yesterday` or `tomorrow`.
121
+ - The session file keeps the session between commands, so the password is needed only after the refresh token has
122
+ expired. It works without the login variables, which then serve as the fallback for a rejected refresh token.
123
+ - The commands that change data are marked in the help, do exactly what the command line says and print what they
124
+ did. `send-message` checks the command line against the account's message types before it uploads or sends
125
+ anything.
126
+ - Exit status 0 on success, 1 for an API, connection or contract error, with a hint where the cause is likely (e.g.
127
+ a module that is not enabled, or an endpoint for teacher accounts), 2 for a usage or configuration error and 130
128
+ when interrupted. docs/cli.md is the complete reference.
129
+
130
+ #### Package and documentation
131
+
132
+ - The package exports the client, the errors, every model class and the main helpers from `bakalari`, ships
133
+ `py.typed` and has no runtime dependencies.
134
+ - Documentation: the README, the library guide (docs/library.md, every method and model), the command line reference
135
+ (docs/cli.md), the endpoint coverage (docs/endpoints.md) and runnable examples: the weekly timetable, new marks,
136
+ unread messages with their attachments, homeworks as an iCalendar file, a persistent session, the class fund QR
137
+ payment and the school search.
138
+ - An offline test suite with a scripted transport and synthetic fixtures, which CI runs on Python 3.9 to 3.14 on
139
+ Linux and on Python 3.13 on macOS and Windows with at least 90 % coverage, and read-only live tests that run only
140
+ when `BAKALARI_URL`, `BAKALARI_USERNAME` and `BAKALARI_PASSWORD` are set.
141
+
142
+ ### Changed
143
+
144
+ - Rename the repository and Python distribution from `bakalari-api` to `bakalari`. Install with
145
+ `python -m pip install bakalari`; the Python import and the `bakalari` and `bakalari-mcp` commands stay the same.
146
+ - Use `bakalari` in CLI version output, MCP server identity, release metadata, documentation and calendar exports,
147
+ and `bakalari-python/<version>` in the default HTTP User-Agent.
148
+
149
+ - The login variables are `BAKALARI_USERNAME` and `BAKALARI_PASSWORD` instead of `BAK_PTRK_USERNAME` and
150
+ `BAK_PTRK_PASSWORD`.
151
+ - The school's address is required: set `BAKALARI_URL` or pass `base_url` (`--base-url` on the command line). There
152
+ is no default school any more.
153
+ - `user_profile()` returns a typed `UserProfile` instead of the raw dict; the server's object is its `raw` field.
154
+ - The timetable models `Timetable`, `Day`, `Lesson`, `Hour` and `Change` are `Model` dataclasses with new fields;
155
+ the existing fields keep their meaning, and `Lesson.to_dict()` keeps its keys and adds `notice`, `week_theme` and
156
+ `homeworks`.
157
+ - `request()` also sends `PUT`, `DELETE` and `PATCH` requests, JSON and raw bodies and extra headers, and accepts a
158
+ set of expected statuses or `None` for any. HTTP 403 and 404 raise `BakalariPermissionError` and
159
+ `BakalariNotFoundError`, which are `BakalariHTTPError` subclasses, so existing handlers keep working.
160
+ - The command line was rewritten; `timetable` keeps its options and output. `bakalari.cli.main()` returns the exit
161
+ status 2 for a usage error instead of letting `SystemExit` escape, and `-v` configures only the package's loggers
162
+ instead of the root logger.
163
+ - Split automation into **Python Package CI** (`python-package.yml`) and **Python Package Publish**
164
+ (`python-publish.yml`), replacing the previous CI and tag-push publishing workflows. Publishing a GitHub
165
+ release, or manually running the publish workflow from its matching tag, validates the release, runs reusable
166
+ CI and publishes its checked distributions through the `pypi` environment using PyPI trusted publishing.
167
+ - Validate that the release tag matches the package version, the changelog has a dated, non-empty release
168
+ section, and no changes remain under "Unreleased" before publishing.
169
+ - Collect both `unittests/` and `tests/` in CI with live tests explicitly disabled, including official MCP SDK
170
+ interoperability and release-validation regression tests. Retain test and coverage reports for 30 days.
171
+ - Check wheel and source-distribution contents, typed-package metadata and installed command entry points;
172
+ keep repository tooling, local state, caches and generated reports out of distributions.
173
+ - Update contributor documentation for the workflow migration, required checks, PyPI trusted-publisher setup
174
+ and release validation.
175
+
176
+ ### Fixed
177
+
178
+ - Fall back to the ordinary attachment filename when the server sends a malformed extended filename header.
179
+ - Remove duplicate legacy tests whose missing helper imports broke full test discovery.
180
+ - Pass the coverage configuration explicitly in CI so subprocesses running outside the checkout use the same
181
+ branch-coverage settings and their results can be combined successfully.
182
+
183
+ ### Security
184
+
185
+ - Block redirects to another origin for requests carrying an Authorization header or body, including login
186
+ requests, and reject HTTPS downgrades. Public HTTP(S) redirects remain supported.
187
+ - Remove query strings and fragments from request paths in logs, HTTP errors and JSON contract errors, so raw
188
+ request paths cannot expose query tokens in diagnostics.
189
+ - Reject malformed installation URLs and embedded URL credentials before they can appear in diagnostics.
190
+ - Redact known passwords and access/refresh tokens echoed in upstream HTTP error bodies, including nested values.
191
+ - Keep stored passwords and tokens in private attributes and exclude token payloads from `repr()`. Request logs
192
+ omit query strings and bodies, and connection error messages omit query strings that can carry push tokens.
193
+ - TLS certificates are verified by default, and there is no cookie jar, because the API is stateless.
194
+ - The command line writes the session file atomically with mode 0600, restricts a session file that other users
195
+ can read when it loads it (on POSIX systems), refuses a file that is not a Bakaláři session, and `logout`
196
+ deletes nothing but a Bakaláři session file.
197
+ - Text output removes the C0 and C1 control characters from server strings (titles, names, message bodies, file
198
+ names) and JSON output escapes them, so a server cannot send escape sequences to the terminal.
199
+ - `AttachmentFile.save()` and the `attachment` command sanitize the server's file name (no directory parts, control
200
+ characters, Windows device names or hidden files) and never overwrite a file unless asked to.
201
+ - `web-login` warns that its one-time link is a credential. Reading a message never marks it as read. The live tests
202
+ call no endpoint that changes data and never log in with a wrong password for a real user, because Bakaláři shows
203
+ failed logins to the account owner.
204
+
205
+ ### Known limitations
206
+
207
+ - The REST API is not officially documented and may change without notice.
208
+ - Not verified against a live server: the teacher endpoints (no teacher account was available), the payments
209
+ payloads (the module was disabled at the test school), the timetable pages of a school that publishes them, and
210
+ the write endpoints (sending, replying, confirming, hiding and deleting messages, uploading attachments,
211
+ confirming marks, ticking homeworks, saving class-book records, registering a push token), which were checked by
212
+ route and method only. Their models follow the community documentation, other clients and captured traffic, and
213
+ keep the server's objects in `raw`.
214
+ - 17 known endpoints have no typed method, mostly because their requests are undocumented (Komens drafts, consent
215
+ answers, emergency alarms, teacher payment prescriptions, class-book attendance writes, the student timetable);
216
+ `request()` and `bakalari api` reach them. Consents can be answered and homeworks handed in electronically on the
217
+ Bakaláři web only.
218
+ - `register_notification()` needs the official app's push token, so a third-party token receives no notifications;
219
+ poll the counters instead. `new_marks_count()` is often 0 even when marks are flagged new.
220
+ - `predict_marks()` takes classic marks only; hypothetical point marks need `what_if()`.
221
+ - `send-message` and `reply` turn plain text into HTML (escaped, line breaks as `<br>`), assuming that the server
222
+ stores messages as HTML like the web editor does; `--html` sends the text unchanged.
223
+ - `class_timetable()` was verified for the account's own class only; the timetable of another class may be refused.
224
+ - Two commands that renew the same session file at the same moment can race, because the file is not locked.
225
+
226
+ [Unreleased]: https://github.com/robco/bakalari/compare/v1.0.0...HEAD
227
+ [1.0.0]: https://github.com/robco/bakalari/releases/tag/v1.0.0
@@ -0,0 +1,36 @@
1
+ # Code of conduct
2
+
3
+ This project adopts the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
4
+ as its code of conduct. It applies to everyone who takes part in the project, in issues, pull requests, reviews
5
+ and every other project space, and to anyone who represents the project in public.
6
+
7
+ ## In short
8
+
9
+ We want taking part in this project to be a harassment-free experience for everyone. We expect you to:
10
+
11
+ - be respectful and kind, and welcome people of every background and level of experience;
12
+ - give and accept constructive feedback gracefully, and focus on what is best for the project and its users;
13
+ - respect privacy. This project works with the school records of children: never publish anyone's personal data,
14
+ and keep data from a Bakaláři account (names, marks, absences, messages) out of issues, pull requests and
15
+ fixtures.
16
+
17
+ Not acceptable:
18
+
19
+ - harassment, insults, and derogatory or discriminatory comments;
20
+ - personal or political attacks, trolling;
21
+ - sexualized language or imagery, and unwelcome sexual attention;
22
+ - publishing other people's private information without their explicit permission;
23
+ - other conduct that could reasonably be considered inappropriate in a professional setting.
24
+
25
+ ## Reporting and enforcement
26
+
27
+ Report unacceptable behaviour privately to the project maintainer, Róbert Malovec, through the repository's
28
+ private reporting form ([Report a vulnerability](https://github.com/robco/bakalari/security/advisories/new))
29
+ with a title that starts with "Code of conduct". Every report is reviewed promptly and fairly, and the reporter's
30
+ privacy is respected.
31
+
32
+ The maintainer enforces this code of conduct following the Contributor Covenant's
33
+ [enforcement guidelines](https://www.contributor-covenant.org/version/2/1/code_of_conduct/#enforcement-guidelines):
34
+ a correction, a warning, a temporary ban or a permanent ban, depending on the impact. Content that breaks GitHub's
35
+ terms can also be
36
+ [reported to GitHub](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam).
@@ -0,0 +1,197 @@
1
+ # Contributing to bakalari
2
+
3
+ Thank you for helping. This guide covers the development setup, the tests, the code conventions and the release
4
+ process. By taking part you agree to follow the [code of conduct](CODE_OF_CONDUCT.md). Report security problems
5
+ privately as described in [SECURITY.md](SECURITY.md), never in a public issue.
6
+
7
+ ## Development setup
8
+
9
+ You need Python 3.9 or newer. The package has no runtime dependencies. Test your changes on the oldest supported
10
+ Python (3.9) and on the newest.
11
+
12
+ ```bash
13
+ git clone https://github.com/robco/bakalari.git
14
+ cd bakalari
15
+ python3 -m venv .venv
16
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
17
+ python -m pip install -e ".[dev]"
18
+ ```
19
+
20
+ mypy stays below version 2 because mypy 2 can no longer check code against Python 3.9 (`python_version` in
21
+ `pyproject.toml`).
22
+
23
+ ## Tests
24
+
25
+ ### Offline tests
26
+
27
+ The offline suite needs no network and no account. It drives the client through a scripted transport
28
+ (`FakeTransport` in `unittests/support.py`) and uses the synthetic fixtures in `unittests/fixtures/`.
29
+
30
+ ```bash
31
+ python -m pytest # the whole offline suite
32
+ python -m unittest unittests.test_marks -v # one test module, no test dependencies
33
+ python -m pytest --cov=bakalari --cov-fail-under=90 # CI coverage threshold
34
+ ```
35
+
36
+ The default pytest discovery path is `unittests/`, so it never selects live tests even when credentials are
37
+ present. Without third-party test tools, run `python -m unittest discover -s unittests -t . -v`.
38
+
39
+ ### MCP SDK interoperability
40
+
41
+ On Python 3.10 or newer, install the optional reference client and run the offline interoperability test:
42
+
43
+ ```bash
44
+ python -m pip install -e ".[dev,mcp-test]"
45
+ python -m pytest tests/test_mcp_sdk.py
46
+ ```
47
+
48
+ CI runs this test alongside the unit tests under coverage. It uses synthetic school responses and requires no
49
+ account. The installed library and MCP server still have no runtime dependencies.
50
+
51
+ ### Live tests (read-only)
52
+
53
+ `tests/test_live.py` runs against a real Bakaláři installation when all three variables are set and
54
+ `BAKALARI_SKIP_LIVE` is not:
55
+
56
+ ```bash
57
+ export BAKALARI_URL=https://... # the address of your school's Bakaláři web
58
+ export BAKALARI_USERNAME=...
59
+ export BAKALARI_PASSWORD=...
60
+ python -m unittest tests.test_live -v
61
+ ```
62
+
63
+ Set `BAKALARI_SKIP_LIVE=1` to skip them even when the credentials are set. CI never runs them. The rules for
64
+ live tests:
65
+
66
+ - **They never change data.** No marking messages as read, no sending messages, no confirming marks, no marking
67
+ homework as done, no consent changes, no notification registration: only read endpoints.
68
+ - **They never use a wrong password with a real username.** Bakaláři counts failed logins and shows them to the
69
+ account owner at the next login. The negative login test uses a random, unknown username.
70
+ - They assert structure only (types, non-negative counters, parsing with validation on), never concrete personal
71
+ values, and they skip (`skipTest`) when the school or account has a module disabled (`BakalariPermissionError`).
72
+ - Their output can contain personal data. Don't paste it into issues or pull requests unless it's redacted.
73
+
74
+ ## Fixture privacy rule
75
+
76
+ Fixtures are synthetic. Copy the **shape** of a real response (keys, nesting, value types, enum values,
77
+ space-padded ids, empty and null fields) but never its values. Invent the names, ids, texts, marks and dates, and
78
+ use `https://school.example/bakaweb` as the address. Never commit a real name, id, school, message text or token,
79
+ not even from your own account, and never commit a captured response with the plan to anonymize it later.
80
+
81
+ ## Code conventions
82
+
83
+ The package uses only the standard library and runs on Python 3.9. Keep `from __future__ import annotations` and
84
+ the `typing` style (`Optional`, `Dict`, `List`, `Tuple`, `Mapping`). Don't use `match`, `X | Y` at runtime,
85
+ `dataclass(slots=..., kw_only=...)`, `zip(strict=...)` or `datetime.UTC`.
86
+
87
+ 1. **One module per Bakaláři area** (`marks.py`, `komens.py`, ...) holds the area's response models and its
88
+ `XxxEndpoints(ClientCore)` mixin, which `BakalariClient` combines. Endpoint methods are thin: they build the
89
+ path (ids go through `quote_segment`), call `get_json`, `post_json` or `request`, and parse the result with the
90
+ model's `from_json`.
91
+ 2. **Models** are `@dataclass(frozen=True)` subclasses of `Model` with a `from_json` classmethod. Entity models end
92
+ with a `raw` field holding the original object, which is hidden from `repr()` and `to_dict()`. Field names are
93
+ the snake_case API names (`school_class` for `Class`), and collections are tuples.
94
+ 3. **Values** go through the `bakalari._parse` helpers: ids exact (`ident`), display strings trimmed (`text`),
95
+ timestamps as aware `datetime` (`parse_datetime`) or `date` (`parse_date`), numbers and booleans via
96
+ `to_int`, `to_float` and `to_bool`, `{Id, Abbrev, Name}` objects as `Item`, and attachments as
97
+ `AttachmentInfo`. Parsing is lenient: a missing optional field gets a default.
98
+ 4. **Contract checks**: a top-level `from_json(data, *, validate=True)` checks the essential shape with
99
+ `check_shape` when it validates, and with `require_mapping` or `require_list` always. Endpoint methods take
100
+ `validate: bool = True` and pass it on. Counter endpoints use `require_int`.
101
+ 5. **Dates in requests** are formatted with `format_date`. Date parameters are named `since` and `until` and
102
+ accept a `date`, a `datetime` or an ISO string.
103
+ 6. **Docstrings** in English on every public class and method: one or two sentences naming the HTTP method and
104
+ path (``GET /api/3/marks``) and notable server behaviour. Comments only explain non-obvious server quirks.
105
+ 7. **Errors** are never swallowed. Endpoints that need a module or a right let `BakalariPermissionError`
106
+ propagate. Tokens and passwords never appear in messages, log records or `repr()`.
107
+ 8. **Tests** are offline and deterministic (`unittest`), one test module per area module (`unittests/test_marks.py`),
108
+ with synthetic fixtures `unittests/fixtures/<area>*.json`. They cover model parsing (padded ids, nulls, missing
109
+ fields), `to_dict()` JSON round trips, the HTTP method, path (including percent-encoded ids), query and body of
110
+ every endpoint method, status handling, contract errors and lenient parsing.
111
+
112
+ ## Lint and type check
113
+
114
+ ```bash
115
+ ruff check . # pycodestyle, pyflakes, isort, bugbear and comprehensions; line length 120
116
+ mypy bakalari # the settings in pyproject.toml, checked against Python 3.9 (needs mypy < 2)
117
+ ```
118
+
119
+ Ruff's pyupgrade (`UP`) rules are off on purpose, so that the `Optional[...]` typing style stays.
120
+
121
+ To check the packaging the way CI does:
122
+
123
+ ```bash
124
+ python -m build
125
+ twine check --strict dist/*
126
+ ```
127
+
128
+ ## Pull requests
129
+
130
+ Keep a pull request focused on one change, add tests, update the documentation and add an entry to
131
+ `CHANGELOG.md` under "Unreleased". CI runs the offline tests on Python 3.9 to 3.14 on Linux and on Python 3.13 on
132
+ macOS and Windows, checks coverage (at least 90 %), runs ruff and mypy, and builds the package, checking the wheel
133
+ by installing it into a fresh virtual environment. Python 3.12 runs the combined test/build job, which also
134
+ checks the MCP SDK and uploads JUnit, HTML and coverage reports. The remaining matrix jobs use only the standard
135
+ library to check compatibility.
136
+
137
+ ## Release process
138
+
139
+ Versions follow [Semantic Versioning](https://semver.org/). The version is set in one place,
140
+ `bakalari/_version.py`. Setuptools reads that value for the distribution, and the library, CLI and MCP server
141
+ use it at runtime. The filename is a project convention, not a GitHub Actions or PyPI requirement
142
+
143
+ 1. Set `__version__ = "X.Y.Z"` in `bakalari/_version.py`.
144
+ 2. In `CHANGELOG.md`, rename "Unreleased" to `## [X.Y.Z] - YYYY-MM-DD`, add a new empty "Unreleased"
145
+ section above it, and update the link references at the bottom.
146
+ 3. Commit the changes, push to `main`, and wait for **Python Package CI** to pass.
147
+ 4. Create and push the release tag:
148
+
149
+ ```bash
150
+ git tag -a vX.Y.Z -m "bakalari X.Y.Z"
151
+ git push origin vX.Y.Z
152
+ ```
153
+
154
+ 5. On GitHub, publish a release for that tag and use the changelog section as its release notes. The
155
+ [Python Package Publish workflow](.github/workflows/python-publish.yml) starts on `release: published`.
156
+ It builds the wheel and source distribution, uploads them as `release-dists`, then publishes them to PyPI.
157
+ A tag push alone does not publish a package.
158
+
159
+ Publishing does not rerun CI or enforce tag naming or changelog formatting. The package version comes from
160
+ `bakalari/_version.py` in the selected checkout, regardless of the branch, tag or release name.
161
+ CI runs tests, lint, type and distribution checks separately; its `python-package` artifact and test and
162
+ coverage reports are retained for 30 days. The publish job uses the `pypi` environment and OpenID Connect
163
+ trusted publishing to upload the distributions from its own build job.
164
+
165
+ To publish manually, select a branch or tag in GitHub's **Run workflow** dialog, or use:
166
+
167
+ ```bash
168
+ gh workflow run python-publish.yml --ref main
169
+ # Or publish a specific tag:
170
+ gh workflow run python-publish.yml --ref vX.Y.Z
171
+ ```
172
+
173
+ A manual run also publishes to **PyPI**. PyPI accepts each distribution filename only once; bump the package
174
+ version before a new release and check a partially completed upload before retrying. For a local build
175
+ check without publishing, use `python -m build`, `python -m twine check --strict dist/*` and
176
+ `python .github/scripts/check_distribution.py`.
177
+
178
+ ### First-time setup and migration
179
+
180
+ 1. On GitHub, create the `pypi` environment (Settings, Environments). Allow the branches and tags you intend
181
+ to publish from; remove any tag-only restriction if you want manual publishing from `main`.
182
+ 2. Configure a [PyPI trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/):
183
+ - PyPI project: `bakalari`
184
+ - Owner: `robco`
185
+ - Repository: `bakalari`
186
+ - Workflow filename: `python-publish.yml`
187
+ - Environment: `pypi`
188
+
189
+ If the project does not exist, add a pending publisher in the account's Publishing settings. No API token
190
+ is needed. If migrating from the previous workflow, replace the publisher for `release.yml` with the one
191
+ above; PyPI matches the workflow filename exactly.
192
+ 3. If branch protection requires the old CI job names, update those required checks to the checks reported by
193
+ **Python Package CI**, including the compatibility matrix and **Test & Build (Python 3.12)**.
194
+ 4. The old tag-push publisher and manual TestPyPI rehearsal are replaced by this release-based workflow.
195
+ The `testpypi` environment and its publisher are no longer used.
196
+ 5. Enable private vulnerability reporting (Settings, Advanced Security, Private vulnerability reporting), which
197
+ SECURITY.md and the issue forms link to.
bakalari-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Róbert Malovec
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,19 @@
1
+ # Source distribution contents in addition to what setuptools adds by itself
2
+ # (the bakalari package with py.typed, README.md, LICENSE and pyproject.toml).
3
+ include CHANGELOG.md CONTRIBUTING.md SECURITY.md CODE_OF_CONDUCT.md
4
+ graft unittests
5
+ graft tests
6
+ graft docs
7
+ graft examples
8
+
9
+ # Never ship repository tooling, local agent state, build output, caches or secrets.
10
+ prune .github
11
+ prune .claude
12
+ prune build
13
+ prune dist
14
+ prune htmlcov
15
+ prune .mypy_cache
16
+ prune .ruff_cache
17
+ prune .pytest_cache
18
+ prune reports
19
+ global-exclude *.py[cod] .DS_Store .env *.session.json