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