import bpy from pathlib import Path from asset_library.common.file_utils import read_file, write_file from copy import deepcopy import time from itertools import groupby class AssetCache: def __init__(self, file_cache, data=None): self.file_cache = file_cache self.catalog = None self.author = None self.description = None self.tags = None self.type = None self.name = None self._metadata = None if data: self.set_data(data) @property def filepath(self): return self.file_cache.filepath @property def library_id(self): return self.file_cache.library_id @property def metadata(self): metadata = { '.library_id': self.library_id, '.filepath': self.filepath } metadata.update(self._metadata) return metadata @property def norm_name(self): return self.name.replace(' ', '_').lower() def unique_name(self): return (self.filepath / self.name).as_posix() def set_data(self, data): catalog = data['catalog'] if isinstance(catalog, (list, tuple)): catalog = '/'.join(catalog) self.catalog = catalog self.author = data.get('author', '') self.description = data.get('description', '') self.tags = data.get('tags', []) self.type = data.get('type') self.name = data['name'] self._metadata = data.get('metadata', {}) def to_dict(self): return dict( catalog=self.catalog, author=self.author, metadata=self.metadata, description=self.description, tags=self.tags, type=self.type, name=self.name ) def __repr__(self): return f'AssetCache(name={self.name}, catalog={self.catalog})' def __eq__(self, other): return self.to_dict() == other.to_dict() class AssetsCache: def __init__(self, file_cache): self.file_cache = file_cache self._data = [] def add(self, asset_cache_data, **kargs): asset_cache = AssetCache(self.file_cache, {**asset_cache_data, **kargs}) self._data.append(asset_cache) return asset_cache def remove(self, asset_cache): if isinstance(asset_cache, str): asset_cache = self.get(asset_cache) def __iter__(self): return self._data.__iter__() def __getitem__(self, key): if isinstance(key, str): return self.to_dict()[key] else: return self._data[key] def to_dict(self): return {a.name: a for a in self} def get(self, name): return next((a for a in self if a.name == name), None) def __repr__(self): return f'AssetsCache({list(self)})' class FileCache: def __init__(self, library_cache, data=None): self.library_cache = library_cache self.filepath = None self.modified = None self.assets = AssetsCache(self) if data: self.set_data(data) @property def library_id(self): return self.library_cache.library_id def set_data(self, data): if 'filepath' in data: self.filepath = Path(data['filepath']) self.modified = data.get('modified', time.time_ns()) if data.get('type') == 'FILE': self.assets.add(data) for asset_cache_data in data.get('assets', []): self.assets.add(asset_cache_data) def to_dict(self): return dict( filepath=self.filepath.as_posix(), modified=self.modified, library_id=self.library_id, assets=[asset_cache.to_dict() for asset_cache in self] ) def __iter__(self): return self.assets.__iter__() def __getitem__(self, key): return self._data[key] def __repr__(self): return f'FileCache(filepath={self.filepath})' class AssetCacheDiff: def __init__(self, library_cache, asset_cache, operation): self.library_cache = library_cache #self.filepath = data['filepath'] self.operation = operation self.asset_cache = asset_cache class LibraryCacheDiff: def __init__(self, old_cache=None, new_cache=None, filepath=None): self.filepath = filepath self._data = [] self.compare(old_cache, new_cache) def add(self, asset_cache_datas, operation): if not isinstance(asset_cache_datas, (list, tuple)): asset_cache_datas = [asset_cache_datas] new_asset_diffs = [] for cache_data in asset_cache_datas: new_asset_diffs.append(AssetCacheDiff(self, cache_data, operation)) self._data += new_asset_diffs return new_asset_diffs def compare(self, old_cache, new_cache): if old_cache is None or new_cache is None: print('Cannot Compare cache with None') cache_dict = {a.unique_name : a for a in old_cache.asset_caches} new_cache_dict = {a.unique_name : a for a in new_cache.asset_caches} assets_added = self.add([v for k, v in new_cache_dict.items() if k not in cache_dict], 'ADD') assets_removed = self.add([v for k, v in cache_dict.items() if k not in new_cache_dict], 'REMOVED') assets_modified = self.add([v for k, v in cache_dict.items() if v not in assets_removed and v!= new_cache_dict[k]], 'MODIFIED') if assets_added: print(f'{len(assets_added)} Assets Added \n{tuple(a.name for a in assets_added[:10])}...\n') if assets_removed: print(f'{len(assets_removed)} Assets Removed \n{tuple(a.name for a in assets_removed[:10])}...\n') if assets_modified: print(f'{len(assets_modified)} Assets Modified \n{tuple(a.name for a in assets_modified[:10])}...\n') if len(self) == 0: print('No change in the library') return self def group_by(self, key): '''Return groups of file cache diff using the key provided''' data = list(self).sort(key=key) return groupby(data, key=key) def __iter__(self): return iter(self._data) def __getitem__(self, key): return self._data[key] def __len__(self): return len(self._data) def __repr__(self): return f'LibraryCacheDiff(operations={[o for o in self][:2]}...)' class LibraryCache: def __init__(self, filepath): self.filepath = Path(filepath) self._data = [] @classmethod def from_library(cls, library): filepath = library.library_path / f"blender_assets.{library.id}.json" return cls(filepath) @property def filename(self): return self.filepath.name @property def library_id(self): return self.filepath.stem.split('.')[-1] #@property #def filepath(self): # """Get the filepath of the library json file relative to the library""" # return self.directory / self.filename def catalogs(self): return set(a.catalog for a in self.asset_caches) @property def asset_caches(self): '''Return an iterator to get all asset caches''' return (asset_cache for file_cache in self for asset_cache in file_cache) @property def tmp_filepath(self): return Path(bpy.app.tempdir) / self.filename def read(self): print(f'Read cache from {self.filepath}') for file_cache_data in read_file(self.filepath): self.add(file_cache_data) return self def write(self, tmp=False): filepath = self.filepath if tmp: filepath = self.tmp_filepath print(f'Write cache file to {filepath}') write_file(filepath, self._data) return filepath def add(self, file_cache_data=None): file_cache = FileCache(self, file_cache_data) self._data.append(file_cache) return file_cache def add_asset_cache(self, asset_cache_data, filepath=None): if filepath is None: filepath = asset_cache_data['filepath'] file_cache = self.get(filepath) if not file_cache: file_cache = self.add() file_cache.assets.add(asset_cache_data) # def unflatten_cache(self, cache): # """ Return a new unflattten list of asset data # grouped by filepath""" # new_cache = [] # cache = deepcopy(cache) # cache.sort(key=lambda x : x['filepath']) # groups = groupby(cache, key=lambda x : x['filepath']) # keys = ['filepath', 'modified', 'library_id'] # for _, asset_datas in groups: # asset_datas = list(asset_datas) # #print(asset_datas[0]) # asset_info = {k:asset_datas[0][k] for k in keys} # asset_info['assets'] = [{k:v for k, v in a.items() if k not in keys+['operation']} for a in asset_datas] # new_cache.append(asset_info) # return new_cache def diff(self, new_cache=None): """Compare the library cache with it current state and return the cache differential""" old_cache = self.read() if new_cache is None: new_cache = self return LibraryCacheDiff(old_cache, new_cache) def update(self, cache_diff): #Update the cache with the operations for asset_cache_diff in cache_diff: file_cache = self.get(asset_cache_diff.filepath) if not asset_cache: print(f'Filepath {asset_cache_diff.filepath} not in {self}' ) continue asset_cache = file_cache.get(asset_cache_diff.name) if not asset_cache: print(f'Asset {asset_cache_diff.name} not in file_cache {file_cache}' ) continue if asset_cache_diff.operation == 'REMOVE': file_cache.assets.remove(asset_cache_diff.name) elif asset_cache_diff.operation in ('MODIFY', 'ADD'): asset_cache.set_data(asset_cache_diff.asset_cache.to_dict()) return self def __len__(self): return len(self._data) def __iter__(self): return iter(self._data) def __getitem__(self, key): if isinstance(key, str): return self.to_dict()[key] else: return self._data[key] def to_dict(self): return {a.filepath: a for a in self} def get(self, filepath): return next((a for a in self if a.filepath == filepath), None) def __repr__(self): return f'LibraryCache(library_id={self.library_id})'