commit 50920532bf75ea3017ceac4c030fa55cfa785624 Author: DerGrumpf Date: Sat Mar 1 01:50:06 2025 +0100 Initial diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/assets/Presentations/DiKum.pdf b/assets/Presentations/DiKum.pdf new file mode 100644 index 0000000..1b41a22 Binary files /dev/null and b/assets/Presentations/DiKum.pdf differ diff --git a/assets/Presentations/MeWi_1.pdf b/assets/Presentations/MeWi_1.pdf new file mode 100644 index 0000000..59ee5fb Binary files /dev/null and b/assets/Presentations/MeWi_1.pdf differ diff --git a/assets/Presentations/MeWi_2.pdf b/assets/Presentations/MeWi_2.pdf new file mode 100644 index 0000000..28b3bd6 Binary files /dev/null and b/assets/Presentations/MeWi_2.pdf differ diff --git a/assets/Presentations/MeWi_3.pdf b/assets/Presentations/MeWi_3.pdf new file mode 100644 index 0000000..044b11d Binary files /dev/null and b/assets/Presentations/MeWi_3.pdf differ diff --git a/assets/Presentations/MeWi_4.pdf b/assets/Presentations/MeWi_4.pdf new file mode 100644 index 0000000..515e701 Binary files /dev/null and b/assets/Presentations/MeWi_4.pdf differ diff --git a/assets/Presentations/MeWi_5.pdf b/assets/Presentations/MeWi_5.pdf new file mode 100644 index 0000000..5252d6f Binary files /dev/null and b/assets/Presentations/MeWi_5.pdf differ diff --git a/assets/Presentations/MeWi_6.pdf b/assets/Presentations/MeWi_6.pdf new file mode 100644 index 0000000..6b957d0 Binary files /dev/null and b/assets/Presentations/MeWi_6.pdf differ diff --git a/assets/Student_list.csv b/assets/Student_list.csv new file mode 100644 index 0000000..f88d8db --- /dev/null +++ b/assets/Student_list.csv @@ -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 diff --git a/assets/WiSe_24_25.db b/assets/WiSe_24_25.db new file mode 100644 index 0000000..1560518 Binary files /dev/null and b/assets/WiSe_24_25.db differ diff --git a/assets/convert.py b/assets/convert.py new file mode 100644 index 0000000..f4ed9e5 --- /dev/null +++ b/assets/convert.py @@ -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 + ) + + diff --git a/assets/covid_faelle_MeWi_2.html b/assets/covid_faelle_MeWi_2.html new file mode 100644 index 0000000..c370e2f --- /dev/null +++ b/assets/covid_faelle_MeWi_2.html @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/learnlytics/dbmodel/__init__.py b/learnlytics/dbmodel/__init__.py new file mode 100644 index 0000000..228f949 --- /dev/null +++ b/learnlytics/dbmodel/__init__.py @@ -0,0 +1,10 @@ +from .utils import ( + tables, + table_labels, + init_db, + save_as_json, + create_from_json +) + +from .model import * + diff --git a/learnlytics/dbmodel/__pycache__/__init__.cpython-312.pyc b/learnlytics/dbmodel/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..f7715d0 Binary files /dev/null and b/learnlytics/dbmodel/__pycache__/__init__.cpython-312.pyc differ diff --git a/learnlytics/dbmodel/__pycache__/model.cpython-312.pyc b/learnlytics/dbmodel/__pycache__/model.cpython-312.pyc new file mode 100644 index 0000000..6c2d830 Binary files /dev/null and b/learnlytics/dbmodel/__pycache__/model.cpython-312.pyc differ diff --git a/learnlytics/dbmodel/__pycache__/utils.cpython-312.pyc b/learnlytics/dbmodel/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..d564837 Binary files /dev/null and b/learnlytics/dbmodel/__pycache__/utils.cpython-312.pyc differ diff --git a/learnlytics/dbmodel/model.dbml b/learnlytics/dbmodel/model.dbml new file mode 100644 index 0000000..20f9fa9 --- /dev/null +++ b/learnlytics/dbmodel/model.dbml @@ -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 +} + + diff --git a/learnlytics/dbmodel/model.py b/learnlytics/dbmodel/model.py new file mode 100644 index 0000000..fcae001 --- /dev/null +++ b/learnlytics/dbmodel/model.py @@ -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) + + diff --git a/learnlytics/dbmodel/utils.py b/learnlytics/dbmodel/utils.py new file mode 100644 index 0000000..612770f --- /dev/null +++ b/learnlytics/dbmodel/utils.py @@ -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 .json to given + + JSON Format: + { + : [ + {: , ...} + ], + ... + } + ''' + + + 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 .db in from given + + Valid JSON Format: + { + : [ + {: , ...} + ], + ... + } + ''' + + 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)) + + + diff --git a/learnlytics/grader/__init__.py b/learnlytics/grader/__init__.py new file mode 100644 index 0000000..a9b0dee --- /dev/null +++ b/learnlytics/grader/__init__.py @@ -0,0 +1,9 @@ +from .valuation import ( + get_gradings, + get_grader, + Std30PercentRule, + Std50PercentRule, + StdGermanGradingMiddleSchool, + StdGermanGradingHighSchool +) + diff --git a/learnlytics/grader/__pycache__/__init__.cpython-312.pyc b/learnlytics/grader/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d321b09 Binary files /dev/null and b/learnlytics/grader/__pycache__/__init__.cpython-312.pyc differ diff --git a/learnlytics/grader/__pycache__/valuation.cpython-312.pyc b/learnlytics/grader/__pycache__/valuation.cpython-312.pyc new file mode 100644 index 0000000..154ad03 Binary files /dev/null and b/learnlytics/grader/__pycache__/valuation.cpython-312.pyc differ diff --git a/learnlytics/grader/valuation.py b/learnlytics/grader/valuation.py new file mode 100644 index 0000000..ef387aa --- /dev/null +++ b/learnlytics/grader/valuation.py @@ -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)) + diff --git a/learnlytics/gui/__init__.py b/learnlytics/gui/__init__.py new file mode 100644 index 0000000..6737ef8 --- /dev/null +++ b/learnlytics/gui/__init__.py @@ -0,0 +1,3 @@ +from .analyzer import analyzer_layout +from .database import database_editor_layout +from .gui import menu_bar, status_bar diff --git a/learnlytics/gui/__pycache__/__init__.cpython-312.pyc b/learnlytics/gui/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..63da1a4 Binary files /dev/null and b/learnlytics/gui/__pycache__/__init__.cpython-312.pyc differ diff --git a/learnlytics/gui/__pycache__/gui.cpython-312.pyc b/learnlytics/gui/__pycache__/gui.cpython-312.pyc new file mode 100644 index 0000000..c914127 Binary files /dev/null and b/learnlytics/gui/__pycache__/gui.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__init__.py b/learnlytics/gui/analyzer/__init__.py new file mode 100644 index 0000000..9c0dbff --- /dev/null +++ b/learnlytics/gui/analyzer/__init__.py @@ -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 + diff --git a/learnlytics/gui/analyzer/__pycache__/__init__.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..cb580a0 Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/__init__.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__pycache__/analyzer.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/analyzer.cpython-312.pyc new file mode 100644 index 0000000..e5cc00d Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/analyzer.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__pycache__/analyzer_state.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/analyzer_state.cpython-312.pyc new file mode 100644 index 0000000..5162582 Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/analyzer_state.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__pycache__/group_graph.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/group_graph.cpython-312.pyc new file mode 100644 index 0000000..b8f442e Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/group_graph.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__pycache__/plot_bar_line.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/plot_bar_line.cpython-312.pyc new file mode 100644 index 0000000..a042e2f Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/plot_bar_line.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__pycache__/plotter.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/plotter.cpython-312.pyc new file mode 100644 index 0000000..090ace9 Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/plotter.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__pycache__/student_graph.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/student_graph.cpython-312.pyc new file mode 100644 index 0000000..7013b86 Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/student_graph.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/__pycache__/student_list.cpython-312.pyc b/learnlytics/gui/analyzer/__pycache__/student_list.cpython-312.pyc new file mode 100644 index 0000000..72b197c Binary files /dev/null and b/learnlytics/gui/analyzer/__pycache__/student_list.cpython-312.pyc differ diff --git a/learnlytics/gui/analyzer/analyzer.py b/learnlytics/gui/analyzer/analyzer.py new file mode 100644 index 0000000..d7850c5 --- /dev/null +++ b/learnlytics/gui/analyzer/analyzer.py @@ -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 + + + diff --git a/learnlytics/gui/analyzer/analyzer_state.py b/learnlytics/gui/analyzer/analyzer_state.py new file mode 100644 index 0000000..77c4d45 --- /dev/null +++ b/learnlytics/gui/analyzer/analyzer_state.py @@ -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} + ''' diff --git a/learnlytics/gui/analyzer/group_graph.py b/learnlytics/gui/analyzer/group_graph.py new file mode 100644 index 0000000..c6f476f --- /dev/null +++ b/learnlytics/gui/analyzer/group_graph.py @@ -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() diff --git a/learnlytics/gui/analyzer/plotter.py b/learnlytics/gui/analyzer/plotter.py new file mode 100644 index 0000000..c2f42ad --- /dev/null +++ b/learnlytics/gui/analyzer/plotter.py @@ -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 + diff --git a/learnlytics/gui/analyzer/student_graph.py b/learnlytics/gui/analyzer/student_graph.py new file mode 100644 index 0000000..62221de --- /dev/null +++ b/learnlytics/gui/analyzer/student_graph.py @@ -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() diff --git a/learnlytics/gui/analyzer/student_list.py b/learnlytics/gui/analyzer/student_list.py new file mode 100644 index 0000000..216732f --- /dev/null +++ b/learnlytics/gui/analyzer/student_list.py @@ -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) diff --git a/learnlytics/gui/database/__init__.py b/learnlytics/gui/database/__init__.py new file mode 100644 index 0000000..8566221 --- /dev/null +++ b/learnlytics/gui/database/__init__.py @@ -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 + diff --git a/learnlytics/gui/database/__pycache__/__init__.cpython-312.pyc b/learnlytics/gui/database/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..08f01d0 Binary files /dev/null and b/learnlytics/gui/database/__pycache__/__init__.cpython-312.pyc differ diff --git a/learnlytics/gui/database/__pycache__/database.cpython-312.pyc b/learnlytics/gui/database/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..b6df750 Binary files /dev/null and b/learnlytics/gui/database/__pycache__/database.cpython-312.pyc differ diff --git a/learnlytics/gui/database/__pycache__/editor.cpython-312.pyc b/learnlytics/gui/database/__pycache__/editor.cpython-312.pyc new file mode 100644 index 0000000..35b5ab9 Binary files /dev/null and b/learnlytics/gui/database/__pycache__/editor.cpython-312.pyc differ diff --git a/learnlytics/gui/database/database.py b/learnlytics/gui/database/database.py new file mode 100644 index 0000000..dd45ef9 --- /dev/null +++ b/learnlytics/gui/database/database.py @@ -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() + + + + diff --git a/learnlytics/gui/database/editor.py b/learnlytics/gui/database/editor.py new file mode 100644 index 0000000..c43153c --- /dev/null +++ b/learnlytics/gui/database/editor.py @@ -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() + + diff --git a/learnlytics/gui/gui.py b/learnlytics/gui/gui.py new file mode 100644 index 0000000..5b68b65 --- /dev/null +++ b/learnlytics/gui/gui.py @@ -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") + + diff --git a/learnlytics/main.py b/learnlytics/main.py new file mode 100644 index 0000000..6c34e37 --- /dev/null +++ b/learnlytics/main.py @@ -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() + + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..07e8f53 --- /dev/null +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c5cdf6e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[tool.poetry] +name = "learnlytics" +version = "0.1.0" +description = "" +authors = ["DerGrumpf "] +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" diff --git a/state b/state new file mode 100644 index 0000000..acb6f81 Binary files /dev/null and b/state differ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29