├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── python-publish.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── docs ├── classes.html ├── classrooms.html ├── cloud.html ├── compression.html ├── custom_request.html ├── dbi.html ├── exceptions.html ├── grades.html ├── index.html ├── login.html ├── lunches.html ├── messages.html ├── module.html ├── parent.html ├── people.html ├── ringing.html ├── subjects.html ├── substitution.html ├── timeline.html ├── timetables.html └── utils.html ├── edupage_api ├── __init__.py ├── classes.py ├── classrooms.py ├── cloud.py ├── compression.py ├── custom_request.py ├── dbi.py ├── exceptions.py ├── grades.py ├── login.py ├── lunches.py ├── messages.py ├── module.py ├── parent.py ├── people.py ├── ringing.py ├── subjects.py ├── substitution.py ├── timeline.py ├── timetables.py └── utils.py ├── examples ├── 2fa.py ├── all_teachers.py ├── decrypt-request.py ├── edupage_api ├── get_homework.py ├── lunch_choose.py ├── lunch_simple.py ├── print_grades.py ├── substitution-cli.py ├── substitution.py ├── teachers.py └── timetables.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Your code** 14 | ```python 15 | Add here your code with bug... 16 | ``` 17 | 18 | **Error message** 19 | ``` 20 | Add here your error message (traceback) 21 | ``` 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Version** 27 | - Edupage API version: **(e.g. 0.9.6)** 28 | - Python version: **(e.g. 3.7.1)** 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature request] " 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | permissions: 7 | contents: write 8 | id-token: write 9 | 10 | on: 11 | push: 12 | tags: 13 | - '**' 14 | 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-latest 18 | 19 | environment: 20 | name: pypi 21 | url: https://pypi.org/p/edupage-api 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: "3.9" 30 | 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install setuptools wheel twine build pdoc3 35 | 36 | - name: Regenerate documentation 37 | run: | 38 | pdoc3 --html edupage_api 39 | rm -rf docs 40 | mv html/edupage_api docs 41 | 42 | - name: Commit and push documentation changes 43 | uses: EndBug/add-and-commit@v9 44 | with: 45 | add: "docs" 46 | default_author: github_actions 47 | message: "Regenerate documentation" 48 | 49 | pathspec_error_handling: ignore 50 | push: origin master 51 | 52 | - name: Build 53 | run: python -m build 54 | - name: Publish to pypi 55 | uses: pypa/gh-action-pypi-publish@release/v1 56 | 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .vscode/* 3 | 4 | MANIFEST 5 | dist 6 | dist/* 7 | 8 | README.rst 9 | 10 | __pycache__ 11 | edupage_api/__pycache__ 12 | 13 | build 14 | build/* 15 | edupage_api.egg-info 16 | edupage_api.egg-info/* 17 | test.py 18 | tests 19 | 20 | USERNAME 21 | PASSWORD 22 | 23 | dump.html 24 | tests/ 25 | test.py -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/edupage-rest"] 2 | path = examples/edupage-rest 3 | url = https://github.com/ivanhrabcak/edupage-rest 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `edupage-api` · [![Current version on PyPI](https://img.shields.io/pypi/v/edupage-api)](https://pypi.org/project/edupage-api/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/edupage-api)](https://pypi.org/project/edupage-api/) [![PyPI - Downloads](https://img.shields.io/pypi/dw/edupage-api)](https://pypistats.org/packages/edupage-api) [![CodeFactor](https://www.codefactor.io/repository/github/EdupageAPI/edupage-api/badge)](https://www.codefactor.io/repository/github/EdupageAPI/edupage-api) 2 | 3 | This Python library allows easy access to EduPage. It's not a Selenium web scraper. It makes requests directly to EduPage's endpoints and parses the HTML document. 4 | 5 | # Installing 6 | __Warning__: Requires Python >= 3.9! 7 | 8 | You can install this library using [`pip`](https://pypi.org/project/pip/): 9 | 10 | ``` 11 | pip install edupage-api 12 | ``` 13 | 14 | # Usage 15 | 16 | ## Login 17 | 18 | You can log in easily, it works with any school: 19 | 20 | ```python 21 | from edupage_api import Edupage 22 | from edupage_api.exceptions import BadCredentialsException, CaptchaException 23 | 24 | edupage = Edupage() 25 | 26 | try: 27 | edupage.login("Username", "Password", "Your school's subdomain") 28 | except BadCredentialsException: 29 | print("Wrong username or password!") 30 | except CaptchaException: 31 | print("Captcha required!") 32 | ``` 33 | 34 | # Documentation 35 | The docs are available [here](https://edupageapi.github.io/edupage-api/) 36 | 37 | # I have a problem or an idea! 38 | 39 | - If you find any issue with this code, or it doesn't work please, let us know by opening an [issue](https://github.com/EdupageAPI/edupage-api/issues/new/choose)! 40 | - Feel free to suggest any other features! Just open an [issue with the _Feature Request_ tag](https://github.com/EdupageAPI/edupage-api/issues/new?labels=feature+request&template=feature_request.md&title=%5BFeature+request%5D+). 41 | - If you, even better, have fixed the issue, added a new feature, or made something work better, please, open a [pull request](https://github.com/EdupageAPI/edupage-api/compare)! 42 | 43 | # Discord 44 | https://discord.gg/fg6zBu9ZAn 45 | -------------------------------------------------------------------------------- /docs/classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | edupage_api.classes API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |
35 |
36 |

Module edupage_api.classes

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Classes

48 |
49 |
50 | class Class 51 | (class_id: int,
name: str,
short: str,
homeroom_teachers: list[EduTeacher] | None,
homeroom: Classroom | None,
grade: int | None)
52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
@dataclass
 59 | class Class:
 60 |     class_id: int
 61 |     name: str
 62 |     short: str
 63 |     homeroom_teachers: Optional[list[EduTeacher]]
 64 |     homeroom: Optional[Classroom]
 65 |     grade: Optional[int]
66 |
67 |

Class(class_id: int, name: str, short: str, homeroom_teachers: Optional[list[edupage_api.people.EduTeacher]], homeroom: Optional[edupage_api.classrooms.Classroom], grade: Optional[int])

68 |

Class variables

69 |
70 |
var class_id : int
71 |
72 |
73 |
74 |
var grade : int | None
75 |
76 |
77 |
78 |
var homeroomClassroom | None
79 |
80 |
81 |
82 |
var homeroom_teachers : list[EduTeacher] | None
83 |
84 |
85 |
86 |
var name : str
87 |
88 |
89 |
90 |
var short : str
91 |
92 |
93 |
94 |
95 |
96 |
97 | class Classes 98 | (edupage: EdupageModule) 99 |
100 |
101 |
102 | 103 | Expand source code 104 | 105 |
class Classes(Module):
106 |     @ModuleHelper.logged_in
107 |     def get_classes(self) -> Optional[list]:
108 |         classes_list = DbiHelper(self.edupage).fetch_class_list()
109 | 
110 |         if classes_list is None:
111 |             return None
112 | 
113 |         classes = []
114 | 
115 |         for class_id_str, class_info in classes_list.items():
116 |             if not class_id_str:
117 |                 continue
118 | 
119 |             home_teacher_ids = [
120 |                 class_info.get("teacherid"),
121 |                 class_info.get("teacher2id"),
122 |             ]
123 |             home_teachers = [
124 |                 People(self.edupage).get_teacher(tid) for tid in home_teacher_ids if tid
125 |             ]
126 |             home_teachers = [ht for ht in home_teachers if ht]
127 | 
128 |             homeroom_id = class_info.get("classroomid")
129 |             homeroom = Classrooms(self.edupage).get_classroom(homeroom_id)
130 | 
131 |             classes.append(
132 |                 Class(
133 |                     int(class_id_str),
134 |                     class_info["name"],
135 |                     class_info["short"],
136 |                     home_teachers if home_teachers else None,
137 |                     homeroom,
138 |                     int(class_info["grade"]) if class_info["grade"] else None,
139 |                 )
140 |             )
141 | 
142 |         return classes
143 | 
144 |     def get_class(self, class_id: Union[int, str]) -> Optional[Class]:
145 |         try:
146 |             class_id = int(class_id)
147 |         except (ValueError, TypeError):
148 |             return None
149 | 
150 |         return next(
151 |             (
152 |                 edu_class
153 |                 for edu_class in self.get_classes()
154 |                 if edu_class.class_id == class_id
155 |             ),
156 |             None,
157 |         )
158 |
159 |
160 |

Ancestors

161 | 164 |

Methods

165 |
166 |
167 | def get_class(self, class_id: int | str) ‑> Class | None 168 |
169 |
170 |
171 | 172 | Expand source code 173 | 174 |
def get_class(self, class_id: Union[int, str]) -> Optional[Class]:
175 |     try:
176 |         class_id = int(class_id)
177 |     except (ValueError, TypeError):
178 |         return None
179 | 
180 |     return next(
181 |         (
182 |             edu_class
183 |             for edu_class in self.get_classes()
184 |             if edu_class.class_id == class_id
185 |         ),
186 |         None,
187 |     )
188 |
189 |
190 |
191 |
192 | def get_classes(self) ‑> list | None 193 |
194 |
195 |
196 | 197 | Expand source code 198 | 199 |
@ModuleHelper.logged_in
200 | def get_classes(self) -> Optional[list]:
201 |     classes_list = DbiHelper(self.edupage).fetch_class_list()
202 | 
203 |     if classes_list is None:
204 |         return None
205 | 
206 |     classes = []
207 | 
208 |     for class_id_str, class_info in classes_list.items():
209 |         if not class_id_str:
210 |             continue
211 | 
212 |         home_teacher_ids = [
213 |             class_info.get("teacherid"),
214 |             class_info.get("teacher2id"),
215 |         ]
216 |         home_teachers = [
217 |             People(self.edupage).get_teacher(tid) for tid in home_teacher_ids if tid
218 |         ]
219 |         home_teachers = [ht for ht in home_teachers if ht]
220 | 
221 |         homeroom_id = class_info.get("classroomid")
222 |         homeroom = Classrooms(self.edupage).get_classroom(homeroom_id)
223 | 
224 |         classes.append(
225 |             Class(
226 |                 int(class_id_str),
227 |                 class_info["name"],
228 |                 class_info["short"],
229 |                 home_teachers if home_teachers else None,
230 |                 homeroom,
231 |                 int(class_info["grade"]) if class_info["grade"] else None,
232 |             )
233 |         )
234 | 
235 |     return classes
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 | 278 |
279 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /docs/classrooms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | edupage_api.classrooms API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |
35 |
36 |

Module edupage_api.classrooms

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Classes

48 |
49 |
50 | class Classroom 51 | (classroom_id: int, name: str, short: str) 52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
@dataclass
 59 | class Classroom:
 60 |     classroom_id: int
 61 |     name: str
 62 |     short: str
63 |
64 |

Classroom(classroom_id: int, name: str, short: str)

65 |

Class variables

66 |
67 |
var classroom_id : int
68 |
69 |
70 |
71 |
var name : str
72 |
73 |
74 |
75 |
var short : str
76 |
77 |
78 |
79 |
80 |
81 |
82 | class Classrooms 83 | (edupage: EdupageModule) 84 |
85 |
86 |
87 | 88 | Expand source code 89 | 90 |
class Classrooms(Module):
 91 |     @ModuleHelper.logged_in
 92 |     def get_classrooms(self) -> Optional[list]:
 93 |         classroom_list = DbiHelper(self.edupage).fetch_classroom_list()
 94 | 
 95 |         if classroom_list is None:
 96 |             return None
 97 | 
 98 |         classrooms = []
 99 | 
100 |         for classroom_id_str in classroom_list:
101 |             if not classroom_id_str:
102 |                 continue
103 | 
104 |             classrooms.append(
105 |                 Classroom(
106 |                     int(classroom_id_str),
107 |                     classroom_list[classroom_id_str]["name"],
108 |                     classroom_list[classroom_id_str]["short"],
109 |                 )
110 |             )
111 | 
112 |         return classrooms
113 | 
114 |     def get_classroom(self, classroom_id: Union[int, str]) -> Optional[Classroom]:
115 |         try:
116 |             classroom_id = int(classroom_id)
117 |         except (ValueError, TypeError):
118 |             return None
119 | 
120 |         return next(
121 |             (
122 |                 classroom
123 |                 for classroom in self.get_classrooms()
124 |                 if classroom.classroom_id == classroom_id
125 |             ),
126 |             None,
127 |         )
128 |
129 |
130 |

Ancestors

131 | 134 |

Methods

135 |
136 |
137 | def get_classroom(self, classroom_id: int | str) ‑> Classroom | None 138 |
139 |
140 |
141 | 142 | Expand source code 143 | 144 |
def get_classroom(self, classroom_id: Union[int, str]) -> Optional[Classroom]:
145 |     try:
146 |         classroom_id = int(classroom_id)
147 |     except (ValueError, TypeError):
148 |         return None
149 | 
150 |     return next(
151 |         (
152 |             classroom
153 |             for classroom in self.get_classrooms()
154 |             if classroom.classroom_id == classroom_id
155 |         ),
156 |         None,
157 |     )
158 |
159 |
160 |
161 |
162 | def get_classrooms(self) ‑> list | None 163 |
164 |
165 |
166 | 167 | Expand source code 168 | 169 |
@ModuleHelper.logged_in
170 | def get_classrooms(self) -> Optional[list]:
171 |     classroom_list = DbiHelper(self.edupage).fetch_classroom_list()
172 | 
173 |     if classroom_list is None:
174 |         return None
175 | 
176 |     classrooms = []
177 | 
178 |     for classroom_id_str in classroom_list:
179 |         if not classroom_id_str:
180 |             continue
181 | 
182 |         classrooms.append(
183 |             Classroom(
184 |                 int(classroom_id_str),
185 |                 classroom_list[classroom_id_str]["name"],
186 |                 classroom_list[classroom_id_str]["short"],
187 |             )
188 |         )
189 | 
190 |     return classrooms
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | 230 |
231 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /docs/custom_request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | edupage_api.custom_request API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |
35 |
36 |

