classresults 0.1.0__tar.gz
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.
- classresults-0.1.0/PKG-INFO +24 -0
- classresults-0.1.0/README.md +7 -0
- classresults-0.1.0/classresults/__about__.py +3 -0
- classresults-0.1.0/classresults/__init__.py +44 -0
- classresults-0.1.0/classresults/__main__.py +8 -0
- classresults-0.1.0/classresults/get_data.py +195 -0
- classresults-0.1.0/pyproject.toml +22 -0
|
@@ -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,7 @@
|
|
|
1
|
+
# Install
|
|
2
|
+
To install this package, use:
|
|
3
|
+
`pip install ClassResults --index-url https://github.com/totolecr/ClassResults.git`
|
|
4
|
+
|
|
5
|
+
# Usage
|
|
6
|
+
To use the command, do `ClassResults <activty-code>`.
|
|
7
|
+
You may need to modify the 'students' variable in the __main__.py file.
|
|
@@ -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,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,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "classresults"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Thomas BOBEE",email = "thomasbobe3@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"requests (>=2.34.2,<3.0.0)",
|
|
13
|
+
"setuptools (>=80.0.0,<81.0.0)",
|
|
14
|
+
"xlsxwriter (>=3.2.9,<4.0.0)"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.poetry.scripts]
|
|
18
|
+
classresults = "classresults:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|