From 6b0c69d8b36c89d614914953cd932a134733764a Mon Sep 17 00:00:00 2001 From: Tue Herlau <tuhe@dtu.dk> Date: Sat, 5 Aug 2023 18:29:47 +0200 Subject: [PATCH] Remote result viewing+hashing --- requirements.txt | 17 ++++---- src/unitgrade.egg-info/PKG-INFO | 2 +- src/unitgrade/__init__.py | 2 + src/unitgrade/dashboard/app.py | 17 ++++---- src/unitgrade/evaluate.py | 21 ++++++---- src/unitgrade/framework.py | 8 ++++ src/unitgrade/utils.py | 71 +++++++++++++++++++++++++++++++++ 7 files changed, 113 insertions(+), 25 deletions(-) diff --git a/requirements.txt b/requirements.txt index f7f816e..777526b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,11 +9,12 @@ xlwings colorama numpy scikit_learn -codesnipper # For the docs. -importnb # Experimental notebook inclusion feature. May not be required. -requests # To read remote files for automatic updating. -diskcache # dashboard -watchdog # dashboard -flask_socketio # dashboard -flask # dashboard -Werkzeug>=2.2.0 # dashboard +pandas # For report extraction from remote sources. +codesnipper # For the docs. +importnb # Experimental notebook inclusion feature. May not be required. +requests # To read remote files for automatic updating. +diskcache # dashboard +watchdog # dashboard +flask_socketio # dashboard +flask # dashboard +Werkzeug>=2.2.0 # dashboard diff --git a/src/unitgrade.egg-info/PKG-INFO b/src/unitgrade.egg-info/PKG-INFO index b3cd176..d175006 100644 --- a/src/unitgrade.egg-info/PKG-INFO +++ b/src/unitgrade.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: unitgrade -Version: 0.1.30.6 +Version: 0.1.30.8 Summary: A student homework/exam evaluation framework build on pythons unittest framework. Home-page: https://lab.compute.dtu.dk/tuhe/unitgrade Author: Tue Herlau diff --git a/src/unitgrade/__init__.py b/src/unitgrade/__init__.py index 05e52e2..d48e2e8 100644 --- a/src/unitgrade/__init__.py +++ b/src/unitgrade/__init__.py @@ -3,6 +3,8 @@ from unitgrade.utils import myround, msum, mfloor, Capturing, ActiveProgress, ca # from unitgrade import hide from unitgrade.framework import Report, UTestCase, NotebookTestCase from unitgrade.evaluate import evaluate_report_student + + # from unitgrade import utils # import os # import lzma diff --git a/src/unitgrade/dashboard/app.py b/src/unitgrade/dashboard/app.py index b43da5d..5b94b13 100644 --- a/src/unitgrade/dashboard/app.py +++ b/src/unitgrade/dashboard/app.py @@ -111,7 +111,6 @@ def mkapp(base_dir="./", use_command_line=True): file = "*/"+c watched_files_dictionary[file] = mkempty(file, "coverage") - # tdir = "*/"+os.path.dirname(current_report['relative_path_token']) + "/" + os.path.basename(current_report['relative_path'])[:-3] + "*.token" tdir = "*/"+current_report['token_stub'] + "*.token" watched_files_dictionary[tdir] = mkempty(tdir, 'token') @@ -233,7 +232,6 @@ def mkapp(base_dir="./", use_command_line=True): # print(environ) # print(sid) - @socketio.on("reconnected", namespace="/status") def client_reconnected(data): """write to the child pty. The pty sees this as if you are typing in a real @@ -257,16 +255,17 @@ def mkapp(base_dir="./", use_command_line=True): return app, socketio, closeables def main(): - from cs108 import deploy - from cs108.report_devel import mk_bad + # from cs108 import deploy + # from cs108.report_devel import mk_bad + # deploy.main(with_coverage=True) # Deploy for debug. + # mk_bad() + # bdir = os.path.dirname(deploy.__file__) + bdir = "/home/tuhe/Documents/02002students_complete/cp/project5" + + args_port = 5000 args_host = "127.0.0.1" - # Deploy local files for debug. - deploy.main(with_coverage=True) - mk_bad() - - bdir = os.path.dirname(deploy.__file__) app, socketio, closeables = mkapp(base_dir=bdir) debug = False logging.info(f"serving on http://{args_host}:{args_port}") diff --git a/src/unitgrade/evaluate.py b/src/unitgrade/evaluate.py index 0e1aba3..eb8233a 100644 --- a/src/unitgrade/evaluate.py +++ b/src/unitgrade/evaluate.py @@ -107,6 +107,17 @@ class SequentialTestLoader(unittest.TestLoader): test_names.sort(key=testcase_methods.index) return test_names +def _print_header(now, big_header=True): + from unitgrade.version import __version__ + if big_header: + ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") + b = "\n".join([l for l in ascii_banner.splitlines() if len(l.strip()) > 0]) + else: + b = "Unitgrade" + dt_string = now.strftime("%d/%m/%Y %H:%M:%S") + print(b + " v" + __version__ + ", started: " + dt_string + "\n") + + def evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False, show_progress_bar=True, show_tol_err=False, @@ -116,14 +127,10 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa ): from unitgrade.version import __version__ + now = datetime.now() - if big_header: - ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") - b = "\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] ) - else: - b = "Unitgrade" - dt_string = now.strftime("%d/%m/%Y %H:%M:%S") - print(b + " v" + __version__ + ", started: " + dt_string+ "\n") + _print_header(now, big_header=big_header) + # print("Started: " + dt_string) report._check_remote_versions() # Check (if report.url is present) that remote files exist and are in sync. s = report.title diff --git a/src/unitgrade/framework.py b/src/unitgrade/framework.py index d482ec0..5075573 100644 --- a/src/unitgrade/framework.py +++ b/src/unitgrade/framework.py @@ -78,6 +78,7 @@ class Report: abbreviate_questions = False # Should the test items start with 'Question ...' or just be q1). version = None # A version number of the report (1.0). Used to compare version numbers with online resources. url = None # Remote location of this problem. + remote_url = None # Remote url of documentation. This will be used to gather results. questions = [] pack_imports = [] @@ -106,6 +107,13 @@ class Report: Note the file is shared between all sub-questions. """ return os.path.join(os.path.dirname(self._file()), "unitgrade_data/main_config_"+ os.path.basename(self._file()[:-3]) + ".artifacts.pkl") + def _manifest_file(self): + """ + The manifest is the file we append all artifact hashes to so we can check results at some later time. + file is plaintext, and can be deleted. + """ + return os.path.join(os.path.dirname(self._file()), "unitgrade_data/token_" + os.path.basename(self._file()[:-3]) + ".manifest") + def _is_run_in_grade_mode(self): """ True if this report is being run as part of a grade run. """ return self._file().endswith("_grade.py") # Not sure I love this convention. diff --git a/src/unitgrade/utils.py b/src/unitgrade/utils.py index 4160451..bdc8645 100644 --- a/src/unitgrade/utils.py +++ b/src/unitgrade/utils.py @@ -276,6 +276,18 @@ def dict2picklestring(dd): b_hash = hashlib.blake2b(b).hexdigest() return base64.b64encode(b).decode("utf-8"), b_hash + +def hash_string(s): + """This is a helper function used to double-hash a something (i.e., hash of a hash). + Right now it is used in the function in 02002 when a creating the index of student-downloadable evaluations.""" + # gfg = hashlib.blake2b() + return hashlib.blake2b(s.encode("utf-8")).hexdigest() + # return base64.b64encode(b).decode("utf-8") + + # gfg.update(s.encode("utf-8")) + # return gfg.digest() + + def picklestring2dict(picklestr): """ Reverse of the above method: Turns the string back into a dictionary. """ b = base64.b64decode(picklestr) @@ -294,14 +306,65 @@ def load_token(file_in): head = token_sep.join(splt[:-2]) plain_text=head.strip() hash, l1 = info.split(" ") + hash = hash.strip() data = "".join( data.strip()[1:-1].splitlines() ) l1 = int(l1) dictionary, b_hash = picklestring2dict(data) + dictionary['metadata'] = {'file_reported_hash':b_hash, 'actual_hash': hash} # This contains information about the hashes right now. assert len(data) == l1 assert b_hash == hash.strip() return dictionary, plain_text +def checkout_remote_results(remote_url, manifest): + """ + + :param remote_url: + :param manifest: + :return: + """ + import urllib + # urllib. + import urllib.request + if remote_url.endswith("/"): + remote_url = remote_url[:-1] + + with urllib.request.urlopen(remote_url +"/index.html") as response: + html = response.read().decode() + + SEP = "-----------" + remote = {ll[0]: ll[1] for ll in [l.strip().split(" ") for l in html.split(SEP)[1].strip().splitlines()] } + # lines = + + mf = [m.strip().split(" ")[-1] for m in manifest.strip().splitlines()] + a = 23 + html = None + for hash in reversed(mf): + if hash_string(hash) in remote: + url = f"{remote_url}/{os.path.dirname( remote[hash_string(hash)] )}/{hash}/index.html" + with urllib.request.urlopen(url) as response: + html = response.read().decode() + # print( html ) + break + + if html is not None: + import pandas as pd + dfs = pd.read_html(html) + df = dfs[0] + # df.__format__() + # tabular + + # print( df.to_string(index=False) ) + # df.as + result = dict(html=html, df=df, score=float( df.iloc[2].to_list()[-1] )) + else: + result=dict(html=html) + + return result + + + + ## Key/value store related. class DKPupDB: @@ -365,3 +428,11 @@ class DKPupDB: return False return item in self.dk[self.name_] #keys() # return item in self.dk + +if __name__ == "__main__": + url = "https://cp.pages.compute.dtu.dk/02002public/_static/evaluation/" + manifest = """ +/home/tuhe/Documents/unitgrade_private/src/unitgrade_private/pipelines/tmp/students/cp/project0/Project0_handin_0_of_10.token 7720b41ab925098956c7db37c8292ce3a7b4ded96f4442234dee493c021fc5f7294e543de78630aaf873b756d25bf7b4fd7eb6e66cec282b54f0c35b83e9071f +""" + checkout_remote_results(url, manifest = manifest) + -- GitLab