Added: Stuff

This commit is contained in:
DerGrumpf 2025-03-07 01:25:41 +01:00
parent ab01d8f75e
commit 2d25d5105a
42 changed files with 150844 additions and 156 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,36 +1,38 @@
First Name,Last Name,Sex,Group,Grader,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis
Abdalaziz,Abunjaila,Male,DiKum,30%,30.5,15,18,28,17,17,17,22,0,18
Marleen,Adolphi,Female,MeWi6,30%,29.5,15,18,32,19,20,17,24,23,0
Sarina,Apel,Female,MeWi1,30%,28.5,15,18,32,20,20,21,24,20,23
Skofiare,Berisha,Female,DiKum,30%,29.5,13,18,34,20,17,20,26,16,0
Aurela,Brahimi,Female,MeWi2,30%,17.5,15,15.5,26,16,17,19,16,0,0
Cam Thu,Do,Female,MeWi3,30%,31,15,18,34,19,20,21.5,22,12,0
Nova,Eib,Female,MeWi4,30%,31,15,15,34,20,20,21,27,19,21
Lena,Fricke,Female,MeWi4,30%,0,0,0,0,0,0,0,0,0,0
Nele,Grundke,Female,MeWi6,30%,23.5,13,16,28,20,17,21,18,22,11
Anna,Grünewald,Female,MeWi3,30%,12,14,16,29,16,15,19,9,0,0
Yannik,Haupt,Male,NoGroup,30%,18,6,14,21,13,2,9,0,0,0
Janna,Heiny,Female,MeWi1,30%,30,15,18,33,18,20,22,25,24,30
Milena,Krieger,Female,MeWi1,30%,30,15,18,33,20,20,21.5,26,20,22
Julia,Limbach,Female,MeWi6,30%,27.5,12,18,29,11,19,17.5,26,24,28
Viktoria,Litza,Female,MeWi5,30%,21.5,15,18,27,13,20,22,21,21,30
Leonie,Manthey,Female,MeWi1,30%,28.5,14,18,29,20,10,18,23,16,28
Izabel,Mike,Female,MeWi2,30%,29.5,15,15,35,11,15,19,21,21,27
Lea,Noglik,Female,MeWi5,30%,22.5,15,17,34,13,10,20,21,19,6
Donika,Nuhiu,Female,MeWi5,30%,31,13.5,18,35,14,10,17,18,19,8
Julia,Renner,Female,MeWi4,30%,27.5,10,14,32,20,17,11,20,24,14
Fabian,Rothberger,Male,MeWi3,30%,30.5,15,18,34,17,17,19,22,18,30
Natascha,Rott,Female,MeWi1,30%,29.5,12,18,32,19,20,21,26,23,26
Isabel,Rudolf,Female,MeWi4,30%,27.5,9,17,34,16,19,19,21,16,14
Melina,Sablotny,Female,MeWi6,30%,31,15,18,33,20,20,21,19,11,28
Alea,Schleier,Female,DiKum,30%,27,14,18,34,16,18,21.5,22,15,22
Flemming,Schur,Male,MeWi3,30%,29.5,15,17,34,19,20,19,22,18,27
Marie,Seeger,Female,DiKum,30%,27.5,15,18,32,14,9,17,22,9,25
Lucy,Thiele,Female,MeWi6,30%,27.5,15,18,27,20,17,19,18,22,25
Lara,Troschke,Female,MeWi2,30%,28.5,14,17,28,13,19,21,25,12,24
Inga-Brit,Turschner,Female,MeWi2,30%,25.5,14,18,34,20,16,19,22,17,30
Alea,Unger,Female,MeWi5,30%,30,12,18,31,20,20,21,22,15,21.5
Marie,Wallbaum,Female,MeWi5,30%,28.5,14,18,34,17,20,19,24,12,22
Katharina,Walz,Female,MeWi4,30%,31,15,18,31,19,19,17,24,17,14.5
Xiaowei,Wang,Male,NoGroup,30%,30.5,14,18,26,19,17,0,0,0,0
Lilly-Lu,Warnken,Female,DiKum,30%,30,15,18,30,14,17,19,14,16,24
First Name,Last Name,Sex,Group,Grader,Study,Tutorial 1,Tutorial 2,Extended Applications,Numpy & MatPlotLib,SciPy,Monte Carlo,Pandas & Seaborn,Folium,Statistical Test Methods,Data Analysis
Abdalaziz,Abunjaila,Male,DiKum,30%,Digitale Kommunikation & Medientechnik,30.5,15,18,28,17,17,17,22,0,18
Marleen,Adolphi,Female,MeWi6,30%,Digitale Kommunikation & Medientechnik,29.5,15,18,32,19,20,17,24,23,0
Sarina,Apel,Female,MeWi1,30%,Medienwissenschaften,28.5,15,18,32,20,20,21,24,20,23
Skofiare,Berisha,Female,DiKum,30%,Medienwissenschaften,29.5,13,18,34,20,17,20,26,16,10
Aurela,Brahimi,Female,MeWi2,30%,Medienwissenschaften,17.5,15,15.5,26,16,17,19,16,16,16
Cam Thu,Do,Female,MeWi3,30%,Medienwissenschaften,31,15,18,34,19,20,21.5,22,12,15
Nova,Eib,Female,MeWi4,30%,Medienwissenschaften,31,15,15,34,20,20,21,27,19,21
Lena,Fricke,Female,MeWi4,30%,Medienwissenschaften,13,14,15,21,15,17,17,18,19,11
Nele,Grundke,Female,MeWi6,30%,Medienwissenschaften,23.5,13,16,28,20,17,21,18,22,11
Anna,Grünewald,Female,MeWi3,30%,Medienwissenschaften,12,14,16,29,16,15,19,9,16,12
Yannik,Haupt,Male,NoGroup,30%,Unknown,18,6,14,21,13,2,9,0,0,0
Janna,Heiny,Female,MeWi1,30%,Technologie Orientiertes Managment,30,15,18,33,18,20,22,25,24,30
Milena,Krieger,Female,MeWi1,30%,Technologie Orientiertes Managment,30,15,18,33,20,20,21.5,26,20,22
Julia,Limbach,Female,MeWi6,30%,Medienwissenschaften,27.5,12,18,29,11,19,17.5,26,24,28
Viktoria,Litza,Female,MeWi5,30%,Medienwissenschaften,21.5,15,18,27,13,20,22,21,21,30
Leonie,Manthey,Female,MeWi1,30%,Medienwissenschaften,28.5,14,18,29,20,10,18,23,16,28
Izabel,Mike,Female,MeWi2,30%,Medienwissenschaften,29.5,15,15,35,11,15,19,21,21,27
Lea,Noglik,Female,MeWi5,30%,Medienwissenschaften,22.5,15,17,34,13,10,20,21,19,6
Donika,Nuhiu,Female,MeWi5,30%,Medienwissenschaften,31,13.5,18,35,14,10,17,18,19,8
Julia,Renner,Female,MeWi4,30%,Medienwissenschaften,27.5,10,14,32,20,17,11,20,24,14
Fabian,Rothberger,Male,MeWi3,30%,Medienwissenschaften,30.5,15,18,34,17,17,19,22,18,30
Natascha,Rott,Female,MeWi1,30%,Medienwissenschaften,29.5,12,18,32,19,20,21,26,23,26
Isabel,Rudolf,Female,MeWi4,30%,Medienwissenschaften,27.5,9,17,34,16,19,19,21,16,14
Melina,Sablotny,Female,MeWi6,30%,Medienwissenschaften,31,15,18,33,20,20,21,19,11,28
Alea,Schleier,Female,DiKum,30%,Medienwissenschaften,27,14,18,34,16,18,21.5,22,15,22
Flemming,Schur,Male,MeWi3,30%,Medienwissenschaften,29.5,15,17,34,19,20,19,22,18,27
Marie,Seeger,Female,DiKum,30%,Medienwissenschaften,27.5,15,18,32,14,9,17,22,9,25
Lucy,Thiele,Female,MeWi6,30%,Medienwissenschaften,27.5,15,18,27,20,17,19,18,22,25
Lara,Troschke,Female,MeWi2,30%,Medienwissenschaften,28.5,14,17,28,13,19,21,25,12,24
Inga-Brit,Turschner,Female,MeWi2,30%,Medienwissenschaften,25.5,14,18,34,20,16,19,22,17,30
Alea,Unger,Female,MeWi5,30%,Medienwissenschaften,30,12,18,31,20,20,21,22,15,21.5
Marie,Wallbaum,Female,MeWi5,30%,Medienwissenschaften,28.5,14,18,34,17,20,19,24,12,22
Katharina,Walz,Female,MeWi4,30%,Medienwissenschaften,31,15,18,31,19,19,17,24,17,14.5
Xiaowei,Wang,Male,NoGroup,30%,Unknown,30.5,14,18,26,19,17,0,0,0,0
Lilly-Lu,Warnken,Female,DiKum,30%,Medienwissenschaften,30,15,18,30,14,17,19,14,16,24
,,,,,,,,,,,,,,,

