diff --git a/README.md b/README.md index 1e5c93f..8260b53 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,19 @@ ## 🚨🚨 Dev branch 🚨🚨: still not usable, lot of change in the core of beampy, work in progress... ### 🚧 TODO -- Tests for core: +- [ ] Tests for core: - position operation - length operation - group operation - layer opertation - define module outside slide and call + - test cache -- Re implement theme system -- Re implement cache system -- Re implement all modules -- Re implement exports + +- [x] Re implement theme system +- [ ] Re implement cache system, almost done (need to add global information to cache like version etc...) +- [ ] Re implement all modules +- [ ] Re implement exports Beampy is a python tool to create slide-show in svg that can be displayed with HTML5 (tested on Firefox and Chromium) diff --git a/beampy/_version.py b/beampy/_version.py new file mode 100644 index 0000000..a63ef4c --- /dev/null +++ b/beampy/_version.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +__version__ = '1.0.0' diff --git a/beampy/core/cache.py b/beampy/core/cache.py index c6ad7d0..fa9d957 100644 --- a/beampy/core/cache.py +++ b/beampy/core/cache.py @@ -6,7 +6,10 @@ Manage cache system for slides """ import os, sys +from pathlib import Path import pickle as pkl +import json +from beampy.core.store import Store import gzip import hashlib @@ -20,30 +23,154 @@ class Cache(): """Manage cache for slide contents in Beampy """ - def __init__(self, cache_dir=None): + def __init__(self, cache_dir=None, register=True): + """Create the cache folder and methods to interact with it. + """ + + # Use this dict to save global information on cache + self.data = {} + + if cache_dir is None: + cache_dir = create_folder_name() + self.folder = cache_dir - self.index_name = 'data.pklz' - self.index_fullpath = os.path.join(self.folder, self.index_name) + self.index_name = 'data.zip' + self.index_fullpath = self.folder/self.index_name + + _log.debug(f'Cache dir: {self.folder}') + + if self.folder.exists(): + self.data = self.read() + else: + self.folder.mkdir() + _log.debug('create cache dir') + + # Do we need to erase cache due to a change in beampy version + if len(self.data) > 0 and self.data['version'] != Store._version: + print('Reset cache because beampy version changed') + self.data = {} + self.remove_files() + + # Set the version in cache data + self.data['version'] = Store._version + + if register: + Store._cache = self def read(self): - """Read the cache file on disk + """Read the cache index file on disk. """ - dataout = None - if os.path.exists(self.index_fullpath): - with gzip.open(self.index_full_path, 'rb') as f: - dataout = pkl.load(f) + dataout = {} + if self.index_fullpath.is_file(): + with gzip.open(self.index_fullpath, 'rb') as f: + dataout = json.load(f) else: _log.debug('Unable to load cache file %s' % str(self.index_fullpath)) return dataout + def is_cached(self, file_id: str) -> bool: + """Test if a given file_id is cached + """ + + if (self.folder/f'{file_id}.json.gz').is_file(): + return True + + return False + + def add(self, content: object): + """Add a content (beampy.core.content.Content object) to a file. + + We split content oject into two parts: + - The content + - The data + """ + + # Need to transform content object to a dictionnary + + # The content + ct = {} + ct["id"] = content.id + ct["type"] = content.type + ct["name"] = content.name + ct["content"] = content.content + ct["width"] = content.width + ct["height"] = content.height + ct["data_id"] = content._data_id + + # The data + data = content.data + + if not self.is_cached(content.id): + self.write_file(content.id, ct) + else: + raise Exception("File already exist for %s" % str(content)) + + if not self.is_cached(ct["data_id"]): + self.write_file(ct["data_id"], data) + + fname = self.folder/f'{content.id}.zip' + _log.debug(f"Write {fname} to cache") + + def load_from_cache(self, content_id: str) -> dict: + """Load content and it's data from cached file with the given content_id. + + Return a dictionnary width Content informations (id, content, width, + height, data_id) + """ + + content = self.read_file(content_id) + + # Check if we need to load data or if they are already in the Store. + if not Store.is_data(content["data_id"]): + tmp_data = self.read_file(content["data_id"]) + Store.add_data(tmp_data, content["data_id"]) + + return content + + def write_file(self, filename, content): + """Write a file to cache folder + """ + + with gzip.open(self.folder/f'{filename}.json.gz', 'wb') as f: + f.write(json.dumps(content).encode('utf-8')) + + def read_file(self, filename): + """Read a content pikles from file + """ + output = None + fname = self.folder/f'{filename}.json.gz' + if fname.is_file(): + with gzip.open(fname, 'rb') as f: + output = json.loads(f.read().decode('utf8')) + else: + raise Exception("Unable to load %s from cache" % filename) + + return output + def remove_files(self): """Remove all files from cache folder """ - for f in glob.glob(self.folder+'/*.pklz'): + for f in self.folder.glob('*.json.gz'): os.remove(f) + +def create_folder_name(): + """Create a folder name for the cache. Depends on how beampy is run. Mostly + by calling: "python3 my_presentation.py", we use sys.argv[0] to get my_presentation.py + """ + + guess_scriptfilename = Path(sys.argv[0]) + if ('ipykernel' in guess_scriptfilename.name or 'ipython' in guess_scriptfilename.name): + cache_folder = guess_scriptfilename.home().joinpath('.beampy_cache_ipython') + print(f"Your in a ipython/notebook session I will put the cache in {cache_folder}") + else: + scriptname = guess_scriptfilename.name.replace(guess_scriptfilename.suffix, '') + cache_folder = Path('./').absolute().joinpath(f'.beampy_cache_{scriptname}') + + return cache_folder + class cache_slides(): def __init__(self, cache_dir, document): diff --git a/beampy/core/content.py b/beampy/core/content.py index 7316047..d5d8822 100644 --- a/beampy/core/content.py +++ b/beampy/core/content.py @@ -8,6 +8,7 @@ import sys import hashlib import logging +import json from beampy.core.store import Store _log = logging.getLogger(__name__) @@ -101,8 +102,53 @@ def height(self, height): self._height = height Store.update_content_size(self) + @property def is_cached(self): - pass + """Check if this content is cached on disk + """ + + if Store.cache() is not None: + return Store.cache().is_cached(self.id) + + return False + + def load_from_cache(self): + """Load a content from cache file + """ + if Store.cache() is not None: + cache_content = Store.cache().load_from_cache(self.id) + # Update the content object + self.content = cache_content["content"] + self._data_id = cache_content["data_id"] + + # Add the content to the Store if needed + if not Store.is_content(self.id): + Store.add_content(self) + + # Update it's width and height + self.width = cache_content["width"] + self.height = cache_content["height"] + + def add_to_cache(self): + """Add the current object to cache files + """ + + if Store.cache() is not None: + Store.cache().add(self) + + @property + def json(self): + """Export content to json + """ + out = {} + out["id"] = self.id + out["content"] = self.content + out["width"] = self.width + out["height"] = self.height + out["data_id"] = self._data_id + out["data"] = self.data + + return json.dumps(out) def __repr__(self): out = f'Content {self.id}\n' diff --git a/beampy/core/module.py b/beampy/core/module.py index 0a04f3d..434282b 100644 --- a/beampy/core/module.py +++ b/beampy/core/module.py @@ -196,9 +196,16 @@ def add_content(self, content, content_type): print(f'This module {self.signature} already exist in the Store, I will use {self.content_id}' ) self._content.load_from_store() else: - # Render the module - self.pre_render() - self.run_render() + # Check if the module is cached + if self._content.is_cached: + # Load from cache + self._content.load_from_cache() + else: + # Render the module + self.pre_render() + self.run_render() + # Add content to cache + self._content.add_to_cache() def update_signature(self, *args, **kwargs): """Create a unique signature of the init method using inspect module. @@ -822,7 +829,7 @@ def render_svgdefs(self): def __repr__(self): out = 'module: %s\n'%self.name out += 'Id: %s\n' % self.id - if self._content is not None: + if hasattr(self, '_content') and self._content is not None: out += 'Content id: %s\n' % self._content.id out += 'width: %s, height: %s\n' % (str(self.width), str(self.height)) out += 'x: %s, y: %s\n' % (self.x, self.y) diff --git a/beampy/core/store.py b/beampy/core/store.py index 0d65812..52613fb 100644 --- a/beampy/core/store.py +++ b/beampy/core/store.py @@ -5,9 +5,9 @@ variable conservation in python """ import logging +from .._version import __version__ _log = logging.getLogger(__name__) - class StoreMetaclass(type): """ Define a metaclass for store to let define some usefull properties for the @@ -33,7 +33,7 @@ def __repr__(self): out += "- %i slides\n" % (len(self)) out += "- %i modules [%i group]\n" % (len(self._contents), ngroups) out += f"- current slide: {self._current_slide}\n" - out += f"- current group: {self.group}\n" + out += f"- current group: {self.group()}\n" return out @@ -62,12 +62,18 @@ class Store(metaclass=StoreMetaclass): # Store slideshow theme _theme = dict() + # The cache class + _cache = None + # Store the slideshow layout (previously denominated document in Beampy < 1.0) _layout = None # Store the current group _current_group = None + # Beampy version + _version = __version__ + @classmethod def __repr__(cls): return '%s' % str(cls._slides) @@ -237,7 +243,7 @@ def set_group(cls, new_group): @classmethod def isgroup(cls): - if isinstance(cls.group, property) or cls.group is None: + if cls.group() is None: return False return True @@ -249,6 +255,10 @@ def theme(cls, module_name): else: raise KeyError(f'Not such module {module_name} defined in theme {cls._theme.keys()}') + @classmethod + def cache(cls): + return cls._cache + @classmethod def clear_all(cls): """ @@ -262,7 +272,8 @@ def clear_all(cls): cls._options = dict() cls._theme = dict() cls._layout = None - cls.group = None + cls._cache = None + cls._current_group = None # Some functions to easily manipulate the store def get_module_position(content_id, slide_id=None): diff --git a/setup.py b/setup.py index 3212639..535a993 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -from beampy import __version__ as beampy_version +from beampy._version import __version__ as beampy_version with open("README.md", "r") as fh: long_description = fh.read()