This commit is contained in:
DerGrumpf 2025-03-01 01:50:06 +01:00
commit 50920532bf
53 changed files with 3138 additions and 0 deletions

0
README.md Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

36
assets/Student_list.csv Normal file
View 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
1 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
2 Abdalaziz Abunjaila Male DiKum 30% 30.5 15 18 28 17 17 17 22 0 18
3 Marleen Adolphi Female MeWi6 30% 29.5 15 18 32 19 20 17 24 23 0
4 Sarina Apel Female MeWi1 30% 28.5 15 18 32 20 20 21 24 20 23
5 Skofiare Berisha Female DiKum 30% 29.5 13 18 34 20 17 20 26 16 0
6 Aurela Brahimi Female MeWi2 30% 17.5 15 15.5 26 16 17 19 16 0 0
7 Cam Thu Do Female MeWi3 30% 31 15 18 34 19 20 21.5 22 12 0
8 Nova Eib Female MeWi4 30% 31 15 15 34 20 20 21 27 19 21
9 Lena Fricke Female MeWi4 30% 0 0 0 0 0 0 0 0 0 0
10 Nele Grundke Female MeWi6 30% 23.5 13 16 28 20 17 21 18 22 11
11 Anna Grünewald Female MeWi3 30% 12 14 16 29 16 15 19 9 0 0
12 Yannik Haupt Male NoGroup 30% 18 6 14 21 13 2 9 0 0 0
13 Janna Heiny Female MeWi1 30% 30 15 18 33 18 20 22 25 24 30
14 Milena Krieger Female MeWi1 30% 30 15 18 33 20 20 21.5 26 20 22
15 Julia Limbach Female MeWi6 30% 27.5 12 18 29 11 19 17.5 26 24 28
16 Viktoria Litza Female MeWi5 30% 21.5 15 18 27 13 20 22 21 21 30
17 Leonie Manthey Female MeWi1 30% 28.5 14 18 29 20 10 18 23 16 28
18 Izabel Mike Female MeWi2 30% 29.5 15 15 35 11 15 19 21 21 27
19 Lea Noglik Female MeWi5 30% 22.5 15 17 34 13 10 20 21 19 6
20 Donika Nuhiu Female MeWi5 30% 31 13.5 18 35 14 10 17 18 19 8
21 Julia Renner Female MeWi4 30% 27.5 10 14 32 20 17 11 20 24 14
22 Fabian Rothberger Male MeWi3 30% 30.5 15 18 34 17 17 19 22 18 30
23 Natascha Rott Female MeWi1 30% 29.5 12 18 32 19 20 21 26 23 26
24 Isabel Rudolf Female MeWi4 30% 27.5 9 17 34 16 19 19 21 16 14
25 Melina Sablotny Female MeWi6 30% 31 15 18 33 20 20 21 19 11 28
26 Alea Schleier Female DiKum 30% 27 14 18 34 16 18 21.5 22 15 22
27 Flemming Schur Male MeWi3 30% 29.5 15 17 34 19 20 19 22 18 27
28 Marie Seeger Female DiKum 30% 27.5 15 18 32 14 9 17 22 9 25
29 Lucy Thiele Female MeWi6 30% 27.5 15 18 27 20 17 19 18 22 25
30 Lara Troschke Female MeWi2 30% 28.5 14 17 28 13 19 21 25 12 24
31 Inga-Brit Turschner Female MeWi2 30% 25.5 14 18 34 20 16 19 22 17 30
32 Alea Unger Female MeWi5 30% 30 12 18 31 20 20 21 22 15 21.5
33 Marie Wallbaum Female MeWi5 30% 28.5 14 18 34 17 20 19 24 12 22
34 Katharina Walz Female MeWi4 30% 31 15 18 31 19 19 17 24 17 14.5
35 Xiaowei Wang Male NoGroup 30% 30.5 14 18 26 19 17 0 0 0 0
36 Lilly-Lu Warnken Female DiKum 30% 30 15 18 30 14 17 19 14 16 24

BIN
assets/WiSe_24_25.db Normal file

Binary file not shown.

67
assets/convert.py Normal file
View 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
)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
from .utils import (
tables,
table_labels,
init_db,
save_as_json,
create_from_json
)
from .model import *

Binary file not shown.

Binary file not shown.

View 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
}

View 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)

View 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))

View File

@ -0,0 +1,9 @@
from .valuation import (
get_gradings,
get_grader,
Std30PercentRule,
Std50PercentRule,
StdGermanGradingMiddleSchool,
StdGermanGradingHighSchool
)

View 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))

View File

@ -0,0 +1,3 @@
from .analyzer import analyzer_layout
from .database import database_editor_layout
from .gui import menu_bar, status_bar

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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}
'''

View 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()

View 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

View 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()

View 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)

View 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

View 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()

View 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
View 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
View 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
View 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
View 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"

BIN
state Normal file

Binary file not shown.

0
tests/__init__.py Normal file
View File