diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/README.md b/README.md
index 8cb6bb750582d138d070135b241dcc5ab163a5df..cacf928aba83eddd087db5dcc049a54585abce35 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,5 @@
 # snipper
 
+
+## Deployment info
+
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6c4c87033382eeafd151eb0589f418af9563285
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,35 @@
+import os
+
+# DONT't import stuff here since install script requires __version__
+
+def cache_write(object, file_name, verbose=True):
+    import compress_pickle
+    dn = os.path.dirname(file_name)
+    if not os.path.exists(dn):
+        os.mkdir(dn)
+    if verbose: print("Writing cache...", file_name)
+    with open(file_name, 'wb', ) as f:
+        compress_pickle.dump(object, f, compression="lzma")
+    if verbose: print("Done!")
+
+
+def cache_exists(file_name):
+    # file_name = cn_(file_name) if cache_prefix else file_name
+    return os.path.exists(file_name)
+
+
+def cache_read(file_name):
+    import compress_pickle # Import here because if you import in top the __version__ tag will fail.
+    # file_name = cn_(file_name) if cache_prefix else file_name
+    if os.path.exists(file_name):
+        try:
+            with open(file_name, 'rb') as f:
+                return compress_pickle.load(f, compression="lzma")
+        except Exception as e:
+            print("Tried to load a bad pickle file at", file_name)
+            print("If the file appears to be automatically generated, you can try to delete it, otherwise download a new version")
+            print(e)
+            # return pickle.load(f)
+    else:
+        return None
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b02a2c7a341496568686e8bc5dfdf87e4dbcb19
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+from setuptools import setup
+import setuptools
+from src.snipper.version import __version__
+
+with open("README.md", "r", encoding="utf-8") as fh:
+    long_description = fh.read()
+
+setup(
+    name="example-pkg-YOUR-USERNAME-HERE",
+    version=__version__,
+    author="Tue Herlau",
+    author_email="tuhe@dtu.dk",
+    description="A small example package",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    url="https://compute.dtu.dk/tuhe/snipper",
+    # project_urls={
+        #"Bug Tracker": "https://github.com/pypa/sampleproject/issues",
+    # },
+    classifiers=[
+        "Programming Language :: Python :: 3",
+     #   "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+    ],
+    package_dir={"src"},
+    packages=setuptools.find_packages(where="src"),
+    python_requires=">=3.6",
+    install_requires=['numpy', 'jinja2', 'tabulate', 'sklearn', 'compress_pickle', "pyfiglet"],
+    # name='homework-snipper-tuhe',
+    # version=__version__,
+    # packages=['snipper'],
+    # url='https://lab.compute.dtu.dk/tuhe/unitgrade',
+    # license='Apache',
+    # author='Tue Herlau',
+    # author_email='tuhe@dtu.dk',
+    # description='A lightweight student evaluation framework build on unittest',
+    # include_package_data=True,
+)
diff --git a/src/snipper/snip_dir.py b/src/snipper/snip_dir.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce188b22dbd278edc3fb0a27c082c573ed883c91
--- /dev/null
+++ b/src/snipper/snip_dir.py
@@ -0,0 +1,70 @@
+import os, shutil
+# from thtools.coursebox.core.info_paths import get_paths
+# from thtools.coursebox.core.info import class_information
+from thtools.coursebox.material.snipper import censor_file
+# from thtools.coursebox.material.homepage_lectures_exercises import fix_shared
+from pathlib import Path
+import time
+
+
+def snip_dir(source_dir, dest_dir, exclude=None, clean_destination_dir=True):
+    if exclude == None:
+        exclude = []
+    if not os.path.exists(dest_dir):
+        os.makedirs(dest_dir)
+    if os.path.samefile(source_dir, dest_dir):
+        raise Exception("Source and destination is the same")
+    if clean_destination_dir:
+        shutil.rmtree(dest_dir)
+    os.makedirs(dest_dir)
+    # Now the destination dir is set up.
+
+    out = dest_dir
+    hw = {'base': source_dir,
+          'exclusion': exclude}
+    # def fix_hw(info, hw, out, run_files=False, cut_files=False, students_irlc_tools=None, **kwargs):
+    # CE = info['CE']
+    # paths = get_paths()
+    print(f"Fixing {hw['base']} -> {out}")
+    if os.path.exists(out):
+        shutil.rmtree(out)
+
+    base = hw['base']
+
+    shutil.copytree(base, out)
+    time.sleep(0.2)
+    ls = Path(out).glob('**/*.py')
+
+    run_out_dirs = ["./output"]
+    n = 0
+    edirs = [os.path.join(out, f_) for f_ in hw['exclusion']]  # Exclude directories on exclude list (speed)
+    edirs = {os.path.normpath(os.path.dirname(f_) if not os.path.isdir(f_) else f_) for f_ in edirs}
+    edirs.remove(os.path.normpath(out))
+    for f in ls:
+        f_dir = os.path.normpath(f if os.path.isdir(f) else os.path.dirname(f))
+        if not any([f_dir.startswith(f_) for f_ in edirs]):
+            slist = hw['solutions'] if not CE else [os.path.basename(f)[:-3]]
+            base = None
+            if students_irlc_tools is not None:
+                base = os.path.relpath(str(f), students_irlc_tools + "/..")
+                base = base.replace("\\", "/")
+
+            if "assignments" in str(f) and "_grade.py" in str(f):
+                continue
+            nrem = censor_file(f, info, paths, run_files=run_files, run_out_dirs=run_out_dirs[:1], cut_files=cut_files, solution_list=slist, include_path_base=base, **kwargs)
+            if nrem > 0:
+                print(f"{nrem}> {f}")
+            n += nrem
+
+    for f in hw['exclusion']:
+        rm_file = os.path.join(out, f)
+        if any([df_ in rm_file for df_ in hw['inclusion']]):
+            continue
+
+        rm_file = os.path.abspath(rm_file)
+        if os.path.isfile(rm_file):
+            os.remove(rm_file)
+        else:
+            if os.path.isdir(rm_file + "\\"):
+                shutil.rmtree(rm_file)
+    return n
diff --git a/src/snipper/snipper.py b/src/snipper/snipper.py
new file mode 100644
index 0000000000000000000000000000000000000000..77f330f055662520674bf3c6c41eda43bcbc7aab
--- /dev/null
+++ b/src/snipper/snipper.py
@@ -0,0 +1,461 @@
+from thtools.coursebox.core.info import find_tex_cite
+import os
+import functools
+from thtools import execute_command
+import textwrap
+import re
+
+COMMENT = '"""'
+def indent(l):
+    v = len(l) - len(l.lstrip())
+    return l[:v]
+
+def fix_r(lines):
+    for i,l in enumerate(lines):
+        if "#!r" in l:
+            lines[i] = indent(l) + l[l.find("#!r") + 3:].lstrip()
+    return lines
+
+def gcoms(s):
+    coms = []
+    while True:
+        i = s.find(COMMENT)
+        if i >= 0:
+            j = s.find(COMMENT, i+len(COMMENT))+3
+        else:
+            break
+        if j < 0:
+            raise Exception("comment tag not closed")
+        coms.append(s[i:j])
+        s = s[:i] + s[j:]
+        if len(coms) > 10:
+            print("long comments in file", i)
+    return coms, s
+
+def strip_tag(lines, tag):
+    lines2 = []
+    for l in lines:
+        dx = l.find(tag)
+        if dx > 0:
+            l = l[:dx]
+            if len(l.strip()) == 0:
+                l = None
+        if l is not None:
+            lines2.append(l)
+    return lines2
+
+def block_process(lines, tag, block_fun):
+    i = 0
+    didfind = False
+    lines2 = []
+    block_out = []
+    cutout = []
+    while i < len(lines):
+        l = lines[i]
+        dx = l.find(tag)
+        if dx >= 0:
+            if l.find(tag, dx+1) > 0:
+                j = i
+            else:
+                for j in range(i + 1, 10000):
+                    if j >= len(lines):
+                        print("\n".join(lines))
+                        print("very bad end-line j while fixing tag", tag)
+                        raise Exception("Bad line while fixing", tag)
+                    if lines[j].find(tag) >= 0:
+                        break
+
+            pbody = lines[i:j+1]
+            if i == j:
+                start_extra = lines[j][dx:lines[j].rfind(tag)].strip()
+                end_extra = lines[j][lines[j].rfind(tag) + len(tag):].strip()
+            else:
+                start_extra = lines[i][dx:].strip()
+                end_extra = lines[j][lines[j].rfind(tag) + len(tag):].strip()
+
+            cutout.append(pbody)
+            tmp_ = start_extra.split("=")
+            arg = None if len(tmp_) <= 1 else tmp_[1].split(" ")[0]
+            start_extra = ' '.join(start_extra.split(" ")[1:] )
+
+            pbody[0] = pbody[0][:dx]
+            if j > i:
+                pbody[-1] = pbody[-1][:pbody[-1].find(tag)]
+
+            nlines, extra = block_fun(lines=pbody, start_extra=start_extra, end_extra=end_extra, art=arg, head=lines[:i], tail=lines[j+1:])
+            lines2 += nlines
+            block_out.append(extra)
+            i = j+1
+            didfind = True
+            if "!b" in end_extra:
+                assert(False)
+        else:
+            lines2.append(l)
+            i += 1
+
+    return lines2, didfind, block_out, cutout
+
+
+def rem_nonprintable_ctrl_chars(txt):
+    """Remove non_printable ascii control characters """
+    #Removes the ascii escape chars
+    try:
+        txt = re.sub(r'[^\x20-\x7E|\x09-\x0A]','', txt)
+        # remove non-ascii characters
+        txt = repr(txt).decode('unicode_escape').encode('ascii','ignore')[1:-1]
+    except Exception as exception:
+        print(exception)
+    return txt
+
+
+def run_i(lines, file, output):
+    extra = dict(python=None, output=output, evaluated_lines=0)
+    def block_fun(lines, start_extra, end_extra, art, head="", tail="", output=None, extra=None):
+        outf = output + ("_" + art if art is not None and len(art) > 0 else "") + ".shell"
+        lines = full_strip(lines)
+        s = "\n".join(lines)
+        s.replace("...", "..") # passive-aggressively truncate ... because of #issues.
+        lines = textwrap.dedent(s).strip().splitlines()
+
+        if extra['python'] is None:
+            import thtools
+            if thtools.is_win():
+                import wexpect as we
+            else:
+                import pexpect as we
+            an = we.spawn("python", encoding="utf-8", timeout=20)
+            an.expect([">>>"])
+            extra['python'] = an
+
+        analyzer = extra['python']
+        def rsession(analyzer, lines):
+            l2 = []
+            for i, l in enumerate(lines):
+                l2.append(l)
+                if l.startswith(" ") and i < len(lines)-1 and not lines[i+1].startswith(" "):
+                    if not lines[i+1].strip().startswith("else:") and not lines[i+1].strip().startswith("elif") :
+                        l2.append("\n")
+
+            lines = l2
+            alines = []
+
+            # indented = False
+            in_dot_mode = False
+            if len(lines[-1]) > 0 and (lines[-1].startswith(" ") or lines[-1].startswith("\t")):
+                lines += [""]
+
+            for i, word in enumerate(lines):
+                analyzer.sendline(word)
+                before = ""
+                while True:
+                    analyzer.expect_exact([">>>", "..."])
+                    before += analyzer.before
+                    if analyzer.before.endswith("\n"):
+                        break
+                    else:
+                        before += analyzer.after
+
+                dotmode = analyzer.after == "..."
+                if 'dir(s)' in word:
+                    pass
+                if 'help(s.find)' in word:
+                    pass
+                if dotmode:
+                    # alines.append("..." + word)
+                    alines.append(">>>" + analyzer.before.rstrip() if not in_dot_mode else "..." + analyzer.before.rstrip())
+                    in_dot_mode = True
+                    # if i < len(lines) - 1 and not lines[i + 1].startswith(" "):
+                    #     analyzer.sendline("\n")  # going out of indentation mode .
+                    #     analyzer.expect_exact([">>>", "..."])
+                    #     alines.append("..." + analyzer.after.rstrip())
+                    #     pass
+                else:
+                    alines.append( ("..." if in_dot_mode else ">>>") + analyzer.before.rstrip())
+                    in_dot_mode = False
+            return alines
+
+        for l in (head[extra['evaluated_lines']:] + ["\n"]):
+            analyzer.sendline(l)
+            analyzer.expect_exact([">>>", "..."])
+
+
+        alines = rsession(analyzer, lines)
+        extra['evaluated_lines'] += len(head) + len(lines)
+        lines = alines
+        return lines, [outf, lines]
+    try:
+        a,b,c,_ = block_process(lines, tag="#!i", block_fun=functools.partial(block_fun, output=output, extra=extra))
+        if extra['python'] is not None:
+            extra['python'].close()
+
+        if len(c)>0:
+            kvs= { v[0] for v in c}
+            for outf in kvs:
+                out = "\n".join( ["\n".join(v[1]) for v in c if v[0] == outf] )
+                out = out.replace("\r", "")
+
+                with open(outf, 'w') as f:
+                    f.write(out)
+
+    except Exception as e:
+        print("lines are")
+        print("\n".join(lines))
+        print("Bad thing in #!i command in file", file)
+        raise e
+    return lines
+
+def save_s(lines, file, output, include_path_base=None): # save file snips to disk
+    def block_fun(lines, start_extra, end_extra, art, output, **kwargs):
+        outf = output + ("_" + art if art is not None and len(art) > 0 else "") + ".py"
+        lines = full_strip(lines)
+        return lines, [outf, lines]
+    try:
+        a,b,c,_ = block_process(lines, tag="#!s", block_fun=functools.partial(block_fun, output=output))
+
+        if len(c)>0:
+            kvs= { v[0] for v in c}
+            for outf in kvs:
+
+                out = "\n".join([f"# {include_path_base}"]  + ["\n".join(v[1]) for v in c if v[0] == outf] )
+
+                with open(outf, 'w') as f:
+                    f.write(out)
+
+    except Exception as e:
+        print("lines are")
+        print("\n".join(lines))
+        print("Bad thing in #!s command in file", file)
+        raise e
+    return lines
+
+def run_o(lines, file, output):
+    def block_fun(lines, start_extra, end_extra, art, output, **kwargs):
+        id = indent(lines[0])
+        outf = output + ("_" + art if art is not None else "") + ".txt"
+        l2 = []
+        l2 += [id + "import sys", id + f"sys.stdout = open('{outf}', 'w')"]
+        l2 += lines
+        # l2 += [indent(lines[-1]) + "sys.stdout.close()"]
+        l2 += [indent(lines[-1]) + "sys.stdout = sys.__stdout__"]
+        return l2, None
+    try:
+        lines2, didfind, extra, _ = block_process(lines, tag="#!o", block_fun=functools.partial(block_fun, output=output) )
+    except Exception as e:
+        print("Bad file: ", file)
+        print("I was cutting the #!o tag")
+        print("\n".join( lines) )
+        raise(e)
+
+    if didfind:
+        fp, ex = os.path.splitext(file)
+        file_run = fp + "_RUN_OUTPUT_CAPTURE" +ex
+        if os.path.exists(file_run):
+            print("file found mumble...")
+        else:
+            with open(file_run, 'w', encoding="utf-8") as f:
+                f.write("\n".join(lines2) )
+            cmd = "python " + file_run
+
+            s,ok = execute_command(cmd.split(), shell=True)
+            print(s)
+            os.remove(file_run)
+
+def fix_f(lines, debug):
+    lines2 = []
+    i = 0
+    while i < len(lines):
+        l = lines[i]
+        dx = l.find("#!f")
+        if dx >= 0:
+            l_head = l[dx+3:].strip()
+            l = l[:dx]
+            lines2.append(l)
+            id = indent(lines[i+1])
+            for j in range(i+1, 10000):
+                jid = len( indent(lines[j]) )
+                if  j+1 == len(lines) or ( jid < len(id) and len(lines[j].strip() ) > 0):
+                    break
+
+            if len(lines[j-1].strip()) == 0:
+                j = j - 1
+            funbody = "\n".join( lines[i+1:j] )
+            if i == j:
+                raise Exception("Empty function body")
+            i = j
+            comments, funrem = gcoms(funbody)
+            comments = [id + c for c in comments]
+            if len(comments) > 0:
+                lines2 += comments[0].split("\n")
+            lines2 += [id+"#!b"]
+            lines2 += (id+funrem.strip()).split("\n")
+            errm = l_head if len(l_head) > 0 else "Implement function body"
+            lines2 += [f'{id}#!b {errm}']
+
+        else:
+            lines2.append(l)
+            i += 1
+    return lines2
+
+def fix_b2(lines):
+    stats = {'n': 0}
+    def block_fun(lines, start_extra, end_extra, art, stats=None, **kwargs):
+        id = indent(lines[0])
+        lines = lines[1:] if len(lines[0].strip()) == 0 else lines
+        lines = lines[:-1] if len(lines[-1].strip()) == 0 else lines
+        cc = len(lines)
+        ee = end_extra.strip()
+        if len(ee) >= 2 and ee[0] == '"':
+            ee = ee[1:-1]
+        start_extra = start_extra.strip()
+        l2 = ([id+start_extra] if len(start_extra) > 0 else []) + [id + f"# TODO: {cc} lines missing.",
+                                         id+f'raise NotImplementedError("{ee}")']
+        # if "\n".join(l2).find("l=l")>0:
+        #     a = 2342342
+        stats['n'] += cc
+        return l2, cc
+    lines2, _, _, cutout = block_process(lines, tag="#!b", block_fun=functools.partial(block_fun, stats=stats))
+    return lines2, stats['n'], cutout
+
+
+def fix_references(lines, info, strict=True):
+    for cmd in info['new_references']:
+        lines = fix_single_reference(lines, cmd, info['new_references'][cmd], strict=strict)
+    return lines
+
+def fix_single_reference(lines, cmd, aux, strict=True):
+    references = aux
+    s = "\n".join(lines)
+    i = 0
+    while True:
+        (i, j), reference, txt = find_tex_cite(s, start=i, key=cmd)
+        if i < 0:
+            break
+        if reference not in references:
+            er = "cref label not found for label: " + reference
+            if strict:
+                raise IndexError(er)
+            else:
+                print(er)
+                continue
+        r = references[reference]
+        rtxt = r['pyref']
+        s = s[:i] + rtxt + s[j + 1:]
+        i = i + len(rtxt)
+        print(cmd, rtxt)
+
+    lines = s.splitlines(keepends=False)
+    return lines
+
+
+def fix_cite(lines, info, strict=True):
+    lines = fix_references(lines, info, strict=strict)
+
+    s = "\n".join(lines)
+    i = 0
+    all_refs = []
+    while True:
+        (i, j), reference, txt = find_tex_cite(s, start=i, key="\\cite")
+        if i < 0:
+            break
+        if reference not in info['references']:
+            raise IndexError("no such reference: " + reference)
+        ref = info['references'][reference]
+        label = ref['label']
+        rtxt = f"({label}" + (", "+txt if txt is not None else "") + ")"
+        r = ref['plain']
+        if r not in all_refs:
+            all_refs.append(r)
+        s = s[:i] + rtxt + s[j+1:]
+        i = i + len(rtxt)
+
+    cpr = "{{copyright}}"
+    if not s.startswith(COMMENT):
+        s = f"{COMMENT}\n{cpr}\n{COMMENT}\n" + s
+    if len(all_refs) > 0:
+        i = s.find(COMMENT, s.find(COMMENT)+1)
+        all_refs = ["  " + r for r in all_refs]
+        s = s[:i] + "\nReferences:\n" + "\n".join(all_refs) + "\n" + s[i:]
+
+    s = s.replace(cpr, info['code_copyright'])
+    return s
+
+def full_strip(lines, tags=None):
+    if tags is None:
+        tags = ["#!s", "#!o", "#!f", "#!b"]
+    for t in tags:
+        lines = strip_tag(lines, t)
+    return lines
+
+def censor_file(file, info, paths, run_files=True, run_out_dirs=None, cut_files=True, solution_list=None,
+                censor_files=True,
+                include_path_base=None,
+                strict=True):
+    dbug = False
+    with open(file, 'r', encoding='utf8') as f:
+        s = f.read()
+        s = s.lstrip()
+        lines = s.split("\n")
+        for k, l in enumerate(lines):
+            if l.find(" # !") > 0:
+                print(f"{file}:{k}> bad snipper tag, fixing")
+            lines[k] = l.replace("# !", "#!")
+
+        try:
+            s = fix_cite(lines, info, strict=strict)
+            lines = s.split("\n")
+        except IndexError as e:
+            print(e)
+            print("Fuckup in file, cite/reference tag not found!>", file)
+            raise e
+
+        if run_files or cut_files:
+            ofiles = []
+            for rod in run_out_dirs:
+                if not os.path.isdir(rod):
+                    os.mkdir(rod)
+                ofiles.append(os.path.join(rod, os.path.basename(file).split(".")[0]) )
+            ofiles[0] = ofiles[0].replace("\\", "/")
+
+            if run_files:
+                run_o(lines, file=file, output=ofiles[0])
+                run_i(lines, file=file, output=ofiles[0])
+            if cut_files:
+                save_s(lines, file=file, output=ofiles[0], include_path_base=include_path_base)  # save file snips to disk
+        lines = full_strip(lines, ["#!s", "#!o", '#!i'])
+
+        # lines = fix_c(lines)
+        if censor_files:
+            lines = fix_f(lines, dbug)
+            lines, nB, cut = fix_b2(lines)
+        else:
+            nB = 0
+        lines = fix_r(lines)
+
+        if censor_files and len(cut) > 0 and solution_list is not None:
+            fname = file.__str__()
+            i = fname.find("irlc")
+            wk = fname[i+5:fname.find("\\", i+6)]
+            sp = paths['02450students'] +"/solutions/"
+            if not os.path.exists(sp):
+                os.mkdir(sp)
+            sp = sp + wk
+            if not os.path.exists(sp):
+                os.mkdir(sp)
+
+            stext = ["\n".join(lines) for lines in cut]
+            for i,sol in enumerate(stext):
+                sout = sp + f"/{os.path.basename(fname)[:-3]}_TODO_{i+1}.py"
+                wsol = any([True for s in solution_list if os.path.basename(sout).startswith(s)])
+                print(sout, "(published)" if wsol else "")
+                if wsol:
+                    with open(sout, "w") as f:
+                        f.write(sol)
+
+        if len(lines[-1])>0:
+            lines.append("")
+        s2 = "\n".join(lines)
+
+    with open(file, 'w', encoding='utf-8') as f:
+        f.write(s2)
+    return nB
+# lines: 294, 399, 420, 270
\ No newline at end of file
diff --git a/src/snipper/version.py b/src/snipper/version.py
new file mode 100644
index 0000000000000000000000000000000000000000..f102a9cadfa89ce554b3b26d2b90bfba2e05273c
--- /dev/null
+++ b/src/snipper/version.py
@@ -0,0 +1 @@
+__version__ = "0.0.1"