Module edupage_api.custom_request

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Classes

48 |
49 |
50 | class CustomRequest 51 | (edupage: EdupageModule) 52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
class CustomRequest(Module):
 59 |     def custom_request(
 60 |         self, url: str, method: str, data: str = "", headers: dict = {}
 61 |     ) -> Response:
 62 |         if method == "GET":
 63 |             response = self.edupage.session.get(url, headers=headers)
 64 |         elif method == "POST":
 65 |             response = self.edupage.session.post(url, data=data, headers=headers)
 66 | 
 67 |         return response
68 |
69 |
70 |

Ancestors

71 | 74 |

Methods

75 |
76 |
77 | def custom_request(self, url: str, method: str, data: str = '', headers: dict = {}) ‑> requests.models.Response 78 |
79 |
80 |
81 | 82 | Expand source code 83 | 84 |
def custom_request(
 85 |     self, url: str, method: str, data: str = "", headers: dict = {}
 86 | ) -> Response:
 87 |     if method == "GET":
 88 |         response = self.edupage.session.get(url, headers=headers)
 89 |     elif method == "POST":
 90 |         response = self.edupage.session.post(url, data=data, headers=headers)
 91 | 
 92 |     return response
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | 123 |
124 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /docs/messages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | edupage_api.messages API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |
35 |
36 |

Module edupage_api.messages

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Classes

48 |
49 |
50 | class Messages 51 | (edupage: EdupageModule) 52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
class Messages(Module):
 59 |     def send_message(
 60 |         self, recipients: Union[list[EduAccount], EduAccount, list[str]], body: str
 61 |     ) -> int:
 62 |         recipient_string = ""
 63 | 
 64 |         if isinstance(recipients, list):
 65 |             if len(recipients) == 0:
 66 |                 raise InvalidRecipientsException("The recipients parameter is empty!")
 67 | 
 68 |             if type(recipients[0]) == EduAccount:
 69 |                 recipient_string = ";".join([r.get_id() for r in recipients])
 70 |             else:
 71 |                 recipient_string = ";".join(recipients)
 72 |         else:
 73 |             recipient_string = recipients.get_id()
 74 | 
 75 |         data = RequestData.encode_request_body(
 76 |             {
 77 |                 "selectedUser": recipient_string,
 78 |                 "text": body,
 79 |                 "attachements": "{}",
 80 |                 "receipt": "0",
 81 |                 "typ": "sprava",
 82 |             }
 83 |         )
 84 | 
 85 |         headers = {"Content-Type": "application/x-www-form-urlencoded"}
 86 | 
 87 |         request_url = f"https://{self.edupage.subdomain}.edupage.org/timeline/?=&akcia=createItem&eqav=1&maxEqav=7"
 88 |         response = self.edupage.session.post(request_url, data=data, headers=headers)
 89 | 
 90 |         response_text = RequestData.decode_response(response.text)
 91 |         if response_text == "0":
 92 |             raise RequestError("Edupage returned an error response")
 93 | 
 94 |         response = json.loads(response_text)
 95 | 
 96 |         changes = response.get("changes")
 97 |         if changes == [] or changes is None:
 98 |             raise RequestError(
 99 |                 "Failed to send message (edupage returned an empty 'changes' array) - https://github.com/EdupageAPI/edupage-api/issues/62"
100 |             )
101 | 
102 |         return int(changes[0].get("timelineid"))
103 |
104 |
105 |

Ancestors

106 | 109 |

Methods

110 |
111 |
112 | def send_message(self,
recipients: list[EduAccount] | EduAccount | list[str],
body: str) ‑> int
113 |
114 |
115 |
116 | 117 | Expand source code 118 | 119 |
def send_message(
120 |     self, recipients: Union[list[EduAccount], EduAccount, list[str]], body: str
121 | ) -> int:
122 |     recipient_string = ""
123 | 
124 |     if isinstance(recipients, list):
125 |         if len(recipients) == 0:
126 |             raise InvalidRecipientsException("The recipients parameter is empty!")
127 | 
128 |         if type(recipients[0]) == EduAccount:
129 |             recipient_string = ";".join([r.get_id() for r in recipients])
130 |         else:
131 |             recipient_string = ";".join(recipients)
132 |     else:
133 |         recipient_string = recipients.get_id()
134 | 
135 |     data = RequestData.encode_request_body(
136 |         {
137 |             "selectedUser": recipient_string,
138 |             "text": body,
139 |             "attachements": "{}",
140 |             "receipt": "0",
141 |             "typ": "sprava",
142 |         }
143 |     )
144 | 
145 |     headers = {"Content-Type": "application/x-www-form-urlencoded"}
146 | 
147 |     request_url = f"https://{self.edupage.subdomain}.edupage.org/timeline/?=&akcia=createItem&eqav=1&maxEqav=7"
148 |     response = self.edupage.session.post(request_url, data=data, headers=headers)
149 | 
150 |     response_text = RequestData.decode_response(response.text)
151 |     if response_text == "0":
152 |         raise RequestError("Edupage returned an error response")
153 | 
154 |     response = json.loads(response_text)
155 | 
156 |     changes = response.get("changes")
157 |     if changes == [] or changes is None:
158 |         raise RequestError(
159 |             "Failed to send message (edupage returned an empty 'changes' array) - https://github.com/EdupageAPI/edupage-api/issues/62"
160 |         )
161 | 
162 |     return int(changes[0].get("timelineid"))
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 | 193 |
194 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /docs/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | edupage_api.parent API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |
35 |
36 |

Module edupage_api.parent

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Classes

48 |
49 |
50 | class Parent 51 | (edupage: EdupageModule) 52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
class Parent(Module):
 59 |     @ModuleHelper.logged_in
 60 |     @ModuleHelper.is_parent
 61 |     def switch_to_child(self, child: Union[EduAccount, int]):
 62 |         params = {"studentid": child.person_id if type(child) == EduAccount else child}
 63 | 
 64 |         url = f"https://{self.edupage.subdomain}.edupage.org/login/switchchild"
 65 |         response = self.edupage.session.get(url, params=params)
 66 | 
 67 |         if response.text != "OK":
 68 |             raise InvalidChildException(
 69 |                 f"{response.text}: Invalid child selected! (not your child?)"
 70 |             )
 71 | 
 72 |     @ModuleHelper.logged_in
 73 |     @ModuleHelper.is_parent
 74 |     def switch_to_parent(self):
 75 |         # variable name is from edupage's code :/
 76 |         rid = f"edupage;{self.edupage.subdomain};{self.edupage.username}"
 77 | 
 78 |         params = {"rid": rid}
 79 | 
 80 |         url = f"https://{self.edupage.subdomain}.edupage.org/login/edupageChange"
 81 |         response = self.edupage.session.get(url, params=params)
 82 | 
 83 |         if "EdupageLoginFailed" in response.url:
 84 |             raise UnknownServerError()
85 |
86 |
87 |

Ancestors

88 | 91 |

Methods

92 |
93 |
94 | def switch_to_child(self,
child: EduAccount | int)
95 |
96 |
97 |
98 | 99 | Expand source code 100 | 101 |
@ModuleHelper.logged_in
102 | @ModuleHelper.is_parent
103 | def switch_to_child(self, child: Union[EduAccount, int]):
104 |     params = {"studentid": child.person_id if type(child) == EduAccount else child}
105 | 
106 |     url = f"https://{self.edupage.subdomain}.edupage.org/login/switchchild"
107 |     response = self.edupage.session.get(url, params=params)
108 | 
109 |     if response.text != "OK":
110 |         raise InvalidChildException(
111 |             f"{response.text}: Invalid child selected! (not your child?)"
112 |         )
113 |
114 |
115 |
116 |
117 | def switch_to_parent(self) 118 |
119 |
120 |
121 | 122 | Expand source code 123 | 124 |
@ModuleHelper.logged_in
125 | @ModuleHelper.is_parent
126 | def switch_to_parent(self):
127 |     # variable name is from edupage's code :/
128 |     rid = f"edupage;{self.edupage.subdomain};{self.edupage.username}"
129 | 
130 |     params = {"rid": rid}
131 | 
132 |     url = f"https://{self.edupage.subdomain}.edupage.org/login/edupageChange"
133 |     response = self.edupage.session.get(url, params=params)
134 | 
135 |     if "EdupageLoginFailed" in response.url:
136 |         raise UnknownServerError()
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | 168 |
169 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /docs/ringing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | edupage_api.ringing API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |
35 |
36 |

Module edupage_api.ringing

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Classes

48 |
49 |
50 | class RingingTime 51 | (type: RingingType,
time: datetime.time)
52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
@dataclass
 59 | class RingingTime:
 60 |     # The thing this ringing is announcing (break or lesson)
 61 |     type: RingingType
 62 |     time: time
63 |
64 |

RingingTime(type: edupage_api.ringing.RingingType, time: datetime.time)

65 |

Class variables

66 |
67 |
var time : datetime.time
68 |
69 |
70 |
71 |
var typeRingingType
72 |
73 |
74 |
75 |
76 |
77 |
78 | class RingingTimes 79 | (edupage: EdupageModule) 80 |
81 |
82 |
83 | 84 | Expand source code 85 | 86 |
class RingingTimes(Module):
 87 |     @staticmethod
 88 |     def __parse_time(s: str) -> time:
 89 |         hours, minutes = s.split(":")
 90 |         return time(int(hours), int(minutes))
 91 | 
 92 |     @staticmethod
 93 |     def __set_hours_and_minutes(dt: datetime, hours: int, minutes: int) -> datetime:
 94 |         return datetime(dt.year, dt.month, dt.day, hours, minutes)
 95 | 
 96 |     @staticmethod
 97 |     def __get_next_workday(date_time: datetime):
 98 |         if date_time.date().weekday() == 5:
 99 |             date_time = RingingTimes.__set_hours_and_minutes(date_time, 0, 0)
100 |             return date_time + timedelta(days=2)
101 |         elif date_time.date().weekday() == 6:
102 |             date_time = RingingTimes.__set_hours_and_minutes(date_time, 0, 0)
103 |             return date_time + timedelta(days=1)
104 |         else:
105 |             return date_time
106 | 
107 |     @ModuleHelper.logged_in
108 |     def get_next_ringing_time(self, date_time: datetime) -> RingingTime:
109 |         date_time = RingingTimes.__get_next_workday(date_time)
110 | 
111 |         ringing_times = self.edupage.data.get("zvonenia")
112 |         for ringing_time in ringing_times:
113 |             start_time = RingingTimes.__parse_time(ringing_time.get("starttime"))
114 |             if date_time.time() < start_time:
115 |                 date_time = RingingTimes.__set_hours_and_minutes(
116 |                     date_time, start_time.hour, start_time.minute
117 |                 )
118 | 
119 |                 return RingingTime(RingingType.LESSON, date_time)
120 | 
121 |             end_time = RingingTimes.__parse_time(ringing_time.get("endtime"))
122 |             if date_time.time() < end_time:
123 |                 date_time = RingingTimes.__set_hours_and_minutes(
124 |                     date_time, end_time.hour, end_time.minute
125 |                 )
126 | 
127 |                 return RingingTime(RingingType.BREAK, date_time)
128 | 
129 |         date_time += timedelta(1)
130 |         date_time = RingingTimes.__set_hours_and_minutes(date_time, 0, 0)
131 | 
132 |         return self.get_next_ringing_time(date_time)
133 |
134 |
135 |

Ancestors

136 | 139 |

Methods

140 |
141 |
142 | def get_next_ringing_time(self, date_time: datetime.datetime) ‑> RingingTime 143 |
144 |
145 |
146 | 147 | Expand source code 148 | 149 |
@ModuleHelper.logged_in
150 | def get_next_ringing_time(self, date_time: datetime) -> RingingTime:
151 |     date_time = RingingTimes.__get_next_workday(date_time)
152 | 
153 |     ringing_times = self.edupage.data.get("zvonenia")
154 |     for ringing_time in ringing_times:
155 |         start_time = RingingTimes.__parse_time(ringing_time.get("starttime"))
156 |         if date_time.time() < start_time:
157 |             date_time = RingingTimes.__set_hours_and_minutes(
158 |                 date_time, start_time.hour, start_time.minute
159 |             )
160 | 
161 |             return RingingTime(RingingType.LESSON, date_time)
162 | 
163 |         end_time = RingingTimes.__parse_time(ringing_time.get("endtime"))
164 |         if date_time.time() < end_time:
165 |             date_time = RingingTimes.__set_hours_and_minutes(
166 |                 date_time, end_time.hour, end_time.minute
167 |             )
168 | 
169 |             return RingingTime(RingingType.BREAK, date_time)
170 | 
171 |     date_time += timedelta(1)
172 |     date_time = RingingTimes.__set_hours_and_minutes(date_time, 0, 0)
173 | 
174 |     return self.get_next_ringing_time(date_time)
175 |
176 |
177 |
178 |
179 |
180 |
181 | class RingingType 182 | (value, names=None, *, module=None, qualname=None, type=None, start=1) 183 |
184 |
185 |
186 | 187 | Expand source code 188 | 189 |
class RingingType(str, Enum):
190 |     BREAK = "BREAK"
191 |     LESSON = "LESSON"
192 |
193 |

