Source code for joy.vui.persist_task

# -*- coding: utf-8 -*-
#
#    Copyright © 2019 Simon Forman
#
#    This file is part of Thun
#
#    Thun is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    Thun is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with Thun.  If not see <http://www.gnu.org/licenses/>.
#
'''

Persist Task
===========================

This module deals with persisting the "resources" (text files and the
stack) to the git repo in the ``JOY_HOME`` directory.

'''
import os, pickle, traceback
from collections import Counter
from dulwich.errors import NotGitRepository
from dulwich.repo import Repo
from joy.vui import core, init_joy_home


[docs]def open_repo(repo_dir=None, initialize=False): ''' Open, or create, and return a Dulwich git repo object for the given directory. If the dir path doesn't exist it will be created. If it does exist but isn't a repo the result depends on the ``initialize`` argument. If it is ``False`` (the default) a ``NotGitRepository`` exception is raised, otherwise ``git init`` is effected in the dir. ''' if not os.path.exists(repo_dir): os.makedirs(repo_dir, 0700) return init_repo(repo_dir) try: return Repo(repo_dir) except NotGitRepository: if initialize: return init_repo(repo_dir) raise
[docs]def init_repo(repo_dir): ''' Initialize a git repository in the directory. Stage and commit all files (toplevel, not those in subdirectories if any) in the dir. ''' repo = Repo.init(repo_dir) init_joy_home.initialize(repo_dir) repo.stage([ fn for fn in os.listdir(repo_dir) if os.path.isfile(os.path.join(repo_dir, fn)) ]) repo.do_commit('Initial commit.', committer=core.COMMITTER) return repo
[docs]def make_repo_relative_path_maker(repo): ''' Helper function to return a function that returns a path given a path, that's relative to the repository. ''' c = repo.controldir() def repo_relative_path(path): return os.path.relpath(path, os.path.commonprefix((c, path))) return repo_relative_path
[docs]class Resource(object): ''' Handle the content of a text files as a list of lines, deal with saving it and staging the changes to a repo. ''' def __init__(self, filename, repo_relative_filename, thing=None): self.filename = filename self.repo_relative_filename = repo_relative_filename self.thing = thing or self._from_file(open(filename)) def _from_file(self, f): return f.read().splitlines() def _to_file(self, f): for line in self.thing: print >> f, line
[docs] def persist(self, repo): ''' Save the lines to the file and stage the file in the repo. ''' with open(self.filename, 'w') as f: os.chmod(self.filename, 0600) self._to_file(f) f.flush() os.fsync(f.fileno()) # For goodness's sake, write it to the disk already! repo.stage([self.repo_relative_filename])
[docs]class PickledResource(Resource): ''' A ``Resource`` subclass that uses ``pickle`` on its file/thing. ''' def _from_file(self, f): return [pickle.load(f)] def _to_file(self, f): pickle.dump(self.thing[0], f)
[docs]class PersistTask(object): ''' This class deals with saving changes to the git repo. ''' LIMIT = 10 MAX_SAVE = 10 def __init__(self, home): self.home = home self.repo = open_repo(home) self._r = make_repo_relative_path_maker(self.repo) self.counter = Counter() self.store = {}
[docs] def open(self, name): ''' Look up the named file in home and return its content_id and data. ''' fn = os.path.join(self.home, name) content_id = name # hash(fn) try: resource = self.store[content_id] except KeyError: R = PickledResource if name.endswith('.pickle') else Resource resource = self.store[content_id] = R(fn, self._r(fn)) return content_id, resource.thing
[docs] def handle(self, message): ''' Handle messages, dispatch to ``handle_FOO()`` methods. ''' if isinstance(message, core.OpenMessage): self.handle_open(message) elif isinstance(message, core.ModifyMessage): self.handle_modify(message) elif isinstance(message, core.PersistMessage): self.handle_persist(message) elif isinstance(message, core.ShutdownMessage): for content_id in self.counter: self.store[content_id].persist(self.repo) self.commit('shutdown')
[docs] def handle_open(self, message): ''' Foo. ''' try: message.content_id, message.thing = self.open(message.name) except: message.traceback = traceback.format_exc() message.status = core.ERROR else: message.status = core.SUCCESS
[docs] def handle_modify(self, message): ''' Foo. ''' try: content_id = message.details['content_id'] except KeyError: return if not content_id: return self.counter[content_id] += 1 if self.counter[content_id] > self.LIMIT: self.persist(content_id) self.commit('due to activity')
[docs] def handle_persist(self, message): ''' Foo. ''' try: resource = self.store[message.content_id] except KeyError: resource = self.handle_persist_new(message) resource.persist(self.repo) self.commit('by request from %r' % (message.sender,))
[docs] def handle_persist_new(self, message): ''' Foo. ''' name = message.content_id check_filename(name) fn = os.path.join(self.home, name) thing = message.details['thing'] R = PickledResource if name.endswith('.pickle') else Resource # !!! refactor! resource = self.store[name] = R(fn, self._r(fn), thing) return resource
[docs] def persist(self, content_id): ''' Persist a resource. ''' del self.counter[content_id] self.store[content_id].persist(self.repo)
[docs] def task_run(self): ''' Stage any outstanding changes. ''' if not self.counter: return for content_id, _ in self.counter.most_common(self.MAX_SAVE): self.persist(content_id) self.commit()
[docs] def commit(self, message='auto-commit'): ''' Commit. ''' return self.repo.do_commit(message, committer=core.COMMITTER)
[docs] def scan(self): ''' Return a sorted list of all the files in the home dir. ''' return sorted([ fn for fn in os.listdir(self.home) if os.path.isfile(os.path.join(self.home, fn)) ])
[docs]def check_filename(name): ''' Sanity checks for filename. ''' # TODO: improve this... if len(name) > 64: raise ValueError('bad name %r' % (name,)) left, dot, right = name.partition('.') if not left.isalnum() or dot and not right.isalnum(): raise ValueError('bad name %r' % (name,))
if __name__ == '__main__': JOY_HOME = os.path.expanduser('~/.thun') pt = PersistTask(JOY_HOME) content_id, thing = pt.open('stack.pickle') pt.persist(content_id) print pt.counter mm = core.ModifyMessage(None, None, content_id=content_id) pt.handle(mm) print pt.counter