Skip to content

Commit 45fcef7

Browse files
authored
fix: login into smartschool when a security question has been asked. (#7)
* updated lockfile * small cleanup via logprise * WIP * Fix login procedure * Fix tests with new birthday introduced. * Some more tests * more coverage for credentials * Mock account verification too * reformat * Fixing sonar issue
1 parent c0cb7b7 commit 45fcef7

24 files changed

+1012
-581
lines changed

credentials.yml.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
username: ...
22
password: ...
3+
birthday: YYYY-mm-dd
34
main_url: school.smartschool.be
45

56
email_from: [email protected]

poetry.lock

Lines changed: 569 additions & 363 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ bs4 = "*"
2323
requests = "*"
2424
pyyaml = "*"
2525
pydantic = "*"
26+
logprise = "*"
2627

2728
[tool.poetry.group.dev.dependencies]
2829
pytest-mock = "*"
@@ -35,6 +36,20 @@ requires = ["poetry-core"]
3536
build-backend = "poetry.core.masonry.api"
3637

3738

39+
[tool.coverage.report]
40+
exclude_also = [
41+
'def __repr__',
42+
'if self.debug:',
43+
'if settings.DEBUG',
44+
'raise AssertionError',
45+
'raise NotImplementedError',
46+
'if 0:',
47+
'if __name__ == .__main__.:',
48+
'if TYPE_CHECKING:',
49+
'class .*\bProtocol\):',
50+
'@(abc\.)?abstractmethod',
51+
]
52+
3853
[tool.pytest.ini_options]
3954
testpaths = [
4055
"tests",
@@ -48,6 +63,7 @@ requests_mock_case_sensitive = true
4863
[tool.ruff]
4964
line-length = 160
5065
fix = true
66+
unsafe-fixes = true
5167

5268
[tool.ruff.lint]
5369
select = [
@@ -122,4 +138,4 @@ ban-relative-imports = "all"
122138
strict = true
123139

124140
[tool.ruff.lint.per-file-ignores]
125-
"tests/**/*.py" = ["D100", "D103", "B018", "FBT001"]
141+
"tests/**/*.py" = ["D100", "D103", "B018", "FBT001"]

scripts/smartschool_report_on_results

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
#!/usr/bin/env python
22

3-
from smartschool import PathCredentials, Results, Smartschool, logger, DownloadError
4-
from smartschool.common import IsSaved, capture_and_email_all_exceptions, save, send_email
3+
from logprise import logger
4+
from smartschool import PathCredentials, Results, Smartschool, DownloadError
5+
from smartschool.common import IsSaved, save, send_email
56
from smartschool.objects import Result
6-
7-
session = Smartschool.start(PathCredentials())
8-
assert 'email_from' in session.creds.other_info
9-
assert 'email_to' in session.creds.other_info
7+
from smartschool.session import session
108

119

1210
def is_punten_json_the_same(previous: Result, current: Result) -> bool:
@@ -125,7 +123,7 @@ def build_text(
125123

126124

127125
def process_result(result: Result) -> None:
128-
logger.info("Processing %s", result.name)
126+
logger.info("Processing {}", result.name)
129127

130128
assert len(result.courses) == 1, f"Multiple courses? {result.courses}"
131129

@@ -141,9 +139,12 @@ def process_result(result: Result) -> None:
141139
send_email(subject=subject, text=text, email_from=session.creds.other_info['email_from'], email_to=session.creds.other_info['email_to'])
142140

143141

144-
@capture_and_email_all_exceptions(email_from=session.creds.other_info['email_from'], email_to=session.creds.other_info['email_to'])
145142
def main():
146143
try:
144+
Smartschool.start(PathCredentials())
145+
assert 'email_from' in session.creds.other_info
146+
assert 'email_to' in session.creds.other_info
147+
147148
for result in Results():
148149
process_result(result)
149150
except DownloadError:

src/smartschool/__init__.py

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import logging
2-
31
from .agenda import SmartschoolHours, SmartschoolLessons, SmartschoolMomentInfos
42
from .courses import Courses, TopNavCourses
53
from .credentials import EnvCredentials, PathCredentials
64
from .exceptions import DownloadError, SmartSchoolException
7-
from .logger import setup_logger
85
from .messages import (
96
AdjustMessageLabel,
107
Attachments,
@@ -25,34 +22,31 @@
2522
from .student_support import StudentSupportLinks
2623

2724
__all__ = [
28-
"PathCredentials",
29-
"EnvCredentials",
30-
"Smartschool",
31-
"logger",
25+
"AdjustMessageLabel",
26+
"Attachments",
27+
"BoxType",
3228
"Courses",
33-
"TopNavCourses",
34-
"Results",
35-
"Periods",
29+
"DownloadError",
30+
"EnvCredentials",
3631
"FutureTasks",
37-
"SortField",
38-
"SortOrder",
39-
"BoxType",
40-
"MessageHeaders",
41-
"StudentSupportLinks",
42-
"SmartschoolHours",
43-
"SmartschoolLessons",
44-
"SmartschoolMomentInfos",
45-
"Message",
46-
"Attachments",
4732
"MarkMessageUnread",
48-
"AdjustMessageLabel",
33+
"Message",
34+
"MessageHeaders",
35+
"MessageLabel",
4936
"MessageMoveToArchive",
5037
"MessageMoveToTrash",
51-
"MessageLabel",
38+
"PathCredentials",
39+
"Periods",
5240
"ResultDetail",
41+
"Results",
5342
# Exceptions
5443
"SmartSchoolException",
55-
"DownloadError",
44+
"Smartschool",
45+
"SmartschoolHours",
46+
"SmartschoolLessons",
47+
"SmartschoolMomentInfos",
48+
"SortField",
49+
"SortOrder",
50+
"StudentSupportLinks",
51+
"TopNavCourses",
5652
]
57-
58-
logger = setup_logger(logging.DEBUG)

src/smartschool/_xml_interface.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import contextlib
44
from abc import ABC, ABCMeta, abstractmethod
55
from datetime import date
6-
from typing import TYPE_CHECKING, Iterator, TypeVar
6+
from typing import TYPE_CHECKING, TypeVar
77
from xml.etree import ElementTree as ET
88
from xml.sax.saxutils import quoteattr
99

1010
from .common import xml_to_dict
1111
from .session import session
1212

1313
if TYPE_CHECKING: # pragma: no cover
14+
from collections.abc import Iterator
1415
from datetime import datetime
1516

1617
_T = TypeVar("_T")

src/smartschool/common.py

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
11
from __future__ import annotations
22

33
import contextlib
4-
import functools
5-
import inspect
64
import json
75
import operator
86
import platform
97
import re
108
import smtplib
11-
import sys
12-
import traceback
139
import warnings
1410
import xml.etree.ElementTree as ET
1511
from email.mime.multipart import MIMEMultipart
1612
from email.mime.text import MIMEText
1713
from enum import Enum, auto
1814
from pathlib import Path
19-
from typing import Any, Callable, Literal
15+
from typing import TYPE_CHECKING, Any, Callable, Literal
2016

2117
from bs4 import BeautifulSoup, FeatureNotFound, GuessedAtParserWarning
2218
from pydantic import RootModel
2319
from pydantic.dataclasses import is_pydantic_dataclass
2420
from requests import Response
2521

22+
if TYPE_CHECKING:
23+
import bs4
24+
2625
__all__ = [
27-
"send_email",
28-
"capture_and_email_all_exceptions",
29-
"save",
3026
"IsSaved",
27+
"as_float",
3128
"bs4_html",
3229
"get_all_values_from_form",
3330
"make_filesystem_safe",
34-
"as_float",
31+
"save",
32+
"send_email",
3533
"xml_to_dict",
3634
]
3735

@@ -109,39 +107,6 @@ def send_email(
109107
)
110108

111109

112-
def capture_and_email_all_exceptions(
113-
email_from: str | list[str], email_to: str | list[str], subject: str = "[⚠Smartschool parser⚠] Something went wrong"
114-
) -> Callable:
115-
def decorator(func):
116-
@functools.wraps(func)
117-
def inner(*args, **kwargs):
118-
frm = inspect.stack()[1]
119-
module_name = Path(frm.filename)
120-
function_signature = f"{module_name.stem}.{func.__name__}"
121-
122-
print(f"[{function_signature}] Start")
123-
try:
124-
result = func(*args, **kwargs)
125-
except Exception as ex:
126-
print(f"[{function_signature}] An exception happened: {ex}")
127-
128-
send_email(
129-
email_to=email_to,
130-
email_from=email_from,
131-
subject=subject,
132-
text="".join(traceback.format_exception(None, ex, ex.__traceback__)),
133-
)
134-
135-
sys.exit(1)
136-
137-
print(f"[{function_signature}] Finished")
138-
return result
139-
140-
return inner
141-
142-
return decorator
143-
144-
145110
def bs4_html(html: str | bytes | Response) -> BeautifulSoup:
146111
global _used_bs4_option
147112

@@ -170,7 +135,7 @@ def bs4_html(html: str | bytes | Response) -> BeautifulSoup:
170135
return BeautifulSoup(html)
171136

172137

173-
def get_all_values_from_form(html, form_selector):
138+
def get_all_values_from_form(html: bs4.BeautifulSoup, form_selector: str):
174139
form = html.select(form_selector)
175140
assert len(form) == 1, f"We should have only 1 form. We got {len(form)}!"
176141
form = form[0]
@@ -211,6 +176,26 @@ def get_all_values_from_form(html, form_selector):
211176
return inputs
212177

213178

179+
def fill_form(response: Response, form_selector, values: dict[str, str]) -> dict[str, str]:
180+
html = bs4_html(response)
181+
inputs = get_all_values_from_form(html, form_selector)
182+
183+
data = {}
184+
values = values.copy()
185+
186+
for input_ in inputs:
187+
name = input_["name"]
188+
for key, _value in values.items():
189+
if key in name:
190+
data[name] = values.pop(key)
191+
break
192+
else:
193+
data[name] = input_["value"]
194+
195+
assert len(values) == 0, f"You didn't use: {sorted(values)}"
196+
return data
197+
198+
214199
def make_filesystem_safe(name: str) -> str:
215200
name = re.sub("[^-_a-z0-9.]+", "_", name, flags=re.IGNORECASE)
216201
name = re.sub("_{2,}", "_", name)

src/smartschool/courses.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
22

33
from functools import cached_property
4-
from typing import Iterator
4+
from typing import TYPE_CHECKING
55

66
from .objects import Course, CourseCondensed
77
from .session import session
88

9+
if TYPE_CHECKING:
10+
from collections.abc import Iterator
11+
912
__all__ = ["Courses", "TopNavCourses"]
1013

1114

src/smartschool/credentials.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,42 @@
1111
class Credentials(ABC):
1212
username: str
1313
password: str
14+
birthday: str
1415
main_url: str
1516

1617
other_info: dict | None = None
1718

1819
def validate(self) -> None:
19-
self.username = (self.username or "").strip()
20-
self.password = (self.password or "").strip()
21-
self.main_url = (self.main_url or "").strip()
20+
required_fields = [
21+
"username",
22+
"password",
23+
"birthday",
24+
"main_url",
25+
]
2226

2327
error = []
24-
if not self.username:
25-
error.append("username")
26-
if not self.password:
27-
error.append("password")
28-
if not self.main_url:
29-
error.append("main_url")
28+
for required in required_fields:
29+
value = (getattr(self, required) or "").strip()
30+
setattr(self, required, value)
31+
if not value:
32+
error.append(required)
3033

3134
if error:
3235
raise RuntimeError(f"Please verify and correct these attributes: {error}")
3336

37+
def as_dict(self) -> dict[str, str | dict]:
38+
data: dict = {
39+
"username": self.username,
40+
"password": self.password,
41+
"birthday": self.birthday,
42+
"main_url": self.main_url,
43+
}
44+
45+
if self.other_info:
46+
data["other_info"] = self.other_info
47+
48+
return data
49+
3450

3551
@dataclass
3652
class PathCredentials(Credentials):
@@ -43,6 +59,7 @@ def __post_init__(self):
4359
self.username = cred_file.pop("username", None)
4460
self.password = cred_file.pop("password", None)
4561
self.main_url = cred_file.pop("main_url", None)
62+
self.birthday = cred_file.pop("birthday", None)
4663

4764
self.other_info = cred_file
4865

@@ -53,3 +70,4 @@ def __post_init__(self):
5370
self.username = os.getenv("SMARTSCHOOL_USERNAME")
5471
self.password = os.getenv("SMARTSCHOOL_PASSWORD")
5572
self.main_url = os.getenv("SMARTSCHOOL_MAIN_URL")
73+
self.birthday = os.getenv("SMARTSCHOOL_BIRTHDAY")

src/smartschool/logger.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)