An enumeration.

194 |

Ancestors

195 |
    196 |
  • builtins.str
  • 197 |
  • enum.Enum
  • 198 |
199 |

Class variables

200 |
201 |
var BREAK
202 |
203 |
204 |
205 |
var LESSON
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | 250 |
251 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /docs/subjects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | edupage_api.subjects API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 33 |
34 |
35 |
36 |

Module edupage_api.subjects

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

Classes

48 |
49 |
50 | class Subject 51 | (subject_id: int, name: str, short: str) 52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
@dataclass
 59 | class Subject:
 60 |     subject_id: int
 61 |     name: str
 62 |     short: str
63 |
64 |

Subject(subject_id: int, name: str, short: str)

65 |

Class variables

66 |
67 |
var name : str
68 |
69 |
70 |
71 |
var short : str
72 |
73 |
74 |
75 |
var subject_id : int
76 |
77 |
78 |
79 |
80 |
81 |
82 | class Subjects 83 | (edupage: EdupageModule) 84 |
85 |
86 |
87 | 88 | Expand source code 89 | 90 |
class Subjects(Module):
 91 |     @ModuleHelper.logged_in
 92 |     def get_subjects(self) -> Optional[list]:
 93 |         subject_list = DbiHelper(self.edupage).fetch_subject_list()
 94 | 
 95 |         if subject_list is None:
 96 |             return None
 97 | 
 98 |         subjects = []
 99 | 
100 |         for subject_id_str in subject_list:
101 |             if not subject_id_str:
102 |                 continue
103 | 
104 |             subjects.append(
105 |                 Subject(
106 |                     int(subject_id_str),
107 |                     subject_list[subject_id_str]["name"],
108 |                     subject_list[subject_id_str]["short"],
109 |                 )
110 |             )
111 | 
112 |         return subjects
113 | 
114 |     def get_subject(self, subject_id: Union[int, str]) -> Optional[Subject]:
115 |         try:
116 |             subject_id = int(subject_id)
117 |         except (ValueError, TypeError):
118 |             return None
119 | 
120 |         return next(
121 |             (
122 |                 subject
123 |                 for subject in self.get_subjects()
124 |                 if subject.subject_id == subject_id
125 |             ),
126 |             None,
127 |         )
128 |
129 |
130 |

Ancestors

131 | 134 |

Methods

