Initial
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								assets/Presentations/DiKum.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Presentations/DiKum.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_1.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_1.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_2.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_2.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_3.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_3.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_4.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_4.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_5.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_5.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_6.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/Presentations/MeWi_6.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										36
									
								
								assets/Student_list.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								assets/Student_list.csv
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
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
 | 
			
		||||
		
		
			
  | 
							
								
								
									
										
											BIN
										
									
								
								assets/WiSe_24_25.db
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/WiSe_24_25.db
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										67
									
								
								assets/convert.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								assets/convert.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
import pandas as pd
 | 
			
		||||
import pprint
 | 
			
		||||
import sys 
 | 
			
		||||
sys.path.append('../learnlytics/')
 | 
			
		||||
from dbmodel import *
 | 
			
		||||
 | 
			
		||||
df = pd.read_csv("Student_list.csv")
 | 
			
		||||
df = df.dropna()
 | 
			
		||||
courses = {
 | 
			
		||||
        'Tutorial 1': 31,
 | 
			
		||||
        'Tutorial 2': 15,
 | 
			
		||||
        'Extended Applications': 18,
 | 
			
		||||
        'Numpy & MatPlotLib': 35,
 | 
			
		||||
        'SciPy': 20,
 | 
			
		||||
        'Monte Carlo': 20,
 | 
			
		||||
        'Pandas & Seaborn': 22,
 | 
			
		||||
        'Folium': 27,
 | 
			
		||||
        'Statistical Test Methods': 24,
 | 
			
		||||
        'Data Analysis': 30
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
groups = {
 | 
			
		||||
    "NoGroup": "No Project",
 | 
			
		||||
    "MeWi1": "Covid-19",
 | 
			
		||||
    "MeWi2": "Covid-19",
 | 
			
		||||
    "MeWi3": "Discovery of Handwashing",
 | 
			
		||||
    "MeWi4": "Uber Trips",
 | 
			
		||||
    "MeWi5": "Extramarital Affairs",
 | 
			
		||||
    "MeWi6": "Hochschulstatistik",
 | 
			
		||||
    "DiKum": "Facebook Data"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print(df)
 | 
			
		||||
init_db('WiSe_24_25.db')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Create Class
 | 
			
		||||
clas = Class.create(name='WiSe 24/25')
 | 
			
		||||
#print(clas.id)
 | 
			
		||||
 | 
			
		||||
# Create Courses
 | 
			
		||||
for k, v in courses.items():
 | 
			
		||||
    Lecture.create(title=k, points=v, class_id=clas.id)
 | 
			
		||||
    #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)
 | 
			
		||||
 | 
			
		||||
for index, row in df.iterrows():
 | 
			
		||||
    s = Student.create(
 | 
			
		||||
        prename=row["First Name"],
 | 
			
		||||
        surname=row["Last Name"],
 | 
			
		||||
        sex=row["Sex"],
 | 
			
		||||
        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:]:
 | 
			
		||||
        Submission.create(
 | 
			
		||||
            student_id=s.id,
 | 
			
		||||
            lecture_id=Lecture.select().where(Lecture.title == title),
 | 
			
		||||
            class_id=clas.id,
 | 
			
		||||
            points=points
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										235
									
								
								assets/covid_faelle_MeWi_2.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								assets/covid_faelle_MeWi_2.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										10
									
								
								learnlytics/dbmodel/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								learnlytics/dbmodel/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
from .utils import (
 | 
			
		||||
    tables,
 | 
			
		||||
    table_labels,
 | 
			
		||||
    init_db,
 | 
			
		||||
    save_as_json,
 | 
			
		||||
    create_from_json
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .model import * 
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								learnlytics/dbmodel/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/dbmodel/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/dbmodel/__pycache__/model.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/dbmodel/__pycache__/model.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/dbmodel/__pycache__/utils.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/dbmodel/__pycache__/utils.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										47
									
								
								learnlytics/dbmodel/model.dbml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								learnlytics/dbmodel/model.dbml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
// Use DBML to define your database structure
 | 
			
		||||
// Docs: https://dbml.dbdiagram.io/docs
 | 
			
		||||
 | 
			
		||||
Table Class {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  name varchar
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table Student {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  name varchar
 | 
			
		||||
  sex varchar
 | 
			
		||||
  class_id integer [ref: < Class.id]
 | 
			
		||||
  group_id integer [ref: < Group.id]
 | 
			
		||||
  grader varchar
 | 
			
		||||
  created_at timestamp 
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table Lecture {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  title varchar
 | 
			
		||||
  points integer
 | 
			
		||||
  class_id integer [ref: < Class.id]
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table Submission {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  student_id integer [ref: < Student.id]
 | 
			
		||||
  lecture_id integer [ref: < Lecture.id]
 | 
			
		||||
  class_id integer [ref: < Class.id]
 | 
			
		||||
  points float
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table Group {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  name varchar
 | 
			
		||||
  project varchar 
 | 
			
		||||
  has_passed boolean
 | 
			
		||||
  presentation varchar
 | 
			
		||||
  class_id integer [ref: < Class.id]
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										88
									
								
								learnlytics/dbmodel/model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								learnlytics/dbmodel/model.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
'''
 | 
			
		||||
peewee ORM Database Model definition 
 | 
			
		||||
Documentation: https://docs.peewee-orm.com
 | 
			
		||||
 | 
			
		||||
please look up model.dbml for Documentation
 | 
			
		||||
Online Viewer: https://dbdiagram.io 
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from peewee import *
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
db = DatabaseProxy()
 | 
			
		||||
 | 
			
		||||
# WIP: Add Switch Function
 | 
			
		||||
if True:
 | 
			
		||||
    database = SqliteDatabase(None, autoconnect=False)
 | 
			
		||||
else:
 | 
			
		||||
    database = PostgresqlDatabase(None, autoconnect=False)
 | 
			
		||||
 | 
			
		||||
db.initialize(database)
 | 
			
		||||
 | 
			
		||||
class BaseModel(Model):
 | 
			
		||||
    '''
 | 
			
		||||
    Base Model (needed for peewee) defines the Class Meta 
 | 
			
		||||
    and bounds Global db obj to every Table
 | 
			
		||||
    '''
 | 
			
		||||
    class Meta:
 | 
			
		||||
        database = db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Class(BaseModel):
 | 
			
		||||
    '''
 | 
			
		||||
    Baseline Order Base 
 | 
			
		||||
    
 | 
			
		||||
    Table for Storing a Class 
 | 
			
		||||
    '''
 | 
			
		||||
    name = CharField()
 | 
			
		||||
    created_at = DateTimeField(default=datetime.now)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Group(BaseModel):
 | 
			
		||||
    '''
 | 
			
		||||
    Table for Storing a project Group 
 | 
			
		||||
    '''
 | 
			
		||||
    name = CharField()
 | 
			
		||||
    project = CharField()
 | 
			
		||||
    has_passed = BooleanField()
 | 
			
		||||
    presentation = CharField(null = True)
 | 
			
		||||
    class_id = ForeignKeyField(Class, backref='class')
 | 
			
		||||
    created_at = DateTimeField(default=datetime.now)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Student(BaseModel):
 | 
			
		||||
    '''
 | 
			
		||||
    Table for Storing a Student and linking him to appropiate Tables
 | 
			
		||||
    '''
 | 
			
		||||
    prename = CharField()
 | 
			
		||||
    surname = CharField()
 | 
			
		||||
    sex = CharField()
 | 
			
		||||
    class_id = ForeignKeyField(Class, backref='class')
 | 
			
		||||
    group_id = ForeignKeyField(Group, backref='group')
 | 
			
		||||
    grader = CharField()
 | 
			
		||||
    created_at = DateTimeField(default=datetime.now)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Lecture(BaseModel):
 | 
			
		||||
    '''
 | 
			
		||||
    Table for defining a Lecture
 | 
			
		||||
    '''
 | 
			
		||||
    title = CharField()
 | 
			
		||||
    points = IntegerField()
 | 
			
		||||
    class_id = ForeignKeyField(Class, backref='class')
 | 
			
		||||
    created_at = DateTimeField(default=datetime.now)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Submission(BaseModel):
 | 
			
		||||
    '''
 | 
			
		||||
    Table for defining a Submission from a Student for Lecture
 | 
			
		||||
    '''
 | 
			
		||||
    student_id = ForeignKeyField(Student, backref='student')
 | 
			
		||||
    lecture_id = ForeignKeyField(Lecture, backref='lecture')
 | 
			
		||||
    class_id = ForeignKeyField(Class, backref='class')
 | 
			
		||||
    points = FloatField()
 | 
			
		||||
    created_at = DateTimeField(default=datetime.now)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										173
									
								
								learnlytics/dbmodel/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								learnlytics/dbmodel/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
'''
 | 
			
		||||
Module provids Utilities to Interface with Database Model
 | 
			
		||||
 | 
			
		||||
Includes:
 | 
			
		||||
    - DateTime De-/Encoder for JSON loads/dumps
 | 
			
		||||
    - Database Initialize Helper
 | 
			
		||||
    - Module Class Summarizer
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import sys, inspect, json 
 | 
			
		||||
from datetime import datetime, date
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from playhouse.shortcuts import model_to_dict, dict_to_model
 | 
			
		||||
from .model import *
 | 
			
		||||
 | 
			
		||||
class DateTimeEncoder(json.JSONEncoder):
 | 
			
		||||
    '''
 | 
			
		||||
    Helper Class converting datetime.datetime -> isoformated String
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def default(self, obj):
 | 
			
		||||
        if isinstance(obj, (datetime, date)):
 | 
			
		||||
            return obj.isoformat()
 | 
			
		||||
 | 
			
		||||
# Predefined used timestamp keys
 | 
			
		||||
KEYNAMES = [
 | 
			
		||||
    "date", "timestamp", "last_updatet", "last_created", "last_editet", "created_at"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
def DateTimeDecoder(obj: dict) -> dict:
 | 
			
		||||
    '''
 | 
			
		||||
    Helper Function converting isoformated String -> datetime.datetime
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    for key, value in obj.items():
 | 
			
		||||
        if key not in KEYNAMES:
 | 
			
		||||
            continue
 | 
			
		||||
        try:
 | 
			
		||||
            obj[key] = datetime.fromisoformat(value)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
    return obj
 | 
			
		||||
   
 | 
			
		||||
 | 
			
		||||
def get_module_cls(module: str) -> tuple[tuple[str, object]]:
 | 
			
		||||
    '''
 | 
			
		||||
    Given a module name function returns a list of Classes defined only in that Module
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    assert type(module) is str, "Provided Module isn't a String"
 | 
			
		||||
    members = inspect.getmembers(sys.modules[module], inspect.isclass)
 | 
			
		||||
    return tuple(member for member in members if member[1].__module__ == module)
 | 
			
		||||
 | 
			
		||||
# precalculated from model.py 
 | 
			
		||||
tables = tuple(table[1] for table in get_module_cls('dbmodel.model') if not table[1] is BaseModel)
 | 
			
		||||
table_labels = tuple(table[0] for table in get_module_cls('dbmodel.model') if not table[1] is BaseModel)
 | 
			
		||||
 | 
			
		||||
def init_db(name: Path | str) -> None:
 | 
			
		||||
    '''
 | 
			
		||||
    (Creates,) Connects and Initializes the db descriptor from a given *.db file 
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    assert isinstance(name, (Path, str)), "Provided Name isn't of type Path | str"
 | 
			
		||||
    
 | 
			
		||||
    # convert to String
 | 
			
		||||
    if type(name) is Path:
 | 
			
		||||
        name = str(name)
 | 
			
		||||
    
 | 
			
		||||
    # Switch Database; Initialize if needed
 | 
			
		||||
    if not db.is_closed():
 | 
			
		||||
        db.close()
 | 
			
		||||
    db.init(name)
 | 
			
		||||
    db.connect()
 | 
			
		||||
    db.create_tables(tables)  # Ensure tables exist
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_as_json(filename: str, path: Path | str = Path('.')) -> Path:
 | 
			
		||||
    '''
 | 
			
		||||
    Saves the current loaded Database as <filename>.json to given <path>
 | 
			
		||||
 | 
			
		||||
    JSON Format:
 | 
			
		||||
        {
 | 
			
		||||
            <Tablename>: [
 | 
			
		||||
                {<Tableparam>: <Value>, ...}
 | 
			
		||||
            ],
 | 
			
		||||
            ...
 | 
			
		||||
        }
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    assert type(filename) is str, "Provided Filename isn't a String"
 | 
			
		||||
    assert isinstance(path, Path | str), "Provided Path isn't of type Path | str"
 | 
			
		||||
 | 
			
		||||
    # Convert given path to Path
 | 
			
		||||
    if type(path) is str:
 | 
			
		||||
        path = Path(path)
 | 
			
		||||
    
 | 
			
		||||
    filename = Path(filename)
 | 
			
		||||
 | 
			
		||||
    # Set Correct Suffix
 | 
			
		||||
    if filename.suffix != '.json':
 | 
			
		||||
        filename = filename.with_suffix('.json')
 | 
			
		||||
 | 
			
		||||
    file = path.resolve().absolute() / filename
 | 
			
		||||
    
 | 
			
		||||
    # dump db
 | 
			
		||||
    database = {
 | 
			
		||||
        table.__name__: list(table.select().dicts())
 | 
			
		||||
        for table in tables
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    # db -> json
 | 
			
		||||
    with open(file, "w") as fp:
 | 
			
		||||
        json.dump(database, fp, cls=DateTimeEncoder)
 | 
			
		||||
 | 
			
		||||
    return file
 | 
			
		||||
 | 
			
		||||
def create_from_json(dbname: str, file: Path | str) -> Path:
 | 
			
		||||
    '''
 | 
			
		||||
    Creates a new Database <dbname>.db in from given <file>
 | 
			
		||||
 | 
			
		||||
    Valid JSON Format:
 | 
			
		||||
        {
 | 
			
		||||
            <Tablename>: [
 | 
			
		||||
                {<Tableparam>: <Value>, ...}
 | 
			
		||||
            ],
 | 
			
		||||
            ...
 | 
			
		||||
        }
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    assert type(dbname) is str, "Provided Database name isn't a String"
 | 
			
		||||
    assert isinstance(file, (Path | str)), "Provided file descriptor isn't of type Path | str"
 | 
			
		||||
 | 
			
		||||
    # Convert file to Path 
 | 
			
		||||
    if type(file) is str:
 | 
			
		||||
        file = Path(file)
 | 
			
		||||
    assert file.suffix == '.json', "File isn't a JSON"
 | 
			
		||||
    
 | 
			
		||||
    # Set correct Suffix and connect
 | 
			
		||||
    dbname = Path(dbname).resolve().absolute()
 | 
			
		||||
    if dbname.suffix != '.db':
 | 
			
		||||
        dbname = dbname.with_suffix('.db')
 | 
			
		||||
    init_db(dbname)
 | 
			
		||||
 | 
			
		||||
    # load from json 
 | 
			
		||||
    with open(file, "r") as fp:
 | 
			
		||||
        data = json.load(fp, object_hook=DateTimeDecoder)
 | 
			
		||||
    
 | 
			
		||||
    assert all([keys == table.__name__ for keys, table in zip(data.keys(), tables)]), f"{file.name} can't be convert to Database"
 | 
			
		||||
 | 
			
		||||
    # Insert Data 
 | 
			
		||||
    for k, v in data.items():
 | 
			
		||||
        if not v:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        table = next((table for table in tables if table.__name__ == k))
 | 
			
		||||
        table.insert_many(v).execute()
 | 
			
		||||
    
 | 
			
		||||
    return dbname 
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    init_db('/home/phil/programming/grapher/assets/WiSe_24_25.db')
 | 
			
		||||
    f = save_as_json("file")
 | 
			
		||||
    print(f)
 | 
			
		||||
    print(create_from_json("test", f))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								learnlytics/grader/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								learnlytics/grader/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
from .valuation import (
 | 
			
		||||
    get_gradings,
 | 
			
		||||
    get_grader,
 | 
			
		||||
    Std30PercentRule,
 | 
			
		||||
    Std50PercentRule,
 | 
			
		||||
    StdGermanGradingMiddleSchool,
 | 
			
		||||
    StdGermanGradingHighSchool
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								learnlytics/grader/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/grader/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/grader/__pycache__/valuation.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/grader/__pycache__/valuation.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										198
									
								
								learnlytics/grader/valuation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								learnlytics/grader/valuation.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
from collections.abc import Sequence, Iterable, Mapping
 | 
			
		||||
from typing import Any
 | 
			
		||||
import inspect
 | 
			
		||||
from abc import ABC, abstractmethod
 | 
			
		||||
import weakref
 | 
			
		||||
 | 
			
		||||
from PIL import ImageColor
 | 
			
		||||
from colour import Color
 | 
			
		||||
PASSED: str = "#1AFE49"
 | 
			
		||||
FAILED: str = "#FF124F"
 | 
			
		||||
 | 
			
		||||
def hex_to_rgba(color: str) -> tuple:
 | 
			
		||||
    return tuple([e/255 for e in ImageColor.getcolor(color, "RGBA")])
 | 
			
		||||
 | 
			
		||||
def gradient(color1: str, color2: str, num: int) -> list[tuple]:
 | 
			
		||||
    c1, c2 = Color(color1), Color(color2)
 | 
			
		||||
    colors = list(c2.range_to(c1, num))
 | 
			
		||||
    colors = [hex_to_rgba(str(c)) for c in colors]
 | 
			
		||||
    return colors
 | 
			
		||||
 | 
			
		||||
class BaseGrading(Mapping, ABC):
 | 
			
		||||
    __instances: list[Mapping] = list()
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, schema: dict[str, int | float], name=None, alt_name=None):
 | 
			
		||||
        all_str = all(isinstance(k, str) for k in schema.keys())
 | 
			
		||||
        assert all_str or all(isinstance(k, int) for k in schema.keys()), "Keys must be all of type (str, int)"
 | 
			
		||||
        assert all(isinstance(v, float) for v in schema.values()), "All values must be floats in range(0,1)"
 | 
			
		||||
        assert all(v <= 1 and v >= 0 for v in schema.values()), "All values must be floats in range(0,1)"
 | 
			
		||||
        if all_str:
 | 
			
		||||
            self.schema = dict()
 | 
			
		||||
            for k, v in schema.items():
 | 
			
		||||
                self.schema[k.title()] = v 
 | 
			
		||||
        else:
 | 
			
		||||
            self.schema = schema
 | 
			
		||||
 | 
			
		||||
        self.__class__.__instances.append(weakref.proxy(self))
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.alt_name = alt_name
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, index):
 | 
			
		||||
        if index >= len(self):
 | 
			
		||||
            raise IndexError
 | 
			
		||||
        return self.schema[index]
 | 
			
		||||
 | 
			
		||||
    def __len__(self) -> int:
 | 
			
		||||
        return len(self.schema) 
 | 
			
		||||
 | 
			
		||||
    def __contains__(self, item: int | str | float) -> bool:
 | 
			
		||||
        if isinstance(item, (int, str)):
 | 
			
		||||
            if isinstance(item, str):
 | 
			
		||||
                item = item.title()
 | 
			
		||||
            return item in self.schema
 | 
			
		||||
        if isinstance(item, float):
 | 
			
		||||
            return item <= 1 and item >= 0
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def __iter__(self) -> Iterable:
 | 
			
		||||
        yield from self.schema
 | 
			
		||||
 | 
			
		||||
    def __reversed__(self) -> Iterable:
 | 
			
		||||
        yield from reversed(self.schema)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def __repr__(self) -> str:
 | 
			
		||||
        return f"<{self.name}: ({str(self.schema)})>"
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other) -> bool:
 | 
			
		||||
        if other == self:
 | 
			
		||||
            return True
 | 
			
		||||
        if isinstance(other, BaseEval):
 | 
			
		||||
            return self.schema == other.schema
 | 
			
		||||
        return NotImplemented 
 | 
			
		||||
 | 
			
		||||
    def __ne__(self, other) -> bool:
 | 
			
		||||
        return not self.__eq__(other)
 | 
			
		||||
 | 
			
		||||
    def get(self, key: int | str) -> float | None:
 | 
			
		||||
        if isinstance(key, str):
 | 
			
		||||
            key = key.title()
 | 
			
		||||
        if key in self:
 | 
			
		||||
            return self.schema[key]
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def keys(self) -> tuple:
 | 
			
		||||
        return list(self.schema.keys())
 | 
			
		||||
 | 
			
		||||
    def values(self) -> tuple:
 | 
			
		||||
        return list(self.schema.values())
 | 
			
		||||
    
 | 
			
		||||
    def items(self) -> list[tuple]:
 | 
			
		||||
        return list(self.schema.items())
 | 
			
		||||
    
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def has_passed(self, value: int | float, max: int | float) -> bool:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_grade(self, value: int | float, max: int | float) -> str | int:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_grade_color(self, value: int | float, max: int | float) -> tuple:
 | 
			
		||||
        pass
 | 
			
		||||
    
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_instances(cls):
 | 
			
		||||
        yield from cls.__instances
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_instance(cls, name: str):
 | 
			
		||||
        for instance in cls.__instances:
 | 
			
		||||
            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 get_grade(self, value: int | float, max: int | float) -> str:
 | 
			
		||||
        return "Passed" if self.has_passed(value, max) else "Not Passed"
 | 
			
		||||
 | 
			
		||||
    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):
 | 
			
		||||
    def has_passed(self, value: int | float, max: int | float) -> bool:
 | 
			
		||||
        return value/max >= 0.45 
 | 
			
		||||
 | 
			
		||||
    def search_grade(self, value: float) -> int:
 | 
			
		||||
        if value <= 0:
 | 
			
		||||
            return min(self.schema.keys())
 | 
			
		||||
 | 
			
		||||
        searched = max(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) -> int:
 | 
			
		||||
        return self.search_grade(value/max)
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
    "Failed": 0.0
 | 
			
		||||
}, "Std30PercentRule", "30%")
 | 
			
		||||
 | 
			
		||||
Std50PercentRule = StdPercentRule({
 | 
			
		||||
    "Passed": 0.5,
 | 
			
		||||
    "Failed": 0.0
 | 
			
		||||
}, "Std50PercentRule", "50%")
 | 
			
		||||
 | 
			
		||||
StdGermanGradingMiddleSchool = StdGermanGrading({
 | 
			
		||||
    1: 0.96,
 | 
			
		||||
    2: 0.80,
 | 
			
		||||
    3: 0.60,
 | 
			
		||||
    4: 0.45,
 | 
			
		||||
    5: 0.16,
 | 
			
		||||
    6: 0.00
 | 
			
		||||
}, "StdGermanGradingMiddleSchool", "Mittelstufe")
 | 
			
		||||
 | 
			
		||||
StdGermanGradingHighSchool = StdGermanGrading({
 | 
			
		||||
    15: 0.95,
 | 
			
		||||
    14: 0.90,
 | 
			
		||||
    13: 0.85,
 | 
			
		||||
    12: 0.80,
 | 
			
		||||
    11: 0.75,
 | 
			
		||||
    10: 0.70,
 | 
			
		||||
    9: 0.65,
 | 
			
		||||
    8: 0.60,
 | 
			
		||||
    7: 0.55,
 | 
			
		||||
    6: 0.50,
 | 
			
		||||
    5: 0.45,
 | 
			
		||||
    4: 0.40,
 | 
			
		||||
    3: 0.33,
 | 
			
		||||
    2: 0.27,
 | 
			
		||||
    1: 0.20,
 | 
			
		||||
    0: 0.00
 | 
			
		||||
}, "StdGermanGradingHighSchool", "Oberstufe")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#print(StdGermanGradingHighSchool.get_grade(0.0, 24))
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								learnlytics/gui/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								learnlytics/gui/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
from .analyzer import analyzer_layout
 | 
			
		||||
from .database import database_editor_layout
 | 
			
		||||
from .gui import menu_bar, status_bar
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/__pycache__/gui.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/__pycache__/gui.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										65
									
								
								learnlytics/gui/analyzer/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								learnlytics/gui/analyzer/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
from imgui_bundle import hello_imgui, imgui
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from .student_list import student_list
 | 
			
		||||
from .student_graph import student_graph
 | 
			
		||||
from .group_graph import group_graph
 | 
			
		||||
 | 
			
		||||
def analyzer_docking_splits() -> List[hello_imgui.DockingSplit]:
 | 
			
		||||
    split_main_misc = hello_imgui.DockingSplit()
 | 
			
		||||
    split_main_misc.initial_dock = "MainDockSpace"
 | 
			
		||||
    split_main_misc.new_dock = "MiscSpace"
 | 
			
		||||
    split_main_misc.direction = imgui.Dir.down
 | 
			
		||||
    split_main_misc.ratio = 0.25
 | 
			
		||||
 | 
			
		||||
    # Then, add a space to the left which occupies a column whose width is 25% of the app width
 | 
			
		||||
    split_main_command = hello_imgui.DockingSplit()
 | 
			
		||||
    split_main_command.initial_dock = "MainDockSpace"
 | 
			
		||||
    split_main_command.new_dock = "CommandSpace"
 | 
			
		||||
    split_main_command.direction = imgui.Dir.left
 | 
			
		||||
    split_main_command.ratio = 0.2
 | 
			
		||||
 | 
			
		||||
    # Then, add CommandSpace2 below MainDockSpace
 | 
			
		||||
    split_main_command2 = hello_imgui.DockingSplit()
 | 
			
		||||
    split_main_command2.initial_dock = "MainDockSpace"
 | 
			
		||||
    split_main_command2.new_dock = "CommandSpace2"
 | 
			
		||||
    split_main_command2.direction = imgui.Dir.down
 | 
			
		||||
    split_main_command2.ratio = 0.25
 | 
			
		||||
 | 
			
		||||
    splits = [split_main_misc, split_main_command, split_main_command2]
 | 
			
		||||
    return splits
 | 
			
		||||
 | 
			
		||||
def set_analyzer_layout() -> List[hello_imgui.DockableWindow]:
 | 
			
		||||
    student_selector = hello_imgui.DockableWindow()
 | 
			
		||||
    student_selector.label = "Class list"
 | 
			
		||||
    student_selector.dock_space_name = "CommandSpace"
 | 
			
		||||
    student_selector.gui_function = lambda: student_list()
 | 
			
		||||
    
 | 
			
		||||
    student_info = hello_imgui.DockableWindow()
 | 
			
		||||
    student_info.label = "Student Analyzer"
 | 
			
		||||
    student_info.dock_space_name = "MainDockSpace"
 | 
			
		||||
    student_info.gui_function = lambda: student_graph()
 | 
			
		||||
 | 
			
		||||
    group_info = hello_imgui.DockableWindow()
 | 
			
		||||
    group_info.label = "Group Analyzer"
 | 
			
		||||
    group_info.dock_space_name = "MainDockSpace"
 | 
			
		||||
    group_info.gui_function = lambda: group_graph()
 | 
			
		||||
 | 
			
		||||
    #student_ranking = hello_imgui.DockableWindow()
 | 
			
		||||
    #student_ranking.label = "Ranking"
 | 
			
		||||
    #student_ranking.dock_space_name = "MainDockSpace"
 | 
			
		||||
    #student_ranking.gui_function = lambda: ranking()
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
        student_selector,
 | 
			
		||||
        student_info,
 | 
			
		||||
        group_info
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
def analyzer_layout() -> hello_imgui.DockingParams:
 | 
			
		||||
    docking_params = hello_imgui.DockingParams()
 | 
			
		||||
    docking_params.layout_name = "Analyzer"
 | 
			
		||||
    docking_params.docking_splits = analyzer_docking_splits() 
 | 
			
		||||
    docking_params.dockable_windows = set_analyzer_layout()
 | 
			
		||||
    return docking_params
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/analyzer.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/analyzer.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/group_graph.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/group_graph.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/plotter.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/analyzer/__pycache__/plotter.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										319
									
								
								learnlytics/gui/analyzer/analyzer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								learnlytics/gui/analyzer/analyzer.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,319 @@
 | 
			
		||||
# Custom 
 | 
			
		||||
from dbmodel import *
 | 
			
		||||
from grader.valuation import *
 | 
			
		||||
 | 
			
		||||
# External
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
        imgui,
 | 
			
		||||
        immapp,
 | 
			
		||||
        hello_imgui,
 | 
			
		||||
        imgui_md,
 | 
			
		||||
        implot,
 | 
			
		||||
        implot3d,
 | 
			
		||||
        immvision,
 | 
			
		||||
        ImVec2,
 | 
			
		||||
        ImVec4,
 | 
			
		||||
        im_file_dialog
 | 
			
		||||
)
 | 
			
		||||
from PIL import ImageColor
 | 
			
		||||
import numpy as np
 | 
			
		||||
from numpy.typing import NDArray
 | 
			
		||||
 | 
			
		||||
# Built In
 | 
			
		||||
from typing import List, Any
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def select_class(app_state: AppState) -> None:
 | 
			
		||||
    statics = select_class
 | 
			
		||||
 | 
			
		||||
    if not app_state.current_class_id:
 | 
			
		||||
        imgui.text("No class found in Database")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.select = 0 
 | 
			
		||||
        statics.classes = Class.select()
 | 
			
		||||
        statics.labels = [c.name for c in statics.classes]
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
    
 | 
			
		||||
    changed, statics.select = imgui.combo("Classes", statics.select, statics.labels)
 | 
			
		||||
    if changed:
 | 
			
		||||
        app_state.current_class_id = statics.classes[statics.select].id
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def select_student(app_state: AppState) -> None:
 | 
			
		||||
    statics = select_student
 | 
			
		||||
    
 | 
			
		||||
    if not app_state.current_class_id:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.select = 0 
 | 
			
		||||
        statics.students = Student.select().where(Student.class_id == app_state.current_class_id)
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if not statics.students:
 | 
			
		||||
        imgui.text("No Studends found")
 | 
			
		||||
        return 
 | 
			
		||||
 | 
			
		||||
    for n, student in enumerate(statics.students, start=1):
 | 
			
		||||
        display = f"{n}. {student.prename} {student.surname}"
 | 
			
		||||
        _, clicked = imgui.selectable(display, statics.select == n-1)
 | 
			
		||||
        if clicked:
 | 
			
		||||
            statics.select = n-1
 | 
			
		||||
            app_state.current_student_id = statics.students[statics.select].id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def student_list(app_state: AppState) -> None:
 | 
			
		||||
    statics = student_list
 | 
			
		||||
    
 | 
			
		||||
    select_class(app_state)
 | 
			
		||||
    imgui.separator()
 | 
			
		||||
    select_student(app_state)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def plot_bar_line_percentage(data: np.array, labels: list, avg: float) -> None:
 | 
			
		||||
    if not data.size > 0:
 | 
			
		||||
        imgui.text("No Data available")
 | 
			
		||||
        return 
 | 
			
		||||
 | 
			
		||||
    name = hash(avg)
 | 
			
		||||
 | 
			
		||||
    if avg.is_integer():
 | 
			
		||||
        avg = int(avg)
 | 
			
		||||
    avg = np.ones(len(data)) * avg
 | 
			
		||||
 | 
			
		||||
    w, h = imgui.get_window_size()
 | 
			
		||||
    implot.push_colormap(implot.Colormap_.hot.value)
 | 
			
		||||
    if implot.begin_plot(f"Performance##{name}", ImVec2(-1, h*0.4), implot.Flags_.no_mouse_text.value | implot.Flags_.no_inputs.value):
 | 
			
		||||
        implot.setup_axes("Lectures", "Percentage")
 | 
			
		||||
        implot.setup_axes_limits(-1, len(data), 0, 110)
 | 
			
		||||
        implot.push_style_var(implot.StyleVar_.fill_alpha.value, 0.6)
 | 
			
		||||
        implot.push_style_var(implot.StyleVar_.line_weight.value, 3)
 | 
			
		||||
        implot.setup_axis_ticks(implot.ImAxis_.x1.value, 0, len(labels), len(labels), [" " for _ in labels], False)
 | 
			
		||||
        implot.plot_bars("Submissions", data)
 | 
			
		||||
        implot.plot_line("Average", avg)
 | 
			
		||||
        
 | 
			
		||||
        implot.push_style_color(implot.Col_.inlay_text, ImVec4(190,190,40,255)/255)
 | 
			
		||||
        for x_pos, label in enumerate(labels):
 | 
			
		||||
            y_pos = 50
 | 
			
		||||
            implot.plot_text(label, x_pos, y_pos//2, ImVec2(0,0), implot.TextFlags_.vertical.value)
 | 
			
		||||
        implot.pop_style_color()
 | 
			
		||||
 | 
			
		||||
        implot.pop_style_var()
 | 
			
		||||
        implot.end_plot()
 | 
			
		||||
 | 
			
		||||
COLOR_TEXT_PASSED = tuple([e/255 for e in ImageColor.getcolor("#1AFE49","RGBA")])
 | 
			
		||||
COLOR_TEXT_FAILED = tuple([e/255 for e in ImageColor.getcolor("#FF124F","RGBA")])
 | 
			
		||||
COLOR_TEXT_PROJECT = tuple([e/255 for e in ImageColor.getcolor("#0A9CF5","RGBA")])
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def student_graph(app_state: AppState) -> None:
 | 
			
		||||
    statics = student_graph
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.id = -1
 | 
			
		||||
        statics.student = None
 | 
			
		||||
        statics.group = None
 | 
			
		||||
        statics.lectures = None
 | 
			
		||||
        statics.points = None
 | 
			
		||||
        statics.sub_points = None
 | 
			
		||||
        statics.subs_data = None 
 | 
			
		||||
        statics.subs_labels = None 
 | 
			
		||||
        statics.max_points = None 
 | 
			
		||||
        statics.avg = None 
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if id != app_state.current_student_id:
 | 
			
		||||
        statics.id = app_state.current_student_id
 | 
			
		||||
    
 | 
			
		||||
        if not app_state.current_student_id:
 | 
			
		||||
            imgui.text("No Students in Database")
 | 
			
		||||
            return
 | 
			
		||||
        statics.student = Student.get_by_id(app_state.current_student_id)
 | 
			
		||||
        submissions = Submission.select().where(Submission.student_id == statics.student.id)
 | 
			
		||||
 | 
			
		||||
        statics.group = Group.get_by_id(statics.student.group_id)
 | 
			
		||||
        statics.lectures = [Lecture.get_by_id(sub.lecture_id) for sub in submissions] 
 | 
			
		||||
        statics.max_points = np.sum([l.points for l in statics.lectures])
 | 
			
		||||
        statics.sub_points = [sub.points for sub in submissions]
 | 
			
		||||
        statics.points = np.sum(statics.sub_points)
 | 
			
		||||
        if statics.points.is_integer():
 | 
			
		||||
            statics.points = int(statics.points)
 | 
			
		||||
        #statics.grader = get_grader("Oberstufe")
 | 
			
		||||
        statics.grader = get_grader(statics.student.grader)
 | 
			
		||||
 | 
			
		||||
        statics.subs_data = np.array([p/mp.points for p, mp in zip(statics.sub_points, statics.lectures)], dtype=np.float32)*100
 | 
			
		||||
        statics.subs_labels = [f"{l.title} {int(points) if points.is_integer() else points}/{l.points}" for l, points in zip(statics.lectures, statics.sub_points)]
 | 
			
		||||
        statics.avg = statics.points/statics.max_points*100
 | 
			
		||||
 | 
			
		||||
    w, h = imgui.get_window_size()
 | 
			
		||||
    imgui_md.render(f"# {statics.student.prename} {statics.student.surname} ({statics.group.name})")
 | 
			
		||||
    imgui_md.render(f"### {statics.points}/{statics.max_points} ({statics.student.grader})")
 | 
			
		||||
    imgui.text(" ")
 | 
			
		||||
    imgui.progress_bar(statics.points/statics.max_points, ImVec2(w*0.9, h*0.05), f"{statics.points}/{statics.max_points} {statics.points/statics.max_points:.1%}")
 | 
			
		||||
    plot_bar_line_percentage(statics.subs_data, statics.subs_labels, statics.avg)
 | 
			
		||||
    imgui.separator()
 | 
			
		||||
    imgui.text_colored(COLOR_TEXT_PROJECT, f"{statics.group.name}: {statics.group.project}")
 | 
			
		||||
    for n, data in enumerate(zip(statics.lectures, statics.sub_points), start=1):
 | 
			
		||||
        lecture, points = data 
 | 
			
		||||
        COLOR = statics.grader.get_grade_color(points, lecture.points)
 | 
			
		||||
        imgui.text_colored(COLOR, f"{n}. {lecture.title} {points}/{lecture.points} ({statics.grader.get_grade(points, lecture.points)}) ")
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)       
 | 
			
		||||
def sex_graph(app_state: AppState) -> None:
 | 
			
		||||
    statics = sex_graph
 | 
			
		||||
   
 | 
			
		||||
    if db.is_closed():
 | 
			
		||||
        imgui.text("No Database loaded")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.max_points = None 
 | 
			
		||||
        statics.male_points = None
 | 
			
		||||
        statics.female_points = None
 | 
			
		||||
        statics.male_percentage = None 
 | 
			
		||||
        statics.female_percentage = None 
 | 
			
		||||
        statics.male_labels = None
 | 
			
		||||
        statics.female_labels = None
 | 
			
		||||
        statics.lectures = None
 | 
			
		||||
        statics.class_id = -1
 | 
			
		||||
        statics.state = 0
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
   
 | 
			
		||||
    if not app_state.current_class_id:
 | 
			
		||||
        imgui.text("No Class found")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if statics.class_id != app_state.current_class_id:
 | 
			
		||||
        statics.class_id = app_state.current_class_id
 | 
			
		||||
        lectures = Lecture.select().where(Lecture.class_id == statics.class_id)
 | 
			
		||||
        statics.lectures = lectures
 | 
			
		||||
 | 
			
		||||
        statics.max_points = np.empty(len(lectures))
 | 
			
		||||
        statics.male_points = np.empty(len(lectures))
 | 
			
		||||
        statics.female_points = np.empty(len(lectures))
 | 
			
		||||
        for n, lecture in enumerate(lectures):
 | 
			
		||||
            statics.max_points[n] = lecture.points # Acc points 
 | 
			
		||||
 | 
			
		||||
            m_count = 0 
 | 
			
		||||
            f_count = 0 
 | 
			
		||||
            m_points = 0 
 | 
			
		||||
            f_points = 0 
 | 
			
		||||
 | 
			
		||||
            for sub in Submission.select().where(Submission.lecture_id == lecture.id):
 | 
			
		||||
                if Student.get_by_id(sub.student_id).sex == 'Male':
 | 
			
		||||
                    m_points += sub.points 
 | 
			
		||||
                    m_count += 1 
 | 
			
		||||
                else:
 | 
			
		||||
                    f_points += sub.points
 | 
			
		||||
                    f_count += 1 
 | 
			
		||||
            statics.male_points[n] = m_points/m_count/lecture.points
 | 
			
		||||
            statics.female_points[n] = f_points/f_count/lecture.points
 | 
			
		||||
 | 
			
		||||
        statics.male_percentage = np.sum(statics.male_points)/len(statics.male_points)
 | 
			
		||||
        statics.female_percentage = np.sum(statics.female_points)/len(statics.female_points)
 | 
			
		||||
 | 
			
		||||
        statics.male_labels = [f"{l.title} | {points:.1%}" for l, points in zip(lectures, statics.male_points)] 
 | 
			
		||||
        statics.female_labels = [f"{l.title} | {points:.1%}" for l, points in zip(lectures, statics.female_points)]
 | 
			
		||||
 | 
			
		||||
    w, h = imgui.get_window_size()
 | 
			
		||||
    if statics.state == 0:
 | 
			
		||||
        imgui_md.render("# Male")
 | 
			
		||||
        imgui.progress_bar(statics.male_percentage, ImVec2(w*0.9, h*0.05), f"{statics.male_percentage:.1%} n={len(Student.select().where(Student.sex == 'Male'))}")
 | 
			
		||||
        plot_bar_line_percentage(statics.male_points*100, statics.male_labels, statics.male_percentage*100)
 | 
			
		||||
 | 
			
		||||
        imgui_md.render("# Female")
 | 
			
		||||
        imgui.progress_bar(statics.female_percentage, ImVec2(w*0.9, h*0.05), f"{statics.female_percentage:.1%} n={len(Student.select().where(Student.sex == 'Female'))}")
 | 
			
		||||
        plot_bar_line_percentage(statics.female_points*100, statics.female_labels, statics.female_percentage*100)
 | 
			
		||||
 | 
			
		||||
    if statics.state == 1:
 | 
			
		||||
        male_xs = np.arange(1, len(statics.male_points)+1, dtype=np.float32) 
 | 
			
		||||
        male_ys = male_xs*0
 | 
			
		||||
        male_zs = np.array(statics.male_points*100, dtype=np.float32)
 | 
			
		||||
        
 | 
			
		||||
        female_xs = np.arange(1, len(statics.female_points)+1, dtype=np.float32)
 | 
			
		||||
        female_ys = np.ones(len(statics.female_points), dtype=np.float32) 
 | 
			
		||||
        female_zs = np.array(statics.female_points*100, dtype=np.float32)
 | 
			
		||||
 | 
			
		||||
        if implot3d.begin_plot("3D Gender Plot", ImVec2(w*0.9, h*0.9)):
 | 
			
		||||
            implot3d.setup_axes("Lecture", "Gender", "Percentage")
 | 
			
		||||
            implot3d.setup_box_scale(1.1, 1.1, 1.1)
 | 
			
		||||
            implot3d.setup_axes_limits(0, len(statics.male_points)+1, -0.2, 1.2, 0, 110)
 | 
			
		||||
 | 
			
		||||
            implot3d.set_next_marker_style(implot3d.Marker_.square.value)
 | 
			
		||||
            implot3d.plot_line("Male", male_xs, male_ys, male_zs)
 | 
			
		||||
            
 | 
			
		||||
            implot3d.set_next_marker_style(implot3d.Marker_.circle.value)
 | 
			
		||||
            implot3d.plot_line("Female", female_xs, female_ys, female_zs)
 | 
			
		||||
 | 
			
		||||
            for n, l in enumerate(statics.lectures, start=1):
 | 
			
		||||
                implot3d.plot_text(l.title, n, np.clip(n/len(statics.lectures)), 50, np.pi/4)
 | 
			
		||||
 | 
			
		||||
            implot3d.end_plot()
 | 
			
		||||
    
 | 
			
		||||
    if imgui.button("Change 2D/3D"):
 | 
			
		||||
        statics.state = not statics.state
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
COLOR_TEXT_FIRST = tuple([e/255 for e in ImageColor.getcolor("#FFCF40","RGBA")])
 | 
			
		||||
COLOR_TEXT_SECOND = tuple([e/255 for e in ImageColor.getcolor("#A5A9B4","RGBA")])
 | 
			
		||||
COLOR_TEXT_THIRD = tuple([e/255 for e in ImageColor.getcolor("#97564A","RGBA")])
 | 
			
		||||
COLOR_TEXT_ELSE = tuple([e/255 for e in ImageColor.getcolor("#85EBD9","RGBA")])
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def ranking() -> None:
 | 
			
		||||
    statics = ranking
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.id = -1
 | 
			
		||||
        statics.rank = None
 | 
			
		||||
        statics.state = 1
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
    
 | 
			
		||||
    imgui_md.render("# Student Ranking")
 | 
			
		||||
 | 
			
		||||
    if not app_state.current_class_id:
 | 
			
		||||
        imgui.text("No class selected")
 | 
			
		||||
        return 
 | 
			
		||||
    
 | 
			
		||||
    if statics.id != app_state.current_class_id:
 | 
			
		||||
        statics.id = app_state.current_class_id
 | 
			
		||||
 | 
			
		||||
        students = Student.select().where(Student.class_id == statics.id)
 | 
			
		||||
        max_points = np.sum([l.points for l in Lecture.select().where(Lecture.class_id == statics.id)])
 | 
			
		||||
 | 
			
		||||
        statics.rank = list()
 | 
			
		||||
        for student in students:
 | 
			
		||||
            points = list()
 | 
			
		||||
            for sub in Submission.select().where(Submission.student_id == student.id):
 | 
			
		||||
                points.append(sub.points)
 | 
			
		||||
            statics.rank.append((student, sum(points)/max_points))
 | 
			
		||||
        statics.rank = sorted(statics.rank, key=lambda item: item[1], reverse=True)
 | 
			
		||||
    
 | 
			
		||||
    if statics.state == 0:
 | 
			
		||||
        for n, data in enumerate(statics.rank, start=1):
 | 
			
		||||
            student, points = data
 | 
			
		||||
            display = f"{n}. {student.prename} {student.surname} {points:.1%}"
 | 
			
		||||
            COLOR = COLOR_TEXT_ELSE
 | 
			
		||||
            if n == 1:
 | 
			
		||||
                COLOR = COLOR_TEXT_FIRST
 | 
			
		||||
            if n == 2:
 | 
			
		||||
                COLOR = COLOR_TEXT_SECOND
 | 
			
		||||
            if n == 3:
 | 
			
		||||
                COLOR = COLOR_TEXT_THIRD
 | 
			
		||||
            imgui.text_colored(COLOR, display)
 | 
			
		||||
 | 
			
		||||
    if statics.state == 1:
 | 
			
		||||
        w, h = imgui.get_window_size()
 | 
			
		||||
        if implot.begin_plot("Ranking", ImVec2(w*0.9, h*0.9)):
 | 
			
		||||
            implot.plot_bars("Ranking", np.array([p[1] for p in reversed(statics.rank)])*100, 0.67, 0, implot.BarsFlags_.horizontal.value)
 | 
			
		||||
            for n, s in enumerate(reversed(statics.rank)):
 | 
			
		||||
                student = s[0]
 | 
			
		||||
                implot.plot_text(f"{student.prename} {student.surname}", s[1]*50, n)
 | 
			
		||||
            implot.end_plot()
 | 
			
		||||
 | 
			
		||||
    if imgui.button("Change"):
 | 
			
		||||
        statics.state = not statics.state
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								learnlytics/gui/analyzer/analyzer_state.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								learnlytics/gui/analyzer/analyzer_state.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
class AnalyzerState:
 | 
			
		||||
    _instance = None
 | 
			
		||||
 | 
			
		||||
    def __new__(cls, *args, **kwargs):
 | 
			
		||||
        if not cls._instance:
 | 
			
		||||
            cls._instance = super(AnalyzerState, cls).__new__(cls, *args, **kwargs)
 | 
			
		||||
        return cls._instance
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.class_id = 1
 | 
			
		||||
        self.group_id = 1
 | 
			
		||||
        self.student_id = 1
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f'''
 | 
			
		||||
        Class: {self.class_id}
 | 
			
		||||
        Group: {self.group_id}
 | 
			
		||||
        Student: {self.student_id}
 | 
			
		||||
        '''
 | 
			
		||||
							
								
								
									
										204
									
								
								learnlytics/gui/analyzer/group_graph.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								learnlytics/gui/analyzer/group_graph.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,204 @@
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
    imgui,
 | 
			
		||||
    immapp,
 | 
			
		||||
    imgui_md,
 | 
			
		||||
    im_file_dialog,
 | 
			
		||||
    ImVec2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
import numpy as np
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import subprocess, os, platform
 | 
			
		||||
from peewee import fn
 | 
			
		||||
 | 
			
		||||
from dbmodel import *
 | 
			
		||||
from .analyzer_state import AnalyzerState
 | 
			
		||||
from .plotter import plot_pie, plot_pdf
 | 
			
		||||
 | 
			
		||||
state = AnalyzerState()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def header(group_id: int) -> None:
 | 
			
		||||
    statics = header 
 | 
			
		||||
 | 
			
		||||
    if group_id < 1:
 | 
			
		||||
        return 
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.group_id = group_id
 | 
			
		||||
        statics.group = Group.get_by_id(group_id)
 | 
			
		||||
        statics.data = [
 | 
			
		||||
            (s, np.sum([sub.points for sub in Submission.select().where(Submission.student_id == s.id)]))
 | 
			
		||||
            for s in Student.select().where(Student.group_id == group_id)
 | 
			
		||||
        ]
 | 
			
		||||
        statics.data.sort(key=lambda tup: tup[1], reverse=True)
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if statics.group_id != group_id:
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
    
 | 
			
		||||
    imgui_md.render(f"# {statics.group.name} - {statics.group.project}")
 | 
			
		||||
    imgui.text("")
 | 
			
		||||
 | 
			
		||||
    if statics.group.name != "NoGroup":
 | 
			
		||||
        changed, _ = imgui.checkbox("Passed", statics.group.has_passed)
 | 
			
		||||
        if changed:
 | 
			
		||||
            statics.group.has_passed = not statics.group.has_passed
 | 
			
		||||
            statics.group.save()
 | 
			
		||||
 | 
			
		||||
    if imgui.begin_table("Students", len(statics.data), imgui.TableFlags_.sizing_fixed_fit):
 | 
			
		||||
        for n, d in enumerate(statics.data, start=1):
 | 
			
		||||
            s, 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}. {s.prename} {s.surname} - {points} Points")
 | 
			
		||||
        imgui.end_table()
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def plot(group_id: int) -> None:
 | 
			
		||||
    statics = plot 
 | 
			
		||||
    
 | 
			
		||||
    if group_id < 1:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.group_id = group_id
 | 
			
		||||
        students = Student.select().where(Student.group_id == group_id)
 | 
			
		||||
 | 
			
		||||
        statics.data = {
 | 
			
		||||
            f"{s.prename} {s.surname}":
 | 
			
		||||
            np.sum([sub.points for sub in Submission.select().where(Submission.student_id == s.id)], dtype=np.float32)
 | 
			
		||||
            for s in students
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
        data = np.array(list(statics.data.values()))
 | 
			
		||||
        statics.labels = list(statics.data.keys())
 | 
			
		||||
        statics.data = data / data.sum() * 100
 | 
			
		||||
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if statics.group_id != group_id:
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
    
 | 
			
		||||
    plot_pie(statics.data, statics.labels)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def presentation(group_id: int) -> None:
 | 
			
		||||
    statics = presentation
 | 
			
		||||
 | 
			
		||||
    if group_id < 1:
 | 
			
		||||
        return 
 | 
			
		||||
    
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.group_id = group_id
 | 
			
		||||
        statics.group = Group.get_by_id(statics.group_id)
 | 
			
		||||
        statics.res = None
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if statics.group_id != group_id:
 | 
			
		||||
        statics.inited = False 
 | 
			
		||||
    
 | 
			
		||||
    if statics.group.name == "NoGroup":
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not statics.group.presentation:
 | 
			
		||||
        imgui.text("No Presentation set for this Group")
 | 
			
		||||
    else:
 | 
			
		||||
        plot_pdf(Path(statics.group.presentation))
 | 
			
		||||
        imgui.same_line()
 | 
			
		||||
 | 
			
		||||
    # Button to open File Selection
 | 
			
		||||
    if imgui.button("Set Presentation"):
 | 
			
		||||
        im_file_dialog.FileDialog.instance().open(
 | 
			
		||||
            "SelectPresentation", "Open Presentation", "Presentation (*.pdf; *.html){.pdf,.html}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Handle File Dialog
 | 
			
		||||
    if im_file_dialog.FileDialog.instance().is_done("SelectPresentation"):
 | 
			
		||||
        if im_file_dialog.FileDialog.instance().has_result():
 | 
			
		||||
            statics.res = im_file_dialog.FileDialog.instance().get_result()
 | 
			
		||||
        im_file_dialog.FileDialog.instance().close()
 | 
			
		||||
    
 | 
			
		||||
    # Save path to database
 | 
			
		||||
    if statics.res:
 | 
			
		||||
        file = statics.res.path()
 | 
			
		||||
        statics.group.presentation = file 
 | 
			
		||||
        statics.group.save()
 | 
			
		||||
        statics.res = None
 | 
			
		||||
    
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
    # Open System Window for PDF 
 | 
			
		||||
    if statics.group.presentation and imgui.button("Open"):
 | 
			
		||||
        # I Hate everything about it 
 | 
			
		||||
        if platform.system() == 'Darwin': # MacOS
 | 
			
		||||
            subprocess.Popen(('open', statics.group.presentation))
 | 
			
		||||
        elif platform.system() == 'Windows': # Windows 
 | 
			
		||||
            os.startfile(statics.group.presentation)
 | 
			
		||||
        else: # Linux & Variants
 | 
			
		||||
            subprocess.Popen(('xdg-open', statics.group.presentation))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def group_plot(class_id: int) -> None:
 | 
			
		||||
    if class_id < 1:
 | 
			
		||||
        return 
 | 
			
		||||
 | 
			
		||||
    statics = group_plot
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.class_id = class_id
 | 
			
		||||
        max_points = Lecture.select(fn.SUM(Lecture.points)).where(Lecture.class_id == class_id).scalar() 
 | 
			
		||||
        data = {
 | 
			
		||||
            group.name: 
 | 
			
		||||
                np.sum([Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == s.id).scalar()
 | 
			
		||||
                / Student.select().where(Student.group_id == group.id).count()
 | 
			
		||||
                for s in Student.select().where(Student.group_id == group.id)])/max_points
 | 
			
		||||
 | 
			
		||||
            for group in Group.select().where(Group.class_id == class_id)
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        statics.labels = list(data.keys())
 | 
			
		||||
        data = np.array(list(data.values()))
 | 
			
		||||
        statics.data = data / data.sum() *100
 | 
			
		||||
 | 
			
		||||
        statics.inited = True 
 | 
			
		||||
 | 
			
		||||
    if statics.class_id != class_id:
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
    
 | 
			
		||||
    plot_pie(statics.data, statics.labels, title="Performance per Group")
 | 
			
		||||
 | 
			
		||||
def group_graph() -> None:
 | 
			
		||||
    if db.is_closed():
 | 
			
		||||
        imgui.text("No DB loaded")
 | 
			
		||||
        return 
 | 
			
		||||
  
 | 
			
		||||
    w, h = imgui.get_content_region_avail()
 | 
			
		||||
    
 | 
			
		||||
    if imgui.begin_child("Header", ImVec2(w * 0.3, h*0.5), imgui.ChildFlags_.borders.value):
 | 
			
		||||
        header(state.group_id)
 | 
			
		||||
        imgui.end_child()
 | 
			
		||||
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
 | 
			
		||||
    if imgui.begin_child("Plot", ImVec2(w * 0.7, h*0.5), imgui.ChildFlags_.borders.value):
 | 
			
		||||
        plot(state.group_id)
 | 
			
		||||
        imgui.end_child()
 | 
			
		||||
    
 | 
			
		||||
    if imgui.begin_child("PDF", ImVec2(w*0.5, h*0.49), imgui.ChildFlags_.borders.value):
 | 
			
		||||
        #plot_pdf(Path("/storage/programming/Learnlytics/assets/MeWi_4.pdf"))
 | 
			
		||||
        presentation(state.group_id)
 | 
			
		||||
        imgui.end_child()
 | 
			
		||||
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
 | 
			
		||||
    if imgui.begin_child("Groups", ImVec2(w*0.5, h*0.49), imgui.ChildFlags_.borders.value):
 | 
			
		||||
        group_plot(state.class_id)
 | 
			
		||||
        imgui.end_child()
 | 
			
		||||
							
								
								
									
										112
									
								
								learnlytics/gui/analyzer/plotter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								learnlytics/gui/analyzer/plotter.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
    implot,
 | 
			
		||||
    imgui,
 | 
			
		||||
    immapp,
 | 
			
		||||
    immvision,
 | 
			
		||||
    ImVec4,
 | 
			
		||||
    ImVec2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from PIL import Image
 | 
			
		||||
import numpy as np
 | 
			
		||||
from pdf2image import convert_from_path
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
immvision.use_rgb_color_order()
 | 
			
		||||
 | 
			
		||||
def plot_bar_line_percentage(data: np.array, labels: list, avg: float) -> None:
 | 
			
		||||
    if not data.size > 0:
 | 
			
		||||
        imgui.text("No Data available")
 | 
			
		||||
        return 
 | 
			
		||||
 | 
			
		||||
    name = hash(avg)
 | 
			
		||||
 | 
			
		||||
    if avg.is_integer():
 | 
			
		||||
        avg = int(avg)
 | 
			
		||||
    avg = np.ones(len(data)) * avg
 | 
			
		||||
 | 
			
		||||
    w, h = imgui.get_window_size()
 | 
			
		||||
    implot.push_colormap(implot.Colormap_.hot.value)
 | 
			
		||||
    if implot.begin_plot(f"Performance##{name}", ImVec2(-1, h*0.8), implot.Flags_.no_mouse_text.value | implot.Flags_.no_inputs.value):
 | 
			
		||||
        implot.setup_axes("Lectures", "Percentage")
 | 
			
		||||
        implot.setup_axes_limits(-1, len(data), 0, 110)
 | 
			
		||||
        implot.push_style_var(implot.StyleVar_.fill_alpha.value, 0.6)
 | 
			
		||||
        implot.push_style_var(implot.StyleVar_.line_weight.value, 3)
 | 
			
		||||
        implot.setup_axis_ticks(implot.ImAxis_.x1.value, 0, len(labels), len(labels), [" " for _ in labels], False)
 | 
			
		||||
        implot.plot_bars("Submissions", data)
 | 
			
		||||
        implot.plot_line("Average", avg)
 | 
			
		||||
        
 | 
			
		||||
        implot.push_style_color(implot.Col_.inlay_text, ImVec4(190,190,40,255)/255)
 | 
			
		||||
        for x_pos, label in enumerate(labels):
 | 
			
		||||
            y_pos = 50
 | 
			
		||||
            implot.plot_text(label, x_pos, y_pos//2, ImVec2(0,0), implot.TextFlags_.vertical.value)
 | 
			
		||||
        implot.pop_style_color()
 | 
			
		||||
 | 
			
		||||
        implot.pop_style_var()
 | 
			
		||||
        implot.end_plot()
 | 
			
		||||
 | 
			
		||||
def plot_pie(data: np.array, labels: list, title: str = "Group Performance") -> None:
 | 
			
		||||
    if not data.size > 0:
 | 
			
		||||
        imgui.text("No Data available")
 | 
			
		||||
        return
 | 
			
		||||
    
 | 
			
		||||
    name = hash(labels[-1])
 | 
			
		||||
    
 | 
			
		||||
    w, h = imgui.get_window_size()
 | 
			
		||||
    
 | 
			
		||||
    implot.push_colormap(implot.Colormap_.hot.value)
 | 
			
		||||
    if implot.begin_plot(f"{title}##{name}", ImVec2(w*0.95, h*0.95), implot.Flags_.no_mouse_text.value | implot.Flags_.no_inputs.value):
 | 
			
		||||
        implot.setup_axes("", "", implot.AxisFlags_.no_decorations.value, implot.AxisFlags_.no_decorations.value)
 | 
			
		||||
        implot.setup_axes_limits(-1.1, 1.1, -1.1, 1.1)
 | 
			
		||||
        implot.plot_pie_chart(labels, data, 0, 0, 1, "%.2f%%", implot.PieChartFlags_.normalize.value) 
 | 
			
		||||
 | 
			
		||||
        implot.end_plot()
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def plot_pdf(pdf: Path, label: str = "Presentation") -> None:
 | 
			
		||||
    if not pdf.exists():
 | 
			
		||||
        return
 | 
			
		||||
    
 | 
			
		||||
    statics = plot_pdf
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.pdf = pdf
 | 
			
		||||
        statics.images = convert_from_path(statics.pdf.resolve(), size=720, fmt='jpeg')
 | 
			
		||||
 | 
			
		||||
        statics.params = immvision.ImageParams(
 | 
			
		||||
            refresh_image = True,
 | 
			
		||||
            show_image_info = False,
 | 
			
		||||
            show_pixel_info = False,
 | 
			
		||||
            show_zoom_buttons = False,
 | 
			
		||||
            show_options_button = False,
 | 
			
		||||
            can_resize = False,
 | 
			
		||||
            zoom_pan_matrix = None,
 | 
			
		||||
            pan_with_mouse = False,
 | 
			
		||||
            zoom_with_mouse_wheel = False,
 | 
			
		||||
            image_display_size = (500, 0)
 | 
			
		||||
        )
 | 
			
		||||
        statics.selected = 0
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if statics.pdf != pdf:
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
    
 | 
			
		||||
        
 | 
			
		||||
    h = imgui.get_window_size().y
 | 
			
		||||
 | 
			
		||||
    image = np.array(statics.images[statics.selected].convert("RGB"))
 | 
			
		||||
    size = (0,int(h*0.8))
 | 
			
		||||
    immvision.image_display(label, image, size, True)
 | 
			
		||||
    
 | 
			
		||||
    if imgui.button("Back"):
 | 
			
		||||
        statics.selected -= 1 
 | 
			
		||||
        if statics.selected < 0:
 | 
			
		||||
            statics.selected = 0 
 | 
			
		||||
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
 | 
			
		||||
    if imgui.button("Next"):
 | 
			
		||||
        statics.selected += 1 
 | 
			
		||||
        if statics.selected > len(statics.images)-1:
 | 
			
		||||
            statics.selected = len(statics.images)-1
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										80
									
								
								learnlytics/gui/analyzer/student_graph.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								learnlytics/gui/analyzer/student_graph.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
    imgui,
 | 
			
		||||
    imgui_md,
 | 
			
		||||
    immapp,
 | 
			
		||||
    ImVec2,
 | 
			
		||||
    ImVec4
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
import numpy as np
 | 
			
		||||
from peewee import fn
 | 
			
		||||
 | 
			
		||||
from dbmodel import *
 | 
			
		||||
 | 
			
		||||
from .analyzer_state import AnalyzerState 
 | 
			
		||||
from .plotter import plot_bar_line_percentage 
 | 
			
		||||
 | 
			
		||||
state = AnalyzerState()
 | 
			
		||||
PROGRESS_BAR_COLOR = ImVec4(190, 190, 40, 255)/255
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def header(student_id: int) -> None:
 | 
			
		||||
    statics = header
 | 
			
		||||
    
 | 
			
		||||
    if student_id < 1:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.student = Student.get_by_id(student_id)
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if statics.student.id != student_id:
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
 | 
			
		||||
    imgui_md.render(f"# {statics.student.prename} {statics.student.surname}")
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def plot(student_id: int) -> None:
 | 
			
		||||
    statics = plot 
 | 
			
		||||
 | 
			
		||||
    if student_id < 1:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.student = Student.get_by_id(student_id)
 | 
			
		||||
        submissions = Submission.select().where(Submission.student_id == statics.student.id)
 | 
			
		||||
        lectures = Lecture.select().where(Lecture.class_id == statics.student.class_id)
 | 
			
		||||
        
 | 
			
		||||
        statics.labels = [l.title for l in lectures]
 | 
			
		||||
        statics.data = np.array([sub.points/l.points for sub, l in zip(submissions, lectures)], dtype=np.float32) * 100
 | 
			
		||||
        statics.avg = np.mean(statics.data)/100
 | 
			
		||||
        statics.points = Submission.select(fn.SUM(Submission.points)).where(Submission.student_id == statics.student.id).scalar()
 | 
			
		||||
        if statics.points.is_integer():
 | 
			
		||||
            statics.points = int(statics.points)
 | 
			
		||||
        statics.max_points = Lecture.select(fn.SUM(Lecture.points)).where(Lecture.class_id == statics.student.class_id).scalar()
 | 
			
		||||
        statics.inited = True 
 | 
			
		||||
 | 
			
		||||
    if statics.student.id != student_id:
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
    
 | 
			
		||||
    imgui.text(f"{statics.points}/{statics.max_points}")
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
    w, h = imgui.get_window_size()
 | 
			
		||||
    imgui.push_style_color(imgui.Col_.plot_histogram.value, PROGRESS_BAR_COLOR)
 | 
			
		||||
    imgui.progress_bar(statics.avg, ImVec2(w*0.9, h*0.07), f"{statics.avg:.1%}")
 | 
			
		||||
    imgui.pop_style_color()
 | 
			
		||||
    plot_bar_line_percentage(statics.data, statics.labels, statics.avg*100)
 | 
			
		||||
 | 
			
		||||
def student_graph() -> None:
 | 
			
		||||
    if db.is_closed():
 | 
			
		||||
        imgui.text("No DB loaded")
 | 
			
		||||
        return
 | 
			
		||||
    
 | 
			
		||||
    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)
 | 
			
		||||
        imgui.end_child()
 | 
			
		||||
    imgui.separator()
 | 
			
		||||
							
								
								
									
										73
									
								
								learnlytics/gui/analyzer/student_list.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								learnlytics/gui/analyzer/student_list.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
    imgui,
 | 
			
		||||
    immapp,
 | 
			
		||||
    imgui_md
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from dbmodel import *
 | 
			
		||||
 | 
			
		||||
from .analyzer_state import AnalyzerState 
 | 
			
		||||
 | 
			
		||||
state = AnalyzerState()
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def class_selector() -> int:
 | 
			
		||||
    statics = class_selector
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.selector = 0
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    labels = (c.name for c in Class.select())
 | 
			
		||||
    _, statics.selector = imgui.combo("##Classes", statics.selector, list(labels))
 | 
			
		||||
    imgui.separator()
 | 
			
		||||
    return Class.select()[statics.selector].id
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def tree(class_id: int) -> None:
 | 
			
		||||
    statics = tree
 | 
			
		||||
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.class_id = class_id
 | 
			
		||||
        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.selected = -1
 | 
			
		||||
        statics.ret = (-1, -1)
 | 
			
		||||
        statics.flags = imgui.TreeNodeFlags_.none.value
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    if statics.class_id != class_id:
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
    
 | 
			
		||||
    if imgui.button("Expand"):
 | 
			
		||||
        statics.flags = imgui.TreeNodeFlags_.default_open.value
 | 
			
		||||
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
 | 
			
		||||
    if imgui.button("Collapse"):
 | 
			
		||||
        statics.flags = imgui.TreeNodeFlags_.none.value
 | 
			
		||||
 | 
			
		||||
    n = 0
 | 
			
		||||
    for group, students in statics.data.items():
 | 
			
		||||
        if imgui.tree_node_ex(f"{group.name} -  {group.project}", statics.flags):
 | 
			
		||||
            for student in students:
 | 
			
		||||
                changed, _ = imgui.selectable(f"{student.prename} {student.surname}", statics.selected == n)
 | 
			
		||||
                if changed:
 | 
			
		||||
                    statics.selected = n
 | 
			
		||||
                    statics.ret = (group.id, student.id)
 | 
			
		||||
                n += 1
 | 
			
		||||
            n += 1
 | 
			
		||||
            imgui.tree_pop()
 | 
			
		||||
 | 
			
		||||
    return statics.ret
 | 
			
		||||
 | 
			
		||||
def student_list() -> None:
 | 
			
		||||
    
 | 
			
		||||
    if db.is_closed():
 | 
			
		||||
        imgui.text("No DB loaded")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    state.class_id = class_selector()
 | 
			
		||||
    state.group_id, state.student_id = tree(state.class_id)
 | 
			
		||||
							
								
								
									
										81
									
								
								learnlytics/gui/database/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								learnlytics/gui/database/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
from imgui_bundle import hello_imgui, imgui
 | 
			
		||||
#from ..app_state import AppState
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from .database import *
 | 
			
		||||
 | 
			
		||||
from .editor import editor
 | 
			
		||||
 | 
			
		||||
def database_docking_splits() -> List[hello_imgui.DockingSplit]:
 | 
			
		||||
    """
 | 
			
		||||
    Defines the docking layout for the database editor.
 | 
			
		||||
    
 | 
			
		||||
    Returns a list of docking splits that define the structure of the editor layout.
 | 
			
		||||
    
 | 
			
		||||
    :return: A list of `hello_imgui.DockingSplit` objects defining docking positions and sizes.
 | 
			
		||||
    """
 | 
			
		||||
    split_main_command = hello_imgui.DockingSplit()
 | 
			
		||||
    split_main_command.initial_dock = "MainDockSpace"
 | 
			
		||||
    split_main_command.new_dock = "CommandSpace"
 | 
			
		||||
    split_main_command.direction = imgui.Dir.down
 | 
			
		||||
    split_main_command.ratio = 0.3
 | 
			
		||||
 | 
			
		||||
    split_main_command2 = hello_imgui.DockingSplit()
 | 
			
		||||
    split_main_command2.initial_dock = "CommandSpace"
 | 
			
		||||
    split_main_command2.new_dock = "CommandSpace2"
 | 
			
		||||
    split_main_command2.direction = imgui.Dir.right
 | 
			
		||||
    split_main_command2.ratio = 0.3
 | 
			
		||||
 | 
			
		||||
    split_main_misc = hello_imgui.DockingSplit()
 | 
			
		||||
    split_main_misc.initial_dock = "MainDockSpace"
 | 
			
		||||
    split_main_misc.new_dock = "MiscSpace"
 | 
			
		||||
    split_main_misc.direction = imgui.Dir.left
 | 
			
		||||
    split_main_misc.ratio = 0.2
 | 
			
		||||
 | 
			
		||||
    return [split_main_misc, split_main_command, split_main_command2]
 | 
			
		||||
 | 
			
		||||
def set_database_editor_layout() -> List[hello_imgui.DockableWindow]:
 | 
			
		||||
    """
 | 
			
		||||
    Defines the dockable windows for the database editor.
 | 
			
		||||
    
 | 
			
		||||
    Creates and returns a list of dockable windows, including the database file selector, log window,
 | 
			
		||||
    table viewer, and editor.
 | 
			
		||||
    
 | 
			
		||||
    :param app_state: The application state.
 | 
			
		||||
    :return: A list of `hello_imgui.DockableWindow` objects representing the UI windows.
 | 
			
		||||
    """
 | 
			
		||||
    file_dialog = hello_imgui.DockableWindow()
 | 
			
		||||
    file_dialog.label = "Database"
 | 
			
		||||
    file_dialog.dock_space_name = "MiscSpace"
 | 
			
		||||
    file_dialog.gui_function = lambda: select_file()
 | 
			
		||||
 | 
			
		||||
    log = hello_imgui.DockableWindow()
 | 
			
		||||
    log.label = "Logs"
 | 
			
		||||
    log.dock_space_name = "CommandSpace2"
 | 
			
		||||
    log.gui_function = hello_imgui.log_gui
 | 
			
		||||
 | 
			
		||||
    table_view = hello_imgui.DockableWindow()
 | 
			
		||||
    table_view.label = "Table"
 | 
			
		||||
    table_view.dock_space_name = "MainDockSpace"
 | 
			
		||||
    table_view.gui_function = lambda: table()
 | 
			
		||||
 | 
			
		||||
    eeditor = hello_imgui.DockableWindow()
 | 
			
		||||
    eeditor.label = "Editor"
 | 
			
		||||
    eeditor.dock_space_name = "CommandSpace"
 | 
			
		||||
    eeditor.gui_function = lambda: editor()
 | 
			
		||||
    
 | 
			
		||||
    return [file_dialog, log, table_view, eeditor]
 | 
			
		||||
 | 
			
		||||
def database_editor_layout() -> hello_imgui.DockingParams:
 | 
			
		||||
    """
 | 
			
		||||
    Configures and returns the docking layout for the database editor.
 | 
			
		||||
    
 | 
			
		||||
    :param app_state: The application state.
 | 
			
		||||
    :return: A `hello_imgui.DockingParams` object defining the layout configuration.
 | 
			
		||||
    """
 | 
			
		||||
    docking_params = hello_imgui.DockingParams()
 | 
			
		||||
    docking_params.layout_name = "Database Editor"
 | 
			
		||||
    docking_params.docking_splits = database_docking_splits() 
 | 
			
		||||
    docking_params.dockable_windows = set_database_editor_layout()
 | 
			
		||||
    return docking_params
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/database/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/database/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/database/__pycache__/database.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/database/__pycache__/database.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								learnlytics/gui/database/__pycache__/editor.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								learnlytics/gui/database/__pycache__/editor.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										348
									
								
								learnlytics/gui/database/database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										348
									
								
								learnlytics/gui/database/database.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,348 @@
 | 
			
		||||
"""
 | 
			
		||||
Database Editor UI Module
 | 
			
		||||
 | 
			
		||||
This module defines the graphical user interface (GUI) components and layout for a database editor using the
 | 
			
		||||
HelloImGui and ImGui frameworks. It provides a class editor, docking layout configurations, and functions
 | 
			
		||||
to set up the database editing environment.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# Custom
 | 
			
		||||
from dbmodel import *
 | 
			
		||||
from gui import * 
 | 
			
		||||
from grader import get_gradings
 | 
			
		||||
 | 
			
		||||
# External
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
    imgui,
 | 
			
		||||
    imgui_ctx,
 | 
			
		||||
    immapp,
 | 
			
		||||
    imgui_md,
 | 
			
		||||
    im_file_dialog,
 | 
			
		||||
    hello_imgui
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
import peewee
 | 
			
		||||
 | 
			
		||||
# Built In 
 | 
			
		||||
from typing import List
 | 
			
		||||
import shelve
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
import pytz
 | 
			
		||||
from tzlocal import get_localzone
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def file_info(path: Path) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Displays file information in an ImGui table.
 | 
			
		||||
    
 | 
			
		||||
    Args:
 | 
			
		||||
        path (Path): The file path whose information is to be displayed.
 | 
			
		||||
    
 | 
			
		||||
    The function retrieves the file's size, last access time, and creation time,
 | 
			
		||||
    formats the data, and presents it using ImGui tables.
 | 
			
		||||
    """
 | 
			
		||||
    # Retrieve file statistics
 | 
			
		||||
    stat = path.stat()
 | 
			
		||||
    modified = datetime.fromtimestamp(stat.st_atime)  # Last access time
 | 
			
		||||
    created = datetime.fromtimestamp(stat.st_ctime)   # Creation time
 | 
			
		||||
    format = '%c'  # Standard date-time format
 | 
			
		||||
 | 
			
		||||
    # Prepare file data dictionary
 | 
			
		||||
    data = {
 | 
			
		||||
        "File": path.name,
 | 
			
		||||
        "Size": f"{stat.st_size/100:.2f} KB",  # Convert bytes to KB (incorrect divisor, should be 1024)
 | 
			
		||||
        "Modified": modified.strftime(format),
 | 
			
		||||
        "Created": created.strftime(format)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    # Create ImGui table to display file information
 | 
			
		||||
    if imgui.begin_table("File Info", 2):
 | 
			
		||||
        imgui.table_setup_column(" ", 0)
 | 
			
		||||
        imgui.table_setup_column(" ")
 | 
			
		||||
        
 | 
			
		||||
        # Iterate over file data and populate table
 | 
			
		||||
        for k, v in data.items():
 | 
			
		||||
            imgui.push_id(k)
 | 
			
		||||
            imgui.table_next_row()
 | 
			
		||||
            imgui.table_next_column()
 | 
			
		||||
            imgui.text(k)
 | 
			
		||||
            imgui.table_next_column()
 | 
			
		||||
            imgui.text(v)
 | 
			
		||||
            imgui.pop_id()
 | 
			
		||||
        
 | 
			
		||||
        imgui.end_table()
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False, res=False)
 | 
			
		||||
def select_file() -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Handles the selection and loading of a database file (JSON or SQLite).
 | 
			
		||||
    It initializes necessary state, allows users to open a file dialog,
 | 
			
		||||
    and processes the selected file by either loading or converting it.
 | 
			
		||||
    """
 | 
			
		||||
    statics = select_file  # Access static variables within the function
 | 
			
		||||
    
 | 
			
		||||
    # Initialize static variables on the first function call
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.res = None  # Stores the selected file result
 | 
			
		||||
        statics.current = None  # Stores the currently loaded database file path
 | 
			
		||||
        
 | 
			
		||||
        # Retrieve the last used database file from persistent storage
 | 
			
		||||
        with shelve.open("state") as state:
 | 
			
		||||
            statics.current = Path(state.get("DB") or "")
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
    
 | 
			
		||||
    # Render UI title and display file information
 | 
			
		||||
    imgui_md.render("# Database Manager")
 | 
			
		||||
    file_info(statics.current)
 | 
			
		||||
    
 | 
			
		||||
    # Button to open the file selection dialog
 | 
			
		||||
    if imgui.button("Open File"):
 | 
			
		||||
        im_file_dialog.FileDialog.instance().open(
 | 
			
		||||
            "SelectDatabase", "Open Database", "Database File (*.json;*.db){.json,.db}"
 | 
			
		||||
        )
 | 
			
		||||
    
 | 
			
		||||
    # Handle the file dialog result
 | 
			
		||||
    if im_file_dialog.FileDialog.instance().is_done("SelectDatabase"):
 | 
			
		||||
        if im_file_dialog.FileDialog.instance().has_result():
 | 
			
		||||
            statics.res = im_file_dialog.FileDialog.instance().get_result()
 | 
			
		||||
        im_file_dialog.FileDialog.instance().close()
 | 
			
		||||
    
 | 
			
		||||
    # Process the selected file if available
 | 
			
		||||
    if statics.res:
 | 
			
		||||
        filename = statics.res.filename()
 | 
			
		||||
        info = Path(statics.res.path())
 | 
			
		||||
        
 | 
			
		||||
        imgui.separator()  # UI separator for clarity
 | 
			
		||||
        file_info(info)  # Display information about the selected file
 | 
			
		||||
        
 | 
			
		||||
        file = None
 | 
			
		||||
        
 | 
			
		||||
        # Load the selected database file
 | 
			
		||||
        if imgui.button("Load"):
 | 
			
		||||
            # Ensure any currently open database is closed before loading a new one
 | 
			
		||||
            if not db.is_closed():
 | 
			
		||||
                db.close()
 | 
			
		||||
            
 | 
			
		||||
            # Handle JSON files by converting them to SQLite databases
 | 
			
		||||
            if statics.res.extension() == '.json':
 | 
			
		||||
                file = filename.removesuffix('.json') + '.db'  # Convert JSON filename to SQLite filename
 | 
			
		||||
                init_db(file) 
 | 
			
		||||
                load_from_json(str(info))  # Convert and load JSON data into the database
 | 
			
		||||
            
 | 
			
		||||
            # Handle SQLite database files directly
 | 
			
		||||
            if statics.res.extension() == '.db':
 | 
			
		||||
                file = str(statics.res.path())
 | 
			
		||||
                init_db(file)
 | 
			
		||||
            
 | 
			
		||||
            # Save the selected database path to persistent storage
 | 
			
		||||
            with shelve.open("state") as state:
 | 
			
		||||
                state["DB"] = file
 | 
			
		||||
            
 | 
			
		||||
            # 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:
 | 
			
		||||
    """
 | 
			
		||||
    Class Editor UI Component.
 | 
			
		||||
    
 | 
			
		||||
    This function initializes and renders the class selection interface within the database editor.
 | 
			
		||||
    It maintains a static state to keep track of the selected class and fetches available classes.
 | 
			
		||||
    """
 | 
			
		||||
    statics = class_editor
 | 
			
		||||
    if db.is_closed():
 | 
			
		||||
        imgui.text("DB")
 | 
			
		||||
        return
 | 
			
		||||
    statics.classes = Class.select()
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.selected = 0
 | 
			
		||||
        statics.value = statics.classes[statics.selected].name if statics.classes else str()
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
 | 
			
		||||
    # Fetch available classes from the database
 | 
			
		||||
 | 
			
		||||
    # Render the UI for class selection
 | 
			
		||||
    imgui_md.render("# Edit Classes")
 | 
			
		||||
    changed, statics.selected = imgui.combo("##Classes", statics.selected, [c.name for c in statics.classes])
 | 
			
		||||
    if changed:
 | 
			
		||||
        statics.value = statics.classes[statics.selected].name
 | 
			
		||||
    _, statics.value = imgui.input_text("##input_class", statics.value)
 | 
			
		||||
 | 
			
		||||
    if imgui.button("Update"):
 | 
			
		||||
        clas = statics.classes[statics.selected]
 | 
			
		||||
        clas.name, old_name = statics.value, clas.name
 | 
			
		||||
        clas.save()
 | 
			
		||||
        
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
 | 
			
		||||
    if imgui.button("New"):
 | 
			
		||||
        Class.create(name=statics.value)
 | 
			
		||||
 | 
			
		||||
    imgui.same_line()
 | 
			
		||||
 | 
			
		||||
    if imgui.button("Delete"):
 | 
			
		||||
        clas = statics.classes[statics.selected]
 | 
			
		||||
        clas.delete_instance()
 | 
			
		||||
        statics.selected -= 1
 | 
			
		||||
        statics.value = statics.classes[statics.selected].name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_editor_popup(table, action: str, selectors: dict) -> None:
 | 
			
		||||
    table_flags = (
 | 
			
		||||
        imgui.TableFlags_.row_bg.value
 | 
			
		||||
    )
 | 
			
		||||
    cols = 2 
 | 
			
		||||
    rows = len(table._meta.fields)
 | 
			
		||||
 | 
			
		||||
    if imgui.begin_table("Editor Grid", cols, table_flags):
 | 
			
		||||
        # Setup Header 
 | 
			
		||||
        for header in ["Attribute", "Value"]:
 | 
			
		||||
            imgui.table_setup_column(header, imgui.TableColumnFlags_.width_stretch.value, 1/len(header))
 | 
			
		||||
        imgui.table_headers_row()
 | 
			
		||||
    
 | 
			
		||||
        id = 0
 | 
			
		||||
        for k, v in table._meta.fields.items():
 | 
			
		||||
            # Don't show Fields
 | 
			
		||||
            match type(v):
 | 
			
		||||
                case peewee.AutoField:
 | 
			
		||||
                    continue 
 | 
			
		||||
                case peewee.DateTimeField:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            imgui.table_next_row()
 | 
			
		||||
            imgui.table_set_column_index(0)
 | 
			
		||||
            label = str(k).removesuffix('_id')
 | 
			
		||||
            imgui.text(label.title())
 | 
			
		||||
            imgui.table_set_column_index(1)
 | 
			
		||||
            
 | 
			
		||||
            # Generate Input for Type
 | 
			
		||||
            match type(v):
 | 
			
		||||
                case peewee.IntegerField:
 | 
			
		||||
                    if id not in selectors:
 | 
			
		||||
                        selectors[id] = int()
 | 
			
		||||
                    _, selectors[id] = imgui.input_int(f"##{id}", selectors[id], 1)
 | 
			
		||||
                case peewee.CharField:
 | 
			
		||||
                    if id not in selectors:
 | 
			
		||||
                        if k == 'grader':
 | 
			
		||||
                            selectors[id] = int()
 | 
			
		||||
                        else:
 | 
			
		||||
                            selectors[id] = str()
 | 
			
		||||
                    
 | 
			
		||||
                    if k == 'grader':
 | 
			
		||||
                        graders = [g.alt_name for g in get_gradings()]
 | 
			
		||||
                        _, selectors[id] = imgui.combo(f"##{id}", selectors[id], graders)
 | 
			
		||||
                    else:
 | 
			
		||||
                        _, selectors[id] = imgui.input_text(f"##{id}", selectors[id])
 | 
			
		||||
                case peewee.FloatField:
 | 
			
		||||
                    if id not in selectors:
 | 
			
		||||
                        selectors[id] = float()
 | 
			
		||||
                    _, selectors[id] = imgui.input_float(f"##{id}", selectors[id])
 | 
			
		||||
                case peewee.ForeignKeyField:
 | 
			
		||||
                    if id not in selectors:
 | 
			
		||||
                        selectors[id] = int()
 | 
			
		||||
                    
 | 
			
		||||
                    labels = list()
 | 
			
		||||
                    match k:
 | 
			
		||||
                        case 'class_id':
 | 
			
		||||
                            labels = [clas.name for clas in Class.select()]
 | 
			
		||||
                        case 'lecture_id':
 | 
			
		||||
                            labels = [lecture.title for lecture in Lecture.select()]
 | 
			
		||||
                    
 | 
			
		||||
                    if not labels:
 | 
			
		||||
                        imgui.text("No Element for this Attribute")
 | 
			
		||||
                    else:
 | 
			
		||||
                        _, selectors[id] = imgui.combo(f"##{id}", selectors[id], labels)
 | 
			
		||||
                case _:
 | 
			
		||||
                    imgui.text(f"Unknown Field {k}")
 | 
			
		||||
            
 | 
			
		||||
            id += 1
 | 
			
		||||
        imgui.end_table()
 | 
			
		||||
 | 
			
		||||
    if imgui.button(action):
 | 
			
		||||
        match action:
 | 
			
		||||
            case "Create":
 | 
			
		||||
                print("Create")
 | 
			
		||||
            case "Update":
 | 
			
		||||
                print("Update")
 | 
			
		||||
            case "Delete":
 | 
			
		||||
                print("Delete")
 | 
			
		||||
            case _:
 | 
			
		||||
                print("Unknown Case")
 | 
			
		||||
 | 
			
		||||
        # Clear & Close Popup
 | 
			
		||||
        selectors.clear()
 | 
			
		||||
        imgui.close_current_popup()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										141
									
								
								learnlytics/gui/database/editor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								learnlytics/gui/database/editor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
    imgui,
 | 
			
		||||
    immapp,
 | 
			
		||||
    imgui_md,
 | 
			
		||||
    hello_imgui
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from grader import get_gradings, get_grader
 | 
			
		||||
 | 
			
		||||
from dbmodel import *
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def editor() -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Database Editor UI Function.
 | 
			
		||||
    
 | 
			
		||||
    Calls the class editor function to render its UI component.
 | 
			
		||||
    """
 | 
			
		||||
    if db.is_closed():
 | 
			
		||||
        imgui.text("Please open a Database")
 | 
			
		||||
        return 
 | 
			
		||||
 | 
			
		||||
    statics = editor
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.selected = 0
 | 
			
		||||
        statics.actions = ["Create", "Update", "Delete"]
 | 
			
		||||
        statics.action = str()
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
        statics.selectors = dict()
 | 
			
		||||
 | 
			
		||||
    imgui.text("Select what you want to Edit:")
 | 
			
		||||
    changed, statics.selected = imgui.combo('##DBSelector', statics.selected, table_labels)
 | 
			
		||||
    for action in statics.actions:
 | 
			
		||||
        if imgui.button(action):
 | 
			
		||||
            imgui.open_popup(table_labels[statics.selected])
 | 
			
		||||
            statics.action = action
 | 
			
		||||
        imgui.same_line()
 | 
			
		||||
    
 | 
			
		||||
    if imgui.begin_popup_modal(table_labels[statics.selected])[0]:
 | 
			
		||||
        table = tables[statics.selected]
 | 
			
		||||
        imgui_md.render(f"# {statics.action} {table_labels[statics.selected]}")
 | 
			
		||||
        
 | 
			
		||||
        if imgui.begin_table("Editor Grid", 2, imgui.TableFlags_.row_bg.value):
 | 
			
		||||
            # Setup Header
 | 
			
		||||
            for header in ["Attribute", "Value"]:
 | 
			
		||||
                imgui.table_setup_column(header, imgui.TableColumnFlags_.width_stretch.value, 1/len(header))
 | 
			
		||||
            imgui.table_headers_row()
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
            student_editor(statics.action)
 | 
			
		||||
 | 
			
		||||
            imgui.end_table()
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        imgui.end_popup()
 | 
			
		||||
 | 
			
		||||
@immapp.static(inited=False)
 | 
			
		||||
def student_editor(action: str) -> dict:
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    statics = student_editor
 | 
			
		||||
    if not statics.inited:
 | 
			
		||||
        statics.classes = tuple(Class.select())
 | 
			
		||||
        statics.residences = tuple(Residence.select())
 | 
			
		||||
        statics.classes_labels = tuple(clas.name for clas in statics.classes)
 | 
			
		||||
        statics.graders = tuple(grader.alt_name for grader in get_gradings())
 | 
			
		||||
        statics.buffer = {
 | 
			
		||||
            'prename': "",
 | 
			
		||||
            'surname': "",
 | 
			
		||||
            'sex': 0,
 | 
			
		||||
            'class_id': 0,
 | 
			
		||||
            'group_id': 0,
 | 
			
		||||
            'grader': 0,
 | 
			
		||||
            'residence': 0,
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        statics.inited = True
 | 
			
		||||
    
 | 
			
		||||
    imgui.table_next_row()
 | 
			
		||||
    imgui.table_set_column_index(0)
 | 
			
		||||
    imgui.text("First Name")
 | 
			
		||||
    imgui.table_set_column_index(1)
 | 
			
		||||
    _, statics.buffer['prename'] = imgui.input_text("##prename", statics.buffer['prename'])
 | 
			
		||||
 | 
			
		||||
    imgui.table_next_row()
 | 
			
		||||
    imgui.table_set_column_index(0)
 | 
			
		||||
    imgui.text("Last Name")
 | 
			
		||||
    imgui.table_set_column_index(1)
 | 
			
		||||
    _, statics.buffer['surname'] = imgui.input_text("##surname", statics.buffer['surname'])
 | 
			
		||||
   
 | 
			
		||||
    imgui.table_next_row()
 | 
			
		||||
    imgui.table_set_column_index(0)
 | 
			
		||||
    imgui.text("Gender")
 | 
			
		||||
    imgui.table_set_column_index(1)
 | 
			
		||||
    _, statics.buffer['sex'] = imgui.combo("##sex", statics.buffer['sex'], ['Male', 'Female'])
 | 
			
		||||
 | 
			
		||||
    imgui.table_next_row()
 | 
			
		||||
    imgui.table_set_column_index(0)
 | 
			
		||||
    imgui.text("Class")
 | 
			
		||||
    imgui.table_set_column_index(1)
 | 
			
		||||
    _, statics.buffer['class_id'] = imgui.combo("##class_id", statics.buffer['class_id'], statics.classes_labels)
 | 
			
		||||
    
 | 
			
		||||
    imgui.table_next_row()
 | 
			
		||||
    imgui.table_set_column_index(0)
 | 
			
		||||
    imgui.text("Project Group")
 | 
			
		||||
    imgui.table_set_column_index(1)
 | 
			
		||||
    groups = tuple(Group.select().where(Group.class_id == statics.classes[statics.buffer['class_id']].id))
 | 
			
		||||
    _, statics.buffer['group_id'] = imgui.combo("##group_id", statics.buffer['group_id'], tuple(group.name for group in groups))
 | 
			
		||||
    
 | 
			
		||||
    imgui.table_next_row()
 | 
			
		||||
    imgui.table_set_column_index(0)
 | 
			
		||||
    imgui.text("Grader")
 | 
			
		||||
    imgui.table_set_column_index(1)
 | 
			
		||||
    _, statics.buffer['grader'] = imgui.combo("##grader", statics.buffer['grader'], statics.graders)
 | 
			
		||||
 | 
			
		||||
    imgui.table_next_row()
 | 
			
		||||
    imgui.table_set_column_index(0)
 | 
			
		||||
    imgui.text("Residence")
 | 
			
		||||
    imgui.table_set_column_index(1)
 | 
			
		||||
    _, statics.buffer['residence'] = imgui.combo("##residence", statics.buffer['residence'], [str(residence.id) for residence in statics.residences])
 | 
			
		||||
 | 
			
		||||
    if imgui.button(action):
 | 
			
		||||
        match action:
 | 
			
		||||
            case "Create":
 | 
			
		||||
                Student.create(
 | 
			
		||||
                    prename = statics.buffer['prename'],
 | 
			
		||||
                    surname = statics.buffer['surname'],
 | 
			
		||||
                    sex = 'Female' if statics.buffer['sex'] else 'Male',
 | 
			
		||||
                    class_id = statics.classes[statics.buffer['class_id']].id,
 | 
			
		||||
                    group_id = groups[statics.buffer['group_id']].id,
 | 
			
		||||
                    residence = statics.residences[statics.buffer['residence']] 
 | 
			
		||||
                )
 | 
			
		||||
            case "Update":
 | 
			
		||||
                pass
 | 
			
		||||
            case "Delete":
 | 
			
		||||
                pass
 | 
			
		||||
        statics.inited = False
 | 
			
		||||
        imgui.close_current_popup()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								learnlytics/gui/gui.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								learnlytics/gui/gui.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
"""
 | 
			
		||||
Student Analyzer Application
 | 
			
		||||
 | 
			
		||||
This script initializes and runs the Student Analyzer application, which provides an interface for
 | 
			
		||||
managing student data, class records, and submissions. It uses the Hello ImGui framework for UI rendering
 | 
			
		||||
and integrates a database to store and manipulate student information.
 | 
			
		||||
 | 
			
		||||
Modules:
 | 
			
		||||
    - Custom Imports: Imports internal models and application state.
 | 
			
		||||
    - Layouts: Defines different UI layouts for the analyzer and database editor.
 | 
			
		||||
    - External Libraries: Uses imgui_bundle and Hello ImGui for UI rendering.
 | 
			
		||||
    - Built-in Libraries: Uses shelve for persistent state storage and typing for type hints.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# Custom
 | 
			
		||||
from dbmodel import *  # Importing database models like Class, Student, Lecture, and Submission
 | 
			
		||||
 | 
			
		||||
# External
 | 
			
		||||
from imgui_bundle import imgui, hello_imgui  # ImGui-based UI framework
 | 
			
		||||
 | 
			
		||||
def menu_bar(runner_params: hello_imgui.RunnerParams) -> None:
 | 
			
		||||
    """Defines the application's menu bar."""
 | 
			
		||||
    hello_imgui.show_app_menu(runner_params)
 | 
			
		||||
    hello_imgui.show_view_menu(runner_params)
 | 
			
		||||
    
 | 
			
		||||
def status_bar() -> None:
 | 
			
		||||
    """Displays the status bar information."""
 | 
			
		||||
    imgui.text("Student Analyzer by @DerGrumpf")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										73
									
								
								learnlytics/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								learnlytics/main.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
import shelve
 | 
			
		||||
 | 
			
		||||
from imgui_bundle import (
 | 
			
		||||
    hello_imgui,
 | 
			
		||||
    immapp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from gui import (
 | 
			
		||||
    analyzer_layout,
 | 
			
		||||
    database_editor_layout,
 | 
			
		||||
    menu_bar,
 | 
			
		||||
    status_bar
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from dbmodel import init_db
 | 
			
		||||
 | 
			
		||||
def main() -> None:
 | 
			
		||||
    """Main function to initialize and run the application."""
 | 
			
		||||
        
 | 
			
		||||
    # Load Database
 | 
			
		||||
    try:
 | 
			
		||||
        with shelve.open("state") as state:
 | 
			
		||||
            v = state.get("DB")  # Retrieve stored database connection info
 | 
			
		||||
            init_db(v)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(e)
 | 
			
		||||
 | 
			
		||||
    # Set Window Parameters
 | 
			
		||||
    runner_params = hello_imgui.RunnerParams()
 | 
			
		||||
    runner_params.app_window_params.window_title = "Analyzer"
 | 
			
		||||
    runner_params.imgui_window_params.menu_app_title = "Analyzer"
 | 
			
		||||
    runner_params.app_window_params.window_geometry.size = (1000, 900)
 | 
			
		||||
    runner_params.app_window_params.restore_previous_geometry = True
 | 
			
		||||
    runner_params.app_window_params.borderless = True
 | 
			
		||||
    runner_params.app_window_params.borderless_movable = True 
 | 
			
		||||
    runner_params.app_window_params.borderless_resizable = True 
 | 
			
		||||
    runner_params.app_window_params.borderless_closable = True 
 | 
			
		||||
 | 
			
		||||
    # Configure UI Elements
 | 
			
		||||
    runner_params.imgui_window_params.show_menu_bar = True
 | 
			
		||||
    runner_params.imgui_window_params.show_menu_app = False
 | 
			
		||||
    runner_params.imgui_window_params.show_menu_view = False
 | 
			
		||||
    runner_params.imgui_window_params.show_status_bar = True
 | 
			
		||||
    runner_params.callbacks.show_menus = lambda: menu_bar(runner_params)
 | 
			
		||||
    runner_params.callbacks.show_status = lambda: status_bar()
 | 
			
		||||
 | 
			
		||||
    # Application layout
 | 
			
		||||
    runner_params.imgui_window_params.default_imgui_window_type = (
 | 
			
		||||
        hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
 | 
			
		||||
    )
 | 
			
		||||
    runner_params.imgui_window_params.enable_viewports = True
 | 
			
		||||
    runner_params.docking_params = analyzer_layout()
 | 
			
		||||
    runner_params.alternative_docking_layouts = [
 | 
			
		||||
        database_editor_layout()
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # Save App Settings
 | 
			
		||||
    runner_params.ini_folder_type = hello_imgui.IniFolderType.app_user_config_folder
 | 
			
		||||
    runner_params.ini_filename = "Analyzer/Analyzer.ini"
 | 
			
		||||
    runner_params.docking_params.layout_condition = hello_imgui.DockingLayoutCondition.application_start
 | 
			
		||||
 | 
			
		||||
    # Run the Application
 | 
			
		||||
    add_ons_params = immapp.AddOnsParams()
 | 
			
		||||
    add_ons_params.with_markdown = True
 | 
			
		||||
    add_ons_params.with_implot = True
 | 
			
		||||
    add_ons_params.with_implot3d = True
 | 
			
		||||
 | 
			
		||||
    immapp.run(runner_params, add_ons_params)
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										696
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										696
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,696 @@
 | 
			
		||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "annotated-types"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
description = "Reusable constraint types to use with typing.Annotated"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
 | 
			
		||||
    {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "colour"
 | 
			
		||||
version = "0.1.5"
 | 
			
		||||
description = "converts and manipulates various color representation (HSL, RVB, web, X11, ...)"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"},
 | 
			
		||||
    {file = "colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
test = ["nose"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "flatpak-pip-generator"
 | 
			
		||||
version = "24.0.0"
 | 
			
		||||
description = "this is an unofficial pypi distribution of the flatpak_pip_generator script"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "flatpak_pip_generator-24.0.0-py3-none-any.whl", hash = "sha256:9ce8dac77eb2b6423af32ed44987204cf348f2e0e76400c1ba59304c124f8b19"},
 | 
			
		||||
    {file = "flatpak_pip_generator-24.0.0.tar.gz", hash = "sha256:cfe72c05b8d13084bd2aaabc107384924f1bcd5132832162b2a3dc0d2d2bda34"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
pyyaml = "*"
 | 
			
		||||
requirements-parser = "*"
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
build = ["build", "setuptools", "twine"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "glfw"
 | 
			
		||||
version = "2.8.0"
 | 
			
		||||
description = "A ctypes-based wrapper for GLFW3."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl", hash = "sha256:28aaef2f022b57cd37525ad1d11ba9049931a273359964fb3cd36762eb093ed1"},
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl", hash = "sha256:0239e478ff065719064fd1272ad29f8542e8178b11c614674bb930d85aa2d1e7"},
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_aarch64.whl", hash = "sha256:44b5313cffa4a037e8e2988bccba6fa7de0a24123509f4ccf3e49b831cf72abb"},
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_x86_64.whl", hash = "sha256:0fd982cf42a8e3137e05e392280826311961f9e99c82a0ccf22a63a0d2acd143"},
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_aarch64.whl", hash = "sha256:523e1fc03898bcb0711f78d6f21eee58c1f865bb764cbd36b9549580a4c43454"},
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl", hash = "sha256:9cd20351b14587c6fe7063afb33cc03152e38dd2bff2b69613cb044bf3bdb635"},
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win32.whl", hash = "sha256:13a75d8d3e8d4bb24595f2354a392ccf7c54ddd20dacb88e1188b760f2a7714b"},
 | 
			
		||||
    {file = "glfw-2.8.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl", hash = "sha256:1416f10d2c2748c39910e9d9e6a10a5473743c5a745518061e4051be4c5caaa1"},
 | 
			
		||||
    {file = "glfw-2.8.0.tar.gz", hash = "sha256:90e90d328b0b26fed6e1631d21801e2d8a7a0c5dcb480e733c177567ec9666f0"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
preview = ["glfw-preview"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "imgui-bundle"
 | 
			
		||||
version = "1.6.2"
 | 
			
		||||
description = "Dear ImGui Bundle: easily create ImGui applications in Python and C++. Batteries included!"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.10"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:dbf2a82baf6d8ab4787ed531baa339c2c3f8f76f2c70f3c764f8f3c3e6ed2097"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:519194a5b4fd3c1748bcf5b9afb6575b883e68c87947ee1332f4fa27fbcfcd14"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80e96d31e1ce56df6de1120c0f0764ab54e6af1efd1752499133cba7e879b1e9"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3baefbb7374909541900dec15d4630f06e4f1413c3e6560ec619e210a0f5d101"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:e6ba109dbc788a217d283cb7159391f40f6efacd79d8d1a4fa61962220573fbc"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:486e8408d4b0e3c2c8ef1814ddfde0aa87d25e6f8a650b7ccda672a2e1b47062"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:1ec8db8526eb6ad7dfe77c38ea4a19d5beaef2f79a0b080515929df77ac0c5fc"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5152504650622110559d5039fa60b09da91a5b7f8bcd4065fdf7f053b1967ad7"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:40a4f4d1a89b1e5b421077cbdde3f217b0b24468875c930625c9f5749421617f"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:add71c6733b6785e48bb059472e10e49d7ef56108fac7eb2bf05692cc6040b73"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f2e4494339209a8417d745658b4d36efe03949536a2b02492a03b2eb8f187850"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2a26dd3475271caf0ea5ca800bde95c9bc4cc889739869d9c6c0335c11d579e4"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3316e7227d25a1618205db478a7c8258a6f667f9e6a532e6a1075e26c33ded1e"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c76d14d2a270ff8b5de55aa8fafc255402570ebe6f3cbe0a6fd030c26f48b217"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:4bf9c0afb85dfdb9ca49e63d388a2a6e5b8935a5d9552fe7c51b3969caa5d4f8"},
 | 
			
		||||
    {file = "imgui_bundle-1.6.2.tar.gz", hash = "sha256:17b2019a0d4ebb66bc29b234c086ea5a3cccc7c0c540517fe569d84372316970"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
glfw = "*"
 | 
			
		||||
munch = "*"
 | 
			
		||||
numpy = "*"
 | 
			
		||||
pillow = "*"
 | 
			
		||||
pydantic = "*"
 | 
			
		||||
PyOpenGL = "*"
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
test = ["pytest"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "munch"
 | 
			
		||||
version = "4.0.0"
 | 
			
		||||
description = "A dot-accessible dictionary (a la JavaScript objects)"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.6"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "munch-4.0.0-py2.py3-none-any.whl", hash = "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4"},
 | 
			
		||||
    {file = "munch-4.0.0.tar.gz", hash = "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
testing = ["astroid (>=2.0)", "coverage", "pylint (>=2.3.1,<2.4.0)", "pytest"]
 | 
			
		||||
yaml = ["PyYAML (>=5.1.0)"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "numpy"
 | 
			
		||||
version = "2.2.3"
 | 
			
		||||
description = "Fundamental package for array computing in Python"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.10"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"},
 | 
			
		||||
    {file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"},
 | 
			
		||||
    {file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d"},
 | 
			
		||||
    {file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9"},
 | 
			
		||||
    {file = "numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e"},
 | 
			
		||||
    {file = "numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4"},
 | 
			
		||||
    {file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "packaging"
 | 
			
		||||
version = "24.2"
 | 
			
		||||
description = "Core utilities for Python packages"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
 | 
			
		||||
    {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pandas"
 | 
			
		||||
version = "2.2.3"
 | 
			
		||||
description = "Powerful data structures for data analysis, time series, and statistics"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.9"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"},
 | 
			
		||||
    {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"},
 | 
			
		||||
    {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""}
 | 
			
		||||
python-dateutil = ">=2.8.2"
 | 
			
		||||
pytz = ">=2020.1"
 | 
			
		||||
tzdata = ">=2022.7"
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"]
 | 
			
		||||
aws = ["s3fs (>=2022.11.0)"]
 | 
			
		||||
clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"]
 | 
			
		||||
compression = ["zstandard (>=0.19.0)"]
 | 
			
		||||
computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"]
 | 
			
		||||
consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
 | 
			
		||||
excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"]
 | 
			
		||||
feather = ["pyarrow (>=10.0.1)"]
 | 
			
		||||
fss = ["fsspec (>=2022.11.0)"]
 | 
			
		||||
gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"]
 | 
			
		||||
hdf5 = ["tables (>=3.8.0)"]
 | 
			
		||||
html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"]
 | 
			
		||||
mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"]
 | 
			
		||||
output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"]
 | 
			
		||||
parquet = ["pyarrow (>=10.0.1)"]
 | 
			
		||||
performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"]
 | 
			
		||||
plot = ["matplotlib (>=3.6.3)"]
 | 
			
		||||
postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"]
 | 
			
		||||
pyarrow = ["pyarrow (>=10.0.1)"]
 | 
			
		||||
spss = ["pyreadstat (>=1.2.0)"]
 | 
			
		||||
sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"]
 | 
			
		||||
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
 | 
			
		||||
xml = ["lxml (>=4.9.2)"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pdf2image"
 | 
			
		||||
version = "1.17.0"
 | 
			
		||||
description = "A wrapper around the pdftoppm and pdftocairo command line tools to convert PDF to a PIL Image list."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2"},
 | 
			
		||||
    {file = "pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
pillow = "*"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "peewee"
 | 
			
		||||
version = "3.17.9"
 | 
			
		||||
description = "a little orm"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "peewee-3.17.9.tar.gz", hash = "sha256:fe15cd001758e324c8e3ca8c8ed900e7397c2907291789e1efc383e66b9bc7a8"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pillow"
 | 
			
		||||
version = "11.1.0"
 | 
			
		||||
description = "Python Imaging Library (Fork)"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.9"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"},
 | 
			
		||||
    {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"},
 | 
			
		||||
    {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"},
 | 
			
		||||
    {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"},
 | 
			
		||||
    {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"},
 | 
			
		||||
    {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"},
 | 
			
		||||
    {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"},
 | 
			
		||||
    {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"},
 | 
			
		||||
    {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"},
 | 
			
		||||
    {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
 | 
			
		||||
fpx = ["olefile"]
 | 
			
		||||
mic = ["olefile"]
 | 
			
		||||
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"]
 | 
			
		||||
typing = ["typing-extensions"]
 | 
			
		||||
xmp = ["defusedxml"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pydantic"
 | 
			
		||||
version = "2.10.6"
 | 
			
		||||
description = "Data validation using Python type hints"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
 | 
			
		||||
    {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
annotated-types = ">=0.6.0"
 | 
			
		||||
pydantic-core = "2.27.2"
 | 
			
		||||
typing-extensions = ">=4.12.2"
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
email = ["email-validator (>=2.0.0)"]
 | 
			
		||||
timezone = ["tzdata"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pydantic-core"
 | 
			
		||||
version = "2.27.2"
 | 
			
		||||
description = "Core functionality for Pydantic validation and serialization"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
 | 
			
		||||
    {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pyopengl"
 | 
			
		||||
version = "3.1.9"
 | 
			
		||||
description = "Standard OpenGL bindings for Python"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "PyOpenGL-3.1.9-py3-none-any.whl", hash = "sha256:15995fd3b0deb991376805da36137a4ae5aba6ddbb5e29ac1f35462d130a3f77"},
 | 
			
		||||
    {file = "pyopengl-3.1.9.tar.gz", hash = "sha256:28ebd82c5f4491a418aeca9672dffb3adbe7d33b39eada4548a5b4e8c03f60c8"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "python-dateutil"
 | 
			
		||||
version = "2.9.0.post0"
 | 
			
		||||
description = "Extensions to the standard Python datetime module"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
 | 
			
		||||
    {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
six = ">=1.5"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pytz"
 | 
			
		||||
version = "2025.1"
 | 
			
		||||
description = "World timezone definitions, modern and historical"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"},
 | 
			
		||||
    {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pyyaml"
 | 
			
		||||
version = "6.0.2"
 | 
			
		||||
description = "YAML parser and emitter for Python"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
 | 
			
		||||
    {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
 | 
			
		||||
    {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "requirements-parser"
 | 
			
		||||
version = "0.11.0"
 | 
			
		||||
description = "This is a small Python module for parsing Pip requirement files."
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "<4.0,>=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "requirements_parser-0.11.0-py3-none-any.whl", hash = "sha256:50379eb50311834386c2568263ae5225d7b9d0867fb55cf4ecc93959de2c2684"},
 | 
			
		||||
    {file = "requirements_parser-0.11.0.tar.gz", hash = "sha256:35f36dc969d14830bf459803da84f314dc3d17c802592e9e970f63d0359e5920"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
packaging = ">=23.2"
 | 
			
		||||
types-setuptools = ">=69.1.0"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "six"
 | 
			
		||||
version = "1.17.0"
 | 
			
		||||
description = "Python 2 and 3 compatibility utilities"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
 | 
			
		||||
    {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "types-setuptools"
 | 
			
		||||
version = "75.8.0.20250225"
 | 
			
		||||
description = "Typing stubs for setuptools"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.9"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "types_setuptools-75.8.0.20250225-py3-none-any.whl", hash = "sha256:94c86b439cc60bcc68c1cda3fd2c301f007f8f9502f4fbb54c66cb5ce9b875af"},
 | 
			
		||||
    {file = "types_setuptools-75.8.0.20250225.tar.gz", hash = "sha256:6038f7e983d55792a5f90d8fdbf5d4c186026214a16bb65dd6ae83c624ae9636"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "typing-extensions"
 | 
			
		||||
version = "4.12.2"
 | 
			
		||||
description = "Backported and Experimental Type Hints for Python 3.8+"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.8"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
 | 
			
		||||
    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tzdata"
 | 
			
		||||
version = "2025.1"
 | 
			
		||||
description = "Provider of IANA time zone data"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=2"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"},
 | 
			
		||||
    {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tzlocal"
 | 
			
		||||
version = "5.3"
 | 
			
		||||
description = "tzinfo object for the local timezone"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.9"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c"},
 | 
			
		||||
    {file = "tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
 | 
			
		||||
 | 
			
		||||
[metadata]
 | 
			
		||||
lock-version = "2.0"
 | 
			
		||||
python-versions = "^3.12"
 | 
			
		||||
content-hash = "7112b24c85d31e228e464b5c9621e985ae19504e26e1fa6c7f32d8afbf33b86d"
 | 
			
		||||
							
								
								
									
										31
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
[tool.poetry]
 | 
			
		||||
name = "learnlytics"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
description = ""
 | 
			
		||||
authors = ["DerGrumpf <p.keier@beyerstedt-it.de>"]
 | 
			
		||||
readme = "README.md"
 | 
			
		||||
 | 
			
		||||
[tool.poetry.group.dev.dependencies]
 | 
			
		||||
flatpak-pip-generator = "^24.0.0"
 | 
			
		||||
pandas = "^2.2.3"
 | 
			
		||||
 | 
			
		||||
[project]
 | 
			
		||||
package-mode = false
 | 
			
		||||
 | 
			
		||||
[project.scripts]
 | 
			
		||||
main = "learnlytics:main.py"
 | 
			
		||||
 | 
			
		||||
[tool.poetry.dependencies]
 | 
			
		||||
python = "^3.12"
 | 
			
		||||
numpy = "^2.2.3"
 | 
			
		||||
imgui-bundle = "^1.6.2"
 | 
			
		||||
peewee = "^3.17.9"
 | 
			
		||||
colour = "^0.1.5"
 | 
			
		||||
pytz = "^2025.1"
 | 
			
		||||
tzlocal = "^5.3"
 | 
			
		||||
pdf2image = "^1.17.0"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[build-system]
 | 
			
		||||
requires = ["poetry-core"]
 | 
			
		||||
build-backend = "poetry.core.masonry.api"
 | 
			
		||||
							
								
								
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
		Reference in New Issue
	
	Block a user