black format

This commit is contained in:
2026-01-07 16:05:47 +01:00
parent f7c125ae7b
commit 3d65bb6e4d
49 changed files with 2817 additions and 2280 deletions
+2 -4
View File
@@ -1,12 +1,10 @@
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
from asset_library.common import catalog
if 'bpy' in locals():
if "bpy" in locals():
import importlib
importlib.reload(file_utils)
@@ -15,4 +13,4 @@ if 'bpy' in locals():
importlib.reload(template)
importlib.reload(catalog)
import bpy
import bpy
+133 -86
View File
@@ -1,4 +1,3 @@
"""
Generic Blender functions
"""
@@ -6,29 +5,31 @@ 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 asset_library.common.file_utils import no
from os.path import abspath
import subprocess
class attr_set():
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
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)) )
self.store.append((prop, attr, getattr(prop, attr)))
for item in attrib_list:
prop, attr = item[:2]
@@ -36,7 +37,7 @@ class attr_set():
try:
setattr(prop, attr, item[2])
except TypeError:
print(f'Cannot set attribute {attr} to {prop}')
print(f"Cannot set attribute {attr} to {prop}")
def __enter__(self):
return self
@@ -48,25 +49,38 @@ class attr_set():
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)
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])
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 get_viewport():
screen = bpy.context.screen
areas = [a for a in screen.areas if a.type == 'VIEW_3D']
areas.sort(key=lambda x : x.width*x.height)
areas = [a for a in screen.areas if a.type == "VIEW_3D"]
areas.sort(key=lambda x: x.width * x.height)
return areas[-1]
@@ -81,7 +95,7 @@ def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.A
def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]:
"""Return area size in pixels."""
return (area.width * area.height)
return area.width * area.height
areas = list(suitable_areas(screen))
if not areas:
@@ -89,6 +103,7 @@ def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.A
return max(areas, key=area_sorting_key)
def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
"""Generator, yield Asset Browser areas."""
@@ -98,6 +113,7 @@ def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]:
continue
yield area
def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
"""Return an Asset Browser suitable for the given category.
@@ -122,6 +138,7 @@ def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]:
return None
def activate_asset(
asset: bpy.types.Action, asset_browser: bpy.types.Area, *, deferred: bool
) -> None:
@@ -131,19 +148,25 @@ def activate_asset(
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:
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]})
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."""
@@ -151,6 +174,7 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
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'''
@@ -166,9 +190,10 @@ def tag_redraw(screen: bpy.types.Screen) -> None:
# 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 = []
@@ -177,7 +202,7 @@ def norm_value(value):
v = json.dumps(v)
values.append(v)
return values
return values
if isinstance(value, Path):
return str(value)
@@ -186,31 +211,35 @@ def norm_value(value):
value = json.dumps(value)
return value
def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'):
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):
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']
cmd += ["--background"]
if not focus and not background:
cmd += ['--no-window-focus']
cmd += ['--window-geometry', '5000', '0', '10', '10']
cmd += ["--no-window-focus"]
cmd += ["--window-geometry", "5000", "0", "10", "10"]
cmd += ['--python-use-system-env']
cmd += ["--python-use-system-env"]
if blendfile:
cmd += [str(blendfile)]
if script:
cmd += ['--python', str(script)]
cmd += ["--python", str(script)]
if kargs:
cmd += ['--']
cmd += ["--"]
for k, v in kargs.items():
k = norm_arg(k)
v = norm_value(v)
@@ -223,18 +252,18 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip
return cmd
def get_addon_prefs():
addon_name = __package__.split('.')[0]
return bpy.context.preferences.addons[addon_name].preferences
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'
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)
@@ -243,16 +272,17 @@ def thumbnail_blend_file(input_blend, output_img):
success = output_img.exists()
if not success:
empty_preview = RESOURCES_DIR / 'empty_preview.png'
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
"""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 = []
@@ -267,14 +297,23 @@ def get_col_parents(col, root=None, cols=None):
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'''
"""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)
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):
@@ -306,6 +345,7 @@ def load_assets_from(filepath: Path) -> List[Datablock]:
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,
@@ -318,51 +358,49 @@ def has_assets(filepath: Path) -> bool:
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}')
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 :
def split_path(path):
try:
bone_name = path.split('["')[1].split('"]')[0]
except :
except:
bone_name = None
try :
try:
prop_name = path.split('["')[2].split('"]')[0]
except :
prop_name = path.split('.')[-1]
except:
prop_name = path.split(".")[-1]
return bone_name, prop_name
def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list:
def load_datablocks(
src, names=None, type="objects", link=True, expr=None, assets_only=False
) -> 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,assets_only=assets_only) as (data_from, data_to):
expr = lambda x: fnmatch(x, pattern)
with bpy.data.libraries.load(str(src), link=link, assets_only=assets_only) 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)
@@ -373,23 +411,26 @@ def load_datablocks(src, names=None, type='objects', link=True, expr=None, asset
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)
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'''
"""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]
@@ -398,12 +439,12 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
# return data_to.collections[0]
context = context or bpy.context
col = load_datablocks(filepath, name, link=link, type='collections')
col = load_datablocks(filepath, name, link=link, type="collections")
## create instance object
inst = bpy.data.objects.new(col.name, None)
inst.instance_collection = col
inst.instance_type = 'COLLECTION'
inst.instance_type = "COLLECTION"
context.scene.collection.objects.link(inst)
# make active
@@ -413,26 +454,29 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
## 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):
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]}
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)
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 !!')
print("Overriden, but no collection found !!")
return
for ob in asset_col.all_objects:
if ob.type != 'ARMATURE':
if ob.type != "ARMATURE":
continue
if rig_pattern and not fnmatch(ob.name, rig_pattern):
continue
@@ -444,37 +488,40 @@ def load_col(filepath, name, link=True, override=True, rig_pattern=None, context
return ob
except Exception as e:
print(f'Override failed on {col.name}')
print(f"Override failed on {col.name}")
print(e)
return inst
def get_preview(asset_path='', asset_name=''):
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)
return next(
(f for f in asset_preview_dir.rglob("*") if f.stem.lower().endswith(name)), None
)
def get_object_libraries(ob):
if ob is None:
return []
libraries = [ob.library]
if ob.data:
libraries += [ob.data.library]
if ob.type in ('MESH', 'CURVE'):
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
return filepaths
+43 -36
View File
@@ -1,4 +1,3 @@
from pathlib import Path
import uuid
import bpy
@@ -6,6 +5,7 @@ import bpy
class CatalogItem:
"""Represent a single item of a catalog"""
def __init__(self, catalog, path=None, name=None, id=None):
self.catalog = catalog
@@ -16,7 +16,7 @@ class CatalogItem:
if isinstance(self.path, Path):
self.path = self.path.as_posix()
if self.path and not self.name:
self.name = self.norm_name(self.path)
@@ -25,10 +25,10 @@ class CatalogItem:
def norm_name(self, name):
"""Get a norm name from a catalog_path entry"""
return name.replace('/', '-')
return name.replace("/", "-")
def __repr__(self):
return f'CatalogItem(name={self.name}, path={self.path}, id={self.id})'
return f"CatalogItem(name={self.name}, path={self.path}, id={self.id})"
class CatalogContext:
@@ -60,11 +60,12 @@ class CatalogContext:
if self.active_item:
return self.active_item.path
return ''
return ""
class Catalog:
"""Represent the catalog of the blender asset browser library"""
def __init__(self, directory=None):
self.directory = None
@@ -72,14 +73,14 @@ class Catalog:
if directory:
self.directory = Path(directory)
self.context = CatalogContext()
@property
def filepath(self):
"""Get the filepath of the catalog text file relative to the directory"""
if self.directory:
return self.directory /'blender_assets.cats.txt'
return self.directory / "blender_assets.cats.txt"
def read(self):
"""Read the catalog file of the library target directory or of the specified directory"""
@@ -88,47 +89,49 @@ class Catalog:
return {}
self._data.clear()
print(f'Read catalog from {self.filepath}')
for line in self.filepath.read_text(encoding="utf-8").split('\n'):
if line.startswith(('VERSION', '#')) or not line:
print(f"Read catalog from {self.filepath}")
for line in self.filepath.read_text(encoding="utf-8").split("\n"):
if line.startswith(("VERSION", "#")) or not line:
continue
cat_id, cat_path, cat_name = line.split(':')
self._data[cat_id] = CatalogItem(self, name=cat_name, id=cat_id, path=cat_path)
cat_id, cat_path, cat_name = line.split(":")
self._data[cat_id] = CatalogItem(
self, name=cat_name, id=cat_id, path=cat_path
)
return self
def write(self, sort=True):
"""Write the catalog file in the library target directory or of the specified directory"""
if not self.filepath:
raise Exception(f'Cannot write catalog {self} no filepath setted')
lines = ['VERSION 1', '']
if not self.filepath:
raise Exception(f"Cannot write catalog {self} no filepath setted")
lines = ["VERSION 1", ""]
catalog_items = list(self)
if sort:
catalog_items.sort(key=lambda x : x.path)
catalog_items.sort(key=lambda x: x.path)
for catalog_item in catalog_items:
lines.append(f"{catalog_item.id}:{catalog_item.path}:{catalog_item.name}")
print(f'Write Catalog at: {self.filepath}')
self.filepath.write_text('\n'.join(lines), encoding="utf-8")
print(f"Write Catalog at: {self.filepath}")
self.filepath.write_text("\n".join(lines), encoding="utf-8")
def get(self, path=None, id=None, fallback=None):
"""Found a catalog item by is path or id"""
if isinstance(path, Path):
path = path.as_posix()
if id:
return self._data.get(id)
for catalog_item in self:
if catalog_item.path == path:
return catalog_item
return fallback
def remove(self, catalog_item):
@@ -140,7 +143,7 @@ class Catalog:
if catalog_item:
return self._data.pop(catalog_item.id)
print(f'Warning: {catalog_item} cannot be remove, not in {self}')
print(f"Warning: {catalog_item} cannot be remove, not in {self}")
return None
def add(self, catalog_path):
@@ -151,7 +154,7 @@ class Catalog:
print(parent, self.get(parent))
if self.get(parent):
continue
cat_item = CatalogItem(self, path=parent)
self._data[cat_item.id] = cat_item
@@ -161,19 +164,23 @@ class Catalog:
self._data[cat_item.id] = cat_item
return cat_item
def update(self, catalogs):
'Add or remove catalog entries if on the list given or not'
catalogs = set(catalogs) # Remove doubles
def update(self, catalogs):
"Add or remove catalog entries if on the list given or not"
catalogs = set(catalogs) # Remove doubles
added = [c for c in catalogs if not self.get(path=c)]
removed = [c.path for c in self if c.path not in catalogs]
if added:
print(f'{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n')
print(
f"{len(added)} Catalog Entry Added \n{tuple(c.name for c in added[:10])}...\n"
)
if removed:
print(f'{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n')
print(
f"{len(removed)} Catalog Entry Removed \n{tuple(c.name for c in removed[:10])}...\n"
)
for catalog_item in removed:
self.remove(catalog_item)
@@ -183,7 +190,7 @@ class Catalog:
def __iter__(self):
return self._data.values().__iter__()
def __getitem__(self, key):
if isinstance(key, int):
return self._data.values()[key]
@@ -191,10 +198,10 @@ class Catalog:
return self._data[key]
def __contains__(self, item):
if isinstance(item, str): # item is the id
if isinstance(item, str): # item is the id
return item in self._data
else:
return item in self
def __repr__(self):
return f'Catalog(filepath={self.filepath})'
return f"Catalog(filepath={self.filepath})"
+134 -93
View File
@@ -1,4 +1,3 @@
"""Generic python functions to make operation on file and names"""
import fnmatch
@@ -15,6 +14,7 @@ import shutil
import contextlib
@contextlib.contextmanager
def cd(path):
"""Changes working directory and returns to previous on exit."""
@@ -25,20 +25,24 @@ def cd(path):
finally:
os.chdir(prev_cwd)
def install_module(module_name, package_name=None):
'''Install a python module with pip or return it if already installed'''
"""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} ....')
print(f"Installing Module {module_name} ....")
subprocess.call([sys.executable, "-m", "ensurepip"])
subprocess.call(
[sys.executable, "-m", "pip", "install", package_name or 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
@@ -46,50 +50,64 @@ def import_module_from_path(path):
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(f"Cannot import file {path}")
print(e)
def norm_str(string, separator='_', format=str.lower, padding=0):
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.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")
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]+\.'
pattern = "_v[0-9]+\."
search = re.search(pattern, filepath)
if search:
filepath = filepath.replace(search.group()[:-1], '')
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)) :
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
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.
@@ -97,7 +115,7 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
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:
@@ -111,7 +129,9 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
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
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)
@@ -119,7 +139,10 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
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])]
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:
@@ -128,17 +151,26 @@ def get_last_files(root, pattern=r'_v\d{3}\.\w+', only_matching=False, ex_file=N
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')
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
for d in dirs: # recursively treat all detected directory
if ex_dir and is_exclude(d.name, ex_dir):
# skip folder with excluded name
# 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)
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:
@@ -147,19 +179,20 @@ def copy_file(src, dst, only_new=False, only_recent=False):
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)
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)])
subprocess.call(["cp", str(src), str(dst)])
def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], includes=[]):
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])
includes = r"|".join([fnmatch.translate(x) for x in includes])
if excludes:
excludes = r'|'.join([fnmatch.translate(x) for x in excludes])
excludes = r"|".join([fnmatch.translate(x) for x in excludes])
if dst.is_dir():
dst.mkdir(exist_ok=True, parents=True)
@@ -170,149 +203,157 @@ def copy_dir(src, dst, only_new=False, only_recent=False, excludes=['.*'], inclu
copy_file(src, dst, only_new=only_new, only_recent=only_recent)
elif src.is_dir():
src_files = list(src.rglob('*'))
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]
dst_files = [dst / f.relative_to(src) for f in src_files]
for src_file, dst_file in zip(src_files, dst_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)
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']
"""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']
cmd += ["/select,"]
else: # linux variants
cmd = ["xdg-open"]
if select:
cmd = ['nemo']
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'):
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')
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')
print("Try to read empty file")
path = Path(path)
if not path.exists():
print('File not exist', path)
print("File not exist", path)
return
if path.suffix not in exts:
print(f'Cannot read file {path}, extension must be 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')
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}')
print(f"Could not load yaml file {path}")
return
elif path.suffix.lower() == '.json':
elif path.suffix.lower() == ".json":
try:
data = json.loads(txt)
except Exception:
print(f'Could not load json file {path}')
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')
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')
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}')
print(f"Cannot read file {path}, extension must be in {exts}")
return
if path.suffix.lower() in ('.yaml', '.yml'):
yaml = install_module('yaml')
if path.suffix.lower() in (".yaml", ".yml"):
yaml = install_module("yaml")
try:
path.write_text(yaml.dump(data), encoding='utf8')
path.write_text(yaml.dump(data), encoding="utf8")
except Exception as e:
print(e)
print(f'Could not write yaml file {path}')
print(f"Could not write yaml file {path}")
return
elif path.suffix.lower() == '.json':
elif path.suffix.lower() == ".json":
try:
path.write_text(json.dumps(data, indent=indent), encoding='utf8')
path.write_text(json.dumps(data, indent=indent), encoding="utf8")
except Exception as e:
print(e)
print(f'Could not write json file {path}')
print(f"Could not write json file {path}")
return
else:
data = path.write_text(data, encoding='utf8')
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)
# 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)
# set_actionlib_dir(custom=custom)
script = Path(__file__).parent / 'synchronize.py'
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),
"--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)
+48 -53
View File
@@ -11,7 +11,8 @@ import os
import re
import time
#from asset_library.constants import ASSETLIB_FILENAME
# 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
@@ -21,34 +22,37 @@ import bpy
def command(func):
'''Decorator to be used from printed functions argument and run time'''
func_name = func.__name__.replace('_', ' ').title()
"""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 ---')
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) ---')
print(
f"[>-] {func_name} --- Finished (total time : {time.time() - t0:.2f}s) ---"
)
return result
return _command
return _command
def asset_warning_callback(self, context):
"""Callback function to display a warning message when ading or modifying an asset"""
self.warning = ''
self.warning = ""
if not self.name:
self.warning = 'You need to specify a name'
self.warning = "You need to specify a name"
return
if not self.catalog:
self.warning = 'You need to specify a catalog'
self.warning = "You need to specify a catalog"
return
lib = get_active_library()
@@ -60,30 +64,33 @@ def asset_warning_callback(self, context):
lib = prefs.libraries[lib.store_library]
if not lib.library_type.get_asset_path(self.name, self.catalog).parents[1].exists():
self.warning = 'A new folder will be created'
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'''
"""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
# 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'''
"""Get the active catalog path"""
lib = get_active_library()
cat_data = lib.library_type.read_catalog()
cat_data = {v['id']:k for k,v in cat_data.items()}
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 ''
return ""
"""
def norm_asset_datas(asset_file_datas):
@@ -181,7 +188,7 @@ def get_asset_source(replace_local=False):
return source_path
"""
'''
"""
def get_catalog_path(filepath=None):
filepath = filepath or bpy.data.filepath
filepath = Path(filepath)
@@ -196,7 +203,7 @@ def get_catalog_path(filepath=None):
catalog.touch(exist_ok=False)
return catalog
'''
"""
# def read_catalog(path, key='path'):
# cat_data = {}
@@ -218,7 +225,7 @@ def get_catalog_path(filepath=None):
# 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):
@@ -302,27 +309,29 @@ def create_catalog_file(json_path : str|Path, keep_existing_category : bool = Tr
return
"""
def clear_env_libraries():
print('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')
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()]
@@ -331,16 +340,17 @@ def clear_env_libraries():
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
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)
# print('Read', path)
library_data = read_file(path)
clear_env_libraries()
@@ -359,7 +369,8 @@ def set_env_libraries(path=None) -> list:
return libs
'''
"""
def get_env_libraries():
env_libraries = {}
@@ -391,21 +402,17 @@ def get_env_libraries():
}
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
first_interval=waiting_time,
)
'''
"""
def set_assetlib_paths():
prefs = bpy.context.preferences
@@ -452,16 +459,4 @@ def set_actionlib_paths():
prefs.filepaths.asset_libraries[lib_id].name = actionlib_name
#prefs.filepaths.asset_libraries[lib_id].path = str(actionlib_dir)
'''
"""
+76 -63
View File
@@ -10,7 +10,7 @@ class AssetCache:
def __init__(self, file_cache, data=None):
self.file_cache = file_cache
self.catalog = None
self.author = None
self.description = None
@@ -32,10 +32,7 @@ class AssetCache:
@property
def metadata(self):
metadata = {
'.library_id': self.library_id,
'.filepath': self.filepath
}
metadata = {".library_id": self.library_id, ".filepath": self.filepath}
metadata.update(self._metadata)
@@ -43,23 +40,23 @@ class AssetCache:
@property
def norm_name(self):
return self.name.replace(' ', '_').lower()
return self.name.replace(" ", "_").lower()
def unique_name(self):
return (self.filepath / self.name).as_posix()
def set_data(self, data):
catalog = data['catalog']
catalog = data["catalog"]
if isinstance(catalog, (list, tuple)):
catalog = '/'.join(catalog)
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', {})
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(
@@ -69,11 +66,11 @@ class AssetCache:
description=self.description,
tags=self.tags,
type=self.type,
name=self.name
name=self.name,
)
def __repr__(self):
return f'AssetCache(name={self.name}, catalog={self.catalog})'
return f"AssetCache(name={self.name}, catalog={self.catalog})"
def __eq__(self, other):
return self.to_dict() == other.to_dict()
@@ -81,7 +78,7 @@ class AssetCache:
class AssetsCache:
def __init__(self, file_cache):
self.file_cache = file_cache
self._data = []
@@ -111,12 +108,12 @@ class AssetsCache:
return next((a for a in self if a.name == name), None)
def __repr__(self):
return f'AssetsCache({list(self)})'
return f"AssetsCache({list(self)})"
class FileCache:
def __init__(self, library_cache, data=None):
self.library_cache = library_cache
self.filepath = None
@@ -132,15 +129,15 @@ class FileCache:
def set_data(self, data):
if 'filepath' in data:
self.filepath = Path(data['filepath'])
if "filepath" in data:
self.filepath = Path(data["filepath"])
self.modified = data.get('modified', time.time_ns())
self.modified = data.get("modified", time.time_ns())
if data.get('type') == 'FILE':
if data.get("type") == "FILE":
self.assets.add(data)
for asset_cache_data in data.get('assets', []):
for asset_cache_data in data.get("assets", []):
self.assets.add(asset_cache_data)
def to_dict(self):
@@ -148,7 +145,7 @@ class FileCache:
filepath=self.filepath.as_posix(),
modified=self.modified,
library_id=self.library_id,
assets=[asset_cache.to_dict() for asset_cache in self]
assets=[asset_cache.to_dict() for asset_cache in self],
)
def __iter__(self):
@@ -158,14 +155,14 @@ class FileCache:
return self._data[key]
def __repr__(self):
return f'FileCache(filepath={self.filepath})'
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.filepath = data['filepath']
self.operation = operation
self.asset_cache = asset_cache
@@ -189,32 +186,49 @@ class LibraryCacheDiff:
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')
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}
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')
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')
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')
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')
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')
print("No change in the library")
return self
def group_by(self, key):
'''Return groups of file cache diff using the key provided'''
"""Return groups of file cache diff using the key provided"""
data = list(self).sort(key=key)
return groupby(data, key=key)
@@ -228,17 +242,17 @@ class LibraryCacheDiff:
return len(self._data)
def __repr__(self):
return f'LibraryCacheDiff(operations={[o for o in self][:2]}...)'
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):
def from_library(cls, library):
filepath = library.library_path / f"blender_assets.{library.id}.json"
return cls(filepath)
@@ -248,28 +262,28 @@ class LibraryCache:
@property
def library_id(self):
return self.filepath.stem.split('.')[-1]
return self.filepath.stem.split(".")[-1]
#@property
#def filepath(self):
# @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 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}')
print(f"Read cache from {self.filepath}")
for file_cache_data in read_file(self.filepath):
self.add(file_cache_data)
@@ -280,7 +294,7 @@ class LibraryCache:
if tmp:
filepath = self.tmp_filepath
print(f'Write cache file to {filepath}')
print(f"Write cache file to {filepath}")
write_file(filepath, self._data)
return filepath
@@ -293,7 +307,7 @@ class LibraryCache:
def add_asset_cache(self, asset_cache_data, filepath=None):
if filepath is None:
filepath = asset_cache_data['filepath']
filepath = asset_cache_data["filepath"]
file_cache = self.get(filepath)
if not file_cache:
@@ -334,28 +348,28 @@ class LibraryCache:
if new_cache is None:
new_cache = self
return LibraryCacheDiff(old_cache, new_cache)
return LibraryCacheDiff(old_cache, new_cache)
def update(self, cache_diff):
#Update the cache with the operations
# 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}' )
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}' )
print(f"Asset {asset_cache_diff.name} not in file_cache {file_cache}")
continue
if asset_cache_diff.operation == 'REMOVE':
if asset_cache_diff.operation == "REMOVE":
file_cache.assets.remove(asset_cache_diff.name)
elif asset_cache_diff.operation in ('MODIFY', 'ADD'):
elif asset_cache_diff.operation in ("MODIFY", "ADD"):
asset_cache.set_data(asset_cache_diff.asset_cache.to_dict())
return self
def __len__(self):
@@ -363,7 +377,7 @@ class LibraryCache:
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
if isinstance(key, str):
return self.to_dict()[key]
@@ -377,5 +391,4 @@ class LibraryCache:
return next((a for a in self if a.filepath == filepath), None)
def __repr__(self):
return f'LibraryCache(library_id={self.library_id})'
return f"LibraryCache(library_id={self.library_id})"
+19 -15
View File
@@ -1,4 +1,3 @@
import argparse
import fnmatch
import importlib.util
@@ -11,7 +10,7 @@ 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", Path(__file__).parent / "file_utils.py"
)
utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(utils)
@@ -19,24 +18,29 @@ spec.loader.exec_module(utils)
def synchronize(src, dst, only_new=False, only_recent=False):
excludes=['*.sync-conflict-*', '.*']
includes=['*.blend', 'blender_assets.cats.txt']
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
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)
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')
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))
+30 -29
View File
@@ -9,51 +9,52 @@ import string
class TemplateFormatter(string.Formatter):
def format_field(self, value, format_spec):
if isinstance(value, str):
spec, sep = [*format_spec.split(':'), None][:2]
spec, sep = [*format_spec.split(":"), None][:2]
if sep:
value = value.replace('_', ' ')
value = value = re.sub(r'([a-z])([A-Z])', rf'\1{sep}\2', value)
value = value.replace(' ', sep)
if spec == 'u':
value = value.replace("_", " ")
value = value = re.sub(r"([a-z])([A-Z])", rf"\1{sep}\2", value)
value = value.replace(" ", sep)
if spec == "u":
value = value.upper()
elif spec == 'l':
elif spec == "l":
value = value.lower()
elif spec == 't':
elif spec == "t":
value = value.title()
return super().format(value, format_spec)
class Template:
field_pattern = re.compile(r'{(\w+)\*{0,2}}')
field_pattern_recursive = re.compile(r'{(\w+)\*{2}}')
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
# asset_data_path = Path(lib_path) / ASSETLIB_FILENAME
self.raw = template
self.formatter = TemplateFormatter()
@property
def glob_pattern(self):
pattern = self.field_pattern_recursive.sub('**', self.raw)
pattern = self.field_pattern.sub('*', pattern)
pattern = self.field_pattern_recursive.sub("**", self.raw)
pattern = self.field_pattern.sub("*", pattern)
return pattern
@property
def re_pattern(self):
pattern = self.field_pattern_recursive.sub('([\\\w -_.\/]+)', self.raw)
pattern = self.field_pattern.sub('([\\\w -_.]+)', pattern)
pattern = pattern.replace('?', '.')
pattern = pattern.replace('*', '.*')
pattern = self.field_pattern_recursive.sub("([\\\w -_.\/]+)", self.raw)
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.raw)
#return [f or '0' for f in fields]
# return [f or '0' for f in fields]
def parse(self, path):
@@ -61,7 +62,7 @@ class Template:
res = self.re_pattern.findall(path)
if not res:
print('Could not parse {path} with {self.re_pattern}')
print("Could not parse {path} with {self.re_pattern}")
return {}
fields = self.fields
@@ -71,7 +72,7 @@ class Template:
else:
field_values = res[0]
return {k:v for k,v in zip(fields, field_values)}
return {k: v for k, v in zip(fields, field_values)}
def norm_data(self, data):
norm_data = {}
@@ -81,7 +82,7 @@ class Template:
v = v.as_posix()
norm_data[k] = v
return norm_data
def format(self, data=None, **kargs):
@@ -89,17 +90,17 @@ class Template:
data = {**(data or {}), **kargs}
try:
#print('FORMAT', self.raw, data)
# print('FORMAT', self.raw, data)
path = self.formatter.format(self.raw, **self.norm_data(data))
except KeyError as e:
print(f'Cannot format {self.raw} with {data}, field {e} is missing')
print(f"Cannot format {self.raw} with {data}, field {e} is missing")
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 given it need to be absolute"""
if pattern is None:
pattern = Path(directory, self.glob_pattern).as_posix()
@@ -114,14 +115,14 @@ class Template:
pattern = self.format(data, **kargs)
pattern_str = str(pattern)
if '*' not in pattern_str and '?' not in pattern_str:
if "*" not in pattern_str and "?" not in pattern_str:
return pattern
paths = glob(pattern.as_posix())
if paths:
return Path(paths[0])
#return pattern
# return pattern
def __repr__(self):
return f'Template({self.raw})'
return f"Template({self.raw})"