First Commit
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
|
||||
#from asset_library.bundle_blend import bundle_blend, bundle_library
|
||||
#from file_utils import (norm_str, norm_value,
|
||||
# norm_arg, get_bl_cmd, copy_file, copy_dir)
|
||||
#from asset_library.functions import
|
||||
|
||||
#from asset_library.common import bundle_blend
|
||||
from asset_library.common import file_utils
|
||||
from asset_library.common import functions
|
||||
from asset_library.common import synchronize
|
||||
from asset_library.common import template
|
||||
|
||||
if 'bpy' in locals():
|
||||
import importlib
|
||||
|
||||
#importlib.reload(bundle_blend)
|
||||
importlib.reload(file_utils)
|
||||
importlib.reload(functions)
|
||||
importlib.reload(synchronize)
|
||||
importlib.reload(template)
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
|
||||
"""
|
||||
Generic Blender functions
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from fnmatch import fnmatch
|
||||
from typing import Any, List, Iterable, Optional, Tuple
|
||||
Datablock = Any
|
||||
|
||||
import bpy
|
||||
from bpy_extras import asset_utils
|
||||
from asset_library.constants import RESOURCES_DIR
|
||||
#from asset_library.common.file_utils import no
|
||||
from os.path import abspath
|
||||
|
||||
|
||||
class attr_set():
|
||||
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
|
||||
entering with-statement : Store existing values, assign wanted value (if any)
|
||||
exiting with-statement: Restore values to their old values
|
||||
'''
|
||||
|
||||
def __init__(self, attrib_list):
|
||||
self.store = []
|
||||
# item = (prop, attr, [new_val])
|
||||
for item in attrib_list:
|
||||
prop, attr = item[:2]
|
||||
self.store.append( (prop, attr, getattr(prop, attr)) )
|
||||
if len(item) >= 3:
|
||||
try:
|
||||
setattr(prop, attr, item[2])
|
||||
except TypeError:
|
||||
print(f'Cannot set attribute {attr} to {prop}')
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
for prop, attr, old_val in self.store:
|
||||
setattr(prop, attr, old_val)
|
||||
|
||||
def get_overriden_col(ob, scene=None):
|
||||
scn = scene or bpy.context.scene
|
||||
|
||||
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
|
||||
|
||||
return next((c for c in cols if ob in c.all_objects[:]
|
||||
if all(not c.override_library for c in get_col_parents(c))), None)
|
||||
|
||||
def get_view3d_persp():
|
||||
windows = bpy.context.window_manager.windows
|
||||
view_3ds = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D']
|
||||
view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0])
|
||||
return view_3d
|
||||
|
||||
def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.Area]:
|
||||
"""Return the asset browser Area that's largest on screen.
|
||||
|
||||
:param screen: context.window.screen
|
||||
|
||||
:return: the Area, or None if no Asset Browser area exists.
|
||||
"""
|
||||
|
||||
def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]:
|
||||
"""Return area size in pixels."""
|
||||
return (area.width * area.height)
|
||||
|
||||
areas = list(suitable_areas(screen))
|
||||
if not areas:
|
||||
return None
|
||||
|
||||
return max(areas, key=area_sorting_key)
|
||||
|
||||
def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
|
||||
"""Generator, yield Asset Browser areas."""
|
||||
|
||||
for area in screen.areas:
|
||||
space_data = area.spaces[0]
|
||||
if not asset_utils.SpaceAssetInfo.is_asset_browser(space_data):
|
||||
continue
|
||||
yield area
|
||||
|
||||
def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
|
||||
"""Return an Asset Browser suitable for the given category.
|
||||
|
||||
Prefers the current Asset Browser if available, otherwise the biggest.
|
||||
"""
|
||||
|
||||
space_data = context.space_data
|
||||
if asset_utils.SpaceAssetInfo.is_asset_browser(space_data):
|
||||
return context.area
|
||||
|
||||
# Try the current screen first.
|
||||
browser_area = biggest_asset_browser_area(context.screen)
|
||||
if browser_area:
|
||||
return browser_area
|
||||
|
||||
for win in context.window_manager.windows:
|
||||
if win.screen == context.screen:
|
||||
continue
|
||||
browser_area = biggest_asset_browser_area(win.screen)
|
||||
if browser_area:
|
||||
return browser_area
|
||||
|
||||
return None
|
||||
|
||||
def activate_asset(
|
||||
asset: bpy.types.Action, asset_browser: bpy.types.Area, *, deferred: bool
|
||||
) -> None:
|
||||
"""Select & focus the asset in the browser."""
|
||||
|
||||
space_data = asset_browser.spaces[0]
|
||||
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
|
||||
space_data.activate_asset_by_id(asset, deferred=deferred)
|
||||
|
||||
def active_catalog_id(asset_browser: bpy.types.Area) -> str:
|
||||
"""Return the ID of the catalog shown in the asset browser."""
|
||||
return params(asset_browser).catalog_id
|
||||
|
||||
def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAssetSelectParams:
|
||||
"""Return the asset browser parameters given its Area."""
|
||||
space_data = asset_browser.spaces[0]
|
||||
assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data)
|
||||
return space_data.params
|
||||
|
||||
def refresh_asset_browsers():
|
||||
for area in suitable_areas(bpy.context.screen):
|
||||
bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]})
|
||||
|
||||
def tag_redraw(screen: bpy.types.Screen) -> None:
|
||||
"""Tag all asset browsers for redrawing."""
|
||||
|
||||
for area in suitable_areas(screen):
|
||||
area.tag_redraw()
|
||||
|
||||
# def get_blender_command(file=None, script=None, background=True, **args):
|
||||
# '''Return a Blender Command as a list to be used in a subprocess'''
|
||||
|
||||
# cmd = [bpy.app.binary_path]
|
||||
|
||||
# if file:
|
||||
# cmd += [str(file)]
|
||||
# if background:
|
||||
# cmd += ['--background']
|
||||
# if script:
|
||||
# cmd += ['--python', str(script)]
|
||||
# if args:
|
||||
# cmd += ['--']
|
||||
# for k, v in args.items():
|
||||
# cmd += [f"--{k.replace('_', '-')}", str(v)]
|
||||
|
||||
# return cmd
|
||||
|
||||
def norm_value(value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
values = []
|
||||
for v in value:
|
||||
if not isinstance(v, str):
|
||||
v = json.dumps(v)
|
||||
values.append(v)
|
||||
|
||||
return values
|
||||
|
||||
if isinstance(value, Path):
|
||||
return str(value)
|
||||
|
||||
if not isinstance(value, str):
|
||||
value = json.dumps(value)
|
||||
return value
|
||||
|
||||
def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
|
||||
arg_name = norm_str(arg_name, format=format, separator=separator)
|
||||
|
||||
return prefix + arg_name
|
||||
|
||||
def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs):
|
||||
cmd = [str(blender)] if blender else [bpy.app.binary_path]
|
||||
|
||||
if background:
|
||||
cmd += ['--background']
|
||||
|
||||
if not focus and not background:
|
||||
cmd += ['--no-window-focus']
|
||||
cmd += ['--window-geometry', '5000', '0', '10', '10']
|
||||
|
||||
if blendfile:
|
||||
cmd += [str(blendfile)]
|
||||
|
||||
if script:
|
||||
cmd += ['--python', str(script)]
|
||||
|
||||
if kargs:
|
||||
cmd += ['--']
|
||||
for k, v in kargs.items():
|
||||
k = norm_arg(k)
|
||||
v = norm_value(v)
|
||||
|
||||
cmd += [k]
|
||||
if isinstance(v, (tuple, list)):
|
||||
cmd += v
|
||||
else:
|
||||
cmd += [v]
|
||||
|
||||
return cmd
|
||||
|
||||
def get_addon_prefs():
|
||||
addon_name = __package__.split('.')[0]
|
||||
return bpy.context.preferences.addons[addon_name].preferences
|
||||
|
||||
|
||||
|
||||
def thumbnail_blend_file(input_blend, output_img):
|
||||
input_blend = Path(input_blend).resolve()
|
||||
output_img = Path(output_img).resolve()
|
||||
|
||||
print(f'Thumbnailing {input_blend} to {output_img}')
|
||||
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
|
||||
|
||||
output_img.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)])
|
||||
|
||||
success = output_img.exists()
|
||||
|
||||
if not success:
|
||||
empty_preview = RESOURCES_DIR / 'empty_preview.png'
|
||||
shutil.copy(str(empty_preview), str(output_img))
|
||||
|
||||
return success
|
||||
|
||||
def get_col_parents(col, root=None, cols=None):
|
||||
'''Return a list of parents collections of passed col
|
||||
root : Pass a collection to search in (recursive)
|
||||
else search in master collection
|
||||
'''
|
||||
if cols is None:
|
||||
cols = []
|
||||
|
||||
if root == None:
|
||||
root = bpy.context.scene.collection
|
||||
|
||||
for sub in root.children:
|
||||
if sub == col:
|
||||
cols.append(root)
|
||||
|
||||
if len(sub.children):
|
||||
cols = get_col_parents(col, root=sub, cols=cols)
|
||||
return cols
|
||||
|
||||
def get_overriden_col(ob, scene=None):
|
||||
'''Get the collection use for making the override'''
|
||||
scn = scene or bpy.context.scene
|
||||
|
||||
cols = [c for c in bpy.data.collections if scn.user_of_id(c)]
|
||||
|
||||
return next((c for c in cols if ob in c.all_objects[:]
|
||||
if all(not c.override_library for c in get_col_parents(c))), None)
|
||||
|
||||
def load_assets_from(filepath: Path) -> List[Datablock]:
|
||||
if not has_assets(filepath):
|
||||
# Avoid loading any datablocks when there are none marked as asset.
|
||||
return []
|
||||
|
||||
# Append everything from the file.
|
||||
with bpy.data.libraries.load(str(filepath)) as (
|
||||
data_from,
|
||||
data_to,
|
||||
):
|
||||
for attr in dir(data_to):
|
||||
setattr(data_to, attr, getattr(data_from, attr))
|
||||
|
||||
# Iterate over the appended datablocks to find assets.
|
||||
def loaded_datablocks() -> Iterable[Datablock]:
|
||||
for attr in dir(data_to):
|
||||
datablocks = getattr(data_to, attr)
|
||||
for datablock in datablocks:
|
||||
yield datablock
|
||||
|
||||
loaded_assets = []
|
||||
for datablock in loaded_datablocks():
|
||||
if not getattr(datablock, "asset_data", None):
|
||||
continue
|
||||
|
||||
# Fake User is lost when appending from another file.
|
||||
datablock.use_fake_user = True
|
||||
loaded_assets.append(datablock)
|
||||
return loaded_assets
|
||||
|
||||
def has_assets(filepath: Path) -> bool:
|
||||
with bpy.data.libraries.load(str(filepath), assets_only=True) as (
|
||||
data_from,
|
||||
_,
|
||||
):
|
||||
for attr in dir(data_from):
|
||||
data_names = getattr(data_from, attr)
|
||||
if data_names:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def copy_frames(start, end, offset, path):
|
||||
for i in range (start, end):
|
||||
src = path.replace('####', f'{i:04d}')
|
||||
dst = src.replace(src.split('_')[-1].split('.')[0], f'{i+offset:04d}')
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
def split_path(path) :
|
||||
try :
|
||||
bone_name = path.split('["')[1].split('"]')[0]
|
||||
except :
|
||||
bone_name = None
|
||||
try :
|
||||
prop_name = path.split('["')[2].split('"]')[0]
|
||||
except :
|
||||
prop_name = path.split('.')[-1]
|
||||
|
||||
return bone_name, prop_name
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def load_datablocks(src, names=None, type='objects', link=True, expr=None) -> list:
|
||||
return_list = not isinstance(names, str)
|
||||
names = names or []
|
||||
|
||||
if not isinstance(names, (list, tuple)):
|
||||
names = [names]
|
||||
|
||||
if isinstance(expr, str):
|
||||
pattern = expr
|
||||
expr = lambda x : fnmatch(x, pattern)
|
||||
|
||||
with bpy.data.libraries.load(str(src), link=link) as (data_from, data_to):
|
||||
datablocks = getattr(data_from, type)
|
||||
if expr:
|
||||
names += [i for i in datablocks if expr(i)]
|
||||
elif not names:
|
||||
names = datablocks
|
||||
|
||||
setattr(data_to, type, names)
|
||||
|
||||
datablocks = getattr(data_to, type)
|
||||
|
||||
if return_list:
|
||||
return datablocks
|
||||
|
||||
elif datablocks:
|
||||
return datablocks[0]
|
||||
|
||||
"""
|
||||
# --- Collection handling
|
||||
"""
|
||||
|
||||
def col_as_asset(col, verbose=False):
|
||||
if col is None:
|
||||
return
|
||||
if verbose:
|
||||
print('linking:', col.name)
|
||||
pcol = bpy.data.collections.new(col.name)
|
||||
bpy.context.scene.collection.children.link(pcol)
|
||||
pcol.children.link(col)
|
||||
pcol.asset_mark()
|
||||
return pcol
|
||||
|
||||
def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None):
|
||||
'''Link a collection by name from a file and override if has armature'''
|
||||
|
||||
# with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
|
||||
# data_to.collections = [c for c in data_from.collections if c == name]
|
||||
# if not data_to.collections:
|
||||
# return
|
||||
# return data_to.collections[0]
|
||||
context = context or bpy.context
|
||||
|
||||
collections = load_datablocks(filepath, name, link=link, type='collections')
|
||||
if not collections:
|
||||
print(f'No collection "{name}" found in: {filepath}')
|
||||
return
|
||||
|
||||
col = collections[0]
|
||||
print('collection:', col.name)
|
||||
|
||||
## create instance object
|
||||
inst = bpy.data.objects.new(col.name, None)
|
||||
inst.instance_collection = col
|
||||
inst.instance_type = 'COLLECTION'
|
||||
context.scene.collection.objects.link(inst)
|
||||
|
||||
# make active
|
||||
inst.select_set(True)
|
||||
context.view_layer.objects.active = inst
|
||||
|
||||
## simple object (no armatures)
|
||||
if not link or not override:
|
||||
return inst
|
||||
if not next((o for o in col.all_objects if o.type == 'ARMATURE'), None):
|
||||
return inst
|
||||
|
||||
## Create the override
|
||||
# Search
|
||||
parent_cols = inst.users_collection
|
||||
child_cols = [child for pcol in parent_cols for child in pcol.children]
|
||||
|
||||
params = {'active_object': inst, 'selected_objects': [inst]}
|
||||
try:
|
||||
bpy.ops.object.make_override_library(params)
|
||||
|
||||
## check which collection is new in parents collection
|
||||
asset_col = next((c for pcol in parent_cols for c in pcol.children if c not in child_cols), None)
|
||||
if not asset_col:
|
||||
print('Overriden, but no collection found !!')
|
||||
return
|
||||
|
||||
for ob in asset_col.all_objects:
|
||||
if ob.type != 'ARMATURE':
|
||||
continue
|
||||
if rig_pattern and not fnmatch(ob.name, rig_pattern):
|
||||
continue
|
||||
|
||||
ob.hide_select = ob.hide_viewport = False
|
||||
ob.select_set(True)
|
||||
context.view_layer.objects.active = ob
|
||||
print(ob.name)
|
||||
return ob
|
||||
|
||||
except Exception as e:
|
||||
print(f'Override failed on {col.name}')
|
||||
print(e)
|
||||
|
||||
return inst
|
||||
|
||||
|
||||
def get_preview(asset_path='', asset_name=''):
|
||||
asset_preview_dir = Path(asset_path).parents[1]
|
||||
name = asset_name.lower()
|
||||
return next((f for f in asset_preview_dir.rglob('*') if f.stem.lower().endswith(name)), None)
|
||||
|
||||
def get_object_libraries(ob):
|
||||
if not ob :
|
||||
return []
|
||||
|
||||
libraries = [ob.library, ob.data.library]
|
||||
|
||||
if ob.type in ('MESH', 'CURVE'):
|
||||
libraries += [m.library for m in ob.data.materials if m]
|
||||
|
||||
filepaths = []
|
||||
for l in libraries:
|
||||
if not l or not l.filepath:
|
||||
continue
|
||||
|
||||
absolute_filepath = abspath(bpy.path.abspath(l.filepath, library=l))
|
||||
if absolute_filepath in filepaths:
|
||||
continue
|
||||
|
||||
filepaths.append(absolute_filepath)
|
||||
|
||||
return filepaths
|
||||
@@ -0,0 +1,307 @@
|
||||
|
||||
"""Generic python functions to make operation on file and names"""
|
||||
|
||||
import fnmatch
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import unicodedata
|
||||
import os
|
||||
from pathlib import Path
|
||||
import importlib
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
|
||||
def install_module(module_name, package_name=None):
|
||||
'''Install a python module with pip or return it if already installed'''
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ModuleNotFoundError:
|
||||
print(f'Installing Module {module_name} ....')
|
||||
|
||||
subprocess.call([sys.executable, '-m', 'ensurepip'])
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', package_name or module_name])
|
||||
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
return module
|
||||
|
||||
def import_module_from_path(path):
|
||||
from importlib import util
|
||||
|
||||
try:
|
||||
path = Path(path)
|
||||
spec = util.spec_from_file_location(path.stem, str(path))
|
||||
mod = util.module_from_spec(spec)
|
||||
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
return mod
|
||||
except Exception as e:
|
||||
print(f'Cannot import file {path}')
|
||||
print(e)
|
||||
|
||||
def norm_str(string, separator='_', format=str.lower, padding=0):
|
||||
string = str(string)
|
||||
string = string.replace('_', ' ')
|
||||
string = string.replace('-', ' ')
|
||||
string = re.sub('[ ]+', ' ', string)
|
||||
string = re.sub('[ ]+\/[ ]+', '/', string)
|
||||
string = string.strip()
|
||||
|
||||
if format:
|
||||
string = format(string)
|
||||
|
||||
# Padd rightest number
|
||||
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
|
||||
|
||||
string = string.replace(' ', separator)
|
||||
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
|
||||
|
||||
return string
|
||||
|
||||
def remove_version(filepath):
|
||||
pattern = '_v[0-9]+\.'
|
||||
search = re.search(pattern, filepath)
|
||||
|
||||
if search:
|
||||
filepath = filepath.replace(search.group()[:-1], '')
|
||||
|
||||
return Path(filepath).name
|
||||
|
||||
def is_exclude(name, patterns) -> bool:
|
||||
# from fnmatch import fnmatch
|
||||
if not isinstance(patterns, (list,tuple)) :
|
||||
patterns = [patterns]
|
||||
return any([fnmatch(name, p) for p in patterns])
|
||||
|
||||
def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=None, ex_dir=None, keep=1, verbose=False) -> list:
|
||||
'''Recursively get last(s) file(s) (when there is multiple versions) in passed directory
|
||||
root -> str: Filepath of the folder to scan.
|
||||
pattern -> str: Regex pattern to group files.
|
||||
only_matching -> bool: Discard files that aren't matched by regex pattern.
|
||||
ex_file -> list : List of fn_match pattern to exclude files.
|
||||
ex_dir -> list : List of fn_match pattern of directory name to skip.
|
||||
keep -> int: Number of lasts versions to keep when there are mutliple versionned files (e.g: 1 keep only last).
|
||||
verbose -> bool: Print infos in console.
|
||||
'''
|
||||
|
||||
files = []
|
||||
if ex_file is None:
|
||||
all_items = [f for f in os.scandir(root)]
|
||||
else:
|
||||
all_items = [f for f in os.scandir(root) if not is_exclude(f.name, ex_file)]
|
||||
|
||||
allfiles = [f for f in all_items if f.is_file()]
|
||||
# Need to sort to effectively group separated key in list
|
||||
allfiles.sort(key=lambda x: x.name)
|
||||
|
||||
dirs = [f for f in all_items if f.is_dir()]
|
||||
|
||||
for i in range(len(allfiles)-1,-1,-1):# fastest way to iterate on index in reverse
|
||||
if not re.search(pattern, allfiles[i].name):
|
||||
if only_matching:
|
||||
allfiles.pop(i)
|
||||
else:
|
||||
files.append(allfiles.pop(i).path)
|
||||
|
||||
# separate remaining files in prefix grouped lists
|
||||
lilist = [list(v) for k, v in groupby(allfiles, key=lambda x: re.split(pattern, x.name)[0])]
|
||||
|
||||
# get only item last of each sorted grouplist
|
||||
for l in lilist:
|
||||
versions = sorted(l, key=lambda x: x.name)[-keep:] # exclude older
|
||||
for f in versions:
|
||||
files.append(f.path)
|
||||
|
||||
if verbose and len(l) > 1:
|
||||
print(f'{root}: keep {str([x.name for x in versions])} out of {len(l)} elements')
|
||||
|
||||
for d in dirs: # recursively treat all detected directory
|
||||
if ex_dir and is_exclude(d.name, ex_dir):
|
||||
# skip folder with excluded name
|
||||
continue
|
||||
files += get_last_files(
|
||||
d.path, pattern=pattern, only_matching=only_matching, ex_file=ex_file, ex_dir=ex_dir, keep=keep)
|
||||
|
||||
return sorted(files)
|
||||
|
||||
def copy_file(src, dst, only_new=False, only_recent=False):
|
||||
if dst.exists():
|
||||
if only_new:
|
||||
return
|
||||
elif only_recent and dst.stat().st_mtime >= src.stat().st_mtime:
|
||||
return
|
||||
|
||||
dst.parent.mkdir(exist_ok=True, parents=True)
|
||||
print(f'Copy file from {src} to {dst}')
|
||||
if platform.system() == 'Windows':
|
||||
subprocess.call(['copy', str(src), str(dst)], shell=True)
|
||||
else:
|
||||
subprocess.call(['cp', str(src), str(dst)])
|
||||
|
||||
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
|
||||
src, dst = Path(src), Path(dst)
|
||||
|
||||
if includes:
|
||||
includes = r'|'.join([fnmatch.translate(x) for x in includes])
|
||||
if excludes:
|
||||
excludes = r'|'.join([fnmatch.translate(x) for x in excludes])
|
||||
|
||||
if dst.is_dir():
|
||||
dst.mkdir(exist_ok=True, parents=True)
|
||||
else:
|
||||
dst.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
if src.is_file():
|
||||
copy_file(src, dst, only_new=only_new, only_recent=only_recent)
|
||||
|
||||
elif src.is_dir():
|
||||
src_files = list(src.rglob('*'))
|
||||
if excludes:
|
||||
src_files = [f for f in src_files if not re.match(excludes, f.name)]
|
||||
|
||||
if includes:
|
||||
src_files = [f for f in src_files if re.match(includes, f.name)]
|
||||
|
||||
dst_files = [dst/f.relative_to(src) for f in src_files]
|
||||
|
||||
for src_file, dst_file in zip(src_files, dst_files) :
|
||||
if src_file.is_dir():
|
||||
dst_file.mkdir(exist_ok=True, parents=True)
|
||||
else:
|
||||
copy_file(src_file, dst_file, only_new=only_new, only_recent=only_recent)
|
||||
|
||||
|
||||
def open_file(filepath, select=False):
|
||||
'''Open a filepath inside the os explorer'''
|
||||
|
||||
if platform.system() == 'Darwin': # macOS
|
||||
cmd = ['open']
|
||||
elif platform.system() == 'Windows': # Windows
|
||||
cmd = ['explorer']
|
||||
if select:
|
||||
cmd += ['/select,']
|
||||
else: # linux variants
|
||||
cmd = ['xdg-open']
|
||||
if select:
|
||||
cmd = ['nemo']
|
||||
|
||||
cmd += [str(filepath)]
|
||||
subprocess.Popen(cmd)
|
||||
|
||||
def open_blender_file(filepath=None):
|
||||
filepath = filepath or bpy.data.filepath
|
||||
|
||||
cmd = sys.argv
|
||||
|
||||
# if no filepath, use command as is to reopen blender
|
||||
if filepath != '':
|
||||
if len(cmd) > 1 and cmd[1].endswith('.blend'):
|
||||
cmd[1] = str(filepath)
|
||||
else:
|
||||
cmd.insert(1, str(filepath))
|
||||
|
||||
subprocess.Popen(cmd)
|
||||
|
||||
def read_file(path):
|
||||
'''Read a file with an extension in (json, yaml, yml, txt)'''
|
||||
|
||||
exts = ('.json', '.yaml', '.yml', '.txt')
|
||||
|
||||
if not path:
|
||||
print('Try to read empty file')
|
||||
|
||||
path = Path(path)
|
||||
if not path.exists():
|
||||
print('File not exist', path)
|
||||
return
|
||||
|
||||
if path.suffix not in exts:
|
||||
print(f'Cannot read file {path}, extension must be in {exts}')
|
||||
return
|
||||
|
||||
txt = path.read_text()
|
||||
data = None
|
||||
|
||||
if path.suffix.lower() in ('.yaml', '.yml'):
|
||||
yaml = install_module('yaml')
|
||||
try:
|
||||
data = yaml.safe_load(txt)
|
||||
except Exception:
|
||||
print(f'Could not load yaml file {path}')
|
||||
return
|
||||
elif path.suffix.lower() == '.json':
|
||||
try:
|
||||
data = json.loads(txt)
|
||||
except Exception:
|
||||
print(f'Could not load json file {path}')
|
||||
return
|
||||
else:
|
||||
data = txt
|
||||
|
||||
return data
|
||||
|
||||
def write_file(path, data, indent=4):
|
||||
'''Read a file with an extension in (json, yaml, yml, text)'''
|
||||
|
||||
exts = ('.json', '.yaml', '.yml', '.txt')
|
||||
|
||||
if not path:
|
||||
print('Try to write empty file')
|
||||
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if path.suffix not in exts:
|
||||
print(f'Cannot read file {path}, extension must be in {exts}')
|
||||
return
|
||||
|
||||
if path.suffix.lower() in ('.yaml', '.yml'):
|
||||
yaml = install_module('yaml')
|
||||
try:
|
||||
path.write_text(yaml.dump(data), encoding='utf8')
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f'Could not write yaml file {path}')
|
||||
return
|
||||
elif path.suffix.lower() == '.json':
|
||||
try:
|
||||
path.write_text(json.dumps(data, indent=indent), encoding='utf8')
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f'Could not write json file {path}')
|
||||
return
|
||||
else:
|
||||
data = path.write_text(data, encoding='utf8')
|
||||
|
||||
|
||||
def synchronize(src, dst, only_new=False, only_recent=False, clear=False):
|
||||
|
||||
#actionlib_dir = get_actionlib_dir(custom=custom)
|
||||
#local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
|
||||
|
||||
try:
|
||||
if clear and Path(dst).exists():
|
||||
shutil.rmtree(dst)
|
||||
|
||||
#set_actionlib_dir(custom=custom)
|
||||
|
||||
script = Path(__file__).parent / 'synchronize.py'
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
script,
|
||||
'--src', str(src),
|
||||
'--dst', str(dst),
|
||||
'--only-new', json.dumps(only_new),
|
||||
'--only-recent', json.dumps(only_recent),
|
||||
]
|
||||
|
||||
subprocess.Popen(cmd)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
"""
|
||||
Function relative to the asset browser addon
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import time
|
||||
#from asset_library.constants import ASSETLIB_FILENAME
|
||||
import inspect
|
||||
from asset_library.common.file_utils import read_file
|
||||
from asset_library.common.bl_utils import get_addon_prefs
|
||||
import uuid
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def command(func):
|
||||
'''Decorator to be used from printed functions argument and run time'''
|
||||
func_name = func.__name__.replace('_', ' ').title()
|
||||
|
||||
def _command(*args, **kargs):
|
||||
|
||||
bound = inspect.signature(func).bind(*args, **kargs)
|
||||
bound.apply_defaults()
|
||||
|
||||
args_str = ', '.join([f'{k}={v}' for k, v in bound.arguments.items()])
|
||||
print(f'\n[>-] {func_name} ({args_str}) --- Start ---')
|
||||
|
||||
t0 = time.time()
|
||||
result = func(*args, **kargs)
|
||||
|
||||
print(f'[>-] {func_name} --- Finished (total time : {time.time() - t0:.2f}s) ---')
|
||||
return result
|
||||
|
||||
return _command
|
||||
|
||||
def asset_warning_callback(self, context):
|
||||
"""Callback function to display a warning message when ading or modifying an asset"""
|
||||
self.warning = ''
|
||||
|
||||
if not self.name:
|
||||
self.warning = 'You need to specify a name'
|
||||
return
|
||||
if not self.catalog:
|
||||
self.warning = 'You need to specify a catalog'
|
||||
return
|
||||
|
||||
lib = get_active_library()
|
||||
action_path = lib.adapter.get_asset_relative_path(self.name, self.catalog)
|
||||
self.path = action_path.as_posix()
|
||||
|
||||
if lib.merge_libraries:
|
||||
prefs = get_addon_prefs()
|
||||
lib = prefs.libraries[lib.store_library]
|
||||
|
||||
if not lib.adapter.get_asset_path(self.name, self.catalog).parents[1].exists():
|
||||
self.warning = 'A new folder will be created'
|
||||
|
||||
def get_active_library():
|
||||
'''Get the pref library properties from the active library of the asset browser'''
|
||||
prefs = get_addon_prefs()
|
||||
asset_lib_ref = bpy.context.space_data.params.asset_library_ref
|
||||
|
||||
#Check for merged library
|
||||
for l in prefs.libraries:
|
||||
if l.library_name == asset_lib_ref:
|
||||
return l
|
||||
|
||||
def get_active_catalog():
|
||||
'''Get the active catalog path'''
|
||||
|
||||
lib = get_active_library()
|
||||
cat_data = lib.adapter.read_catalog()
|
||||
cat_data = {v['id']:k for k,v in cat_data.items()}
|
||||
|
||||
cat_id = bpy.context.space_data.params.catalog_id
|
||||
if cat_id in cat_data:
|
||||
return cat_data[cat_id]
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
def norm_asset_datas(asset_file_datas):
|
||||
''' Return a new flat list of asset data
|
||||
the filepath keys are merge with the assets keys'''
|
||||
|
||||
asset_datas = []
|
||||
for asset_file_data in asset_file_datas:
|
||||
asset_file_data = asset_file_data.copy()
|
||||
if 'assets' in asset_file_data:
|
||||
|
||||
assets = asset_file_data.pop('assets')
|
||||
for asset_data in assets:
|
||||
|
||||
asset_datas.append({**asset_file_data, **asset_data})
|
||||
|
||||
else:
|
||||
asset_datas.append(asset_file_data)
|
||||
|
||||
return asset_datas
|
||||
|
||||
def cache_diff(cache, new_cache):
|
||||
'''Compare and return the difference between two asset datas list'''
|
||||
|
||||
#TODO use an id to be able to tell modified asset if renamed
|
||||
#cache = {a.get('id', a['name']) : a for a in norm_asset_datas(cache)}
|
||||
#new_cache = {a.get('id', a['name']) : a for a in norm_asset_datas(new_cache)}
|
||||
|
||||
cache = {f"{a['filepath']}/{a['name']}": a for a in norm_asset_datas(cache)}
|
||||
new_cache = {f"{a['filepath']}/{a['name']}" : a for a in norm_asset_datas(new_cache)}
|
||||
|
||||
assets_added = [v for k, v in new_cache.items() if k not in cache]
|
||||
assets_removed = [v for k, v in cache.items() if k not in new_cache]
|
||||
assets_modified = [v for k, v in cache.items() if v not in assets_removed and v!= new_cache[k]]
|
||||
|
||||
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')
|
||||
|
||||
assets_added = [dict(a, operation='ADD') for a in assets_added]
|
||||
assets_removed = [dict(a, operation='REMOVE') for a in assets_removed]
|
||||
assets_modified = [dict(a, operation='MODIFY') for a in assets_modified]
|
||||
|
||||
assets_diff = assets_added + assets_removed + assets_modified
|
||||
if not assets_diff:
|
||||
print('No change in the library')
|
||||
|
||||
return assets_diff
|
||||
|
||||
def clean_default_lib():
|
||||
prefs = bpy.context.preferences
|
||||
|
||||
if not prefs.filepaths.asset_libraries:
|
||||
print('[>-] No Asset Libraries Filepaths Setted.')
|
||||
return
|
||||
|
||||
lib, lib_id = get_lib_id(
|
||||
library_name='User Library',
|
||||
asset_libraries=prefs.filepaths.asset_libraries
|
||||
)
|
||||
if lib:
|
||||
bpy.ops.preferences.asset_library_remove(index=lib_id)
|
||||
|
||||
def get_asset_source(replace_local=False):
|
||||
sp = bpy.context.space_data
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
asset_file_handle = bpy.context.asset_file_handle
|
||||
|
||||
if asset_file_handle is None:
|
||||
return None
|
||||
|
||||
if asset_file_handle.local_id:
|
||||
publish_path = os.path.expandvars(scn.actionlib.get('publish_path'))
|
||||
if not publish_path:
|
||||
print('[>.] No \'Publish Dir\' found. Publish file first.' )
|
||||
return None
|
||||
|
||||
return Path(publish_path)
|
||||
|
||||
asset_library_ref = bpy.context.asset_library_ref
|
||||
source_path = bpy.types.AssetHandle.get_full_library_path(asset_file_handle, asset_library_ref)
|
||||
|
||||
if replace_local:
|
||||
if 'custom' in sp.params.asset_library_ref.lower():
|
||||
actionlib_path = prefs.action.custom_path
|
||||
actionlib_path_local = prefs.action.custom_path_local
|
||||
else:
|
||||
actionlib_path = prefs.action.path
|
||||
actionlib_path_local = prefs.action.path_local
|
||||
|
||||
source_path = re.sub(actionlib_dir_local, actionlib_dir, source_path)
|
||||
|
||||
return source_path
|
||||
|
||||
def get_catalog_path(filepath=None):
|
||||
filepath = filepath or bpy.data.filepath
|
||||
filepath = Path(filepath)
|
||||
|
||||
if filepath.is_file():
|
||||
filepath = filepath.parent
|
||||
|
||||
filepath.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
catalog = filepath / 'blender_assets.cats.txt'
|
||||
if not catalog.exists():
|
||||
catalog.touch(exist_ok=False)
|
||||
|
||||
return catalog
|
||||
|
||||
|
||||
# def read_catalog(path, key='path'):
|
||||
# cat_data = {}
|
||||
|
||||
# supported_keys = ('path', 'id', 'name')
|
||||
|
||||
# if key not in supported_keys:
|
||||
# raise Exception(f'Not supported key: {key} for read catalog, supported keys are {supported_keys}')
|
||||
|
||||
# for line in Path(path).read_text(encoding="utf-8").split('\n'):
|
||||
# if line.startswith(('VERSION', '#')) or not line:
|
||||
# continue
|
||||
|
||||
# cat_id, cat_path, cat_name = line.split(':')
|
||||
|
||||
# if key == 'id':
|
||||
# cat_data[cat_id] = {'path':cat_path, 'name':cat_name}
|
||||
# elif key == 'path':
|
||||
# cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
|
||||
# elif key =='name':
|
||||
# cat_data[cat_name] = {'id':cat_id, 'path':cat_path}
|
||||
|
||||
# return cat_data
|
||||
|
||||
def read_catalog(path):
|
||||
cat_data = {}
|
||||
|
||||
for line in Path(path).read_text(encoding="utf-8").split('\n'):
|
||||
if line.startswith(('VERSION', '#')) or not line:
|
||||
continue
|
||||
|
||||
cat_id, cat_path, cat_name = line.split(':')
|
||||
cat_data[cat_path] = {'id':cat_id, 'name':cat_name}
|
||||
|
||||
return cat_data
|
||||
|
||||
def write_catalog(path, data):
|
||||
lines = ['VERSION 1', '']
|
||||
|
||||
# Add missing parents catalog
|
||||
norm_data = {}
|
||||
for cat_path, cat_data in data.items():
|
||||
norm_data[cat_path] = cat_data
|
||||
for p in Path(cat_path).parents[:-1]:
|
||||
if p in data or p in norm_data:
|
||||
continue
|
||||
|
||||
norm_data[p.as_posix()] = {'id': str(uuid.uuid4()), 'name': '-'.join(p.parts)}
|
||||
|
||||
for cat_path, cat_data in sorted(norm_data.items()):
|
||||
cat_name = cat_data['name'].replace('/', '-')
|
||||
lines.append(f"{cat_data['id']}:{cat_path}:{cat_name}")
|
||||
|
||||
print(f'Catalog writen at: {path}')
|
||||
Path(path).write_text('\n'.join(lines), encoding="utf-8")
|
||||
|
||||
def create_catalog_file(json_path : str|Path, keep_existing_category : bool = True):
|
||||
'''create asset catalog file from json
|
||||
if catalog already exists, keep existing catalog uid'''
|
||||
|
||||
json_path = Path(json_path)
|
||||
# if not json.exists(): return
|
||||
assert json_path.exists(), 'Json not exists !'
|
||||
|
||||
category_datas = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
|
||||
catalog_path = json_path.parent / 'blender_assets.cats.txt'
|
||||
catalog_data = {}
|
||||
if catalog_path.exists():
|
||||
catalog_data = read_catalog(catalog_path)
|
||||
## retrun a format catalog_data[path] = {'id':id, 'name':name}
|
||||
## note: 'path' in catalog is 'name' in category_datas
|
||||
|
||||
catalog_lines = ['VERSION 1', '']
|
||||
|
||||
## keep existing
|
||||
for c in category_datas:
|
||||
# keep same catalog line for existing category keys
|
||||
if keep_existing_category and catalog_data.get(c['name']):
|
||||
print(c['name'], 'category exists')
|
||||
cat = catalog_data[c['name']] #get
|
||||
catalog_lines.append(f"{cat['id']}:{c['name']}:{cat['name']}")
|
||||
else:
|
||||
print(c['name'], 'new category')
|
||||
# add new category
|
||||
catalog_lines.append(f"{c['id']}:{c['name']}:{c['name'].replace('/', '-')}")
|
||||
|
||||
## keep category that are non-existing in json ?
|
||||
if keep_existing_category:
|
||||
for k in catalog_data.keys():
|
||||
if next((c['name'] for c in category_datas if c['name'] == k), None):
|
||||
continue
|
||||
print(k, 'category not existing in json')
|
||||
cat = catalog_data[k]
|
||||
# rebuild existing line
|
||||
catalog_lines.append(f"{cat['id']}:{k}:{cat['name']}")
|
||||
|
||||
## write_text overwrite the file
|
||||
catalog_path.write_text('\n'.join(catalog_lines), encoding="utf-8")
|
||||
|
||||
print(f'Catalog saved at: {catalog_path}')
|
||||
|
||||
return
|
||||
|
||||
def clear_env_libraries():
|
||||
print('clear_env_libraries')
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
|
||||
|
||||
for env_lib in prefs.env_libraries:
|
||||
name = env_lib.get('asset_library')
|
||||
if not name:
|
||||
continue
|
||||
|
||||
asset_lib = asset_libraries.get(name)
|
||||
if not asset_lib:
|
||||
continue
|
||||
|
||||
index = list(asset_libraries).index(asset_lib)
|
||||
bpy.ops.preferences.asset_library_remove(index=index)
|
||||
|
||||
prefs.env_libraries.clear()
|
||||
|
||||
'''
|
||||
env_libs = get_env_libraries()
|
||||
paths = [Path(l['path']).resolve().as_posix() for n, l in env_libs.items()]
|
||||
|
||||
for i, l in reversed(enumerate(libs)):
|
||||
lib_path = Path(l.path).resolve().as_posix()
|
||||
|
||||
if (l.name in env_libs or lib_path in paths):
|
||||
libs.remove(i)
|
||||
'''
|
||||
|
||||
def set_env_libraries(path=None) -> list:
|
||||
'''Read the environments variables and create the libraries'''
|
||||
|
||||
#from asset_library.prefs import AssetLibraryOptions
|
||||
prefs = get_addon_prefs()
|
||||
path = path or prefs.config_directory
|
||||
|
||||
#print('Read', path)
|
||||
library_data = read_file(path)
|
||||
|
||||
clear_env_libraries()
|
||||
|
||||
if not library_data:
|
||||
return
|
||||
|
||||
libs = []
|
||||
|
||||
for lib_info in library_data:
|
||||
lib = prefs.env_libraries.add()
|
||||
|
||||
lib.set_dict(lib_info)
|
||||
|
||||
libs.append(lib)
|
||||
|
||||
return libs
|
||||
|
||||
'''
|
||||
def get_env_libraries():
|
||||
env_libraries = {}
|
||||
|
||||
for k, v in os.environ.items():
|
||||
if not re.findall('ASSET_LIBRARY_[0-9]', k):
|
||||
continue
|
||||
|
||||
lib_infos = v.split(os.pathsep)
|
||||
|
||||
if len(lib_infos) == 5:
|
||||
name, data_type, tpl, src_path, bdl_path = lib_infos
|
||||
elif len(lib_infos) == 4:
|
||||
name, data_type, tpl, src_path = lib_infos
|
||||
bdl_path = ''
|
||||
else:
|
||||
print(f'Wrong env key {k}', lib_infos)
|
||||
continue
|
||||
|
||||
source_type = 'TEMPLATE'
|
||||
if tpl.lower().endswith(('.json', '.yml', 'yaml')):
|
||||
source_type = 'DATA_FILE'
|
||||
|
||||
env_libraries[name] = {
|
||||
'data_type': data_type,
|
||||
'source_directory': src_path,
|
||||
'bundle_directory': bdl_path,
|
||||
'source_type': source_type,
|
||||
'template': tpl,
|
||||
}
|
||||
|
||||
return env_libraries
|
||||
'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def resync_lib(name, waiting_time):
|
||||
bpy.app.timers.register(
|
||||
lambda: bpy.ops.assetlib.synchronize(only_recent=True, name=name),
|
||||
first_interval=waiting_time
|
||||
)
|
||||
|
||||
|
||||
|
||||
'''
|
||||
def set_assetlib_paths():
|
||||
prefs = bpy.context.preferences
|
||||
|
||||
assetlib_name = 'Assets'
|
||||
assetlib = prefs.filepaths.asset_libraries.get(assetlib_name)
|
||||
|
||||
if not assetlib:
|
||||
bpy.ops.preferences.asset_library_add(directory=str(assetlib_path))
|
||||
assetlib = prefs.filepaths.asset_libraries[-1]
|
||||
assetlib.name = assetlib_name
|
||||
|
||||
assetlib.path = str(actionlib_dir)
|
||||
|
||||
def set_actionlib_paths():
|
||||
prefs = bpy.context.preferences
|
||||
|
||||
actionlib_name = 'Action Library'
|
||||
actionlib_custom_name = 'Action Library Custom'
|
||||
|
||||
actionlib = prefs.filepaths.asset_libraries.get(actionlib_name)
|
||||
|
||||
if not assetlib:
|
||||
bpy.ops.preferences.asset_library_add(directory=str(assetlib_path))
|
||||
assetlib = prefs.filepaths.asset_libraries[-1]
|
||||
assetlib.name = assetlib_name
|
||||
|
||||
actionlib_dir = get_actionlib_dir(custom=custom)
|
||||
local_actionlib_dir = get_actionlib_dir(local=True, custom=custom)
|
||||
|
||||
if local_actionlib_dir:
|
||||
actionlib_dir = local_actionlib_dir
|
||||
|
||||
if actionlib_name not in prefs.filepaths.asset_libraries:
|
||||
bpy.ops.preferences.asset_library_add(directory=str(actionlib_dir))
|
||||
|
||||
#lib, lib_id = get_lib_id(
|
||||
# library_path=actionlib_dir,
|
||||
# asset_libraries=prefs.filepaths.asset_libraries
|
||||
#)
|
||||
|
||||
#if not lib:
|
||||
# print(f'Cannot set dir for {actionlib_name}')
|
||||
# return
|
||||
|
||||
prefs.filepaths.asset_libraries[lib_id].name = actionlib_name
|
||||
#prefs.filepaths.asset_libraries[lib_id].path = str(actionlib_dir)
|
||||
'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# import module utils without excuting __init__
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"utils", Path(__file__).parent/"file_utils.py"
|
||||
)
|
||||
utils = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(utils)
|
||||
|
||||
|
||||
def synchronize(src, dst, only_new=False, only_recent=False):
|
||||
|
||||
excludes=['*.sync-conflict-*', '.*']
|
||||
includes=['*.blend', 'blender_assets.cats.txt']
|
||||
|
||||
utils.copy_dir(
|
||||
src, dst,
|
||||
only_new=only_new, only_recent=only_recent,
|
||||
excludes=excludes, includes=includes
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__' :
|
||||
parser = argparse.ArgumentParser(description='Add Comment To the tracker',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument('--src')
|
||||
parser.add_argument('--dst')
|
||||
parser.add_argument('--only-new', type=json.loads, default='false')
|
||||
parser.add_argument('--only-recent', type=json.loads, default='false')
|
||||
|
||||
args = parser.parse_args()
|
||||
synchronize(**vars(args))
|
||||
@@ -0,0 +1,89 @@
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
from fnmatch import fnmatch
|
||||
from glob import glob
|
||||
|
||||
|
||||
class Template:
|
||||
field_pattern = re.compile(r'{(\w+)\*{0,2}}')
|
||||
field_pattern_recursive = re.compile(r'{(\w+)\*{2}}')
|
||||
|
||||
def __init__(self, template):
|
||||
#asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
|
||||
|
||||
self.template = template
|
||||
|
||||
@property
|
||||
def glob_pattern(self):
|
||||
pattern = self.field_pattern_recursive.sub('**', self.template)
|
||||
pattern = self.field_pattern.sub('*', pattern)
|
||||
return pattern
|
||||
|
||||
@property
|
||||
def re_pattern(self):
|
||||
pattern = self.field_pattern_recursive.sub('([\\\w -_.\/]+)', self.template)
|
||||
pattern = self.field_pattern.sub('([\\\w -_.]+)', pattern)
|
||||
pattern = pattern.replace('?', '.')
|
||||
pattern = pattern.replace('*', '.*')
|
||||
|
||||
return re.compile(pattern)
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
return self.field_pattern.findall(self.template)
|
||||
#return [f or '0' for f in fields]
|
||||
|
||||
def parse(self, path):
|
||||
|
||||
path = Path(path).as_posix()
|
||||
|
||||
res = self.re_pattern.findall(path)
|
||||
if not res:
|
||||
print('Could not parse {path} with {self.re_pattern}')
|
||||
return {}
|
||||
|
||||
fields = self.fields
|
||||
|
||||
if len(fields) == 1:
|
||||
field_values = res
|
||||
else:
|
||||
field_values = res[0]
|
||||
|
||||
return {k:v for k,v in zip(fields, field_values)}
|
||||
|
||||
def format(self, data=None, **kargs):
|
||||
|
||||
#print('format', self.template, data, kargs)
|
||||
|
||||
data = {**(data or {}), **kargs}
|
||||
|
||||
try:
|
||||
path = self.template.format(**data)
|
||||
except KeyError:
|
||||
print(f'Cannot format {self.template} with {data}')
|
||||
return
|
||||
|
||||
path = os.path.expandvars(path)
|
||||
return Path(path)
|
||||
|
||||
def glob(self, directory, pattern=None):
|
||||
'''If pattern is given it need to be absolute'''
|
||||
if pattern is None:
|
||||
pattern = Path(directory, self.glob_pattern).as_posix()
|
||||
|
||||
for entry in os.scandir(directory):
|
||||
entry_path = Path(entry.path)
|
||||
if entry.is_file() and fnmatch(entry_path.as_posix(), pattern):
|
||||
yield entry_path
|
||||
elif entry.is_dir():
|
||||
yield from self.glob(entry.path, pattern)
|
||||
|
||||
def find(self, data, **kargs):
|
||||
pattern = self.format(data, **kargs)
|
||||
paths = glob(pattern.as_posix())
|
||||
if paths:
|
||||
return Path(paths[0])
|
||||
|
||||
def __repr__(self):
|
||||
return f'Template({self.template})'
|
||||
Reference in New Issue
Block a user