classresults 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ __authors__ = "Thomas BOBEE"
2
+ __authors_email__ = "thomasbobe3@gmail.com"
3
+ __version__ = "0.1.0"
@@ -0,0 +1,44 @@
1
+ import click
2
+ import sys
3
+ import re
4
+
5
+ from classresults.get_data import get_data
6
+
7
+ @click.command()
8
+ @click.argument('activity')
9
+ def main(activity: str):
10
+ students: list[str] = ["adam.mcibah", "ahash.likitharan", "ahmed.mezghani", "alex.debelli-gonin",
11
+ "alexis.kouvoua-fouka",
12
+ "baptiste.ducruy", "carol.legout-sales", "clement.raulo", "davy.beninos", "enzo.dugas",
13
+ "farah.tab",
14
+ "ghali.aboulal", "hong-cheng.taing", "ikram.heroual", "ilian.ben-senouci", "jacques.jiao",
15
+ "joachim.joron",
16
+ "julie.domrane", "kilen.millot", "lehen.taranne", "levana.zran", "loic.rabetrano",
17
+ "marc-shadrack.bumsong-bell", "maxence.gouvaert-de-jesus", "milena.dziekan",
18
+ "mohamed-aziz.el-guares",
19
+ "mohamed-djibril.touil", "mohamed-rayan.belkacemi", "nada.habbane", "nathanael.isla-y-ortiz",
20
+ "noam.assouline", "omar.seif", "quentin.martinet", "robin.menoux", "samuel.huang",
21
+ "shadanaa.sivaraja",
22
+ "theo.egea", "theo.laradji", "timeo.borghesi", "victor.rousseau", "yoann.papadacci"]
23
+
24
+ # Comment this line once done
25
+ print("This needs a manual setup for students: edit the ClassResult/__main__.py file's students variable according to your students.")
26
+ regex = re.compile("prog-[0-9]0[0-9]-[pe]-[0-9]{2}-[0-9]{4}")
27
+ if len(sys.argv) != 2 or sys.argv[1] == "--help":
28
+ print("Usage: ClassResult <activity>")
29
+ print(f"\tactivity being the activity code with regex: {regex.pattern}")
30
+ print(f"\texample: ClassResult prog-101-02-p-2031 for the 2nd practicals of the 2031's first year and first bimester")
31
+ sys.exit(1)
32
+
33
+ activity = sys.argv[1]
34
+
35
+ if not regex.match(activity):
36
+ print(f"Activity name does not obey regular expression: {regex.pattern}")
37
+ sys.exit(1)
38
+
39
+ get_data(sys.argv[1], students)
40
+
41
+
42
+
43
+
44
+
@@ -0,0 +1,8 @@
1
+ if __name__ == "__main__":
2
+ import os
3
+ import sys
4
+
5
+ sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
6
+ from classresults import main
7
+
8
+ main()
@@ -0,0 +1,195 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+ import poulpy
5
+ import csv
6
+
7
+ import xlsxwriter
8
+
9
+ def get_jwt() -> str:
10
+ """
11
+ Returns a jwt token using poulpy
12
+ """
13
+ return poulpy.get_credentials(poulpy.DefaultProfiles[poulpy.DefaultProfiles.prod.name].value).jwt
14
+
15
+ def get_raw_data(activity: str, jwt: str) -> list[dict[str, Any]]:
16
+ """
17
+ Gets the trace data from the operator's api
18
+ :param activity: The activity code to get the data from (format: prog-<bimester>-p-<tp number>-<year>)
19
+ :param jwt: the jwt token
20
+ :return: The json formated trace data
21
+ """
22
+ url = (f"https://operator.forge.epita.fr/api/traces/epita-prepa-computer-science%2F{activity}/explore?pageNum=0&"
23
+ f"pageSize=100&published=true&submissionDefinitionUri=epita-prepa-computer-science%2F{activity}%2Froot%2F"
24
+ f"{activity}%2Fsubmit&submissionStatus=SUCCEEDED")
25
+ headers = {"Accept": "application/json", "Authorization": f"Bearer {jwt}"}
26
+
27
+ print("Sending request to operator's api")
28
+ response = requests.get(url, headers=headers)
29
+ if response.status_code != 200:
30
+ raise Exception(f"Invalid response: {response.status_code}")
31
+
32
+ json = response.json()
33
+ traces: list = json["results"]
34
+ pages = json["pageCount"]
35
+ print(f"Got: {pages} to load")
36
+ print("Loaded page number 1")
37
+ for i in range(1, pages):
38
+ url = (f"https://operator.forge.epita.fr/api/traces/epita-prepa-computer-science%2F{activity}/explore?"
39
+ f"pageNum={i}&pageSize=100&published=true&submissionDefinitionUri=epita-prepa-computer-science%2F"
40
+ f"{activity}%2Froot%2F{activity}%2Fsubmit&submissionStatus=SUCCEEDED")
41
+ response = requests.get(url, headers=headers)
42
+ if response.status_code != 200:
43
+ raise Exception(f"Invalid response: {response.status_code}")
44
+
45
+ traces.extend(response.json()["results"])
46
+ print(f"Loaded page number {i + 1}")
47
+
48
+ print("Finished loading traces")
49
+ return traces
50
+
51
+
52
+ def filter_class(data: list[dict[str, Any]], students) -> list[dict[str, Any]]:
53
+ """
54
+ Filters data according to students' login
55
+ :param data: The data to filter
56
+ :param students: The logins to keep
57
+ :return: The filtered data
58
+ """
59
+ filtered_data = []
60
+ for trace in data:
61
+ if trace["author"] in students and trace["author"] not in map(lambda d: d["author"], filtered_data):
62
+ filtered_data.append(trace)
63
+
64
+ print(f"Got trace data for {len(filtered_data)} students out of {len(students)}")
65
+
66
+ return filtered_data
67
+
68
+
69
+ def recreate_trace_url(activity: str, id: str) -> str:
70
+ """
71
+ Assembles the operator's trace url from activity and trace id
72
+ """
73
+ return f"https://operator.forge.epita.fr/epita-prepa-computer-science/{activity}/traces/{id}"
74
+
75
+
76
+ def recreate_impersonate_url(activity: str, login: str, id: str) -> str:
77
+ """
78
+ Assembles the intranet's impersonate url from activity and trace id
79
+ """
80
+ return f"https://intra.forge.epita.fr/epita-prepa-computer-science/{activity}/root/{activity}/{id}?login={login}"
81
+
82
+
83
+ def to_student_trace(studentdata: list[dict[str, Any]], activity: str) -> list[list]:
84
+ """
85
+ Returns a list with all useful data extracted from each trace data
86
+ """
87
+ studentTraces = []
88
+ for student in studentdata:
89
+ # s = studentTrace(student["author"], student["currentJob"]["successPercent"], student["currentJob"]["traceUrl"])
90
+ login = student["author"]
91
+ percent = student["currentJob"]["successPercent"]
92
+ studentTraces.append([login, percent, recreate_trace_url(activity, student["id"]),
93
+ recreate_impersonate_url(activity, login, student["id"])])
94
+
95
+ return studentTraces
96
+
97
+ def add_unsubmitted_students(traces: list[list], students: list[str]) -> list[list]:
98
+ """
99
+ Addes all student whose submission was not found as 'Did not submit' students
100
+ """
101
+ logins = list(map(lambda r: r[0], traces))
102
+
103
+ for student in students:
104
+ if student not in logins:
105
+ traces.append([student, "Did not submit", "Did not submit", "Did not submit"])
106
+
107
+ traces.sort(key=lambda r: r[0])
108
+ print(f"Added {len(traces) - len(logins)} students that did not submit")
109
+ return traces
110
+
111
+
112
+ def to_csv(traces, filename):
113
+ """
114
+ Saves the data to a csv file
115
+ """
116
+ with open(filename, "w", newline="") as f:
117
+ writer = csv.writer(f)
118
+ writer.writerow(["login", "percent", "operator url", "impersonate url"])
119
+ writer.writerows(traces)
120
+
121
+
122
+ def to_xlsx(traces, filename):
123
+ workbook = xlsxwriter.Workbook(filename)
124
+ sheet = workbook.add_worksheet()
125
+ sheet.write(0, 0, "login")
126
+ sheet.write(0, 1, "percent")
127
+ sheet.write(0, 2, "operator trace url")
128
+ sheet.write(0, 3, "impersonate url")
129
+ sheet.write(0, 4, "Did submit")
130
+
131
+ for row, (login, percent, url, impersonate) in enumerate(traces):
132
+ row += 1
133
+ sheet.write(row, 0, login)
134
+ if percent != "Did not submit":
135
+ sheet.write(row, 1, percent / 100)
136
+ sheet.write_url(row, 2, url, string="operator url")
137
+ sheet.write_url(row, 3, impersonate, string="impersonate url")
138
+ sheet.write(row, 4, 1)
139
+ else:
140
+ sheet.write(row, 1, 0)
141
+ sheet.write(row, 4, 0)
142
+
143
+ bad_grade = workbook.add_format({'bg_color': '#FF6D6D', 'num_format': 9})
144
+ low_grade = workbook.add_format({'bg_color': '#FFA365', 'num_format': 9})
145
+ mid_grade = workbook.add_format({'bg_color': '#FFFF8F', 'num_format': 9})
146
+ high_grade = workbook.add_format({'bg_color': '#A3FFA3', 'num_format': 9})
147
+ full_grade = workbook.add_format({'bg_color': '#57FF57', 'num_format': 9})
148
+ percent = workbook.add_format({'num_format': 9})
149
+
150
+ # Add conditionals
151
+ sheet.conditional_format(1, 1, len(traces), 1,
152
+ {"type": "cell", "criteria": "=", "value": 1, "format": full_grade})
153
+ sheet.conditional_format(1, 1, len(traces), 1,
154
+ {"type": "cell", "criteria": ">=", "value": .75, "format": high_grade})
155
+ sheet.conditional_format(1, 1, len(traces), 1,
156
+ {"type": "cell", "criteria": ">=", "value": .50, "format": mid_grade})
157
+ sheet.conditional_format(1, 1, len(traces), 1,
158
+ {"type": "cell", "criteria": ">=", "value": .25, "format": low_grade})
159
+ sheet.conditional_format(1, 1, len(traces), 1,
160
+ {"type": "cell", "criteria": "<", "value": .25, "format": bad_grade})
161
+
162
+ sheet.write(8, 8, "Moyenne des derniers submits: ")
163
+ sheet.write_formula(8, 9,
164
+ f"=AVERAGE(FILTER(B2:B{len(traces) + 1},E2:E{len(traces) + 1}))")
165
+ # Add conditionals
166
+ sheet.conditional_format(8, 9, 8, 9,
167
+ {"type": "cell", "criteria": "=", "value": 1, "format": full_grade})
168
+ sheet.conditional_format(8, 9, 8, 9,
169
+ {"type": "cell", "criteria": ">=", "value": .75, "format": high_grade})
170
+ sheet.conditional_format(8, 9, 8, 9,
171
+ {"type": "cell", "criteria": ">=", "value": .50, "format": mid_grade})
172
+ sheet.conditional_format(8, 9, 8, 9,
173
+ {"type": "cell", "criteria": ">=", "value": .25, "format": low_grade})
174
+ sheet.conditional_format(8, 9, 8, 9,
175
+ {"type": "cell", "criteria": "<", "value": .25, "format": bad_grade})
176
+
177
+ sheet.write(9, 8, "Nombre d'élèves ayant au moins 1 submit: ")
178
+ sheet.write_formula(9, 9, f"=SUM(E2:E{len(traces) + 1})")
179
+
180
+ sheet.write(10, 8, "Pourcentage des élèves ayant au moins 1 submit: ")
181
+ sheet.write_formula(10, 9, f"=ROUND(J10/COUNT(E2:E{len(traces) + 1}),2)", percent)
182
+
183
+ sheet.autofit()
184
+
185
+ workbook.close()
186
+
187
+ print(f"Wrote data to {filename}")
188
+
189
+
190
+ def get_data(activity: str, students: list[str]):
191
+ raw = get_raw_data(activity, get_jwt())
192
+ filtered = filter_class(raw, students)
193
+ traces = to_student_trace(filtered, activity)
194
+ traces = add_unsubmitted_students(traces, students)
195
+ to_xlsx(traces, "students.xlsx")
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: classresults
3
+ Version: 0.1.0
4
+ Summary:
5
+ License-Expression: MIT
6
+ Author: Thomas BOBEE
7
+ Author-email: thomasbobe3@gmail.com
8
+ Requires-Python: >=3.14
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: Programming Language :: Python :: 3.15
12
+ Requires-Dist: requests (>=2.34.2,<3.0.0)
13
+ Requires-Dist: setuptools (>=80.0.0,<81.0.0)
14
+ Requires-Dist: xlsxwriter (>=3.2.9,<4.0.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Install
18
+ To install this package, use:
19
+ `pip install ClassResults --index-url https://github.com/totolecr/ClassResults.git`
20
+
21
+ # Usage
22
+ To use the command, do `ClassResults <activty-code>`.
23
+ You may need to modify the 'students' variable in the __main__.py file.
24
+
@@ -0,0 +1,8 @@
1
+ classresults/__about__.py,sha256=5419reUIwxd7VTzfNBjwgvgcGwXeHKXK_7vPYl12pxI,95
2
+ classresults/__init__.py,sha256=fj0gOMR4MLzvmVIAskxYaWTQSaX23dM6xcY2GAdT6wI,2053
3
+ classresults/__main__.py,sha256=Nk9jVS9_a0piGjgEzFgNYTtICRo582lz9rufHViEi5o,173
4
+ classresults/get_data.py,sha256=F-pvR0RwhvRBXFFCWBC9qnFWenDBusYFGM3oZQMela8,8059
5
+ classresults-0.1.0.dist-info/METADATA,sha256=9wai6WUYjAhMS3bnSBlNaIW1hJZ7cMQW5GVtIHT5GnM,757
6
+ classresults-0.1.0.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
7
+ classresults-0.1.0.dist-info/entry_points.txt,sha256=QuxNXZwyNiuB8RssTzAafkD4pfkRJBYYiiZ9ZU4PT6E,50
8
+ classresults-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.5.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ classresults=classresults:main
3
+