1 First Name Last Name Sex Group Grader Study Tutorial 1 Tutorial 2 Extended Applications Numpy & MatPlotLib SciPy Monte Carlo Pandas & Seaborn Folium Statistical Test Methods Data Analysis
2 Abdalaziz Abunjaila Male DiKum 30% Digitale Kommunikation & Medientechnik 30.5 15 18 28 17 17 17 22 0 18
3 Marleen Adolphi Female MeWi6 30% Digitale Kommunikation & Medientechnik 29.5 15 18 32 19 20 17 24 23 0
4 Sarina Apel Female MeWi1 30% Medienwissenschaften 28.5 15 18 32 20 20 21 24 20 23
5 Skofiare Berisha Female DiKum 30% Medienwissenschaften 29.5 13 18 34 20 17 20 26 16 0 10
6 Aurela Brahimi Female MeWi2 30% Medienwissenschaften 17.5 15 15.5 26 16 17 19 16 0 16 0 16
7 Cam Thu Do Female MeWi3 30% Medienwissenschaften 31 15 18 34 19 20 21.5 22 12 0 15
8 Nova Eib Female MeWi4 30% Medienwissenschaften 31 15 15 34 20 20 21 27 19 21
9 Lena Fricke Female MeWi4 30% Medienwissenschaften 0 13 0 14 0 15 0 21 0 15 0 17 0 17 0 18 0 19 0 11
10 Nele Grundke Female MeWi6 30% Medienwissenschaften 23.5 13 16 28 20 17 21 18 22 11
11 Anna Grünewald Female MeWi3 30% Medienwissenschaften 12 14 16 29 16 15 19 9 0 16 0 12
12 Yannik Haupt Male NoGroup 30% Unknown 18 6 14 21 13 2 9 0 0 0
13 Janna Heiny Female MeWi1 30% Technologie Orientiertes Managment 30 15 18 33 18 20 22 25 24 30
14 Milena Krieger Female MeWi1 30% Technologie Orientiertes Managment 30 15 18 33 20 20 21.5 26 20 22
15 Julia Limbach Female MeWi6 30% Medienwissenschaften 27.5 12 18 29 11 19 17.5 26 24 28
16 Viktoria Litza Female MeWi5 30% Medienwissenschaften 21.5 15 18 27 13 20 22 21 21 30
17 Leonie Manthey Female MeWi1 30% Medienwissenschaften 28.5 14 18 29 20 10 18 23 16 28
18 Izabel Mike Female MeWi2 30% Medienwissenschaften 29.5 15 15 35 11 15 19 21 21 27
19 Lea Noglik Female MeWi5 30% Medienwissenschaften 22.5 15 17 34 13 10 20 21 19 6
20 Donika Nuhiu Female MeWi5 30% Medienwissenschaften 31 13.5 18 35 14 10 17 18 19 8
21 Julia Renner Female MeWi4 30% Medienwissenschaften 27.5 10 14 32 20 17 11 20 24 14
22 Fabian Rothberger Male MeWi3 30% Medienwissenschaften 30.5 15 18 34 17 17 19 22 18 30
23 Natascha Rott Female MeWi1 30% Medienwissenschaften 29.5 12 18 32 19 20 21 26 23 26
24 Isabel Rudolf Female MeWi4 30% Medienwissenschaften 27.5 9 17 34 16 19 19 21 16 14
25 Melina Sablotny Female MeWi6 30% Medienwissenschaften 31 15 18 33 20 20 21 19 11 28
26 Alea Schleier Female DiKum 30% Medienwissenschaften 27 14 18 34 16 18 21.5 22 15 22
27 Flemming Schur Male MeWi3 30% Medienwissenschaften 29.5 15 17 34 19 20 19 22 18 27
28 Marie Seeger Female DiKum 30% Medienwissenschaften 27.5 15 18 32 14 9 17 22 9 25
29 Lucy Thiele Female MeWi6 30% Medienwissenschaften 27.5 15 18 27 20 17 19 18 22 25
30 Lara Troschke Female MeWi2 30% Medienwissenschaften 28.5 14 17 28 13 19 21 25 12 24
31 Inga-Brit Turschner Female MeWi2 30% Medienwissenschaften 25.5 14 18 34 20 16 19 22 17 30
32 Alea Unger Female MeWi5 30% Medienwissenschaften 30 12 18 31 20 20 21 22 15 21.5
33 Marie Wallbaum Female MeWi5 30% Medienwissenschaften 28.5 14 18 34 17 20 19 24 12 22
34 Katharina Walz Female MeWi4 30% Medienwissenschaften 31 15 18 31 19 19 17 24 17 14.5
35 Xiaowei Wang Male NoGroup 30% Unknown 30.5 14 18 26 19 17 0 0 0 0
36 Lilly-Lu Warnken Female DiKum 30% Medienwissenschaften 30 15 18 30 14 17 19 14 16 24
37
38

Binary file not shown.

View File

