history.py 5.46 KB
Newer Older
Alan Marchiori's avatar
Alan Marchiori committed
1
2
import os
import os.path
Alan Marchiori's avatar
Alan Marchiori committed
3
import platform
Alan Marchiori's avatar
Alan Marchiori committed
4
import json
5
import sys
Alan Marchiori's avatar
Alan Marchiori committed
6
import config
7
from config.echo import debug, error
Alan Marchiori's avatar
Alan Marchiori committed
8
9
10
from pymongo import MongoClient
class HistoryConfigurationException(Exception):
    pass
Alan Marchiori's avatar
Alan Marchiori committed
11
12
13
14
15
class History(dict):
    """A simple class to store grade history.
    This is a dict class that is persisted as a json object.
    It is meant to be used inside the with context manager.

Alan Marchiori's avatar
Alan Marchiori committed
16
17
18
    rev2: now backend storage is configurable to a json file or mongodb.
    'gradebackend' can be 'jsonfile' or 'mongodb'
    if mongodb you must also set 'gradedb' to the mongodb connection string.
Alan Marchiori's avatar
Alan Marchiori committed
19

Alan Marchiori's avatar
Alan Marchiori committed
20
    eg:
Alan Marchiori's avatar
Alan Marchiori committed
21
22
23
24
    with History() as h:
        h['...'] = ...

    """
Alan Marchiori's avatar
Alan Marchiori committed
25
26
27
    backend_mongodb = 'mongodb'
    backend_json = 'jsonfile'
    dbclient = None
Alan Marchiori's avatar
Alan Marchiori committed
28
29
30
    def exists(location=".", filename='.history.json'):
        "class method to determine if a history file exists (w/o creation)"
        return os.path.exists(os.path.join(location, filename))
Alan Marchiori's avatar
Alan Marchiori committed
31

Alan Marchiori's avatar
Alan Marchiori committed
32
    def __init__(self, location=".", dbpath=None,
Alan Marchiori's avatar
Alan Marchiori committed
33
34
35
36
                 filename = '.history.json',
                 save = True,
                 *args, **kwargs):
        "if save is false, never save the history file (read only copy)!"
Alan Marchiori's avatar
Alan Marchiori committed
37
        dict.__init__(self, *args, **kwargs)
Alan Marchiori's avatar
Alan Marchiori committed
38
        self.filename = filename
Alan Marchiori's avatar
Alan Marchiori committed
39
        self.dbpath = dbpath
Alan Marchiori's avatar
Alan Marchiori committed
40
        self.pathfile = os.path.join(location, filename)
Alan Marchiori's avatar
Alan Marchiori committed
41
        self.rawjson = None
Alan Marchiori's avatar
Alan Marchiori committed
42
        self.save = save
Alan Marchiori's avatar
Alan Marchiori committed
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
        self.who = "{}@{}".format(os.environ['USER'], platform.node())
        # new feature, force userconfig backend config.
        with config.UserConfig() as uc:
            if 'gradebackend' not in uc.cfg:
                raise HistoryConfigurationException("You must first configure the 'gradebackend' value in your userconfig.")
            self.gradebackend = uc.cfg['gradebackend']
            if 'gradedb' in uc.cfg:
                self.gradedb = uc.cfg['gradedb']
            else:
                self.gradedb = None

        if self.gradebackend == History.backend_mongodb:
            # connect
            if self.gradedb == None:
                raise HistoryConfigurationException("You must first configure the 'gradedb' value in your userconfig.")
            debug("using mongodb for history")
        elif self.gradebackend == History.backend_json:
Alan Marchiori's avatar
Alan Marchiori committed
60

Alan Marchiori's avatar
Alan Marchiori committed
61
62
63
            debug("using history file {}".format(self.pathfile))
        else:
            raise HistoryConfigurationException("Unsupported 'gradebackend' value in your userconfig.")
Alan Marchiori's avatar
Alan Marchiori committed
64
65
    def __enter__(self):
        "load history"
Alan Marchiori's avatar
Alan Marchiori committed
66
        debug("HISTORY ENTER")
Alan Marchiori's avatar
Alan Marchiori committed
67
68
69
70
71
72
73
74
75
76
77
78

        if self.gradebackend == History.backend_mongodb:
            if History.dbclient == None:
                # keep a global client to reuse
                History.dbclient = MongoClient(self.gradedb)
            self.mc = History.dbclient.get_default_database()[config.labtool_mongodb_collection]
            d = self.mc.find_one({'path': self.dbpath})
            if d and 'lock' in d:
                warn ("The grade is currently locked by {}".format(d['lock']))
                # prompt to take lock or abort

            if d != None:
79
                self.update(d)
Alan Marchiori's avatar
Alan Marchiori committed
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
                # lock the document
                if self.save:
                    self.mc.update_one(
                        {'_id': d['_id']},
                        {'$set': {'lock': self.who}})
            else:
                # insert empty document
                if self.save:
                    r = self.mc.insert_one({
                        'path': self.dbpath,
                        'lock': self.who})
                else:
                    r = self.mc.insert_one({'path': self.dbpath})
                self['_id'] = r.inserted_id

        else:
            if os.path.exists(self.pathfile):
                try:
                    with open(self.pathfile, 'r') as f:
                        self.rawjson = f.read()
                    try:
                        d = json.loads(self.rawjson)
                    except json.decoder.JSONDecodeError:
                        error("Grading history file at {} is corrupt. Manually remove or fix this file to continue running.".format(self.pathfile))
                        exit(212)
                    self.update(d)
                except Exception as x:
                    print(x)
                    sys.exit()
Alan Marchiori's avatar
Alan Marchiori committed
109
110
111
        return self
    def __exit__(self, type, value, traceback):
        "save history"
Alan Marchiori's avatar
Alan Marchiori committed
112
        debug("HISTORY EXIT")
Alan Marchiori's avatar
Alan Marchiori committed
113
114
        if not self.save:
            return
Alan Marchiori's avatar
Alan Marchiori committed
115

Alan Marchiori's avatar
Alan Marchiori committed
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
        if self.gradebackend == History.backend_mongodb:
            # unlock so when saved, the document is not locked
            if 'lock' in self:
                del self['lock']
            self.mc.replace_one(
                {'_id': self['_id']},
                self)

        else:
            # only save history if the target path exists
            if os.path.exists(os.path.dirname(self.pathfile)):
                s = json.dumps(
                    self,
                    indent=4,
                    sort_keys=True
                )
                # check if dirty.
                if self.rawjson and self.rawjson == s:
                    debug("HISTORY is clean, no write.")
                    return
                debug("HISTORY: write to {}".format(self.pathfile))
                with open(self.pathfile, 'w') as f:
                    f.write(s)
Alan Marchiori's avatar
Alan Marchiori committed
139
140
141
142
143
144
145
146
147
148
149
150
151

if __name__=="__main__":
    from pprint import pprint
    print("testing history.")
    with History() as h:
        pprint(h)
        h['test'] = 123
        pprint(h)

    with History() as h:
        pprint(h)
        del h['test']
        pprint(h)