135 |
136 |
137 | def get_subject(self, subject_id: int | str) ‑> Subject | None 138 |
139 |
140 |
141 | 142 | Expand source code 143 | 144 |
def get_subject(self, subject_id: Union[int, str]) -> Optional[Subject]:
145 |     try:
146 |         subject_id = int(subject_id)
147 |     except (ValueError, TypeError):
148 |         return None
149 | 
150 |     return next(
151 |         (
152 |             subject
153 |             for subject in self.get_subjects()
154 |             if subject.subject_id == subject_id
155 |         ),
156 |         None,
157 |     )
158 |
159 |
160 |
161 |
162 | def get_subjects(self) ‑> list | None 163 |
164 |
165 |
166 | 167 | Expand source code 168 | 169 |
@ModuleHelper.logged_in
170 | def get_subjects(self) -> Optional[list]:
171 |     subject_list = DbiHelper(self.edupage).fetch_subject_list()
172 | 
173 |     if subject_list is None:
174 |         return None
175 | 
176 |     subjects = []
177 | 
178 |     for subject_id_str in subject_list:
179 |         if not subject_id_str:
180 |             continue
181 | 
182 |         subjects.append(
183 |             Subject(
184 |                 int(subject_id_str),
185 |                 subject_list[subject_id_str]["name"],
186 |                 subject_list[subject_id_str]["short"],
187 |             )
188 |         )
189 | 
190 |     return subjects
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | 230 |
231 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /edupage_api/__init__.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from datetime import date, datetime 3 | from io import TextIOWrapper 4 | from typing import Optional, Union 5 | 6 | import requests 7 | from requests import Response 8 | 9 | from edupage_api.classes import Class, Classes 10 | from edupage_api.classrooms import Classroom, Classrooms 11 | from edupage_api.cloud import Cloud, EduCloudFile 12 | from edupage_api.custom_request import CustomRequest 13 | from edupage_api.grades import EduGrade, Grades, Term 14 | from edupage_api.login import Login, TwoFactorLogin 15 | from edupage_api.lunches import Lunches, Meals 16 | from edupage_api.messages import Messages 17 | from edupage_api.module import EdupageModule 18 | from edupage_api.parent import Parent 19 | from edupage_api.people import ( 20 | EduAccount, 21 | EduStudent, 22 | EduStudentSkeleton, 23 | EduTeacher, 24 | People, 25 | ) 26 | from edupage_api.ringing import RingingTime, RingingTimes 27 | from edupage_api.subjects import Subject, Subjects 28 | from edupage_api.substitution import Substitution, TimetableChange 29 | from edupage_api.timeline import TimelineEvent, TimelineEvents 30 | from edupage_api.timetables import Timetable, Timetables 31 | 32 | 33 | class Edupage(EdupageModule): 34 | def __init__(self, request_timeout=5): 35 | """Initialize `Edupage` object. 36 | 37 | Args: 38 | request_timeout (int, optional): Length of request timeout in seconds. 39 | If want to upload bigger files, you will have to increase its value. 40 | Defaults to `5`. 41 | """ 42 | 43 | self.data = None 44 | self.is_logged_in = False 45 | self.subdomain = None 46 | self.gsec_hash = None 47 | self.username = None 48 | 49 | self.session = requests.session() 50 | self.session.request = functools.partial( 51 | self.session.request, timeout=request_timeout 52 | ) 53 | 54 | def login( 55 | self, username: str, password: str, subdomain: str 56 | ) -> Optional[TwoFactorLogin]: 57 | """Login while specifying the subdomain to log into. 58 | 59 | Args: 60 | username (str): Your username. 61 | password (str): Your password. 62 | subdomain (str): Subdomain of your school (https://{subdomain}.edupage.org). 63 | 64 | Returns: 65 | Optional[TwoFactorLogin]: Returns `None` if no second factor was needed to login, 66 | or the `TwoFactorLogin` object that is used to complete 2fa. 67 | """ 68 | 69 | return Login(self).login(username, password, subdomain) 70 | 71 | def login_auto(self, username: str, password: str) -> Optional[TwoFactorLogin]: 72 | """Login using https://portal.edupage.org. If this doesn't work, please use `Edupage.login`. 73 | 74 | Args: 75 | username (str): Your username. 76 | password (str): Your password. 77 | 78 | Returns: 79 | Optional[TwoFactorLogin]: Returns `None` if no second factor was needed to login, 80 | or the `TwoFactorLogin` object that is used to complete 2fa. 81 | """ 82 | 83 | return Login(self).login(username, password) 84 | 85 | def get_students(self) -> Optional[list[EduStudent]]: 86 | """Get list of all students in your class. 87 | 88 | Returns: 89 | Optional[list[EduStudent]]: List of `EduStudent`s. 90 | """ 91 | 92 | return People(self).get_students() 93 | 94 | def get_all_students(self) -> Optional[list[EduStudentSkeleton]]: 95 | """Get list of all students in your school. 96 | 97 | Returns: 98 | Optional[list[EduStudentSkeleton]]: List of `EduStudentSkeleton`s. 99 | """ 100 | 101 | return People(self).get_all_students() 102 | 103 | def get_teachers(self) -> Optional[list[EduTeacher]]: 104 | """Get list of all teachers in your school. 105 | 106 | Returns: 107 | Optional[list[EduTeacher]]: List of `EduTeacher`s. 108 | """ 109 | 110 | return People(self).get_teachers() 111 | 112 | def get_classrooms(self) -> Optional[list[Classroom]]: 113 | """Get list of all classrooms in your school. 114 | 115 | Returns: 116 | Optional[list[Classroom]]: List of `Classroom`s. 117 | """ 118 | 119 | return Classrooms(self).get_classrooms() 120 | 121 | def get_classes(self) -> Optional[list[Class]]: 122 | """Get list of all classes in your school. 123 | 124 | Returns: 125 | Optional[list[Class]]: List of `Class`es. 126 | """ 127 | 128 | return Classes(self).get_classes() 129 | 130 | def get_subjects(self) -> Optional[list[Subject]]: 131 | """Get list of all subjects in your school. 132 | 133 | Returns: 134 | Optional[list[Subject]]: List of `Subject`s. 135 | """ 136 | 137 | return Subjects(self).get_subjects() 138 | 139 | def send_message( 140 | self, recipients: Union[list[EduAccount], EduAccount], body: str 141 | ) -> int: 142 | """Send message. 143 | 144 | Args: 145 | recipients (Optional[list[EduAccount]]): Recipients of your message (list of `EduAccount`s). 146 | body (str): Body of your message. 147 | 148 | Returns: 149 | int: The timeline id of the new message. 150 | """ 151 | 152 | return Messages(self).send_message(recipients, body) 153 | 154 | def get_my_timetable(self, date: date) -> Optional[Timetable]: 155 | """Get timetable for the logged-in user on a specified date. 156 | 157 | Args: 158 | date (datetime.date): The date for which you want to get timetable 159 | 160 | Returns: 161 | Optional[Timetable]: `Timetable` object for the specified date, if available; otherwise, `None`. 162 | """ 163 | 164 | return Timetables(self).get_my_timetable(date) 165 | 166 | def get_meals(self, date: date) -> Optional[Meals]: 167 | """Get lunches. 168 | 169 | Args: 170 | date (datetime.date): Date from which you want to get lunches. 171 | 172 | Returns: 173 | Optional[Lunch]: Lunch object for entered date. 174 | """ 175 | 176 | return Lunches(self).get_meals(date) 177 | 178 | def get_notifications(self) -> list[TimelineEvent]: 179 | """Get list of all available notifications. 180 | 181 | Returns: 182 | list[TimelineEvent]: List of `TimelineEvent`s. 183 | """ 184 | 185 | return TimelineEvents(self).get_notifications() 186 | 187 | def cloud_upload(self, fd: TextIOWrapper) -> EduCloudFile: 188 | """Upload file to EduPage cloud. 189 | 190 | Args: 191 | fd (TextIOWrapper): File you want to upload. 192 | 193 | Returns: 194 | EduCloudFile: Object of uploaded file. 195 | """ 196 | 197 | return Cloud(self).upload_file(fd) 198 | 199 | def get_grades(self) -> list[EduGrade]: 200 | """Get a list of all available grades. 201 | 202 | Returns: 203 | list[EduGrade]: List of `EduGrade`s. 204 | """ 205 | 206 | return Grades(self).get_grades(year=None, term=None) 207 | 208 | def get_grades_for_term(self, year: int, term: Term) -> list[EduGrade]: 209 | """Get a list of all available grades for a given year and term 210 | 211 | Returns: 212 | list[EduGrade]: List of `EduGrade`s 213 | """ 214 | 215 | return Grades(self).get_grades(year=year, term=term) 216 | 217 | def get_user_id(self) -> str: 218 | """Get your EduPage user ID. 219 | 220 | Returns: 221 | str: Your EduPage user ID. 222 | """ 223 | 224 | return self.data.get("userid") 225 | 226 | def custom_request( 227 | self, url: str, method: str, data: str = "", headers: dict = {} 228 | ) -> Response: 229 | """Send custom request to EduPage. 230 | 231 | Args: 232 | url (str): URL endpoint. 233 | method (str): Method (`GET` or `POST`). 234 | data (str, optional): Request data. Defaults to `""`. 235 | headers (dict, optional): Request headers. Defaults to `{}`. 236 | 237 | Returns: 238 | Response: Response. 239 | """ 240 | 241 | return CustomRequest(self).custom_request(url, method, data, headers) 242 | 243 | def get_missing_teachers(self, date: date) -> list[EduTeacher]: 244 | """Get missing teachers for a given date. 245 | 246 | Args: 247 | date (datetime.date): The date you want to get this information for. 248 | 249 | Returns: 250 | list[EduTeacher]: List of the missing teachers for `date`. 251 | """ 252 | return Substitution(self).get_missing_teachers(date) 253 | 254 | def get_timetable_changes(self, date: date) -> list[TimetableChange]: 255 | """Get the changes in the timetable for a given date. 256 | 257 | Args: 258 | date (datetime.date): The date you want to get this information for. 259 | 260 | Returns: 261 | list[TimetableChange]: List of changes in the timetable. 262 | """ 263 | return Substitution(self).get_timetable_changes(date) 264 | 265 | def get_school_year(self) -> int: 266 | """Returns the current school year. 267 | 268 | Returns: 269 | int: The starting year of the current school year. 270 | """ 271 | return Timetables(self).get_school_year() 272 | 273 | def get_timetable( 274 | self, 275 | target: Union[EduTeacher, EduStudent, Class, Classroom], 276 | date: date, 277 | ) -> Optional[Timetable]: 278 | """Get timetable of a teacher, student, class, or classroom for a specific date. 279 | 280 | Args: 281 | target (Union[EduTeacher, EduStudent, Class, Classroom]): The target entity whose timetable you want. 282 | date (datetime.date): The date for which you want the timetable. 283 | 284 | Returns: 285 | Optional[Timetable]: `Timetable` object for the specified date, if available; otherwise, `None`. 286 | """ 287 | 288 | return Timetables(self).get_timetable(target, date) 289 | 290 | def get_next_ringing_time(self, date_time: datetime) -> RingingTime: 291 | """Get the next lesson's ringing time for given `date_time`. 292 | 293 | Args: 294 | date_time (datetime.datetime): The (date)time you want to get this information for. 295 | 296 | Returns: 297 | RingingTime: The type (break or lesson) and time of the next ringing. 298 | """ 299 | return RingingTimes(self).get_next_ringing_time(date_time) 300 | 301 | def switch_to_child(self, child: Union[EduAccount, int]): 302 | """Switch to an account of a child - can only be used on parent accounts 303 | 304 | Args: 305 | child (EduAccount | int): The account or `person_id` of the child you want to switch to 306 | 307 | Note: When you switch to a child account, all other methods will return data as if you were logged in as `child` 308 | """ 309 | Parent(self).switch_to_child(child) 310 | 311 | def switch_to_parent(self): 312 | """Switches back to your parent account - can only be used on parent accounts""" 313 | Parent(self).switch_to_parent() 314 | 315 | @classmethod 316 | def from_session_id(cls, session_id: str, subdomain: str): 317 | """Create an `Edupage` instance with a session id and subdomain. 318 | 319 | Args: 320 | session_id (str): The `PHPSESSID` cookie. 321 | subdomain (str): Subdomain of the school which cookie is from. 322 | 323 | Returns: 324 | Edupage: A new `Edupage` instance. 325 | """ 326 | instance = cls() 327 | 328 | Login(instance).reload_data(subdomain, session_id) 329 | 330 | return instance 331 | -------------------------------------------------------------------------------- /edupage_api/classes.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Union 3 | 4 | from edupage_api.classrooms import Classroom, Classrooms 5 | from edupage_api.dbi import DbiHelper 6 | from edupage_api.module import Module, ModuleHelper 7 | from edupage_api.people import EduTeacher, People 8 | 9 | 10 | @dataclass 11 | class Class: 12 | class_id: int 13 | name: str 14 | short: str 15 | homeroom_teachers: Optional[list[EduTeacher]] 16 | homeroom: Optional[Classroom] 17 | grade: Optional[int] 18 | 19 | 20 | class Classes(Module): 21 | @ModuleHelper.logged_in 22 | def get_classes(self) -> Optional[list]: 23 | classes_list = DbiHelper(self.edupage).fetch_class_list() 24 | 25 | if classes_list is None: 26 | return None 27 | 28 | classes = [] 29 | 30 | for class_id_str, class_info in classes_list.items(): 31 | if not class_id_str: 32 | continue 33 | 34 | home_teacher_ids = [ 35 | class_info.get("teacherid"), 36 | class_info.get("teacher2id"), 37 | ] 38 | home_teachers = [ 39 | People(self.edupage).get_teacher(tid) for tid in home_teacher_ids if tid 40 | ] 41 | home_teachers = [ht for ht in home_teachers if ht] 42 | 43 | homeroom_id = class_info.get("classroomid") 44 | homeroom = Classrooms(self.edupage).get_classroom(homeroom_id) 45 | 46 | classes.append( 47 | Class( 48 | int(class_id_str), 49 | class_info["name"], 50 | class_info["short"], 51 | home_teachers if home_teachers else None, 52 | homeroom, 53 | int(class_info["grade"]) if class_info["grade"] else None, 54 | ) 55 | ) 56 | 57 | return classes 58 | 59 | def get_class(self, class_id: Union[int, str]) -> Optional[Class]: 60 | try: 61 | class_id = int(class_id) 62 | except (ValueError, TypeError): 63 | return None 64 | 65 | return next( 66 | ( 67 | edu_class 68 | for edu_class in self.get_classes() 69 | if edu_class.class_id == class_id 70 | ), 71 | None, 72 | ) 73 | -------------------------------------------------------------------------------- /edupage_api/classrooms.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Union 3 | 4 | from edupage_api.dbi import DbiHelper 5 | from edupage_api.module import Module, ModuleHelper 6 | 7 | 8 | @dataclass 9 | class Classroom: 10 | classroom_id: int 11 | name: str 12 | short: str 13 | 14 | 15 | class Classrooms(Module): 16 | @ModuleHelper.logged_in 17 | def get_classrooms(self) -> Optional[list]: 18 | classroom_list = DbiHelper(self.edupage).fetch_classroom_list() 19 | 20 | if classroom_list is None: 21 | return None 22 | 23 | classrooms = [] 24 | 25 | for classroom_id_str in classroom_list: 26 | if not classroom_id_str: 27 | continue 28 | 29 | classrooms.append( 30 | Classroom( 31 | int(classroom_id_str), 32 | classroom_list[classroom_id_str]["name"], 33 | classroom_list[classroom_id_str]["short"], 34 | ) 35 | ) 36 | 37 | return classrooms 38 | 39 | def get_classroom(self, classroom_id: Union[int, str]) -> Optional[Classroom]: 40 | try: 41 | classroom_id = int(classroom_id) 42 | except (ValueError, TypeError): 43 | return None 44 | 45 | return next( 46 | ( 47 | classroom 48 | for classroom in self.get_classrooms() 49 | if classroom.classroom_id == classroom_id 50 | ), 51 | None, 52 | ) 53 | -------------------------------------------------------------------------------- /edupage_api/cloud.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from io import TextIOWrapper 4 | 5 | from edupage_api.exceptions import FailedToUploadFileException 6 | from edupage_api.module import EdupageModule, Module, ModuleHelper 7 | 8 | 9 | @dataclass 10 | class EduCloudFile: 11 | cloud_id: str 12 | extension: str 13 | file_type: str 14 | file: str 15 | name: str 16 | 17 | def get_url(self, edupage: EdupageModule): 18 | """Get url of given `EduCloudFile`. 19 | 20 | Args: 21 | edupage (EdupageModule): `Edupage` object. 22 | 23 | Returns: 24 | str: Direct URL to file. 25 | """ 26 | 27 | return f"https://{edupage.subdomain}.edupage.org{self.file}" 28 | 29 | @staticmethod 30 | def parse(data: dict): 31 | """Parse `EduCloudFile` data. 32 | 33 | Args: 34 | data (dict): Data to parse. 35 | 36 | Returns: 37 | EduCloudFile: `EduCloudFile` object. 38 | """ 39 | 40 | return EduCloudFile( 41 | data.get("cloudid"), 42 | data.get("extension"), 43 | data.get("type"), 44 | data.get("file"), 45 | data.get("name"), 46 | ) 47 | 48 | 49 | class Cloud(Module): 50 | @ModuleHelper.logged_in 51 | def upload_file(self, fd: TextIOWrapper) -> EduCloudFile: 52 | """Upload file to EduPage cloud. 53 | 54 | The file will be hosted forever (and for free) on EduPage's servers. The file is tied to 55 | your user account, but anybody with a link can view it. 56 | 57 | **Warning!** EduPage limits file size to 50 MB and the file can have only some extensions. 58 | You can find all supported file extensions on this 59 | [Edupage help site](https://help.edupage.org/?p=u1/u113/u132/u362/u467). 60 | 61 | If you are willing to upload some files, you will probably have to increase the request 62 | timeout. 63 | 64 | ``` 65 | >>> with open("image.jpg", "rb") as f: 66 | ... edupage.cloud_upload(f) 67 | 68 | ``` 69 | 70 | Args: 71 | fd (TextIOWrapper): File you want to upload. 72 | 73 | Raises: 74 | FailedToUploadFileException: There was a problem with uploading your file. 75 | 76 | Returns: 77 | EduCloudFile: `EduCloudFile` object. 78 | """ 79 | 80 | request_url = ( 81 | f"https://{self.edupage.subdomain}.edupage.org/timeline/?akcia=uploadAtt" 82 | ) 83 | 84 | files = {"att": fd} 85 | 86 | response = self.edupage.session.post(request_url, files=files).content.decode() 87 | 88 | try: 89 | response_json = json.loads(response) 90 | if response_json.get("status") != "ok": 91 | raise FailedToUploadFileException("Edupage returned a failing status") 92 | 93 | metadata = response_json.get("data") 94 | return EduCloudFile.parse(metadata) 95 | except json.JSONDecodeError: 96 | raise FailedToUploadFileException("Failed to decode json response") 97 | -------------------------------------------------------------------------------- /edupage_api/compression.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | from hashlib import sha1 3 | from typing import Union 4 | 5 | from edupage_api.exceptions import Base64DecodeError 6 | from edupage_api.module import ModuleHelper 7 | 8 | 9 | # compression parameters from https://github.com/rgnter/epea_cpp 10 | # encoding and decoding from https://github.com/jsdom/abab 11 | class RequestData: 12 | @staticmethod 13 | def __compress(data: bytes) -> bytes: 14 | compressor = zlib.compressobj( 15 | -1, zlib.DEFLATED, -15, 8, zlib.Z_DEFAULT_STRATEGY 16 | ) 17 | 18 | compressor.compress(data) 19 | return compressor.flush(zlib.Z_FINISH) 20 | 21 | @staticmethod 22 | def chromium_base64_encode(data: str) -> str: 23 | # "The btoa() method must throw an "InvalidCharacterError" DOMException if 24 | # data contains any character whose code point is greater than U+00FF." 25 | for ch in data: 26 | if ord(ch) > 255: 27 | return None 28 | 29 | length = len(data) 30 | i = 0 31 | 32 | # Lookup table for btoa(), which converts a six-bit number into the 33 | # corresponding ASCII character. 34 | chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 35 | 36 | def btoa_lookup(index): 37 | if index >= 0 and index < 64: 38 | return chars[index] 39 | 40 | return None 41 | 42 | out = "" 43 | for i in range(0, length, 3): 44 | groups_of_six = [None, None, None, None] 45 | groups_of_six[0] = ord(data[i]) >> 2 46 | groups_of_six[1] = (ord(data[i]) & 0x03) << 4 47 | 48 | if length > i + 1: 49 | groups_of_six[1] |= ord(data[i + 1]) >> 4 50 | groups_of_six[2] = (ord(data[i + 1]) & 0x0F) << 2 51 | 52 | if length > i + 2: 53 | groups_of_six[2] |= ord(data[i + 2]) >> 6 54 | groups_of_six[3] = ord(data[i + 2]) & 0x3F 55 | 56 | for k in groups_of_six: 57 | if k is None: 58 | out += "=" 59 | else: 60 | out += btoa_lookup(k) 61 | 62 | i += 3 63 | 64 | return out 65 | 66 | def chromium_base64_decode(data: str) -> str: 67 | # "Remove all ASCII whitespace from data." 68 | [data := data.replace(char, "") for char in "\t\n\f\r"] 69 | 70 | # "If data's code point length divides by 4 leaving no remainder, then: if data ends 71 | # with one or two U+003D (=) code points, then remove them from data." 72 | if len(data) % 4 == 0: 73 | if data.endswith("=="): 74 | data = data.replace("==", "") 75 | elif data.endswith("="): 76 | data = data.replace("=", "") 77 | 78 | allowed_chars = ( 79 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 80 | ) 81 | 82 | def atob_lookup(ch: str): 83 | try: 84 | return allowed_chars.index(ch) 85 | except ValueError: 86 | return None 87 | 88 | # "If data's code point length divides by 4 leaving a remainder of 1, then return 89 | # failure." 90 | # 91 | # "If data contains a code point that is not one of 92 | # 93 | # U+002B (+) 94 | # U+002F (/) 95 | # ASCII alphanumeric 96 | # 97 | # then return failure." 98 | data_contains_invalid_chars = False in [ch in allowed_chars for ch in data] 99 | if len(data) % 4 == 1 or data_contains_invalid_chars: 100 | return None 101 | 102 | # "Let output be an empty byte sequence." 103 | output = "" 104 | 105 | # "Let buffer be an empty buffer that can have bits appended to it." 106 | # 107 | # We append bits via left-shift and or. accumulatedBits is used to track 108 | # when we've gotten to 24 bits. 109 | buffer = 0 110 | accumulated_bits = 0 111 | 112 | # "Let position be a position variable for data, initially pointing at the 113 | # start of data." 114 | # 115 | # "While position does not point past the end of data:" 116 | for ch in data: 117 | # "Find the code point pointed to by position in the second column of 118 | # Table 1: The Base 64 Alphabet of RFC 4648. Let n be the number given in 119 | # the first cell of the same row. 120 | # 121 | # "Append to buffer the six bits corresponding to n, most significant bit 122 | # first." 123 | # 124 | # atob_lookup() implements the table from RFC 4648. 125 | buffer <<= 6 126 | buffer |= atob_lookup(ch) 127 | accumulated_bits += 6 128 | 129 | # "If buffer has accumulated 24 bits, interpret them as three 8-bit 130 | # big-endian numbers. Append three bytes with values equal to those 131 | # numbers to output, in the same order, and then empty buffer." 132 | if accumulated_bits == 24: 133 | output += chr((buffer & 0xFF0000) >> 16) 134 | output += chr((buffer & 0xFF00) >> 8) 135 | output += chr(buffer & 0xFF) 136 | 137 | buffer = 0 138 | accumulated_bits = 0 139 | 140 | # "If buffer is not empty, it contains either 12 or 18 bits. If it contains 141 | # 12 bits, then discard the last four and interpret the remaining eight as 142 | # an 8-bit big-endian number. If it contains 18 bits, then discard the last 143 | # two and interpret the remaining 16 as two 8-bit big-endian numbers. Append 144 | # the one or two bytes with values equal to those one or two numbers to 145 | # output, in the same order." 146 | if accumulated_bits == 12: 147 | buffer >>= 4 148 | output += chr(buffer) 149 | elif accumulated_bits == 18: 150 | buffer >>= 2 151 | output += chr((buffer & 0xFF00) >> 8) 152 | output += chr(buffer & 0xFF) 153 | 154 | return output 155 | 156 | @staticmethod 157 | def __encode_data(data: str) -> bytes: 158 | compressed = RequestData.__compress(data.encode()) 159 | 160 | encoded = RequestData.chromium_base64_encode( 161 | "".join([chr(ch) for ch in compressed]) 162 | ) 163 | 164 | return encoded 165 | 166 | @staticmethod 167 | def __decode_data(data: str) -> str: 168 | return RequestData.chromium_base64_decode(data) 169 | 170 | @staticmethod 171 | def encode_request_body(request_data: Union[dict, str]) -> str: 172 | encoded_data = ( 173 | ModuleHelper.encode_form_data(request_data) 174 | if type(request_data) == dict 175 | else request_data 176 | ) 177 | encoded_data = RequestData.__encode_data(encoded_data) 178 | data_hash = sha1(encoded_data.encode()).hexdigest() 179 | 180 | return ModuleHelper.encode_form_data( 181 | { 182 | "eqap": f"dz:{encoded_data}", 183 | "eqacs": data_hash, 184 | "eqaz": "1", # use "encryption"? (compression) 185 | } 186 | ) 187 | 188 | @staticmethod 189 | def decode_response(response: str) -> str: 190 | # error 191 | if response.startswith("eqwd:"): 192 | return RequestData.chromium_base64_decode(response[5:]) 193 | 194 | # response not compressed 195 | if not response.startswith("eqz:"): 196 | return response 197 | 198 | decoded = RequestData.__decode_data(response[4:]) 199 | if decoded is None: 200 | raise Base64DecodeError("Failed to decode response.") 201 | 202 | return decoded 203 | -------------------------------------------------------------------------------- /edupage_api/custom_request.py: -------------------------------------------------------------------------------- 1 | from requests import Response 2 | 3 | from edupage_api.module import Module 4 | 5 | 6 | class CustomRequest(Module): 7 | def custom_request( 8 | self, url: str, method: str, data: str = "", headers: dict = {} 9 | ) -> Response: 10 | if method == "GET": 11 | response = self.edupage.session.get(url, headers=headers) 12 | elif method == "POST": 13 | response = self.edupage.session.post(url, data=data, headers=headers) 14 | 15 | return response 16 | -------------------------------------------------------------------------------- /edupage_api/dbi.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from edupage_api.module import Module, ModuleHelper 4 | 5 | 6 | class DbiHelper(Module): 7 | def __get_dbi(self) -> dict: 8 | return self.edupage.data.get("dbi") 9 | 10 | def __get_item_group(self, item_group_name: str) -> Optional[dict]: 11 | dbi = self.__get_dbi() 12 | if dbi is None: 13 | return None 14 | 15 | return dbi.get(item_group_name) 16 | 17 | def __get_item_with_id(self, item_group_name: str, item_id: str) -> Optional[dict]: 18 | items_in_group = self.__get_item_group(item_group_name) 19 | if items_in_group is None: 20 | return None 21 | 22 | return items_in_group.get(str(item_id)) 23 | 24 | def __get_full_name(self, person_item: dict) -> str: 25 | first_name = person_item.get("firstname") 26 | last_name = person_item.get("lastname") 27 | 28 | return f"{first_name} {last_name}" 29 | 30 | def fetch_subject_name(self, subject_id: int) -> Optional[str]: 31 | subject_item = self.__get_item_with_id("subjects", subject_id) 32 | if subject_item is not None: 33 | return subject_item.get("short") 34 | 35 | def fetch_classroom_number(self, classroom_id: str) -> Optional[str]: 36 | classroom_item = self.__get_item_with_id("classrooms", classroom_id) 37 | if classroom_item is not None: 38 | return classroom_item.get("short") 39 | 40 | def fetch_class_name(self, class_id: int) -> Optional[str]: 41 | class_item = self.__get_item_with_id("classes", class_id) 42 | if class_item is not None: 43 | return class_item.get("short") 44 | 45 | def fetch_teacher_name(self, teacher_id: int) -> Optional[str]: 46 | teacher_item = self.__get_item_with_id("teachers", teacher_id) 47 | if teacher_item is not None: 48 | return self.__get_full_name(teacher_item) 49 | 50 | def fetch_student_name(self, student_id: int) -> Optional[str]: 51 | student_item = self.__get_item_with_id("students", student_id) 52 | if student_item is not None: 53 | return self.__get_full_name(student_item) 54 | 55 | def fetch_student_list(self) -> Optional[list]: 56 | return self.__get_item_group("students") 57 | 58 | def fetch_teacher_list(self) -> Optional[list]: 59 | return self.__get_item_group("teachers") 60 | 61 | def fetch_subject_list(self) -> Optional[list]: 62 | return self.__get_item_group("subjects") 63 | 64 | def fetch_classroom_list(self) -> Optional[list]: 65 | return self.__get_item_group("classrooms") 66 | 67 | def fetch_class_list(self) -> Optional[list]: 68 | return self.__get_item_group("classes") 69 | 70 | def fetch_teacher_data(self, teacher_id: int) -> Optional[dict]: 71 | return self.__get_item_with_id("teachers", teacher_id) 72 | 73 | def fetch_student_data(self, student_id: int) -> Optional[dict]: 74 | return self.__get_item_with_id("students", student_id) 75 | 76 | def fetch_student_data_by_name(self, student_name: str) -> Optional[dict]: 77 | item_group = self.__get_item_group("students") 78 | 79 | for student_id in item_group: 80 | student_data = item_group.get(student_id) 81 | if self.__get_full_name(student_data) in student_name: 82 | student_data["id"] = student_id 83 | return student_data 84 | 85 | def fetch_teacher_data_by_name(self, teacher_name: str) -> Optional[dict]: 86 | item_group = self.__get_item_group("teachers") 87 | 88 | for teacher_id in item_group: 89 | teacher_data = item_group.get(teacher_id) 90 | if self.__get_full_name(teacher_data) in teacher_name: 91 | teacher_data["id"] = teacher_id 92 | return teacher_data 93 | 94 | def fetch_parent_data_by_name(self, parent_name: str) -> Optional[dict]: 95 | item_group = self.__get_item_group("parents") 96 | 97 | for parent_id in item_group: 98 | parent_data = item_group.get(parent_id) 99 | if self.__get_full_name(parent_data) in parent_name: 100 | parent_data["id"] = parent_id 101 | return parent_data 102 | 103 | def fetch_person_data_by_name(self, name: str) -> Optional[dict]: 104 | teacher_data = self.fetch_teacher_data_by_name(name) 105 | student_data = self.fetch_student_data_by_name(name) 106 | parent_data = self.fetch_parent_data_by_name(name) 107 | 108 | return ModuleHelper.return_first_not_null( 109 | teacher_data, student_data, parent_data 110 | ) 111 | -------------------------------------------------------------------------------- /edupage_api/exceptions.py: -------------------------------------------------------------------------------- 1 | class NotLoggedInException(Exception): 2 | pass 3 | 4 | 5 | class MissingDataException(Exception): 6 | pass 7 | 8 | 9 | class BadCredentialsException(Exception): 10 | pass 11 | 12 | 13 | class NotAnOnlineLessonError(Exception): 14 | pass 15 | 16 | 17 | class FailedToRateException(Exception): 18 | pass 19 | 20 | 21 | class FailedToChangeMealError(Exception): 22 | pass 23 | 24 | 25 | class FailedToUploadFileException(Exception): 26 | pass 27 | 28 | 29 | class FailedToParseGradeDataError(Exception): 30 | pass 31 | 32 | 33 | class ExpiredSessionException(Exception): 34 | pass 35 | 36 | 37 | class InvalidTeacherException(Exception): 38 | pass 39 | 40 | 41 | class RequestError(Exception): 42 | pass 43 | 44 | 45 | class InvalidMealsData(Exception): 46 | pass 47 | 48 | 49 | class Base64DecodeError(Exception): 50 | pass 51 | 52 | 53 | class InvalidRecipientsException(Exception): 54 | pass 55 | 56 | 57 | class InvalidChildException(Exception): 58 | pass 59 | 60 | 61 | class UnknownServerError(Exception): 62 | pass 63 | 64 | 65 | class NotParentException(Exception): 66 | pass 67 | 68 | 69 | class SecondFactorFailedException(Exception): 70 | pass 71 | 72 | 73 | class InsufficientPermissionsException(Exception): 74 | pass 75 | 76 | 77 | class CaptchaException(Exception): 78 | pass 79 | -------------------------------------------------------------------------------- /edupage_api/grades.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from datetime import datetime 4 | from enum import Enum 5 | from typing import Optional, Union 6 | 7 | from edupage_api.dbi import DbiHelper 8 | from edupage_api.exceptions import FailedToParseGradeDataError 9 | from edupage_api.module import Module, ModuleHelper 10 | from edupage_api.people import EduTeacher 11 | 12 | 13 | @dataclass 14 | class EduGrade: 15 | event_id: int 16 | title: str 17 | grade_n: Optional[Union[int, float, str]] 18 | comment: Optional[str] 19 | date: datetime 20 | subject_id: int 21 | subject_name: Optional[str] 22 | teacher: Optional[EduTeacher] 23 | max_points: Optional[float] 24 | importance: float 25 | verbal: bool 26 | percent: float 27 | 28 | 29 | class Term(Enum): 30 | FIRST = "P1" 31 | SECOND = "P2" 32 | 33 | 34 | class Grades(Module): 35 | def __parse_grade_data(self, data: str) -> dict: 36 | json_string = data.split(".znamkyStudentViewer(")[1].split( 37 | ");\r\n\t\t});\r\n\t\t" 38 | )[0] 39 | 40 | return json.loads(json_string) 41 | 42 | def __get_grade_data(self): 43 | request_url = f"https://{self.edupage.subdomain}.edupage.org/znamky/" 44 | response = self.edupage.session.get(request_url).content.decode() 45 | 46 | try: 47 | return self.__parse_grade_data(response) 48 | except (json.JSONDecodeError, IndexError): 49 | raise FailedToParseGradeDataError("Failed to parse JSON") 50 | 51 | def __get_grade_data_for_term(self, term: Term, year: int): 52 | request_url = f"https://{self.edupage.subdomain}.edupage.org/znamky/?what=studentviewer&znamky_yearid={year}&nadobdobie={term.value}" 53 | response = self.edupage.session.post(request_url).content.decode() 54 | 55 | try: 56 | return self.__parse_grade_data(response) 57 | except (json.JSONDecodeError, IndexError): 58 | raise FailedToParseGradeDataError("Failed to parse JSON") 59 | 60 | @ModuleHelper.logged_in 61 | def get_grades(self, term: Optional[Term], year: Optional[int]) -> list[EduGrade]: 62 | grade_data = ( 63 | self.__get_grade_data_for_term(term, year) 64 | if term and year 65 | else self.__get_grade_data() 66 | ) 67 | 68 | grades = grade_data.get("vsetkyZnamky") 69 | grade_details = grade_data.get("vsetkyUdalosti").get("edupage") 70 | 71 | output = [] 72 | for grade in grades: 73 | # ID 74 | event_id_str = grade.get("udalostid") 75 | if not event_id_str: 76 | continue 77 | 78 | event_id = int(event_id_str) 79 | 80 | # Title 81 | details = grade_details.get(event_id_str) 82 | title = details.get("p_meno") 83 | 84 | # Date 85 | date_str = grade.get("datum") 86 | date = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") 87 | 88 | # Subject ID and name 89 | subject_id_str = details.get("PredmetID") 90 | if subject_id_str is None or subject_id_str == "vsetky": 91 | continue 92 | 93 | subject_id = int(subject_id_str) 94 | subject_name = DbiHelper(self.edupage).fetch_subject_name(subject_id) 95 | 96 | # Teacher 97 | teacher_id_str = details.get("UcitelID") 98 | if teacher_id_str is None: 99 | teacher = None 100 | else: 101 | teacher_id = int(teacher_id_str) 102 | teacher_data = DbiHelper(self.edupage).fetch_teacher_data(teacher_id) 103 | 104 | if teacher_data is None: 105 | teacher = None 106 | else: 107 | teacher = EduTeacher.parse(teacher_data, teacher_id, self.edupage) 108 | 109 | # Maximal points and importance 110 | grade_type = details.get("p_typ_udalosti") 111 | if grade_type == "1": 112 | # Normal grade (1 – 5) 113 | max_points = None 114 | importance = float(details.get("p_vaha")) / 20 115 | elif grade_type == "2": 116 | # Points grade (in points – e.g. 0 – 20 points) 117 | max_points = float(details.get("p_vaha")) 118 | importance = None 119 | elif grade_type == "3": 120 | # Percental grade (0 – 100 %) 121 | max_points = float(details.get("p_vaha_body")) 122 | importance = float(details.get("p_vaha")) / 20 123 | 124 | # Grade 125 | grade_raw = grade.get("data").split(" (", 1) 126 | if grade_raw[0].isdigit(): 127 | grade_n = float(grade_raw[0]) 128 | else: 129 | grade_n = grade_raw[0] 130 | 131 | try: 132 | comment = grade_raw[1].rsplit(")", 1)[0] 133 | except IndexError: 134 | comment = None 135 | 136 | # Verbal and percents 137 | try: 138 | verbal = False 139 | 140 | if max_points: 141 | percent = round(float(grade_n) / float(max_points) * 100, 2) 142 | elif max_points == 0: 143 | percent = float("inf") 144 | else: 145 | percent = None 146 | except: 147 | verbal = True 148 | 149 | grade = EduGrade( 150 | event_id, 151 | title, 152 | grade_n, 153 | comment, 154 | date, 155 | subject_id, 156 | subject_name, 157 | teacher, 158 | max_points, 159 | importance, 160 | verbal, 161 | percent, 162 | ) 163 | output.append(grade) 164 | 165 | return output 166 | -------------------------------------------------------------------------------- /edupage_api/login.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from json import JSONDecodeError 4 | from typing import Optional 5 | 6 | from edupage_api.exceptions import ( 7 | BadCredentialsException, 8 | CaptchaException, 9 | MissingDataException, 10 | RequestError, 11 | SecondFactorFailedException, 12 | ) 13 | from edupage_api.module import EdupageModule, Module 14 | 15 | 16 | @dataclass 17 | class TwoFactorLogin: 18 | __authentication_endpoint: str 19 | __authentication_token: str 20 | __csrf_token: str 21 | __edupage: EdupageModule 22 | 23 | __code: Optional[str] = None 24 | 25 | def is_confirmed(self): 26 | """Check if the second factor process was finished by confirmation with a device. 27 | 28 | If this function returns true, you can safely use `TwoFactorLogin.finish` to finish the second factor authentication process. 29 | 30 | Returns: 31 | bool: True if the second factor was confirmed with a device. 32 | """ 33 | 34 | request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=checkIfConfirmed" 35 | response = self.__edupage.session.post(request_url) 36 | 37 | data = response.json() 38 | if data.get("status") == "fail": 39 | return False 40 | elif data.get("status") != "ok": 41 | raise MissingDataException( 42 | f"Invalid response from edupage's server!: {str(data)}" 43 | ) 44 | 45 | self.__code = data["data"] 46 | 47 | return True 48 | 49 | def resend_notifications(self): 50 | """Resends the confirmation notification to all devices.""" 51 | 52 | request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=resendNotifs" 53 | response = self.__edupage.session.post(request_url) 54 | 55 | data = response.json() 56 | if data.get("status") != "ok": 57 | raise RequestError(f"Failed to resend notifications: {str(data)}") 58 | 59 | def __finish(self, code: str): 60 | request_url = ( 61 | f"https://{self.__edupage.subdomain}.edupage.org/login/edubarLogin.php" 62 | ) 63 | parameters = { 64 | "csrfauth": self.__csrf_token, 65 | "t2fasec": code, 66 | "2fNoSave": "y", 67 | "2fform": "1", 68 | "gu": self.__authentication_endpoint, 69 | "au": self.__authentication_token, 70 | } 71 | 72 | response = self.__edupage.session.post(request_url, parameters) 73 | 74 | if "window.location = gu;" in response.text: 75 | cookies = self.__edupage.session.cookies.get_dict( 76 | f"{self.__edupage.subdomain}.edupage.org" 77 | ) 78 | 79 | Login(self.__edupage).reload_data( 80 | self.__edupage.subdomain, cookies["PHPSESSID"], self.__edupage.username 81 | ) 82 | 83 | return 84 | 85 | raise SecondFactorFailedException( 86 | f"Second factor failed! (wrong/expired code? expired session?)" 87 | ) 88 | 89 | def finish(self): 90 | """Finish the second factor authentication process. 91 | This function should be used when using a device to confirm the login. If you are using email 2fa codes, please use `TwoFactorLogin.finish_with_code`. 92 | 93 | Notes: 94 | - This function can only be used after `TwoFactorLogin.is_confirmed` returned `True`. 95 | - This function can raise `SecondFactorFailedException` if there is a big delay from calling `TwoFactorLogin.is_confirmed` (and getting `True` as a result) to calling `TwoFactorLogin.finish`. 96 | 97 | Raises: 98 | BadCredentialsException: You didn't call and get the `True` result from `TwoFactorLogin.is_confirmed` before calling this function. 99 | SecondFactorFailedException: The delay between calling `TwoFactorLogin.is_confirmed` and `TwoFactorLogin.finish` was too long, or there was another error with the second factor authentication confirmation process. 100 | """ 101 | 102 | if self.__code is None: 103 | raise BadCredentialsException( 104 | "Not confirmed! (you can only call finish after `TwoFactorLogin.is_confirmed` has returned True)" 105 | ) 106 | 107 | self.__finish(self.__code) 108 | 109 | def finish_with_code(self, code: str): 110 | """Finish the second factor authentication process. 111 | This function should be used when email 2fa codes are used to confirm the login. If you are using a device to confirm the login, please use `TwoFactorLogin.finish`. 112 | 113 | Args: 114 | code (str): The 2fa code from your email or from the mobile app. 115 | 116 | Raises: 117 | SecondFactorFailedException: An invalid 2fa code was provided. 118 | """ 119 | self.__finish(code) 120 | 121 | 122 | class Login(Module): 123 | def __parse_login_data(self, data): 124 | json_string = ( 125 | data.split("userhome(", 1)[1] 126 | .rsplit(");", 2)[0] 127 | .replace("\t", "") 128 | .replace("\n", "") 129 | .replace("\r", "") 130 | ) 131 | 132 | self.edupage.data = json.loads(json_string) 133 | self.edupage.is_logged_in = True 134 | 135 | self.edupage.gsec_hash = data.split('ASC.gsechash="')[1].split('"')[0] 136 | 137 | def login( 138 | self, username: str, password: str, subdomain: str = "login1" 139 | ) -> Optional[TwoFactorLogin]: 140 | """Login to your school's Edupage account (optionally with 2 factor authentication). 141 | 142 | If you do not have 2 factor authentication set up, this function will return `None`. 143 | The login will still work and succeed. 144 | 145 | See the `Edupage.TwoFactorLogin` documentation or the examples for more details 146 | of the 2 factor authentication process. 147 | 148 | Args: 149 | username (str): Your username. 150 | password (str): Your password. 151 | subdomain (str): Subdomain of your school (https://{subdomain}.edupage.org). 152 | 153 | Returns: 154 | Optional[TwoFactorLogin]: The object that can be used to complete the second factor 155 | (or `None` — if the second factor is not set up) 156 | 157 | Raises: 158 | BadCredentialsException: Your credentials are invalid. 159 | CaptchaException: The login process failed because of a captcha. 160 | SecondFactorFailed: The second factor login timed out 161 | or there was another problem with the second factor. 162 | """ 163 | 164 | request_url = f"https://{subdomain}.edupage.org/login/index.php" 165 | 166 | response = self.edupage.session.get(request_url) 167 | data = response.content.decode() 168 | 169 | csrf_token = data.split('name="csrfauth" value="')[1].split('"')[0] 170 | 171 | parameters = { 172 | "csrfauth": csrf_token, 173 | "username": username, 174 | "password": password, 175 | } 176 | 177 | request_url = f"https://{subdomain}.edupage.org/login/edubarLogin.php" 178 | 179 | response = self.edupage.session.post(request_url, parameters) 180 | 181 | if "cap=1" in response.url or "lerr=b43b43" in response.url: 182 | raise CaptchaException() 183 | 184 | if "bad=1" in response.url: 185 | raise BadCredentialsException() 186 | 187 | data = response.content.decode() 188 | 189 | if subdomain == "login1": 190 | subdomain = data.split("-->")[0].split(" ")[-1] 191 | 192 | self.edupage.subdomain = subdomain 193 | self.edupage.username = username 194 | 195 | if "twofactor" not in response.url: 196 | # 2FA not needed 197 | self.__parse_login_data(data) 198 | return 199 | 200 | request_url = ( 201 | f"https://{self.edupage.subdomain}.edupage.org/login/twofactor?sn=1" 202 | ) 203 | 204 | two_factor_response = self.edupage.session.get(request_url) 205 | 206 | data = two_factor_response.content.decode() 207 | 208 | csrf_token = data.split('csrfauth" value="')[1].split('"')[0] 209 | 210 | authentication_token = data.split('au" value="')[1].split('"')[0] 211 | authentication_endpoint = data.split('gu" value="')[1].split('"')[0] 212 | 213 | return TwoFactorLogin( 214 | authentication_endpoint, authentication_token, csrf_token, self.edupage 215 | ) 216 | 217 | def reload_data(self, subdomain: str, session_id: str, username: str): 218 | request_url = f"https://{subdomain}.edupage.org/user" 219 | 220 | self.edupage.session.cookies.set("PHPSESSID", session_id) 221 | 222 | response = self.edupage.session.get(request_url) 223 | 224 | try: 225 | self.__parse_login_data(response.content.decode()) 226 | self.edupage.subdomain = subdomain 227 | self.edupage.username = username 228 | except (TypeError, JSONDecodeError) as e: 229 | raise BadCredentialsException(f"Invalid session id: {e}") 230 | -------------------------------------------------------------------------------- /edupage_api/lunches.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from datetime import date, datetime 4 | from typing import List, Optional 5 | from enum import Enum 6 | 7 | from edupage_api.exceptions import ( 8 | FailedToChangeMealError, 9 | FailedToRateException, 10 | InvalidMealsData, 11 | NotLoggedInException, 12 | ) 13 | from edupage_api.module import EdupageModule, Module, ModuleHelper 14 | 15 | 16 | @dataclass 17 | class Rating: 18 | __date: str 19 | __boarder_id: str 20 | 21 | quality_average: float 22 | quality_ratings: float 23 | 24 | quantity_average: float 25 | quantity_ratings: float 26 | 27 | def rate(self, edupage: EdupageModule, quantity: int, quality: int): 28 | if not edupage.is_logged_in: 29 | raise NotLoggedInException() 30 | 31 | request_url = f"https://{edupage.subdomain}.edupage.org/menu/" 32 | 33 | data = { 34 | "akcia": "ulozHodnotenia", 35 | "stravnikid": self.__boarder_id, 36 | "mysqlDate": self.__date, 37 | "jedlo_dna": "2", 38 | "kvalita": str(quality), 39 | "mnozstvo": str(quantity), 40 | } 41 | 42 | response = edupage.session.post(request_url, data=data) 43 | parsed_response = json.loads(response.content.decode()) 44 | 45 | error = parsed_response.get("error") 46 | if error is None or error != "": 47 | raise FailedToRateException() 48 | 49 | 50 | @dataclass 51 | class Menu: 52 | name: str 53 | allergens: str 54 | weight: str 55 | number: str 56 | rating: Optional[Rating] 57 | 58 | class MealType(Enum): 59 | SNACK = 1 60 | LUNCH = 2 61 | AFTERNOON_SNACK = 3 62 | 63 | @dataclass 64 | class Meal: 65 | served_from: Optional[datetime] 66 | served_to: Optional[datetime] 67 | amount_of_foods: int 68 | chooseable_menus: list[str] 69 | can_be_changed_until: datetime 70 | title: str 71 | menus: List[Menu] 72 | date: datetime 73 | ordered_meal: Optional[str] 74 | meal_type: MealType 75 | __boarder_id: str 76 | __meal_index: str 77 | 78 | def __iter__(self): 79 | return iter(self.menus) 80 | 81 | def __make_choice(self, edupage: EdupageModule, choice_str: str): 82 | request_url = f"https://{edupage.subdomain}.edupage.org/menu/" 83 | 84 | boarder_menu = { 85 | "stravnikid": self.__boarder_id, 86 | "mysqlDate": self.date.strftime("%Y-%m-%d"), 87 | "jids": {self.__meal_index: choice_str}, 88 | "view": "pc_listok", 89 | "pravo": "Student", 90 | } 91 | 92 | data = { 93 | "akcia": "ulozJedlaStravnika", 94 | "jedlaStravnika": json.dumps(boarder_menu), 95 | } 96 | 97 | response = edupage.session.post( 98 | request_url, data=data 99 | ).content.decode() 100 | 101 | if json.loads(response).get("error") != "": 102 | raise FailedToChangeMealError() 103 | 104 | def choose(self, edupage: EdupageModule, number: int): 105 | letters = "ABCDEFGH" 106 | letter = letters[number - 1] 107 | 108 | self.__make_choice(edupage, letter) 109 | self.ordered_meal = letter 110 | 111 | def sign_off(self, edupage: EdupageModule): 112 | self.__make_choice(edupage, "AX") 113 | self.ordered_meal = None 114 | 115 | @dataclass 116 | class Meals: 117 | snack: Optional[Meal] 118 | lunch: Optional[Meal] 119 | afternoon_snack: Optional[Meal] 120 | 121 | 122 | 123 | class Lunches(Module): 124 | def parse_meal(self, meal_index: str, meal: dict, boarder_id: str, date: date) -> Optional[Meal]: 125 | if meal is None: 126 | return None 127 | 128 | if meal.get("isCooking") == False: 129 | return None 130 | 131 | ordered_meal = None 132 | meal_record = meal.get("evidencia") 133 | 134 | if meal_record is not None: 135 | ordered_meal = meal_record.get("stav") 136 | 137 | if ordered_meal == "V": 138 | ordered_meal = meal_record.get("obj") 139 | 140 | served_from_str = meal.get("vydaj_od") 141 | served_to_str = meal.get("vydaj_do") 142 | 143 | if served_from_str: 144 | served_from = datetime.strptime(served_from_str, "%H:%M") 145 | else: 146 | served_from = None 147 | 148 | if served_to_str: 149 | served_to = datetime.strptime(served_to_str, "%H:%M") 150 | else: 151 | served_to = None 152 | 153 | title = meal.get("nazov") 154 | 155 | amount_of_foods = meal.get("druhov_jedal") 156 | chooseable_menus = list(meal.get("choosableMenus").keys()) 157 | 158 | can_be_changed_until = meal.get("zmen_do") 159 | 160 | menus = [] 161 | 162 | for food in meal.get("rows"): 163 | if not food: 164 | continue 165 | 166 | name = food.get("nazov") 167 | allergens = food.get("alergenyStr") 168 | weight = food.get("hmotnostiStr") 169 | number = food.get("menusStr") 170 | rating = None 171 | 172 | if number is not None: 173 | number = number.replace(": ", "") 174 | rating = meal.get("hodnotenia") 175 | if rating is not None and rating: 176 | rating = rating.get(number) 177 | 178 | [quality, quantity] = rating 179 | 180 | quality_average = quality.get("priemer") 181 | quality_ratings = quality.get("pocet") 182 | 183 | quantity_average = quantity.get("priemer") 184 | quantity_ratings = quantity.get("pocet") 185 | 186 | rating = Rating( 187 | date.strftime("%Y-%m-%d"), 188 | boarder_id, 189 | quality_average, 190 | quantity_average, 191 | quality_ratings, 192 | quantity_ratings, 193 | ) 194 | else: 195 | rating = None 196 | menus.append(Menu(name, allergens, weight, number, rating)) 197 | 198 | return Meal( 199 | served_from, 200 | served_to, 201 | amount_of_foods, 202 | chooseable_menus, 203 | can_be_changed_until, 204 | title, 205 | menus, 206 | date, 207 | ordered_meal, 208 | MealType(int(meal_index)), 209 | boarder_id, 210 | meal_index 211 | ) 212 | 213 | @ModuleHelper.logged_in 214 | def get_meals(self, date: date) -> Optional[Meals]: 215 | date_strftime = date.strftime("%Y%m%d") 216 | request_url = f"https://{self.edupage.subdomain}.edupage.org/menu/?date={date_strftime}" 217 | response = self.edupage.session.get(request_url).content.decode() 218 | 219 | lunch_data = json.loads( 220 | response.split("edupageData: ")[1].split(",\r\n")[0] 221 | ) 222 | lunches_data = lunch_data.get(self.edupage.subdomain) 223 | try: 224 | boarder_id = ( 225 | lunches_data.get("novyListok").get("addInfo").get("stravnikid") 226 | ) 227 | except AttributeError as e: 228 | raise InvalidMealsData(f"Missing boarder id: {e}") 229 | 230 | meals = lunches_data.get("novyListok").get(date.strftime("%Y-%m-%d")) 231 | if meals is None: 232 | return None 233 | 234 | snack = self.parse_meal("1", meals.get("1"), boarder_id, date) 235 | lunch = self.parse_meal("2", meals.get("2"), boarder_id, date) 236 | afternoon_snack = self.parse_meal("3", meals.get("3"), boarder_id, date) 237 | 238 | return Meals(snack, lunch, afternoon_snack) 239 | 240 | -------------------------------------------------------------------------------- /edupage_api/messages.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Union 3 | 4 | from edupage_api.compression import RequestData 5 | from edupage_api.exceptions import InvalidRecipientsException, RequestError 6 | from edupage_api.module import Module 7 | from edupage_api.people import EduAccount 8 | 9 | 10 | class Messages(Module): 11 | def send_message( 12 | self, recipients: Union[list[EduAccount], EduAccount, list[str]], body: str 13 | ) -> int: 14 | recipient_string = "" 15 | 16 | if isinstance(recipients, list): 17 | if len(recipients) == 0: 18 | raise InvalidRecipientsException("The recipients parameter is empty!") 19 | 20 | if type(recipients[0]) == EduAccount: 21 | recipient_string = ";".join([r.get_id() for r in recipients]) 22 | else: 23 | recipient_string = ";".join(recipients) 24 | else: 25 | recipient_string = recipients.get_id() 26 | 27 | data = RequestData.encode_request_body( 28 | { 29 | "selectedUser": recipient_string, 30 | "text": body, 31 | "attachements": "{}", 32 | "receipt": "0", 33 | "typ": "sprava", 34 | } 35 | ) 36 | 37 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 38 | 39 | request_url = f"https://{self.edupage.subdomain}.edupage.org/timeline/?=&akcia=createItem&eqav=1&maxEqav=7" 40 | response = self.edupage.session.post(request_url, data=data, headers=headers) 41 | 42 | response_text = RequestData.decode_response(response.text) 43 | if response_text == "0": 44 | raise RequestError("Edupage returned an error response") 45 | 46 | response = json.loads(response_text) 47 | 48 | changes = response.get("changes") 49 | if changes == [] or changes is None: 50 | raise RequestError( 51 | "Failed to send message (edupage returned an empty 'changes' array) - https://github.com/EdupageAPI/edupage-api/issues/62" 52 | ) 53 | 54 | return int(changes[0].get("timelineid")) 55 | -------------------------------------------------------------------------------- /edupage_api/module.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | from datetime import datetime 3 | from enum import Enum 4 | from functools import wraps 5 | from typing import Optional 6 | 7 | import requests 8 | 9 | from edupage_api.exceptions import ( 10 | MissingDataException, 11 | NotAnOnlineLessonError, 12 | NotLoggedInException, 13 | NotParentException, 14 | ) 15 | 16 | 17 | class EdupageModule: 18 | subdomain: str 19 | session: requests.Session 20 | data: dict 21 | is_logged_in: bool 22 | gsec_hash: str 23 | username: str 24 | 25 | 26 | class Module: 27 | def __init__(self, edupage: EdupageModule): 28 | self.edupage = edupage 29 | 30 | 31 | class ModuleHelper: 32 | # Helper Functions 33 | 34 | @staticmethod 35 | def parse_int(val: str) -> Optional[int]: 36 | try: 37 | return int("".join(filter(str.isdigit, val))) 38 | except ValueError: 39 | return None 40 | 41 | """ 42 | If any argument of this function is none, it throws MissingDataException 43 | """ 44 | 45 | @staticmethod 46 | def assert_none(*args): 47 | if None in args: 48 | raise MissingDataException() 49 | 50 | @staticmethod 51 | def parse_enum(string: str, enum_type: Enum): 52 | filtered = list(filter(lambda x: x.value == string, list(enum_type))) 53 | 54 | if not filtered: 55 | return None 56 | 57 | return filtered[0] 58 | 59 | @staticmethod 60 | def return_first_not_null(*args): 61 | for x in args: 62 | if x: 63 | return x 64 | 65 | @staticmethod 66 | def urlencode(string: str) -> str: 67 | return urllib.parse.quote(string) 68 | 69 | @staticmethod 70 | def encode_form_data(data: dict) -> str: 71 | output = "" 72 | for i, key in enumerate(data.keys(), start=0): 73 | value = data[key] 74 | entry = f"{ModuleHelper.urlencode(key)}={ModuleHelper.urlencode(value)}" 75 | 76 | output += f"&{entry}" if i != 0 else entry 77 | return output 78 | 79 | @staticmethod 80 | def strptime_or_none(date_string: str, format: str) -> Optional[datetime]: 81 | try: 82 | return datetime.strptime(date_string, format) 83 | except ValueError: 84 | return None 85 | 86 | # Decorators 87 | 88 | """ 89 | Throws NotLoggedInException if someone uses a method with this decorator 90 | and hasn't logged in yet 91 | """ 92 | 93 | @staticmethod 94 | def logged_in(method): 95 | @wraps(method) 96 | def __impl(self: Module, *method_args, **method_kwargs): 97 | if not self.edupage.is_logged_in: 98 | raise NotLoggedInException() 99 | 100 | return method(self, *method_args, **method_kwargs) 101 | 102 | return __impl 103 | 104 | @staticmethod 105 | def online_lesson(method): 106 | @wraps(method) 107 | def __impl(self, *method_args, **method_kwargs): 108 | if self.online_lesson_link is None: 109 | raise NotAnOnlineLessonError() 110 | return method(self, *method_args, **method_kwargs) 111 | 112 | return __impl 113 | 114 | """ 115 | Throws NotParentException if someone uses a method with this decorator 116 | and is not using a parent account 117 | """ 118 | 119 | @staticmethod 120 | def is_parent(method): 121 | @wraps(method) 122 | def __impl(self: Module, *method_args, **method_kwargs): 123 | if "Rodic" not in self.edupage.get_user_id(): 124 | raise NotParentException() 125 | 126 | return method(self, *method_args, **method_kwargs) 127 | 128 | return __impl 129 | -------------------------------------------------------------------------------- /edupage_api/parent.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from edupage_api.exceptions import InvalidChildException, UnknownServerError 4 | from edupage_api.module import Module, ModuleHelper 5 | from edupage_api.people import EduAccount 6 | 7 | 8 | class Parent(Module): 9 | @ModuleHelper.logged_in 10 | @ModuleHelper.is_parent 11 | def switch_to_child(self, child: Union[EduAccount, int]): 12 | params = {"studentid": child.person_id if type(child) == EduAccount else child} 13 | 14 | url = f"https://{self.edupage.subdomain}.edupage.org/login/switchchild" 15 | response = self.edupage.session.get(url, params=params) 16 | 17 | if response.text != "OK": 18 | raise InvalidChildException( 19 | f"{response.text}: Invalid child selected! (not your child?)" 20 | ) 21 | 22 | @ModuleHelper.logged_in 23 | @ModuleHelper.is_parent 24 | def switch_to_parent(self): 25 | # variable name is from edupage's code :/ 26 | rid = f"edupage;{self.edupage.subdomain};{self.edupage.username}" 27 | 28 | params = {"rid": rid} 29 | 30 | url = f"https://{self.edupage.subdomain}.edupage.org/login/edupageChange" 31 | response = self.edupage.session.get(url, params=params) 32 | 33 | if "EdupageLoginFailed" in response.url: 34 | raise UnknownServerError() 35 | -------------------------------------------------------------------------------- /edupage_api/people.py: -------------------------------------------------------------------------------- 1 | # For postponed evaluation of annotations 2 | from __future__ import annotations 3 | 4 | import json 5 | from dataclasses import dataclass 6 | from datetime import datetime 7 | from enum import Enum 8 | from typing import Optional, Union 9 | 10 | from edupage_api.dbi import DbiHelper 11 | from edupage_api.module import EdupageModule, Module, ModuleHelper 12 | 13 | 14 | class Gender(str, Enum): 15 | MALE = "M" 16 | FEMALE = "F" 17 | 18 | @staticmethod 19 | def parse(gender_str: str) -> Optional[Gender]: 20 | return ModuleHelper.parse_enum(gender_str, Gender) 21 | 22 | 23 | class EduAccountType(str, Enum): 24 | STUDENT = "Student" 25 | TEACHER = "Teacher" 26 | PARENT = "Rodic" 27 | 28 | 29 | @dataclass 30 | class EduAccount: 31 | person_id: int 32 | name: str 33 | gender: Gender 34 | in_school_since: Optional[datetime] 35 | account_type: EduAccountType 36 | 37 | @staticmethod 38 | def recognize_account_type(person_data: dict) -> EduAccountType: 39 | if person_data.get("numberinclass") is not None: 40 | return EduAccountType.STUDENT 41 | elif person_data.get("classroomid") is not None: 42 | return EduAccountType.TEACHER 43 | else: 44 | return EduAccountType.PARENT 45 | 46 | @staticmethod 47 | def parse( 48 | person_data: dict, person_id: int, edupage: EdupageModule 49 | ) -> Optional[EduAccount]: 50 | account_type = EduAccount.recognize_account_type(person_data) 51 | 52 | if account_type == EduAccountType.STUDENT: 53 | class_id = ModuleHelper.parse_int(person_data.get("classid")) 54 | name = DbiHelper(edupage).fetch_student_name(person_id) 55 | gender = Gender.parse(person_data.get("gender")) 56 | student_since = ModuleHelper.strptime_or_none( 57 | person_data.get("datefrom"), "%Y-%m-%d" 58 | ) 59 | number_in_class = ModuleHelper.parse_int(person_data.get("numberinclass")) 60 | 61 | ModuleHelper.assert_none(name) 62 | 63 | return EduStudent( 64 | person_id, name, gender, student_since, class_id, number_in_class 65 | ) 66 | elif account_type == EduAccountType.TEACHER: 67 | classroom_id = person_data.get("classroomid") 68 | classroom_name = DbiHelper(edupage).fetch_classroom_number(classroom_id) 69 | 70 | name = DbiHelper(edupage).fetch_teacher_name(person_id) 71 | 72 | gender = Gender.parse(person_data.get("gender")) 73 | if teacher_since_str := person_data.get("datefrom"): 74 | teacher_since = datetime.strptime(teacher_since_str, "%Y-%m-%d") 75 | else: 76 | teacher_since = None 77 | 78 | if teacher_to_str := person_data.get("dateto"): 79 | teacher_to = datetime.strptime(teacher_to_str, "%Y-%m-%d") 80 | else: 81 | teacher_to = None 82 | 83 | return EduTeacher( 84 | person_id, name, gender, teacher_since, classroom_name, teacher_to 85 | ) 86 | else: 87 | return None 88 | 89 | def get_id(self): 90 | return f"{self.account_type.value}{self.person_id}" 91 | 92 | 93 | @dataclass 94 | class EduStudent(EduAccount): 95 | def __init__( 96 | self, 97 | person_id: int, 98 | name: str, 99 | gender: Gender, 100 | in_school_since: Optional[datetime], 101 | class_id: int, 102 | number_in_class: int, 103 | ): 104 | super().__init__( 105 | person_id, name, gender, in_school_since, EduAccountType.STUDENT 106 | ) 107 | 108 | self.class_id = class_id 109 | self.number_in_class = number_in_class 110 | 111 | self.__student_only = False 112 | 113 | def get_id(self): 114 | if not self.__student_only: 115 | return super().get_id() 116 | else: 117 | return super().get_id().replace("Student", "StudentOnly") 118 | 119 | def set_student_only(self, student_only: bool): 120 | self.__student_only = student_only 121 | 122 | 123 | @dataclass 124 | class EduStudentSkeleton: 125 | person_id: int 126 | name_short: str 127 | class_id: int 128 | 129 | 130 | @dataclass 131 | class EduParent(EduAccount): 132 | def __init__( 133 | self, 134 | person_id: int, 135 | name: str, 136 | gender: Gender, 137 | in_school_since: Optional[datetime], 138 | ): 139 | super().__init__( 140 | person_id, name, gender, in_school_since, EduAccountType.PARENT 141 | ) 142 | 143 | 144 | @dataclass 145 | class EduTeacher(EduAccount): 146 | def __init__( 147 | self, 148 | person_id: int, 149 | name: str, 150 | gender: Gender, 151 | in_school_since: Optional[datetime], 152 | classroom_name: str, 153 | teacher_to: Optional[datetime], 154 | ): 155 | super().__init__( 156 | person_id, name, gender, in_school_since, EduAccountType.TEACHER 157 | ) 158 | 159 | self.teacher_to = teacher_to 160 | self.classroom_name = classroom_name 161 | 162 | 163 | class People(Module): 164 | @ModuleHelper.logged_in 165 | def get_students(self) -> Optional[list]: 166 | students = DbiHelper(self.edupage).fetch_student_list() 167 | if students is None: 168 | return None 169 | 170 | result = [] 171 | for student_id_str in students: 172 | if not student_id_str: 173 | continue 174 | 175 | student_id = int(student_id_str) 176 | student_data = students.get(student_id_str) 177 | 178 | student = EduAccount.parse(student_data, student_id, self.edupage) 179 | result.append(student) 180 | 181 | return result 182 | 183 | @ModuleHelper.logged_in 184 | def get_all_students(self) -> Optional[list[EduStudent]]: 185 | request_url = f"https://{self.edupage.subdomain}.edupage.org/rpr/server/maindbi.js?__func=mainDBIAccessor" 186 | data = { 187 | "__args": [ 188 | None, 189 | self.edupage.get_school_year(), 190 | {}, 191 | { 192 | "op": "fetch", 193 | "needed_part": { 194 | "students": ["id", "classid", "short"], 195 | }, 196 | }, 197 | ], 198 | "__gsh": self.edupage.gsec_hash, 199 | } 200 | 201 | response = self.edupage.session.post(request_url, json=data).content.decode() 202 | students = json.loads(response).get("r").get("tables")[0].get("data_rows") 203 | 204 | result = [] 205 | for student in students: 206 | student_id = int(student["id"]) 207 | student_class_id = int(student["classid"]) if student["classid"] else None 208 | student_name_short = student["short"] 209 | 210 | student = EduStudentSkeleton( 211 | student_id, student_name_short, student_class_id 212 | ) 213 | result.append(student) 214 | 215 | return result 216 | 217 | @ModuleHelper.logged_in 218 | def get_teacher(self, teacher_id: Union[int, str]) -> Optional[EduTeacher]: 219 | try: 220 | teacher_id = int(teacher_id) 221 | except (ValueError, TypeError): 222 | return None 223 | 224 | return next( 225 | ( 226 | teacher 227 | for teacher in self.get_teachers() 228 | if teacher.person_id == teacher_id 229 | ), 230 | None, 231 | ) 232 | 233 | @ModuleHelper.logged_in 234 | def get_student(self, student_id: Union[int, str]) -> Optional[EduStudent]: 235 | try: 236 | student_id = int(student_id) 237 | except (ValueError, TypeError): 238 | return None 239 | 240 | return next( 241 | ( 242 | student 243 | for student in self.get_students() 244 | if student.person_id == student_id 245 | ), 246 | None, 247 | ) 248 | 249 | @ModuleHelper.logged_in 250 | def get_teachers(self) -> Optional[list]: 251 | teachers = DbiHelper(self.edupage).fetch_teacher_list() 252 | if teachers is None: 253 | return None 254 | 255 | result = [] 256 | for teacher_id_str in teachers: 257 | if not teacher_id_str: 258 | continue 259 | 260 | teacher_id = int(teacher_id_str) 261 | teacher_data = teachers.get(teacher_id_str) 262 | 263 | teacher = EduAccount.parse(teacher_data, teacher_id, self.edupage) 264 | result.append(teacher) 265 | 266 | return result 267 | -------------------------------------------------------------------------------- /edupage_api/ringing.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime, time, timedelta 3 | from enum import Enum 4 | 5 | from edupage_api.module import Module, ModuleHelper 6 | 7 | 8 | class RingingType(str, Enum): 9 | BREAK = "BREAK" 10 | LESSON = "LESSON" 11 | 12 | 13 | @dataclass 14 | class RingingTime: 15 | # The thing this ringing is announcing (break or lesson) 16 | type: RingingType 17 | time: time 18 | 19 | 20 | class RingingTimes(Module): 21 | @staticmethod 22 | def __parse_time(s: str) -> time: 23 | hours, minutes = s.split(":") 24 | return time(int(hours), int(minutes)) 25 | 26 | @staticmethod 27 | def __set_hours_and_minutes(dt: datetime, hours: int, minutes: int) -> datetime: 28 | return datetime(dt.year, dt.month, dt.day, hours, minutes) 29 | 30 | @staticmethod 31 | def __get_next_workday(date_time: datetime): 32 | if date_time.date().weekday() == 5: 33 | date_time = RingingTimes.__set_hours_and_minutes(date_time, 0, 0) 34 | return date_time + timedelta(days=2) 35 | elif date_time.date().weekday() == 6: 36 | date_time = RingingTimes.__set_hours_and_minutes(date_time, 0, 0) 37 | return date_time + timedelta(days=1) 38 | else: 39 | return date_time 40 | 41 | @ModuleHelper.logged_in 42 | def get_next_ringing_time(self, date_time: datetime) -> RingingTime: 43 | date_time = RingingTimes.__get_next_workday(date_time) 44 | 45 | ringing_times = self.edupage.data.get("zvonenia") 46 | for ringing_time in ringing_times: 47 | start_time = RingingTimes.__parse_time(ringing_time.get("starttime")) 48 | if date_time.time() < start_time: 49 | date_time = RingingTimes.__set_hours_and_minutes( 50 | date_time, start_time.hour, start_time.minute 51 | ) 52 | 53 | return RingingTime(RingingType.LESSON, date_time) 54 | 55 | end_time = RingingTimes.__parse_time(ringing_time.get("endtime")) 56 | if date_time.time() < end_time: 57 | date_time = RingingTimes.__set_hours_and_minutes( 58 | date_time, end_time.hour, end_time.minute 59 | ) 60 | 61 | return RingingTime(RingingType.BREAK, date_time) 62 | 63 | date_time += timedelta(1) 64 | date_time = RingingTimes.__set_hours_and_minutes(date_time, 0, 0) 65 | 66 | return self.get_next_ringing_time(date_time) 67 | -------------------------------------------------------------------------------- /edupage_api/subjects.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Union 3 | 4 | from edupage_api.dbi import DbiHelper 5 | from edupage_api.module import Module, ModuleHelper 6 | 7 | 8 | @dataclass 9 | class Subject: 10 | subject_id: int 11 | name: str 12 | short: str 13 | 14 | 15 | class Subjects(Module): 16 | @ModuleHelper.logged_in 17 | def get_subjects(self) -> Optional[list]: 18 | subject_list = DbiHelper(self.edupage).fetch_subject_list() 19 | 20 | if subject_list is None: 21 | return None 22 | 23 | subjects = [] 24 | 25 | for subject_id_str in subject_list: 26 | if not subject_id_str: 27 | continue 28 | 29 | subjects.append( 30 | Subject( 31 | int(subject_id_str), 32 | subject_list[subject_id_str]["name"], 33 | subject_list[subject_id_str]["short"], 34 | ) 35 | ) 36 | 37 | return subjects 38 | 39 | def get_subject(self, subject_id: Union[int, str]) -> Optional[Subject]: 40 | try: 41 | subject_id = int(subject_id) 42 | except (ValueError, TypeError): 43 | return None 44 | 45 | return next( 46 | ( 47 | subject 48 | for subject in self.get_subjects() 49 | if subject.subject_id == subject_id 50 | ), 51 | None, 52 | ) 53 | -------------------------------------------------------------------------------- /edupage_api/substitution.py: -------------------------------------------------------------------------------- 1 | # For postponed evaluation of annotations 2 | from __future__ import annotations 3 | 4 | import json 5 | from dataclasses import dataclass 6 | from datetime import date 7 | from enum import Enum 8 | from typing import Optional, Union 9 | 10 | from edupage_api.exceptions import ExpiredSessionException, InvalidTeacherException 11 | from edupage_api.module import Module, ModuleHelper 12 | from edupage_api.people import EduTeacher, People 13 | 14 | 15 | class Action(str, Enum): 16 | ADDITION = "add" 17 | CHANGE = "change" 18 | DELETION = "remove" 19 | 20 | @staticmethod 21 | def parse(string: str) -> Optional[Action]: 22 | return ModuleHelper.parse_enum(string, Action) 23 | 24 | 25 | @dataclass 26 | class TimetableChange: 27 | change_class: str 28 | lesson_n: int 29 | title: str 30 | action: Union[Action, tuple[int, int]] 31 | 32 | 33 | class Substitution(Module): 34 | def __get_substitution_data(self, date: date) -> str: 35 | url = ( 36 | f"https://{self.edupage.subdomain}.edupage.org/substitution/server/viewer.js" 37 | "?__func=getSubstViewerDayDataHtml" 38 | ) 39 | 40 | data = { 41 | "__args": [None, {"date": date.strftime("%Y-%m-%d"), "mode": "classes"}], 42 | "__gsh": self.edupage.gsec_hash, 43 | } 44 | 45 | response = self.edupage.session.post(url, json=data).content.decode() 46 | response = json.loads(response) 47 | 48 | if response.get("reload"): 49 | raise ExpiredSessionException( 50 | "Invalid gsec hash! " "(Expired session, try logging in again!)" 51 | ) 52 | 53 | return response.get("r") 54 | 55 | @ModuleHelper.logged_in 56 | def get_missing_teachers(self, date: date) -> Optional[list[EduTeacher]]: 57 | html = self.__get_substitution_data(date) 58 | missing_teachers_string = html.split('')[ 59 | 1 60 | ].split("")[0] 61 | 62 | if not missing_teachers_string: 63 | return None 64 | 65 | _title, missing_teachers = missing_teachers_string.split(": ") 66 | 67 | all_teachers = People(self.edupage).get_teachers() 68 | 69 | missing_teachers = [ 70 | item 71 | for sublist in [ 72 | (t.strip().split(" (")[0]).split(" + ") 73 | for t in missing_teachers.split(", ") 74 | ] 75 | for item in sublist 76 | ] 77 | 78 | try: 79 | missing_teachers = [ 80 | list(filter(lambda x: x.name == t, all_teachers))[0] 81 | for t in missing_teachers 82 | ] 83 | except IndexError: 84 | raise InvalidTeacherException( 85 | "Invalid teacher in substitution! " 86 | "(The teacher is no longer frequenting this school)" 87 | ) 88 | 89 | return missing_teachers 90 | 91 | @ModuleHelper.logged_in 92 | def get_timetable_changes(self, date: date) -> Optional[list[TimetableChange]]: 93 | html = self.__get_substitution_data(date) 94 | 95 | class_delim = ( 96 | '