@ -44,19 +44,24 @@ for k, v in courses.items():
#print(l.title, l.points, l.class_id, l.id)
for k, v in groups.items():
Group.create(name=k, project=v, has_passed=True, class_id=clas.id)
Group.create(name=k, project=v, has_passed=True if k != 'NoGroup' else False, class_id=clas.id)
for index, row in df.iterrows():
study, _ = Study.get_or_create(name=row['Study'])
s = Student.create(
prename=row["First Name"],
surname=row["Last Name"],
sex=row["Sex"],
study_id=study.id,
class_id=clas.id,
group_id=Group.select().where(Group.name == row["Group"]),
grader=row["Grader"],
)
for title, points in list(row.to_dict().items())[5:]:
for title, points in list(row.to_dict().items())[6:]:
Submission.create(
student_id=s.id,
lecture_id=Lecture.select().where(Lecture.title == title),

13874
assets/documents/document.pdf Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

29
assets/learnlytics.svg Normal file
View File

@ -0,0 +1,29 @@
<svg
width="300" height="150" viewBox="0 0 300 150"
xmlns="http://www.w3.org/2000/svg"
fill="none" stroke-linecap="round" stroke-linejoin="round"
>
<!-- Background Grid -->
<rect width="100%" height="100%" fill="#0D1B2A" />
<!-- Bar Chart -->
<rect x="50" y="90" width="30" height="30" fill="#2EC4B6" />
<rect x="90" y="60" width="30" height="60" fill="#2EC4B6" />
<rect x="130" y="80" width="30" height="40" fill="#2EC4B6" />
<rect x="170" y="40" width="30" height="80" fill="#2EC4B6" />
<rect x="210" y="75" width="30" height="45" fill="#2EC4B6" />
<rect x="250" y="30" width="30" height="90" fill="#2EC4B6" />
<!-- Analytics Chart - Dynamic Graph Lines -->
<polyline points="50,110 90,70 130,100 170,50 210,90 250,40" stroke="#F4A261" stroke-width="6" stroke-dasharray="8 4" />
<circle cx="50" cy="110" r="5" fill="#F4A261" />
<circle cx="90" cy="70" r="5" fill="#F4A261" />
<circle cx="130" cy="100" r="5" fill="#F4A261" />
<circle cx="170" cy="50" r="5" fill="#F4A261" />
<circle cx="210" cy="90" r="5" fill="#F4A261" />
<circle cx="250" cy="40" r="5" fill="#F4A261" />
<!-- Text: Learnlytics -->
<text x="100" y="140" fill="#E0E1DD" font-family="Arial, sans-serif" font-size="20" font-weight="bold">Learnlytics</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/logo_IFN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

198
assets/logo_IFN.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -52,6 +52,14 @@ class Group(BaseModel):
created_at = DateTimeField(default=datetime.now)
class Study(BaseModel):
'''
Table for Storing a Study Program
'''
name = CharField()
created_at = DateTimeField(default=datetime.now)
class Student(BaseModel):
'''
Table for Storing a Student and linking him to appropiate Tables
@ -59,6 +67,7 @@ class Student(BaseModel):
prename = CharField()
surname = CharField()
sex = CharField()
study = ForeignKeyField(Study, backref='study')
class_id = ForeignKeyField(Class, backref='class')
group_id = ForeignKeyField(Group, backref='group')
grader = CharField()

View File

@ -3,6 +3,7 @@ from typing import Any
import inspect
from abc import ABC, abstractmethod
import weakref
from collections import Counter
from PIL import ImageColor
from colour import Color
@ -95,9 +96,17 @@ class BaseGrading(Mapping, ABC):
@abstractmethod
def has_passed(self, value: int | float, max: int | float) -> bool:
pass
@abstractmethod
def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
pass
@abstractmethod
def get_grade(self, value: int | float, max: int | float) -> str | int:
def get_grade(self, value: int | float, max: int | float, is_html: bool) -> str | int:
pass
@abstractmethod
def get_final_grade(self, values: list[int | float], maxs: list[int | float], is_html: bool) -> str | int:
pass
@abstractmethod
@ -114,24 +123,42 @@ class BaseGrading(Mapping, ABC):
if instance.alt_name == name:
return instance
get_gradings = lambda: BaseGrading.get_instances()
get_grader = lambda name: BaseGrading.get_instance(name)
class StdPercentRule(BaseGrading):
def has_passed(self, value: int | float, max: int | float) -> bool:
return value >= max * self.schema["Passed"]
def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
checks = [self.has_passed(value, max) for value, max in zip(values, maxs)]
check = Counter(checks)
return check[False] < 2
def get_grade(self, value: int | float, max: int | float) -> str:
return "Passed" if self.has_passed(value, max) else "Not Passed"
def get_grade(self, value: int | float, max: int | float, is_html=True) -> str:
if is_html:
return '&check;' if self.has_passed(value, max) else '&cross;'
return 'Passed' if self.has_passed(value, max) else 'Failed'
def get_final_grade(self, values: list[int | float], maxs: int | float, is_html=True) -> str:
if is_html:
return '&check;' if self.has_passed_course(values, maxs) else '&cross;'
return 'Passed' if self.has_passed_course(values, maxs) else 'Failed'
def get_grade_color(self, value: int | float, max: int | float) -> tuple:
if self.has_passed(value, max):
return hex_to_rgba(PASSED)
return hex_to_rgba(FAILED)
class StdGermanGrading(BaseGrading):
class StdGermanHighSchoolGrading(BaseGrading):
def has_passed(self, value: int | float, max: int | float) -> bool:
return value/max >= 0.45
def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
return True
def search_grade(self, value: float) -> int:
if value <= 0:
@ -146,14 +173,50 @@ class StdGermanGrading(BaseGrading):
searched -= 1
return searched
def get_grade(self, value: int | float, max: int | float) -> int:
def get_grade(self, value: int | float, max: int | float, is_html=False) -> int:
return self.search_grade(value/max)
def get_final_grade(self, values: list[int | float], maxs: list[int | float], is_html=False) -> int:
return self.search_grade(sum(values)/sum(maxs))
def get_grade_color(self, value: float, max: int | float) -> tuple:
grade = self.get_grade(value, max)
colors = gradient(PASSED, FAILED, len(self.schema))
return colors[grade]
class StdGermanMiddleSchoolGrading(BaseGrading):
def has_passed(self, value: int | float, max: int | float) -> bool:
return value/max >= 0.45
def has_passed_course(self, values: list[int | float], maxs: list[int | float]) -> bool:
return True
def search_grade(self, value: float) -> int:
if value <= 0:
return max(self.schema.keys())
searched = min(self.schema.keys())
found = False
while not found:
if self.schema[searched] <= value:
found = True
else:
searched += 1
return searched
def get_grade(self, value: int | float, max: int | float, is_html=False) -> int:
return self.search_grade(value/max)
def get_final_grade(self, values: list[int | float], maxs: list[int | float], is_html=False) -> int:
return self.search_grade(sum(values)/sum(maxs))
def get_grade_color(self, value: float, max: int | float) -> tuple:
grade = self.get_grade(value, max)
colors = gradient(PASSED, FAILED, len(self.schema))
return colors[grade]
# Definitions
Std30PercentRule = StdPercentRule({
"pAssed": 0.3,
@ -165,7 +228,7 @@ Std50PercentRule = StdPercentRule({
"Failed": 0.0
}, "Std50PercentRule", "50%")
StdGermanGradingMiddleSchool = StdGermanGrading({
StdGermanGradingMiddleSchool = StdGermanMiddleSchoolGrading({
1: 0.96,
2: 0.80,
3: 0.60,
@ -174,7 +237,7 @@ StdGermanGradingMiddleSchool = StdGermanGrading({
6: 0.00
}, "StdGermanGradingMiddleSchool", "Mittelstufe")
StdGermanGradingHighSchool = StdGermanGrading({
StdGermanGradingHighSchool = StdGermanHighSchoolGrading({
15: 0.95,
14: 0.90,
13: 0.85,
@ -194,5 +257,5 @@ StdGermanGradingHighSchool = StdGermanGrading({
}, "StdGermanGradingHighSchool", "Oberstufe")
#print(StdGermanGradingHighSchool.get_grade(0.0, 24))
#print(StdGermanGradingMiddleSchool.get_grade(189.5, 242))

View File

@ -5,7 +5,8 @@ from .student_list import student_list
from .student_graph import student_graph
from .group_graph import group_graph
from .class_graph import class_graph
from .submission_table import submission_table
from .document_creator import document_creator
def analyzer_docking_splits() -> List[hello_imgui.DockingSplit]:
split_main_misc = hello_imgui.DockingSplit()
@ -51,17 +52,24 @@ def set_analyzer_layout() -> List[hello_imgui.DockableWindow]:
class_info.label = "Class Analyzer"
class_info.dock_space_name = "MainDockSpace"
class_info.gui_function = lambda: class_graph()
#student_ranking = hello_imgui.DockableWindow()
#student_ranking.label = "Ranking"
#student_ranking.dock_space_name = "MainDockSpace"
#student_ranking.gui_function = lambda: ranking()
submission_info = hello_imgui.DockableWindow()
submission_info.label = "Submissions"
submission_info.dock_space_name = "MainDockSpace"
submission_info.gui_function = lambda: submission_table()
document = hello_imgui.DockableWindow()
document.label = "Document Creator"
document.dock_space_name = "MainDockSpace"
document.gui_function = lambda: document_creator()
return [
student_selector,
student_info,
group_info,
class_info
class_info,
submission_info,
document
]
def analyzer_layout() -> hello_imgui.DockingParams:

View File

@ -2,15 +2,28 @@ from imgui_bundle import (
imgui,
immapp,
imgui_md,
im_file_dialog,
immvision,
ImVec2
)
from peewee import fn
from slugify import slugify
import cairosvg
from PIL import Image
import numpy as np
from pathlib import Path
import subprocess, os, platform
from datetime import datetime
from itertools import compress
import io
from dbmodel import *
from grader import get_grader, get_gradings
from .analyzer_state import AnalyzerState
from .plotter import plot_html
from .plotter import plot_pdf
from pdf import *
state = AnalyzerState()
@ -25,7 +38,7 @@ def ranking(class_id: int) -> None:
if not statics.inited:
statics.class_id = class_id
statics.data = [
(student, Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == student.id).scalar())
(f"{student.prename} {student.surname}", Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == student.id).scalar())
for student in Student.select().where(Student.class_id == class_id)
]
statics.data.sort(key = lambda tup: tup[1], reverse=True)
@ -34,20 +47,368 @@ def ranking(class_id: int) -> None:
if statics.class_id != class_id:
statics.inited = False
imgui_md.render(f"# Ranking - {Class.get_by_id(statics.class_id).name}")
imgui.text("")
imgui_md.render_unindented(f"# Ranking - {Class.get_by_id(statics.class_id).name}")
if len(statics.data) < 1:
return
if imgui.begin_table("Ranking1", len(statics.data), imgui.TableFlags_.sizing_fixed_fit.value):
if imgui.begin_table("Ranking", 3, imgui.TableFlags_.sizing_fixed_fit.value):
for n, d in enumerate(statics.data, start=1):
student, points = d
if points.is_integer():
points = int(points)
imgui.table_next_row()
imgui.table_next_column()
#imgui.set_next_item_width(-1)
imgui.text(f"{n}. {student.prename} {student.surname} - {points} Points")
imgui.text(f"{n}.")
imgui.table_next_column()
imgui.text(student)
imgui.table_next_column()
imgui.text(f"{points} Points")
imgui.end_table()
@immapp.static(inited=False)
def lecture_list(class_id: int) -> None:
imgui_md.render_unindented("# Lectures")
if class_id < 1:
return
statics = lecture_list
if not statics.inited:
statics.class_id = class_id
statics.data = [
(lecture, Submission.select().where(Submission.lecture_id == lecture.id).count())
for lecture in Lecture.select().where(Lecture.class_id == class_id)
]
statics.inited = True
if statics.class_id != class_id:
statics.inited = False
if imgui.button("Add Lecture"):
imgui.open_popup("Lecture")
if add_lecture(statics.class_id):
statics.inited = False
if len(statics.data) < 1:
return
if imgui.begin_table("Lectures", 2, imgui.TableFlags_.sizing_fixed_fit.value):
for n, d in enumerate(statics.data, start=1):
lecture, sub_count = d
imgui.table_next_row()
imgui.table_next_column()
if imgui.button(f"X##{lecture.title}"):
lecture.delete_instance()
statics.inited = False
imgui.same_line()
imgui.text(lecture.title)
imgui.table_next_column()
imgui.text(f"{lecture.points} Points")
imgui.end_table()
@immapp.static(inited=False)
def add_lecture(class_id: int) -> bool:
statics = add_lecture
if not statics.inited:
statics.title = str()
statics.points = float()
statics.inited = True
center = imgui.get_main_viewport().get_center()
imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
if imgui.begin_popup("Lecture", imgui.WindowFlags_.always_auto_resize.value):
imgui_md.render_unindented("# New Lecture")
_, statics.title = imgui.input_text("Title", statics.title)
_, statics.points = imgui.input_float("Points", statics.points, 0.5, 2.0, "%.1f")
if statics.points < 0:
statics.points = float()
if imgui.button("Add"):
lecture = Lecture.create(
title = statics.title,
points = statics.points,
class_id = class_id
)
sub_data = [
{"student_id": s.id, "lecture_id": lecture.id, "class_id": class_id, "points": 0.0}
for s in Student.select().where(Student.class_id == class_id)
]
Submission.insert_many(sub_data).execute()
statics.inited = False
imgui.close_current_popup()
imgui.end_popup()
return True
imgui.same_line()
if imgui.button("Cancel"):
imgui.close_current_popup()
statics.inited = False
imgui.end_popup()
return False
@immapp.static(inited=False)
def group_list(class_id: int) -> None:
imgui_md.render_unindented('# Groups')
if class_id < 1:
return
statics = group_list
if not statics.inited:
statics.class_id = class_id
statics.groups = list(Group.select().where(Group.class_id == class_id))
statics.inited = True
if statics.class_id != class_id:
statics.inited = False
if imgui.button("Add Group"):
imgui.open_popup("Group")
if add_group(statics.class_id):
statics.inited = False
if imgui.begin_table("Groups", 2, imgui.TableFlags_.sizing_fixed_fit.value):
for n, group in enumerate(statics.groups, start=1):
imgui.table_next_row()
imgui.table_next_column()
if group.name != 'NoGroup':
if imgui.button(f"X##{group.name}"):
# Put Students in NoGroup
students = Student.select().where(Student.group_id == group.id)
nogroup = Group.select().where(Group.class_id == statics.class_id and Group.name == 'NoGroup').get()
for student in students:
student.group_id = nogroup.id
with db.atomic():
Student.bulk_update(students, fields=[Student.group_id], batch_size=50)
group.delete_instance()
statics.inited = False
imgui.same_line()
imgui.text(group.name)
imgui.table_next_column()
imgui.text(f"{group.project}")
imgui.end_table()
@immapp.static(inited=False)
def add_group(class_id: int) -> bool:
statics = add_group
if not statics.inited:
statics.class_id = class_id
statics.name = str()
statics.project = str()
nogroup = None
for group in Group.select().where(Group.class_id == class_id):
if group.name == 'NoGroup':
nogroup = group
break
statics.students = list(Student.select().where(Student.class_id == class_id and Student.group_id == nogroup.id))
statics.selected = [False] * len(statics.students)
statics.inited = True
if statics.class_id != class_id:
statics.inited = False
center = imgui.get_main_viewport().get_center()
imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
if imgui.begin_popup("Group", imgui.WindowFlags_.always_auto_resize.value):
imgui_md.render_unindented("# New Group")
_, statics.name = imgui.input_text("Name", statics.name)
_, statics.project = imgui.input_text("Project", statics.project)
for n, student in enumerate(statics.students):
changed, _ = imgui.checkbox(f'{student.prename} {student.surname}', statics.selected[n])
if changed:
statics.selected[n] = not statics.selected[n]
if imgui.button("Add"):
if not statics.name:
imgui.close_current_popup()
imgui.end_popup()
return False
if not statics.project:
statics.project = "NoProject"
group = Group.create(
name = statics.name,
project = statics.project,
has_passed = False,
class_id = class_id
)
students = list(compress(statics.students, statics.selected))
if students:
for student in students:
student.group_id = group.id
with db.atomic():
Student.bulk_update(students, fields=[Student.group_id], batch_size=50)
statics.inited = False
imgui.close_current_popup()
imgui.end_popup()
return True
imgui.same_line()
if imgui.button("Cancel"):
imgui.close_current_popup()
statics.inited = False
imgui.end_popup()
return False
@immapp.static(inited=False)
def student_list(class_id: int) -> None:
imgui_md.render_unindented('# Students')
if class_id < 1:
return
statics = student_list
if not statics.inited:
statics.class_id = class_id
statics.students = list(Student.select().where(Student.class_id == class_id))
statics.inited = True
if statics.class_id != class_id:
statics.inited = False
if imgui.button("Add Student"):
imgui.open_popup("Student")
if add_student(statics.class_id):
statics.inited = False
if imgui.begin_table("Students", 2, imgui.TableFlags_.sizing_fixed_fit.value):
for n, student in enumerate(statics.students, start=1):
imgui.table_next_row()
imgui.table_next_column()
if imgui.button(f"X##{student.surname}{student.id}"):
Submission.delete().where(Submission.student_id == student.id).execute()
student.delete_instance()
statics.inited = False
imgui.end_table()
return
imgui.same_line()
imgui.text(f'{student.prename} {student.surname}')
#imgui.table_next_column()
#imgui.text(f"{group.project}")
imgui.end_table()
from playhouse.shortcuts import model_to_dict
from pprint import pprint
@immapp.static(inited=False)
def add_student(class_id: int) -> bool:
statics = add_student
if not statics.inited:
statics.class_id = class_id
statics.prename = str()
statics.surname = str()
statics.genders = ["Male", "Female"]
statics.gender_select = 0
statics.studys = list(Study.select())
statics.study_labels = [study.name for study in statics.studys]
statics.study_select = 0
statics.groups = list(Group.select().where(Group.class_id == class_id))
statics.group_labels = [group.name for group in statics.groups]
statics.group_select = 0
statics.graders = [grader.alt_name for grader in get_gradings()]
statics.grader_select = 0
statics.inited = True
if statics.class_id != class_id or len(statics.studys) != Study.select().count() or len(statics.groups) != Group.select().where(Group.class_id == statics.class_id).count():
statics.inited = False
center = imgui.get_main_viewport().get_center()
imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
if imgui.begin_popup("Student", imgui.WindowFlags_.always_auto_resize.value):
imgui_md.render_unindented("# New Student")
_, statics.prename = imgui.input_text("First Name", statics.prename)
_, statics.surname = imgui.input_text("Last Name", statics.surname)
_, statics.gender_select = imgui.combo("Gender", statics.gender_select, statics.genders)
_, statics.study_select = imgui.combo("Study Progamm", statics.study_select, statics.study_labels)
_, statics.group_select = imgui.combo("Group", statics.group_select, statics.group_labels)
_, statics.grader_select = imgui.combo("Grader", statics.grader_select, statics.graders)
if imgui.button("Add"):
s = Student.create(
prename = statics.prename,
surname = statics.surname,
sex = statics.genders[statics.gender_select],
study_id = statics.studys[statics.study_select].id,
class_id = class_id,
group_id = statics.groups[statics.group_select].id,
grader = statics.graders[statics.grader_select]
)
data = [
{'student_id': s.id, 'lecture_id': lecture.id, 'class_id': s.class_id, 'points': 0.0}
for lecture in Lecture.select().where(Lecture.class_id == s.class_id)
]
Submission.insert_many(data).execute()
statics.inited = False
imgui.close_current_popup()
imgui.end_popup()
return True
imgui.same_line()
if imgui.button("Cancel"):
imgui.close_current_popup()
statics.inited = False
imgui.end_popup()
return False
def class_graph() -> None:
if db.is_closed():
imgui.text("No DB loaded")
@ -55,22 +416,28 @@ def class_graph() -> None:
w, h = imgui.get_content_region_avail()
if imgui.begin_child("Ranking", ImVec2(w*0.3, h*0.5), imgui.ChildFlags_.borders.value):
ranking(state.class_id)
imgui.end_child()
imgui.same_line()
if imgui.begin_child("Presentations", ImVec2(w*0.7, h*0.7), imgui.ChildFlags_.borders.value):
html = Path("/storage/programming/Learnlytics/assets/covid_faelle_MeWi_2.html")
plot_html(html)
if imgui.begin_child("Group1", ImVec2(w, h*0.5)):
w1, h1 = imgui.get_content_region_avail()
if imgui.begin_child("Groups", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
group_list(state.class_id)
imgui.end_child()
imgui.same_line()
if imgui.button("Open in Browser"):
# I Hate everything about it
if platform.system() == 'Darwin': # MacOS
subprocess.Popen(('open', html))
elif platform.system() == 'Windows': # Windows
os.startfile(html)
else: # Linux & Variants
subprocess.Popen(('xdg-open', html))
if imgui.begin_child("Lectures", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
lecture_list(state.class_id)
imgui.end_child()
imgui.same_line()
if imgui.begin_child("Students", ImVec2(w1*0.32, h1), imgui.ChildFlags_.borders.value):
student_list(state.class_id)
imgui.end_child()
imgui.end_child()
if imgui.begin_child("Group2", ImVec2(w, h*0.5)):
w1, h1 = imgui.get_content_region_avail()
if imgui.begin_child("Ranking", ImVec2(w1/3, h1), imgui.ChildFlags_.borders.value):
ranking(state.class_id)
imgui.end_child()
imgui.end_child()

View File

@ -0,0 +1,259 @@
from imgui_bundle import (
imgui,
immapp,
imgui_md,
immvision,
im_file_dialog,
ImVec2
)
from pathlib import Path
from itertools import compress
import io
import pickle
from dataclasses import dataclass
from PIL import Image
import numpy as np
import cairosvg
from slugify import slugify
from dbmodel import *
from pdf import *
from .plotter import plot_pdf
from .analyzer_state import AnalyzerState
state = AnalyzerState()
@dataclass
class DocumentProperties:
logo: Path
save_dir: Path
file_name: str
author: str
def svg_to_png(svg: Path) -> Image:
with open(svg) as f:
svg = f.read()
return Image.open(io.BytesIO(cairosvg.svg2png(svg))).convert("RGBA")
def dump_properties(properties: DocumentProperties) -> None:
with open('./pickles/document_properties.pkl', 'wb') as f:
pickle.dump(properties, f)
def load_properties() -> DocumentProperties:
try:
with open('./pickles/document_properties.pkl', 'rb') as f:
properties = pickle.load(f)
return properties
except FileNotFoundError:
return None
@immapp.static(inited=False)
def document_properties(class_id: int) -> None:
# TO DO: Store Properties persistent
statics = document_properties
if not statics.inited:
statics.properties = load_properties()
if not statics.properties:
statics.properties = DocumentProperties(
logo = Path("./assets/learnlytics.svg"),
save_dir = Path.home(),
file_name = "document",
author = "Learnlytics"
)
statics.image = svg_to_png(statics.properties.logo) if statics.properties.logo.suffix == ".svg" else Image.open(statics.properties.logo)
statics.inited = True
imgui_md.render_unindented('### Properties')
imgui.text("")
if imgui.begin_table("Properties", 2, imgui.TableFlags_.sizing_fixed_fit.value):
imgui.table_next_row()
imgui.table_next_column()
imgui.text("Author:")
imgui.table_next_column()
changed, statics.properties.author = imgui.input_text("", statics.properties.author)
if changed:
dump_properties(statics.properties)
imgui.table_next_row()
imgui.table_next_column()
if imgui.button("Add Logo"):
im_file_dialog.FileDialog.instance().open(
"SelectLogo", "Open Logo", "Logo (*.png; *.jpg; *.jpeg; *.svg){.png,.jpg,.jpeg,.svg}", False, str(Path.home())
)
imgui.table_next_column()
imgui.text(str(statics.properties.logo))
imgui.table_next_row()
imgui.table_next_column()
if imgui.button("Save to"):
im_file_dialog.FileDialog.instance().open(
"SelectSaveDir", "Select Save Directory", "", False, str(Path.home())
)
imgui.table_next_column()
imgui.text(str(statics.properties.save_dir))
imgui.table_next_row()
imgui.table_next_column()
if imgui.button("Generate Name"):
date = datetime.now()
statics.properties.file_name = slugify(f'{Class.get_by_id(class_id).name}_{date.strftime("%a_%d.%m.%Y_%H:%M")}')
imgui.table_next_column()
changed, statics.properties.file_name = imgui.input_text(".pdf", statics.properties.file_name)
if changed:
dump_properties(statics.properties)
imgui.end_table()
if statics.properties.logo:
w, h = imgui.get_content_region_avail()
x, y = statics.image.size
size = (
int(x) if x < w*0.8 else int(w*0.8),
int(y) if y < h*0.5 else int(h*0.5)
)
immvision.image_display("Logo", np.array(statics.image), size, True)
if im_file_dialog.FileDialog.instance().is_done("SelectLogo"):
if im_file_dialog.FileDialog.instance().has_result():
statics.properties.logo = im_file_dialog.FileDialog.instance().get_result()
statics.properties.logo = Path(statics.properties.logo.path()).relative_to(Path('.').resolve())
statics.image = svg_to_png(statics.properties.logo) if statics.properties.logo.suffix == ".svg" else Image.open(statics.properties.logo)
dump_properties(statics.properties)
im_file_dialog.FileDialog.instance().close()
if im_file_dialog.FileDialog.instance().is_done("SelectSaveDir"):
if im_file_dialog.FileDialog.instance().has_result():
save_dir = im_file_dialog.FileDialog.instance().get_result()
statics.properties.save_dir = Path(save_dir.path())
dump_properties(statics.properties)
im_file_dialog.FileDialog.instance().close()
return statics.properties
@immapp.static(inited=False)
def filters() -> list[Study]:
statics = filters
if not statics.inited:
statics.studys = list(Study.select())
statics.checked = [True] * len(statics.studys)
statics.inited = True
imgui_md.render_unindented("### Study Filters")
imgui.text("")
if imgui.begin_table("Create", 2, imgui.TableFlags_.sizing_fixed_fit.value):
for n, study in enumerate(statics.studys):
imgui.table_next_row()
imgui.table_next_column()
_, statics.checked[n] = imgui.checkbox(f"##{study.name}", statics.checked[n])
imgui.table_next_column()
imgui.text(study.name)
imgui.end_table()
return list(compress(statics.studys, statics.checked))
@immapp.static(inited=False)
def order_by() -> str:
statics = order_by
if not statics.inited:
statics.order_by = ["Study Program", "Group", "Ranking"]
statics.selected = 1
statics.inited = True
imgui_md.render_unindented("### Order by")
imgui.text("")
if imgui.begin_table("Order", 2, imgui.TableFlags_.sizing_fixed_fit.value):
for n, order in enumerate(statics.order_by):
imgui.table_next_row()
imgui.table_next_column()
if imgui.radio_button(f"## {order}", statics.selected == n):
statics.selected = n
imgui.table_next_column()
imgui.text(order)
imgui.end_table()
return statics.order_by[statics.selected]
@immapp.static(inited=False)
def create_document(class_id: int) -> None:
imgui_md.render_unindented("# Create Documents")
statics = create_document
if not statics.inited:
statics.class_id = class_id
try:
with open("./pickles/last_file.txt") as f:
statics.file = Path(f.read())
except FileNotFoundError:
statics.file = None
statics.order_by = None
statics.inited = True
if statics.class_id != class_id:
statics.inited = False
w, h = imgui.get_content_region_avail()
if imgui.begin_child("PDF", ImVec2(w*0.45, h), imgui.ChildFlags_.borders.value):
imgui_md.render_unindented('### Document')
imgui.text("")
if statics.file:
plot_pdf(statics.file, "Viewer")
else:
imgui.text("No document created yet.")
imgui.end_child()
imgui.same_line()
if imgui.begin_child("Filters", ImVec2(w*0.54, h), imgui.ChildFlags_.borders.value):
w1, h1 = imgui.get_content_region_avail()
if imgui.begin_child("Study Filters", ImVec2(w1, h1*0.2)):
study_filters = filters()
imgui.end_child()
if imgui.begin_child("Order By", ImVec2(w1, h1*0.2)):
statics.order_by = order_by()
imgui.end_child()
if imgui.begin_child("Document Properties", ImVec2(w1, h1*0.58)):
properties = document_properties(statics.class_id)
if imgui.button("Create"):
pdf, css = get_pdf(statics.class_id)
header = get_pdf_header(state.class_id, [study.name for study in study_filters], properties.author, properties.logo)
match statics.order_by:
case "Group":
sections = create_group_pdf(study_filters)
case "Study Program":
sections = create_study_pdf(study_filters)
case 'Ranking':
sections = create_ranking_pdf(study_filters)
pdf.add_section(header, user_css=css)
for section in sections:
pdf.add_section(section, user_css=css)
statics.file = properties.save_dir / (properties.file_name + ".pdf")
pdf.save(statics.file)
with open("./pickles/last_file.txt", "w") as f:
f.write(str(statics.file))
imgui.end_child()
imgui.end_child()
imgui.same_line()
def document_creator():
create_document(state.class_id)

View File

@ -23,6 +23,7 @@ def header(group_id: int) -> None:
statics = header
if group_id < 1:
imgui_md.render_unindented("# Student Ranking")
return
if not statics.inited:
@ -46,6 +47,13 @@ def header(group_id: int) -> None:
if changed:
statics.group.has_passed = not statics.group.has_passed
statics.group.save()
imgui.same_line()
if imgui.button("Edit Group"):
imgui.open_popup("EditGroup")
if edit_group(statics.group_id):
statics.inited = False
if imgui.begin_table("Students", len(statics.data), imgui.TableFlags_.sizing_fixed_fit.value):
for n, d in enumerate(statics.data, start=1):
@ -57,7 +65,56 @@ def header(group_id: int) -> None:
imgui.set_next_item_width(-1)
imgui.text(f"{n}. {s.prename} {s.surname} - {points} Points")
imgui.end_table()
@immapp.static(inited=False)
def edit_group(group_id: int) -> bool:
statics = edit_group
if not statics.inited:
statics.group_id = group_id
statics.group = Group.get_by_id(group_id)
statics.name = statics.group.name
statics.project = statics.group.project
statics.has_passed = statics.group.has_passed
statics.inited = True
if statics.group_id != group_id:
statics.inited = False
center = imgui.get_main_viewport().get_center()
imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
if imgui.begin_popup("EditGroup", imgui.WindowFlags_.always_auto_resize.value):
imgui_md.render_unindented(f"# Edit {statics.group.name}")
_, statics.name = imgui.input_text("Name", statics.name)
_, statics.project = imgui.input_text("Project", statics.project)
changed, _ = imgui.checkbox("Passed?", statics.has_passed)
if changed:
statics.has_passed = not statics.has_passed
if imgui.button("Change"):
statics.group.name = statics.name
statics.group.project = statics.project
statics.group.has_passed = statics.has_passed
statics.group.save()
imgui.close_current_popup()
imgui.end_popup()
statics.inited = False
return True
imgui.same_line()
if imgui.button("Cancel"):
imgui.close_current_popup()
statics.inited = False
imgui.end_popup()
return False
@immapp.static(inited=False)
@ -65,6 +122,7 @@ def plot(group_id: int) -> None:
statics = plot
if group_id < 1:
imgui_md.render_unindented("# Group Performance Graph")
return
if not statics.inited:
@ -94,6 +152,7 @@ def presentation(group_id: int) -> None:
statics = presentation
if group_id < 1:
imgui_md.render_unindented("# Group Presentation")
return
if not statics.inited:
@ -154,6 +213,10 @@ def group_plot(class_id: int) -> None:
if not statics.inited:
statics.class_id = class_id
if Student.select().where(Student.class_id == class_id).count() < 1:
return
max_points = Lecture.select(fn.SUM(Lecture.points)).where(Lecture.class_id == class_id).scalar()
data = {
group.name:

View File

@ -105,7 +105,6 @@ def plot_html(html: Path, label: str = "Presentation") -> None:
statics.quality = 100 if statics.high_quality else 10
statics.reload = True
imgui.same_line()
if imgui.button("Back"):

View File

@ -10,6 +10,7 @@ import numpy as np
from peewee import fn
from dbmodel import *
from grader import get_gradings, get_grader
from .analyzer_state import AnalyzerState
from .plotter import plot_bar_line_percentage
@ -19,27 +20,41 @@ PROGRESS_BAR_COLOR = ImVec4(190, 190, 40, 255)/255
@immapp.static(inited=False)
def header(student_id: int) -> None:
def header(student_id: int, reset: bool) -> None:
statics = header
if student_id < 1:
return
if reset:
statics.inited = False
if not statics.inited:
statics.student = Student.get_by_id(student_id)
statics.study = Study.get_by_id(statics.student.study.id)
points = [sub.points for sub in Submission.select().where(Submission.student_id == student_id)]
max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == statics.student.class_id)]
statics.has_passed = get_grader(statics.student.grader).get_final_grade(points, max_points, False)
statics.inited = True
if statics.student.id != student_id:
statics.inited = False
imgui_md.render(f"# {statics.student.prename} {statics.student.surname}")
imgui_md.render_unindented(f"# {statics.student.prename} {statics.student.surname} - {statics.has_passed}")
imgui_md.render_unindented(f"Degree Program: **{statics.study.name}**")
@immapp.static(inited=False)
def plot(student_id: int) -> None:
def plot(student_id: int, reset: bool) -> None:
statics = plot
if student_id < 1:
imgui_md.render_unindented("# Student Analyzer")
return
if reset:
statics.inited = False
if not statics.inited:
statics.student = Student.get_by_id(student_id)
@ -66,15 +81,144 @@ def plot(student_id: int) -> None:
imgui.pop_style_color()
plot_bar_line_percentage(statics.data, statics.labels, statics.avg*100)
@immapp.static(inited=False)
def dialog(student_id: int) -> None:
imgui_md.render_unindented("# Student Attributes")
if student_id < 1:
return
statics = dialog
if not statics.inited:
statics.student_id = student_id
statics.student = Student.get_by_id(student_id)
statics.prename = statics.student.prename
statics.surname = statics.student.surname
statics.sex = 0 if statics.student.sex == "Male" else 1
statics.studys = list(Study.select())
s = Study.get_by_id(statics.student.study_id)
statics.study = statics.studys.index(s)
statics.groups = list(Group.select().where(Group.class_id == statics.student.class_id))
g = Group.get_by_id(statics.student.group_id)
statics.group = statics.groups.index(g)
statics.graders = [grade.alt_name for grade in get_gradings()]
statics.grader = statics.graders.index(statics.student.grader)
statics.inited = True
if statics.student_id != student_id:
statics.inited = False
if imgui.begin_table("Attributes", 2):#, imgui.TableFlags_.sizing_fixed_fit.value):
imgui.table_next_row()
imgui.table_next_column()
imgui.text("First Name")
imgui.table_next_column()
_, statics.prename = imgui.input_text("##First Name", statics.prename)
imgui.table_next_row()
imgui.table_next_column()
imgui.text("Last Name")
imgui.table_next_column()
_, statics.surname = imgui.input_text("##Last Name", statics.surname)
imgui.table_next_row()
imgui.table_next_column()
imgui.text("Sex")
imgui.table_next_column()
_, statics.sex = imgui.combo("##Sex", statics.sex, ["Male", "Female"])
imgui.table_next_row()
imgui.table_next_column()
imgui.text("Study Program")
imgui.table_next_column()
_, statics.study = imgui.combo("##Study", statics.study, [s.name for s in statics.studys])
imgui.table_next_row()
imgui.table_next_column()
imgui.text("Group")
imgui.table_next_column()
_, statics.group = imgui.combo("##Group", statics.group, [g.name for g in statics.groups])
imgui.table_next_row()
imgui.table_next_column()
imgui.text("Grader")
imgui.table_next_column()
_, statics.grader = imgui.combo("##Grader", statics.grader, statics.graders)
imgui.end_table()
if imgui.button("Save"):
statics.student.prename = statics.prename
statics.student.surname = statics.surname
statics.student.sex = "Male" if statics.sex == 0 else "Female"
statics.student.study_id = statics.studys[statics.study].id
statics.student.group_id = statics.groups[statics.group].id
statics.student.grader = get_grader(statics.graders[statics.grader]).alt_name
statics.student.save()
statics.inited = False
return True
return False
@immapp.static(inited=False)
def lecture_lists(student_id: int) -> None:
imgui_md.render_unindented("# Lectures")
if student_id < 1:
return
statics = lecture_lists
if not statics.inited:
statics.student_id = student_id
statics.student = Student.get_by_id(student_id)
statics.subs = list(Submission.select().where(Submission.student_id == statics.student.id))
statics.inited = True
if statics.student_id != student_id:
statics.inited = False
if imgui.begin_table("Lecture List", 2, imgui.TableFlags_.none.value):
for sub in statics.subs:
imgui.table_next_row()
imgui.table_next_column()
imgui.text(sub.lecture_id.title)
imgui.table_next_column()
imgui.text(f'{int(sub.points) if sub.points.is_integer() else sub.points}/{sub.lecture_id.points} Points')
imgui.end_table()
@immapp.static(inited=False)
def student_graph() -> None:
if db.is_closed():
imgui.text("No DB loaded")
return
statics = student_graph
if not statics.inited:
statics.reset = False
statics.inited = True
w, h = imgui.get_content_region_avail()
if imgui.begin_child("Header", ImVec2(w, h*0.5), imgui.ChildFlags_.borders.value):
header(state.student_id)
plot(state.student_id)
if imgui.begin_child("Header", ImVec2(w, h*0.65), imgui.ChildFlags_.borders.value):
header(state.student_id, statics.reset)
plot(state.student_id, statics.reset)
imgui.end_child()
imgui.separator()
if imgui.begin_child("Dialog", ImVec2(w*0.4, h*0.34), imgui.ChildFlags_.borders.value):
statics.reset = dialog(state.student_id)
imgui.end_child()
imgui.same_line()
if imgui.begin_child("Lectures", ImVec2(w*0.3, h*0.34), imgui.ChildFlags_.borders.value):
lecture_lists(state.student_id)
imgui.end_child()

View File

@ -1,7 +1,8 @@
from imgui_bundle import (
imgui,
immapp,
imgui_md
imgui_md,
ImVec2
)
from dbmodel import *
@ -20,8 +21,119 @@ def class_selector() -> int:
labels = (c.name for c in Class.select())
_, statics.selector = imgui.combo("##Classes", statics.selector, list(labels))
current = Class.select()[statics.selector].id
if imgui.button("New Class"):
imgui.open_popup("NewC")
new_class()
imgui.same_line()
if imgui.button("Delete Class"):
imgui.open_popup("DeleteC")
if delete_class(current):
statics.inited = False
imgui.separator()
return Class.select()[statics.selector].id
return current
@immapp.static(inited=False)
def new_class() -> None:
statics = new_class
if not statics.inited:
statics.name = str()
statics.inited = True
center = imgui.get_main_viewport().get_center()
imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
if imgui.begin_popup("NewC"):
imgui_md.render_unindented("# New Class")
_, statics.name = imgui.input_text("Name", statics.name)
if imgui.button("Add"):
clas = Class.create(name=statics.name)
Group.create(
name="NoGroup",
project="NoProject",
has_passed=False,
class_id = clas.id
)
imgui.close_current_popup()
statics.inited = False
imgui.same_line()
if imgui.button("Cancel"):
imgui.close_current_popup()
statics.inited = False
imgui.end_popup()
@immapp.static(inited=False)
def delete_class(class_id: int) -> bool:
statics = delete_class
if not statics.inited:
statics.class_id = class_id
statics.data = {
"Students": Student.select().where(Student.class_id == class_id).count(),
"Groups": Group.select().where(Group.class_id == class_id).count(),
"Lectures": Lecture.select().where(Lecture.class_id == class_id).count(),
"Submissions": Submission.select().where(Submission.class_id == class_id).count()
}
statics.inited = True
statics.ret = False
if statics.class_id != class_id:
statics.inited = False
center = imgui.get_main_viewport().get_center()
imgui.set_next_window_pos(center, imgui.Cond_.appearing.value, ImVec2(0.5, 0.5))
size = imgui.get_main_viewport().size
imgui.set_next_window_size(ImVec2(size.x * 0.1, size.y * 0.25))
if imgui.begin_popup("DeleteC"):
imgui_md.render_unindented("# Delete Class")
imgui_md.render_unindented("**Are you sure?**")
imgui_md.render_unindented("This Operation deletes:")
if imgui.begin_table("Attributes", len(statics.data)):
for attr, count in statics.data.items():
imgui.table_next_row()
imgui.table_next_column()
imgui.text(attr)
imgui.table_next_column()
imgui.text(str(count))
imgui.end_table()
if imgui.button("Delete"):
Submission.delete().where(Submission.class_id == statics.class_id).execute()
Group.delete().where(Group.class_id == statics.class_id).execute()
Lecture.delete().where(Lecture.class_id == statics.class_id).execute()
Student.delete().where(Student.class_id == statics.class_id).execute()
Class.get_by_id(statics.class_id).delete_instance()
imgui.close_current_popup()
statics.inited = False
statics.ret = True
imgui.same_line()
if imgui.button("Cancel"):
imgui.close_current_popup()
statics.inited = False
imgui.end_popup()
return statics.ret
@immapp.static(inited=False)
def tree(class_id: int) -> None:
@ -36,11 +148,19 @@ def tree(class_id: int) -> None:
statics.selected = -1
statics.ret = (-1, -1)
statics.flags = imgui.TreeNodeFlags_.none.value
statics.counter = 0
statics.inited = True
if statics.class_id != class_id:
statics.inited = False
if statics.counter > 60:
statics.data = {
group: list(Student.select().where(Student.group_id == group.id))
for group in Group.select().where(Group.class_id == statics.class_id)
}
statics.counter = 0
if imgui.button("Expand"):
statics.flags = imgui.TreeNodeFlags_.default_open.value
@ -61,6 +181,7 @@ def tree(class_id: int) -> None:
n += 1
imgui.tree_pop()
statics.counter += 1
return statics.ret
def student_list() -> None:

View File

@ -0,0 +1,94 @@
from imgui_bundle import (
imgui,
immapp,
imgui_md
)
from dbmodel import *
from .analyzer_state import AnalyzerState
state = AnalyzerState()
@immapp.static(inited=False)
def table(class_id: int) -> None:
if class_id < 1:
return
statics = table
if not statics.inited:
statics.class_id = class_id
statics.table_flags = (
imgui.TableFlags_.row_bg.value
| imgui.TableFlags_.borders.value
| imgui.TableFlags_.resizable.value
| imgui.TableFlags_.sizing_stretch_same.value
)
statics.students = Student.select().where(Student.class_id == statics.class_id)
statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
statics.rows = len(statics.students)
statics.cols = len(statics.lectures)
statics.grid = [
list(Submission.select().where(Submission.student_id == student.id))
for student in statics.students
]
statics.table_header = [f"{lecture.title} ({lecture.points})" for lecture in statics.lectures]
statics.inited = True
if statics.class_id != class_id:
statics.inited = False
if statics.cols != Lecture.select().where(Lecture.class_id == statics.class_id).count():
statics.inited = False
if len(statics.students) != Student.select().where(Student.class_id == statics.class_id).count():
statics.inited = False
if imgui.begin_table("Student Grid", statics.cols+1, statics.table_flags):
# Setup Header
imgui.table_setup_column("Students")
for header in statics.table_header:
imgui.table_setup_column(header)
imgui.table_headers_row()
# Fill Student names
for row in range(statics.rows):
imgui.table_next_row()
imgui.table_set_column_index(0)
student = statics.students[row]
imgui.text(f"{student.prename} {student.surname}")
for col in range(statics.cols):
imgui.table_set_column_index(col+1)
changed, value = imgui.input_float(f"##{statics.grid[row][col]}", statics.grid[row][col].points, 0.0, 0.0, "%.1f")
if changed:
# Boundary Check
if value < 0:
value = 0
if value > statics.lectures[col].points:
value = statics.lectures[col].points
old_value = statics.grid[row][col].points
statics.grid[row][col].points = value
statics.grid[row][col].save()
student = statics.students[row]
lecture = statics.lectures[col]
sub = statics.grid[row][col]
imgui.end_table()
def submission_table() -> None:
imgui_md.render_unindented("# Submissions")
if db.is_closed():
imgui.text("No DB loaded")
return
table(state.class_id)

View File

@ -142,75 +142,6 @@ def select_file() -> None:
# Update application state and reset selection result
statics.res = None
@immapp.static(inited=False)
def table() -> None:
statics = table
if db.is_closed():
imgui.text("DB")
return
if not statics.inited:
statics.table_flags = (
imgui.TableFlags_.row_bg.value
| imgui.TableFlags_.borders.value
| imgui.TableFlags_.resizable.value
| imgui.TableFlags_.sizing_stretch_same.value
)
statics.class_id = None
statics.inited = True
statics.students = Student.select().where(Student.class_id == statics.class_id)
statics.lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
statics.rows = len(statics.students)
statics.cols = len(statics.lectures)
statics.grid = list()
for student in statics.students:
t_list = list()
sub = Submission.select().where(Submission.student_id == student.id)
for s in sub:
t_list.append(s)
statics.grid.append(t_list)
statics.table_header = [f"{lecture.title} ({lecture.points})" for lecture in statics.lectures]
if imgui.begin_table("Student Grid", statics.cols+1, statics.table_flags):
# Setup Header
imgui.table_setup_column("Students")
for header in statics.table_header:
imgui.table_setup_column(header)
imgui.table_headers_row()
# Fill Student names
for row in range(statics.rows):
imgui.table_next_row()
imgui.table_set_column_index(0)
student = statics.students[row]
imgui.text(f"{student.prename} {student.surname}")
for col in range(statics.cols):
imgui.table_set_column_index(col+1)
changed, value = imgui.input_float(f"##{statics.grid[row][col]}", statics.grid[row][col].points, 0.0, 0.0, "%.1f")
if changed:
# Boundary Check
if value < 0:
value = 0
if value > statics.lectures[col].points:
value = statics.lectures[col].points
old_value = statics.grid[row][col].points
statics.grid[row][col].points = value
statics.grid[row][col].save()
student = statics.students[row]
lecture = statics.lectures[col]
sub = statics.grid[row][col]
imgui.end_table()
@immapp.static(inited=False)
def editor() -> None:
"""

View File

@ -0,0 +1,4 @@
from .utils import get_pdf, get_pdf_header
from .ranking_pdf import create_ranking_pdf
from .study_pdf import create_study_pdf
from .group_pdf import create_group_pdf

View File

@ -0,0 +1,98 @@
/* All */
@font-face {
font-family: 'NexusSansPro-Regular';
src: url('./assets/Nexus/NexusSansPro-Regular.otf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'NexusSansPro-Bold';
src: url('./assets/Nexus/NexusSansPro-Bold.otf');
font-weight: normal;
font-style: normal;
}
* {
font-size: 120%;
font-family: 'NexusSansPro-Regular';
color: #000;
}
/* Headers */
h1 {
text-align: center;
font-size: 28px;
font-family: 'NexusSansPro-Bold';
}
h2 {
font-size: 22px;
padding-bottom: -15px;
font-family: 'NexusSansPro-Bold';
}
h4 {
font-size: 16px;
padding-bottom: -10px;
font-family: 'NexusSansPro-Bold';
}
h5 {
font-size: 14px;
padding-bottom: -15px;
}
/* Image */
img {
padding: inherit;
margin: 0 auto;
text-align: center;
width: 50%;
}
/* List */
ol {
list-style-type: upper-roman;
}
li {
font-size: 14px;
}
/* Miscelaneous */
p {
font-size: 12px;
}
hr {
border: 3px solid #be1e3c;
}
/* Table */
table {
margin: 0 auto;
width: 100%;
}
th, td {
padding-right: 40px;
}
/* Table Header */
th {
text-align: left;
font-size: 16px;
font-family: 'NexusSansPro-Bold';
}
/* Table Content */
td {
text-align: left;
font-size: 13px;
padding-left: 10px;
padding-top: -1px;
padding-bottom: 5px;
}

View File

@ -0,0 +1,52 @@
from dbmodel import *
from grader import get_grader
from markdown_pdf import Section
from functools import reduce
import operator
def create_group_pdf(filter: list[Study]) -> list[Section]:
sections = list()
text = ''
filter = [(Student.study_id == study.id) for study in filter]
expr = reduce(operator.or_, filter)
students = list(Student.select().where(expr))
group_ids = set([student.group_id for student in students])
group_filter = [(Group.id == id) for id in group_ids]
group_expr = reduce(operator.or_, group_filter)
data = {
group: [student for student in Student.select().where(Student.group_id == group.id) if student.study in filter]
for group in Group.select().where(group_expr)
}
max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == next(iter(data.keys())).class_id)]
n = 0
for group, students in data.items():
text += f'## {group.name}\n'
text += f'#### Project: {group.project}\n'
passed = '&check;' if group.has_passed else '&cross;'
text += f'Passed Exam {passed}\n'
text += '|Student|Study Program|Passed?|\n'
text += '|-|-|-|\n'
for student in students:
grader = get_grader(student.grader)
overall_points = [sub.points for sub in Submission.select().where(Submission.student_id == student.id)]
passed = grader.get_final_grade(overall_points, max_points)
text += f'|{student.prename} {student.surname}|{student.study.name}|{passed}|\n'
text += '\n---\n\n'
n += 1
if n == 2:
sections.append(Section(text))
n = 0
text = ''
if text:
sections.append(Section(text))
return sections

View File

@ -0,0 +1,58 @@
from dbmodel import *
from peewee import fn
from markdown_pdf import Section
from functools import reduce
from itertools import compress
import operator
from grader import get_grader
from collections import Counter
def create_header(number_of_students: int) -> str:
text = '## Ranking\n'
text += f'#### Number of Students: {number_of_students}\n'
text += '|Student|Points|Percentage|Passed|\n'
text += '|-|-|-|-|\n'
return text
def create_ranking_pdf(filter: list[int]) -> list[Section]:
filter = [(Student.study_id == id) for id in filter]
expr = reduce(operator.or_, filter)
students = [
(student, Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == student.id).scalar())
for student in Student.select().where(expr)
]
students.sort(key = lambda tup: tup[1], reverse=True)
sections = list()
max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == students[0][0].class_id)]
max = sum(max_points)
# Table Header
text = create_header(len(students))
limit = 0
for n, tup in enumerate(students, start=1):
student, points = tup
student_data = f'{n}. {student.prename} {student.surname}'
points = int(points) if points.is_integer() else points
grader = get_grader(student.grader)
p = [sub.points for sub in Submission.select().where(Submission.student_id == student.id)]
passed = grader.get_final_grade(p, max_points)
text += f'|{student_data}|{points}/{max}|{points/max:.1%}|{passed}|\n'
limit += 1
if limit > 29:
sections.append(Section(text))
limit = 0
text = create_header(len(students))
if text:
sections.append(Section(text))
return sections

View File

@ -0,0 +1,37 @@
from dbmodel import *
from grader import get_grader
from markdown_pdf import Section
def create_study_pdf(filter: list[int]) -> list[Section]:
sections = list()
text = ''
data = {
study: list(Student.select().where(Student.study_id == study.id))
for study in filter
}
max_points = [lecture.points for lecture in Lecture.select().where(Lecture.class_id == next(iter(data.values()))[0].class_id)]
max = sum(max_points)
for study, students in data.items():
text += f'## {study.name}\n'
text += f'#### Number of Students: {Student.select().where(Student.study_id == study.id).count()}\n'
text += '|Student|Points|Percentage|Passed?|\n'
text += '|-|-|-|-|\n'
for student in students:
grader = get_grader(student.grader)
overall_points = [sub.points for sub in Submission.select().where(Submission.student_id == student.id)]
passed = grader.get_final_grade(overall_points, max_points)
points = sum(overall_points)
points = int(points) if points.is_integer() else points
text += f'|{student.prename} {student.surname}|{points}/{max}|{points/max:.1%}|{passed}|\n'
sections.append(Section(text))
text = ''
return sections

47
learnlytics/pdf/utils.py Normal file
View File

@ -0,0 +1,47 @@
from dbmodel import Class
from markdown_pdf import MarkdownPdf, Section
from datetime import datetime
from collections.abc import Sequence
from pathlib import Path
def get_pdf(class_id: int) -> tuple[MarkdownPdf, str]:
clas = Class.get_by_id(class_id)
# Create PDF
pdf = MarkdownPdf(toc_level=2, mode='gfm-like')
pdf.meta['title'] = clas.name
pdf.meta['author'] = 'Learnlytics by @DerGrumpf'
pdf.meta['producer'] = 'Learnlytics'
pdf.meta['subject'] = f'Passed List - {clas.name}'
pdf.meta['keywords'] = f'Passed List,{clas.name}'
css = None
with open('./learnlytics/pdf/document_style.css') as f:
css = f.read()
return (pdf, css)
def get_pdf_header(class_id: int, filter: Sequence[str], author: str, logo_file: Path = None) -> Section:
text = ''
# Append Logo
if logo_file:
text += f'![logo]({logo_file})\n'
# Title
text += f'# Passed List - {Class.get_by_id(class_id).name}\n'
# Filters applied
text += '#### Study Programms:\n'
for n, filter_el in enumerate(filter, start=1):
text += f'{n}. {filter_el}\n'
text += '---\n\n'
# Metadata
text += f'Author: {author}\n\n'
date = datetime.now()
text += f'Created: {date.strftime("%A %d.%m.%Y %H:%M")}\n\n'
text += 'This document is system-generated and may not require manual signatures.'
return Section(text)

Binary file not shown.

1
pickles/last_file.txt Normal file
View File

@ -0,0 +1 @@
/storage/programming/Learnlytics/assets/documents/wise-23-24-fri-07-03-2025-01-15a.pdf

364
poetry.lock generated
View File

@ -11,6 +11,126 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "cairocffi"
version = "1.7.1"
description = "cffi-based cairo bindings for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f"},
{file = "cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b"},
]
[package.dependencies]
cffi = ">=1.1.0"
[package.extras]
doc = ["sphinx", "sphinx_rtd_theme"]
test = ["numpy", "pikepdf", "pytest", "ruff"]
xcb = ["xcffib (>=1.4.0)"]
[[package]]
name = "cairosvg"
version = "2.7.1"
description = "A Simple SVG Converter based on Cairo"
optional = false
python-versions = ">=3.5"
files = [
{file = "CairoSVG-2.7.1-py3-none-any.whl", hash = "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b"},
{file = "CairoSVG-2.7.1.tar.gz", hash = "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0"},
]
[package.dependencies]
cairocffi = "*"
cssselect2 = "*"
defusedxml = "*"
pillow = "*"
tinycss2 = "*"
[package.extras]
doc = ["sphinx", "sphinx-rtd-theme"]
test = ["flake8", "isort", "pytest"]
[[package]]
name = "cffi"
version = "1.17.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
{file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
{file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
{file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
{file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
{file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
{file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
{file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
{file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
{file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
{file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
{file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
{file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
[package.dependencies]
pycparser = "*"
[[package]]
name = "colour"
version = "0.1.5"
@ -25,6 +145,36 @@ files = [
[package.extras]
test = ["nose"]
[[package]]
name = "cssselect2"
version = "0.8.0"
description = "CSS selectors for Python ElementTree"
optional = false
python-versions = ">=3.9"
files = [
{file = "cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e"},
{file = "cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a"},
]
[package.dependencies]
tinycss2 = "*"
webencodings = "*"
[package.extras]
doc = ["furo", "sphinx"]
test = ["pytest", "ruff"]
[[package]]
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "flatpak-pip-generator"
version = "24.0.0"
@ -186,6 +336,76 @@ PyOpenGL = "*"
[package.extras]
test = ["pytest"]
[[package]]
name = "linkify-it-py"
version = "2.0.3"
description = "Links recognition library with FULL unicode support."
optional = false
python-versions = ">=3.7"
files = [
{file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"},
{file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"},
]
[package.dependencies]
uc-micro-py = "*"
[package.extras]
benchmark = ["pytest", "pytest-benchmark"]
dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"]
doc = ["myst-parser", "sphinx", "sphinx-book-theme"]
test = ["coverage", "pytest", "pytest-cov"]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "markdown-pdf"
version = "1.3.3"
description = "Markdown to pdf renderer"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown_pdf-1.3.3-py3-none-any.whl", hash = "sha256:0b9b425030fbe4871cbb5842a30ba8d23c6ca1f5ba40a2bb3bd4f286e17d28fc"},
{file = "markdown_pdf-1.3.3.tar.gz", hash = "sha256:5cdb054afa20de0b590a5cbffc0569545bf3681f70263d6ffdc15adf99b515aa"},
]
[package.dependencies]
markdown-it-py = "3.0.0"
PyMuPDF = "1.24.6"
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "munch"
version = "4.0.0"
@ -490,6 +710,17 @@ files = [
greenlet = ">=3.1.1,<4.0.0"
pyee = ">=12,<13"
[[package]]
name = "pycparser"
version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]]
name = "pydantic"
version = "2.10.6"
@ -639,6 +870,66 @@ typing-extensions = "*"
[package.extras]
dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
[[package]]
name = "pymupdf"
version = "1.24.6"
description = "A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents."
optional = false
python-versions = ">=3.8"
files = [
{file = "PyMuPDF-1.24.6-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:3a3689394c7ab2851be73f3c82300747e2a089dc37d563be8bda3f71c603c5c4"},
{file = "PyMuPDF-1.24.6-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:64fb6b2c00b3d3fa31c36d3735cb7694e5f459635883e02b086fdc44fb9398ee"},
{file = "PyMuPDF-1.24.6-cp310-none-manylinux2014_x86_64.whl", hash = "sha256:83c2b34c7d6ee261e5b6db643947ec22e1aa85d3fa2ab826af50e9909c2b739f"},
{file = "PyMuPDF-1.24.6-cp310-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e0ca0051f55063e366c26bf8619784c36f796114840b0506958297d58dcfda1"},
{file = "PyMuPDF-1.24.6-cp310-none-win32.whl", hash = "sha256:d6caebcaffba5179d3ac62df29858b88dba026ea15e987b4a619ec7e72114b7c"},
{file = "PyMuPDF-1.24.6-cp310-none-win_amd64.whl", hash = "sha256:57f49d90c546bca7ec46c89d1d6e4c3a1c861a6f04a5e038e12cf3419532fb4d"},
{file = "PyMuPDF-1.24.6-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:9bd685fadc2e7e94af8cd0a92adf9c84dbcf246f4471d180186087d908e0f0c4"},
{file = "PyMuPDF-1.24.6-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:cb095bae3bf9be8543128e8834d5cd9101b4ab43e3d84e58a73b8a72c93ca9a7"},
{file = "PyMuPDF-1.24.6-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:6ce5703d30ebe8d710b58d4cfd1a8c6d0627c2f9a34bf9a9a1aef2e496a5df7b"},
{file = "PyMuPDF-1.24.6-cp311-none-musllinux_1_2_x86_64.whl", hash = "sha256:5fdccd3fdbe61f1cc93d38d9b234880b7f9d7fc703d0221198be788a74de0a77"},
{file = "PyMuPDF-1.24.6-cp311-none-win32.whl", hash = "sha256:90856c84c8babb5692f5d64504f371eb5f147c33cd0a51724a4e1e530b7a1e4b"},
{file = "PyMuPDF-1.24.6-cp311-none-win_amd64.whl", hash = "sha256:b0ce01fe4a3153604ded32a78e48c647f30bad80ce0a051563f11db85980da5a"},
{file = "PyMuPDF-1.24.6-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:aee2999e8cd042ded9ff5ba4997dfbcbbb4e4f8a09c5e95c9a3c293a651a919d"},
{file = "PyMuPDF-1.24.6-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:c85b8c4e389a71d57aba4c8e6115d7f20ec3b5025018023f3360cf176bbd294a"},
{file = "PyMuPDF-1.24.6-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:64cb67a3938c32614c2ba41cbd37168223aa9983bc882dc9a6c8c6bb207ade60"},
{file = "PyMuPDF-1.24.6-cp312-none-musllinux_1_2_x86_64.whl", hash = "sha256:7e099cc4a0deca70173692fe10b08eb486ca86222377d34bfcadb3bcb2da3ff8"},
{file = "PyMuPDF-1.24.6-cp312-none-win32.whl", hash = "sha256:24f11aa94e606f466e11163bc4fb5ab3328236549c75c26991c6269342d8dcba"},
{file = "PyMuPDF-1.24.6-cp312-none-win_amd64.whl", hash = "sha256:10c373c9dce565779eced2a88730229e12c57d9c388cc1e184b1565e641979f7"},
{file = "PyMuPDF-1.24.6-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:407ae5cfc32cae18fd50c47e4a40b5cee77ffc27b285f9fa28c50d088a5d9624"},
{file = "PyMuPDF-1.24.6-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:4b62e9c088de896e962864d948da8b263f12572d89e5e65a8929bd51280a12ca"},
{file = "PyMuPDF-1.24.6-cp38-none-manylinux2014_x86_64.whl", hash = "sha256:5073416516e97bb1323076ae295224684fb11d3ddf7d39d9c142eed824648067"},
{file = "PyMuPDF-1.24.6-cp38-none-musllinux_1_2_x86_64.whl", hash = "sha256:71ed2e7b23d1cb50e577258e8b1dee986d8c06fca2261239c12872b6c43c40df"},
{file = "PyMuPDF-1.24.6-cp38-none-win32.whl", hash = "sha256:c915f5b019c4fd4afa47d39ee5871440200328d11c2377a43a364f5cb70d3c0d"},
{file = "PyMuPDF-1.24.6-cp38-none-win_amd64.whl", hash = "sha256:b9742faefcddda1ee793ef26a686370f468128ade356cd90d2ea2e01e98b07a1"},
{file = "PyMuPDF-1.24.6-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:bb55d6ce5165c7a8eac2fdce3ba83c71a227816bbbfd3da3aa48af38ad91cced"},
{file = "PyMuPDF-1.24.6-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:fdc464085549998a88b53d49aef44312e77e1c2a5ef2d2b7b03a623b9fff982f"},
{file = "PyMuPDF-1.24.6-cp39-none-manylinux2014_x86_64.whl", hash = "sha256:7b74e0ae95b8449069966cbeb6be8e1536f61f388ceb74ff3924f91cd788462a"},
{file = "PyMuPDF-1.24.6-cp39-none-musllinux_1_2_x86_64.whl", hash = "sha256:e059984723e3c64c5c49b9edf481b42c50d66b5d59f50ab86d56101d8e378a02"},
{file = "PyMuPDF-1.24.6-cp39-none-win32.whl", hash = "sha256:9b7ce753a3e6c2625963815df171f268ad4002243d1d950b246c0181183bd919"},
{file = "PyMuPDF-1.24.6-cp39-none-win_amd64.whl", hash = "sha256:b1879e5c175cacee0cc140a7ee19dae901310b95b066a02d6a3829ccbfc4a5fa"},
{file = "PyMuPDF-1.24.6.tar.gz", hash = "sha256:029dd99df1cebcbcd4240940809c5e373353d12e6c8483934d42f59ceacfb037"},
]
[package.dependencies]
PyMuPDFb = "1.24.6"
[[package]]
name = "pymupdfb"
version = "1.24.6"
description = "MuPDF shared libraries for PyMuPDF."
optional = false
python-versions = ">=3.8"
files = [
{file = "PyMuPDFb-1.24.6-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:21e3ed890f736def68b9a031122ae1fb854d5cb9a53aa144b6e2ca3092416a6b"},
{file = "PyMuPDFb-1.24.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8704d2dfadc9448ce184597d8b0f9c30143e379ac948a517f9c4db7c0c71ed51"},
{file = "PyMuPDFb-1.24.6-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01662584d5cfa7a91f77585f13fc23a12291cfd76a57e0a28dd5a56bf521cb2c"},
{file = "PyMuPDFb-1.24.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1f7657353529ae3f88575c83ee49eac9adea311a034b9c97248a65cee7df0e5"},
{file = "PyMuPDFb-1.24.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cebc2cedb870d1e1168e2f502eb06f05938f6df69103b0853a2b329611ec19a7"},
{file = "PyMuPDFb-1.24.6-py3-none-win32.whl", hash = "sha256:ac4b865cd1e239db04674f85e02844a0e405f8255ee7a74dfee0d86aad0d3576"},
{file = "PyMuPDFb-1.24.6-py3-none-win_amd64.whl", hash = "sha256:9224e088a0d3c188dea03831807789e245b812fbd071c8d498da8f7cc33142b2"},
{file = "PyMuPDFb-1.24.6.tar.gz", hash = "sha256:f5a40b1732d65a1e519916d698858b9ce7473e23edf9001ddd085c5293d59d30"},
]
[[package]]
name = "pyopengl"
version = "3.1.9"
@ -664,6 +955,23 @@ files = [
[package.dependencies]
six = ">=1.5"
[[package]]
name = "python-slugify"
version = "8.0.4"
description = "A Python slugify application that also handles Unicode"
optional = false
python-versions = ">=3.7"
files = [
{file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"},
{file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"},
]
[package.dependencies]
text-unidecode = ">=1.3"
[package.extras]
unidecode = ["Unidecode (>=1.1.1)"]
[[package]]
name = "pytz"
version = "2025.1"
@ -763,6 +1071,35 @@ files = [
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]]
name = "text-unidecode"
version = "1.3"
description = "The most basic Text::Unidecode port"
optional = false
python-versions = "*"
files = [
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
]
[[package]]
name = "tinycss2"
version = "1.4.0"
description = "A tiny CSS parser"
optional = false
python-versions = ">=3.8"
files = [
{file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"},
{file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"},
]
[package.dependencies]
webencodings = ">=0.4"
[package.extras]
doc = ["sphinx", "sphinx_rtd_theme"]
test = ["pytest", "ruff"]
[[package]]
name = "types-setuptools"
version = "75.8.0.20250225"
@ -813,7 +1150,32 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
[[package]]
name = "uc-micro-py"
version = "1.0.3"
description = "Micro subset of unicode data files for linkify-it-py projects."
optional = false
python-versions = ">=3.7"
files = [
{file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"},
{file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"},
]
[package.extras]
test = ["coverage", "pytest", "pytest-cov"]
[[package]]
name = "webencodings"
version = "0.5.1"
description = "Character encoding aliases for legacy web content"
optional = false
python-versions = "*"
files = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "bb8a55ca695106ae590ae540a5261bed6905ea6686e6d38a3e0487e5ebb16d1d"
content-hash = "0f368a19ff46c53164f16b88dee00580f42b9559212a4fa7fa71b9aad0721824"

View File

@ -25,6 +25,10 @@ pytz = "^2025.1"
tzlocal = "^5.3"
pdf2image = "^1.17.0"
playwright = "^1.50.0"
markdown-pdf = "^1.3.3"
linkify-it-py = "^2.0.3"
python-slugify = "^8.0.4"
cairosvg = "^2.7.1"
[build-system]