Skip to content

Commit adba952

Browse files
authored
feat: adding reports integration (#26)
* Rework the results a little bit to account for Mauros missing component. * Added stub generation script * classes loading correctly * WIP (not working) * Generating a nice stub already * Already working really really nice! * Already working really really really nice! * Only type attribute is generated wrongly * Different approach, more unified * Working nicely * WIP * WIP (but not working yet) * results is fine now * implement type checking * Working on "courses" * re-ran everything * Fix floating point comparisons * reformat * Resolving sonar issues * Resolving sonar issues * Adding reports * Version bump * forgotten file * remove assignment * Auto-restubbing * Adding "__all__" & reformat * Cleanup * Tests stuff * Just run everything at once if possible * Checkout the PR branch, but only on PR * restubbing needs full poetry environment * Fail fast * Cleaning up a bit * list installed packages * Run through poetry
1 parent 5ea25c7 commit adba952

File tree

17 files changed

+393
-60
lines changed

17 files changed

+393
-60
lines changed

.gitattributes

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1+
scripts/list_all_agenda text eol=lf
12
scripts/list_all_courses text eol=lf
3+
scripts/list_all_future_tasks text eol=lf
4+
scripts/list_all_results text eol=lf
5+
scripts/smartschool_browse_docs text eol=lf
6+
scripts/smartschool_cache.sqlite text eol=lf
7+
scripts/smartschool_download_all_documents text eol=lf
28
scripts/smartschool_report_on_future_tasks text eol=lf
9+
scripts/smartschool_report_on_planned_tasks text eol=lf
310
scripts/smartschool_report_on_results text eol=lf
4-
*.py text eol=lf
11+
12+
scripts/smartschool_report_on_future_tasks text eol=lf
13+
scripts/smartschool_report_on_results text eol=lf
14+
15+
**/*.py text eol=lf
16+
run text eol=lf
17+
restub text eol=lf

.github/workflows/tests.yml

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,69 @@ jobs:
3939
#----------------------------------------------
4040

4141

42+
generate-stubs:
43+
runs-on: ubuntu-latest
44+
if: github.event_name == 'pull_request'
45+
steps:
46+
- uses: actions/checkout@v4
47+
with:
48+
token: ${{ secrets.GITHUB_TOKEN }}
49+
fetch-depth: 0
50+
ref: ${{ github.head_ref }}
51+
52+
- name: Set up Python 3.11
53+
uses: actions/setup-python@v5
54+
with:
55+
python-version: 3.11
56+
57+
- name: cache poetry install
58+
uses: actions/cache@v4
59+
with:
60+
path: ~/.local
61+
key: poetry-ubuntu-latest-3.11
62+
63+
- name: Install Poetry
64+
uses: snok/install-poetry@v1
65+
with:
66+
virtualenvs-create: true
67+
virtualenvs-in-project: true
68+
69+
- name: Load cached venv
70+
id: cached-poetry-dependencies
71+
uses: actions/cache@v4
72+
with:
73+
path: .venv
74+
key: venv-ubuntu-latest-3.11-${{ hashFiles('**/poetry.lock') }}
75+
76+
- name: Install dependencies
77+
run: poetry install --no-interaction --no-root
78+
79+
- name: Install ${{ github.event.repository.name }}
80+
run: poetry install --no-interaction
81+
82+
- name: Generate stubs
83+
run: poetry run ./restub
84+
85+
- name: Check for changes
86+
id: check-changes
87+
run: |
88+
git config --local user.email "[email protected]"
89+
git config --local user.name "GitHub Action"
90+
91+
if git diff --quiet src/smartschool; then
92+
echo "changes_detected=false" >> $GITHUB_OUTPUT
93+
else
94+
echo "changes_detected=true" >> $GITHUB_OUTPUT
95+
fi
96+
97+
- name: Commit and push changes
98+
if: steps.check-changes.outputs.changes_detected == 'true'
99+
run: |
100+
git add src/smartschool
101+
git commit -m "Auto-update stub files"
102+
git push
103+
104+
42105
tests:
43106
strategy:
44107
fail-fast: false
@@ -90,4 +153,4 @@ jobs:
90153
- name: Upload coverage reports to Codecov
91154
uses: codecov/codecov-action@v5
92155
env:
93-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
156+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

dev/generate_stubs.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import argparse
77
import ast
8+
import collections.abc
89
import importlib.util
910
import inspect
1011
import re
@@ -59,6 +60,9 @@ def _format_import(annotation: type, current_module: types.ModuleType) -> str:
5960
if module == "builtins":
6061
return ""
6162

63+
if module == "typing" and getattr(collections.abc, name, None) is not None:
64+
module = "collections.abc"
65+
6266
current_parts = current_module.__name__.split(".")
6367
target_parts = module.split(".")
6468

@@ -215,6 +219,7 @@ def load_module_from_file(file_path: Path):
215219

216220
spec = importlib.util.spec_from_file_location(full_module_name, file_path)
217221
if not spec or not spec.loader:
222+
logger.warning(f"Failed to load module {full_module_name} from {file_path}")
218223
return None
219224

220225
module = importlib.util.module_from_spec(spec)
@@ -340,12 +345,12 @@ def generate_stub_from_class_info(class_info: ClassInfo, imports_needed: set, cu
340345

341346
# Add methods
342347
for method in class_info.methods:
343-
stub += _generate_method_stub(method)
348+
stub += _generate_method_stub(method, imports_needed, current_module)
344349

345350
return stub + "\n"
346351

347352

348-
def _generate_method_stub(method: MethodInfo | str | None) -> str:
353+
def _generate_method_stub(method: MethodInfo | str | None, imports_needed: set[str], current_module: types.ModuleType) -> str:
349354
if not method:
350355
return ""
351356

@@ -368,11 +373,14 @@ def _generate_method_stub(method: MethodInfo | str | None) -> str:
368373
params.append(param_str)
369374

370375
if method.return_annotation and method.return_annotation is not inspect.Signature.empty:
371-
return_type = f" -> {method.return_annotation}"
376+
if isinstance(method.return_annotation, str):
377+
return_type = f" -> {method.return_annotation}"
378+
else:
379+
return_type = f" -> {format_type_annotation(method.return_annotation, imports_needed, current_module)}"
372380
else:
373381
return_type = ""
374382

375-
return f" def {method.name}({', '.join(params)}){return_type}: ...\n"
383+
return f" def {method.name}({', '.join(params)},){return_type}: ...\n"
376384

377385

378386
def _inject_typechecking_imports(tree: ast.Module, imports: list[str], module: types.ModuleType) -> None:
@@ -405,10 +413,10 @@ def generate_stub_file(python_file: Path) -> str:
405413
classes, imports, ast_tree = parse_class_ast_info(python_file)
406414
module = load_module_from_file(python_file)
407415
if not module:
408-
return "# Failed to load module\n"
416+
raise ModuleNotFoundError(f"Failed to load module {python_file}")
409417

410418
if not classes:
411-
return "# No classes found\n"
419+
raise ValueError("No classes found")
412420

413421
_inject_typechecking_imports(ast_tree, imports, module)
414422

@@ -418,7 +426,7 @@ def generate_stub_file(python_file: Path) -> str:
418426
if inspect.isclass(obj) and obj.__module__ == module.__name__:
419427
classes[obj.__name__].real_class = obj
420428

421-
imports_needed = set()
429+
imports_needed = set(imports)
422430

423431
# Extract data for all classes
424432
for cls in classes.values():
@@ -442,31 +450,48 @@ def generate_stub_file(python_file: Path) -> str:
442450

443451
def reformat_file(output_file):
444452
try:
445-
subprocess.run(["ruff", "format", str(output_file)], check=True, capture_output=True)
446-
subprocess.run(["ruff", "check", "--select", "I,F,E", "--fix", str(output_file)], check=True, capture_output=True)
453+
subprocess.run(
454+
["ruff", "format", str(output_file)],
455+
check=True,
456+
stdout=subprocess.PIPE,
457+
stderr=subprocess.STDOUT,
458+
text=True,
459+
)
460+
subprocess.run(
461+
["ruff", "check", "--select", "I,F,E", "--fix", "--unsafe-fixes", str(output_file)],
462+
check=True,
463+
stdout=subprocess.PIPE,
464+
stderr=subprocess.STDOUT,
465+
text=True,
466+
)
447467
logger.info(f"Generated and formatted {output_file}")
448468
except subprocess.CalledProcessError as e:
449469
logger.warning(f"Generated {output_file} but ruff formatting failed: {e}")
470+
for line in e.output.splitlines():
471+
logger.warning(line)
450472
except FileNotFoundError:
451473
logger.warning(f"Generated {output_file} but ruff not found - install ruff for formatting")
452474

453475

454476
def main():
455477
parser = argparse.ArgumentParser(description="Generate .pyi stub files")
456-
parser.add_argument("python_file", type=Path, help="Python file to analyze")
457-
parser.add_argument("-o", "--output", type=Path, help="Output .pyi file")
478+
parser.add_argument("python_files", nargs="+", type=Path, help="Python file to analyze")
458479

459480
args = parser.parse_args()
460481

461-
if not args.python_file.exists():
462-
logger.error(f"File {args.python_file} does not exist")
463-
return
482+
for file in args.python_files:
483+
file = file.resolve().absolute()
484+
if file.suffix == ".pyi":
485+
file = file.with_suffix(".py")
464486

465-
output_file = args.output or args.python_file.with_suffix(".pyi")
487+
if not file.exists():
488+
logger.error(f"File {file} does not exist")
489+
continue
466490

467-
stub_content = generate_stub_file(args.python_file)
468-
output_file.write_text(stub_content)
469-
reformat_file(output_file)
491+
stub_content = generate_stub_file(file)
492+
output_file = file.with_suffix(".pyi")
493+
output_file.write_text(stub_content)
494+
reformat_file(output_file)
470495

471496

472497
if __name__ == "__main__":

poetry.lock

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

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "smartschool"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
description = "Unofficial API interface to the smartschool system."
55
authors = ["Steven Van Ingelgem <[email protected]>"]
66
readme = "README.md"

restub

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash -e
2+
3+
pyi_files=$(find src/smartschool -name "*.pyi" -type f)
4+
python dev/generate_stubs.py $pyi_files

src/smartschool/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from .periods import Periods
2121
from .planner import ApplicableAssignmentTypes, PlannedElements
22+
from .reports import Report, Reports
2223
from .results import Result, Results
2324
from .session import Smartschool
2425
from .student_support import StudentSupportLinks
@@ -50,6 +51,8 @@
5051
"PathCredentials",
5152
"Periods",
5253
"PlannedElements",
54+
"Report",
55+
"Reports",
5356
"Result",
5457
"Results",
5558
"SmartSchoolAuthenticationError",

src/smartschool/courses.pyi

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,44 @@ class CourseCondensed(objects.CourseCondensed, SessionMixin):
1515
name: str
1616
teacher: str
1717
url: str
18-
id: int
19-
platformId: int
18+
id: int | None
19+
platformId: int | None
2020
descr: str
2121
icon: str
2222
def __init__(
23-
self, session: Smartschool, name: str, teacher: str, url: str, id: int | None = None, platformId: int | None = None, descr: str = "", icon: str = ""
23+
self,
24+
session: Smartschool,
25+
name: str,
26+
teacher: str,
27+
url: str,
28+
id: int | None = None,
29+
platformId: int | None = None,
30+
descr: str = "",
31+
icon: str = "",
32+
): ...
33+
def __str__(
34+
self,
2435
): ...
25-
def __str__(self): ...
2636

2737
class TopNavCourses(SessionMixin):
2838
session: Smartschool
29-
def __init__(self, session: Smartschool): ...
30-
def __iter__(self) -> Iterator[CourseCondensed]: ...
39+
def __init__(
40+
self,
41+
session: Smartschool,
42+
): ...
43+
def __iter__(
44+
self,
45+
) -> Iterator[CourseCondensed]: ...
3146

3247
class Courses(SessionMixin):
3348
session: Smartschool
34-
def __init__(self, session: Smartschool): ...
35-
def __iter__(self) -> Iterator[Course]: ...
49+
def __init__(
50+
self,
51+
session: Smartschool,
52+
): ...
53+
def __iter__(
54+
self,
55+
) -> Iterator[Course]: ...
3656

3757
class FileItem(SessionMixin):
3858
session: Smartschool
@@ -56,7 +76,11 @@ class FileItem(SessionMixin):
5676
download_url: str | None = None,
5777
view_url: str | None = None,
5878
): ...
59-
def download_to_dir(self, target_directory: Path, overwrite: bool = False) -> Path: ...
79+
def download_to_dir(
80+
self,
81+
target_directory: Path,
82+
overwrite: bool = False,
83+
) -> Path: ...
6084
@overload
6185
def download(self, to_file: Path | str, *, overwrite: bool) -> Path: ...
6286
@overload
@@ -94,4 +118,11 @@ class FolderItem(SessionMixin):
94118
course: CourseCondensed
95119
name: str
96120
browse_url: str | None
97-
def __init__(self, session: Smartschool, parent: FolderItem | None, course: CourseCondensed, name: str, browse_url: str | None = None): ...
121+
def __init__(
122+
self,
123+
session: Smartschool,
124+
parent: FolderItem | None,
125+
course: CourseCondensed,
126+
name: str,
127+
browse_url: str | None = None,
128+
): ...

0 commit comments

Comments
 (0)