black format

This commit is contained in:
Joseph HENRY 2025-12-01 15:34:07 +01:00
parent f1aaa1dfca
commit 2b692432c2
10 changed files with 739 additions and 452 deletions

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.12

View File

@ -5,23 +5,21 @@ from bpy.props import *
from .properties import CustomShelfProps
class Types():
class Types:
def initialyse(self, label):
package = __package__.replace('_','').replace('-','').lower()
idname = label.replace(' ','_').replace('-','_').lower()
package = __package__.replace("_", "").replace("-", "").lower()
idname = label.replace(" ", "_").replace("-", "_").lower()
self.bl_label = title(label)
self.bl_idname = '%s.%s'%(package, idname)
self.bl_idname = "%s.%s" % (package, idname)
# self.bl_props = bpy.context.scene.CustomShelf
# self.prefs = ""
class CSHELF_OT_shelf_popup(Types, Operator):
# props = None
bl_options = {'REGISTER', 'UNDO'}
bl_options = {"REGISTER", "UNDO"}
@property
def props(self):
@ -41,8 +39,7 @@ class CSHELF_OT_shelf_popup(Types, Operator):
self.args = dic_to_args(info)
Props = type("props",(PropertyGroup,),{'__annotations__' : dict(self.args)})
Props = type("props", (PropertyGroup,), {"__annotations__": dict(self.args)})
bpy.utils.register_class(Props)
setattr(CustomShelfProps, arg_name(label), PointerProperty(type=Props))
@ -50,12 +47,8 @@ class CSHELF_OT_shelf_popup(Types, Operator):
self.info = info
self.prop = arg_name(label)
# print(self.args)
# properties = self.get_props().bl_rna.properties
# props = {k:v for k,v in self.args.items() if k in properties or hasattr(v,"bl_idname")}
@ -71,7 +64,7 @@ class CSHELF_OT_shelf_popup(Types, Operator):
areas = []
text_editor = None
for area in context.screen.areas:
if area.type == 'TEXT_EDITOR' :
if area.type == "TEXT_EDITOR":
text_editor = area
else:
@ -88,7 +81,7 @@ class CSHELF_OT_shelf_popup(Types, Operator):
text_editor.spaces[0].show_line_numbers = True
context_copy = context.copy()
context_copy['area'] = text_editor
context_copy["area"] = text_editor
# bpy.ops.text.properties(context_copy)
# bpy.ops.view3d.toolshelf(context_copy)
@ -99,21 +92,18 @@ class CSHELF_OT_shelf_popup(Types, Operator):
if self.args:
# set default value for collection
for k, v in self.info.items():
if isinstance(v,dict) and v.get('collection') :
if isinstance(v, dict) and v.get("collection"):
collection = getattr(bpy.data, id_type[v["collection"]])
setattr(self.props, k, collection.get(v["value"]))
return context.window_manager.invoke_props_dialog(self)
else:
return self.execute(context)
def bl_report(self, *args):
if len(args) and args[0] in ('INFO','ERROR', 'WARNING') :
return self.report({args[0]},' '.join(args[1:]))
if len(args) and args[0] in ("INFO", "ERROR", "WARNING"):
return self.report({args[0]}, " ".join(args[1:]))
else:
print(*args)
@ -125,21 +115,21 @@ class CSHELF_OT_shelf_popup(Types, Operator):
value = getattr(self.props, arg)
if hasattr(value, "name"):
values[arg] = {'value': value.name}
values[arg] = {"value": value.name}
elif isinstance(info.get(arg), dict):
values[arg] = {'value': value}
values[arg] = {"value": value}
else:
values[arg] = value
info_str = "info = %s\n\n" % values
lines_str = '\n'.join(lines)
lines_str = "\n".join(lines)
script = f"{info_str}\n\n{lines_str}"
exec(script, {'info': values, 'print': self.bl_report})
exec(script, {"info": values, "print": self.bl_report})
return {'FINISHED'}
return {"FINISHED"}
def draw(self, context):
layout = self.layout
@ -153,17 +143,15 @@ class CSHELF_OT_shelf_popup(Types, Operator):
row = layout.row(align=True)
row.label(text=title(k))
row.prop(self.props,k,text='')
row.prop(self.props, k, text="")
# row.prop_search(self.props,k,self.props,k+"_prop_search",text='')
# op = self.args[k+"_prop_search"]
# row.operator(op.bl_idname,text="",icon ="COLLAPSEMENU" )
def check(self, context):
return True
"""
class ShelfPropSearch(Types,Operator):
bl_property = "enum"
@ -214,16 +202,15 @@ class CSHELF_PT_shelf_panel(Types,Panel):
# print("settings",self.settings)
is_taged = True
if self.settings and self.settings.get('tags') :
if self.settings and self.settings.get("tags"):
tag = prefs.tag_filter
if not tag:
tag = os.getenv("CUSTOM_SHELF_TAG")
is_taged = tag in self.settings['tags']
is_taged = tag in self.settings["tags"]
return any(shelf) and is_taged
def draw(self, context):
layout = self.layout
bl_props = getattr(context.scene.CustomShelf, self.cat)
@ -231,9 +218,11 @@ class CSHELF_PT_shelf_panel(Types,Panel):
for script in self.scripts:
args = script.copy()
if not self.search_filter(search,script["text"]) : continue
if not self.search_filter(search, script["text"]):
continue
if bl_props.filter and script["text"].startswith('_') : continue
if bl_props.filter and script["text"].startswith("_"):
continue
args["text"] = title(script["text"])
@ -241,14 +230,12 @@ class CSHELF_PT_shelf_panel(Types,Panel):
layout.operator(**args)
class CSHELF_PT_shelf_header(Panel):
bl_label = "Custom Shelf"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
layout = self.layout
bl_props = bpy.context.scene.CustomShelf
@ -258,14 +245,18 @@ class CSHELF_PT_shelf_header(Panel):
row = layout.row(align=True)
row.operator_menu_enum("customshelf.set_tag_filter",'tag_filter',icon= "DOWNARROW_HLT",text='')
row.operator("customshelf.refresh", text='',emboss=True,icon= "FILE_REFRESH")
row.operator_menu_enum(
"customshelf.set_tag_filter", "tag_filter", icon="DOWNARROW_HLT", text=""
)
row.operator("customshelf.refresh", text="", emboss=True, icon="FILE_REFRESH")
# row.separator()
row.prop(cat_props, "filter",text='',emboss=True,icon= "FILTER")
row.prop(cat_props, "filter", text="", emboss=True, icon="FILTER")
# row.separator()
row.prop(cat_props,"search",icon = "VIEWZOOM",text='')
row.prop(cat_props, "search", icon="VIEWZOOM", text="")
# row.separator()
row.operator("customshelf.open_shelf_folder", text='',emboss=True,icon= "FILE").path = self.path
row.operator(
"customshelf.open_shelf_folder", text="", emboss=True, icon="FILE"
).path = self.path

View File

@ -8,12 +8,13 @@ bl_info = {
"doc_url": "https://gitlab.com/autour-de-minuit/blender/custom_shelf/-/blob/main/README.md",
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/custom_shelf/-/issues",
"support": "COMMUNITY",
"category": "User"}
"category": "User",
}
if "bpy" in locals():
import importlib
importlib.reload(operators)
importlib.reload(panels)
importlib.reload(functions)
@ -41,11 +42,12 @@ bl_classes = [
operators.CSHELF_OT_open_shelf_folder,
operators.CSHELF_OT_add_script,
operators.CSHELF_OT_set_tag_filter,
ui.CSHELF_MT_text_editor
ui.CSHELF_MT_text_editor,
]
def draw_menu(self, context):
self.layout.menu('CSHELF_MT_text_editor')
self.layout.menu("CSHELF_MT_text_editor")
def register():
@ -53,7 +55,9 @@ def register():
bpy.utils.register_class(bl_class)
bpy.types.Scene.CustomShelf = bpy.props.PointerProperty(type=CustomShelfSettings)
bpy.types.WindowManager.CustomShelf = bpy.props.PointerProperty(type=CustomShelfProps)
bpy.types.WindowManager.CustomShelf = bpy.props.PointerProperty(
type=CustomShelfProps
)
bpy.types.TEXT_MT_editor_menus.append(draw_menu)
@ -72,6 +76,7 @@ def register():
read_shelves()
def unregister():
# unregister panel :
for panel in CustomShelfSettings.panel_list:

View File

@ -5,36 +5,40 @@ from .properties import CustomShelfSettings,CustomShelfPrefs
from .Types import *
SHELF_DIR = join(dirname(__file__),'shelves')
SHELF_DIR = join(dirname(__file__), "shelves")
def operator(idname, args):
op_execute = args['execute']
op_execute = args["execute"]
def execute(self, context):
op_execute(context, self)
return {"FINISHED"}
package = __package__.replace('_','').replace('-','').lower()
package = __package__.replace("_", "").replace("-", "").lower()
args['bl_idname'] = "%s.%s"%(package,idname)
args['bl_label'] = title(idname)
args['execute'] = execute
args["bl_idname"] = "%s.%s" % (package, idname)
args["bl_label"] = title(idname)
args["execute"] = execute
op = type('operator',(Operator,),args)
op = type("operator", (Operator,), args)
bpy.utils.register_class(op)
return op
def register_bl_class(bl_class, args):
a = type('test',(bl_class,),{})
a = type("test", (bl_class,), {})
a.initialyse(a, *args)
bpy.utils.register_class(a)
return a
def filter_dir(dir_list):
return [d for d in dir_list if d.is_dir() and not d.name.startswith('_')]
return [d for d in dir_list if d.is_dir() and not d.name.startswith("_")]
def get_shelves_folder():
prefs = bpy.context.preferences.addons[__package__].preferences
@ -43,7 +47,6 @@ def get_shelves_folder():
return [p for p in additionnal_shelves_path + [shelves_path] if exists(p)]
def get_dirs(paths):
if isinstance(paths, (tuple, list)):
dirs = []
@ -55,17 +58,23 @@ def get_dirs(paths) :
else:
return filter_dir(scandir(paths))
def get_categories():
return [d.name for d in get_dirs(get_shelves_folder())]
def get_tabs():
return [d.name for d in get_dirs(get_dirs(get_shelves_folder()))]
def get_category_path(category):
category_dir = [f for f in get_shelves_folder() if category in [d.name for d in get_dirs(f)]]
category_dir = [
f for f in get_shelves_folder() if category in [d.name for d in get_dirs(f)]
]
if category_dir:
return join(category_dir[0], category)
def get_category_tabs(category):
return [d.name for d in get_dirs(get_category_path(category))]
@ -83,26 +92,29 @@ def read_shelves() :
pass
CustomShelfSettings.panel_list.remove(panel)
for cat in get_dirs(get_shelves_folder()):
cat_name = cat.name.replace('_',' ').title()
cat_name = cat.name.replace("_", " ").title()
header_info = {
"bl_region_type": "UI", # petit ajout pour voir si foncitonne...
"bl_category": cat_name,
"bl_idname" : "%s_PT_%s_header"%(__package__.upper(),cat.name),# -> "%s.%s_header" makes _PT_ warning
"bl_idname": "%s_PT_%s_header"
% (__package__.upper(), cat.name), # -> "%s.%s_header" makes _PT_ warning
"cat": cat.name,
"path" : cat.path
"path": cat.path,
}
header = type("Header", (CSHELF_PT_shelf_header,), header_info)
bpy.utils.register_class(header)
# enable bool prefs
# panel_props = {t.name : BoolProperty(default = False) for t in get_dirs(cat.path)}# give "should be an annotation" field warning
## need annoattion type : https://blender.stackexchange.com/questions/118118/blender-2-8-field-property-declaration-and-dynamic-class-creation
panel_props = {'__annotations__': {t.name : BoolProperty(default = False) for t in get_dirs(cat.path)}}
panel_props = {
"__annotations__": {
t.name: BoolProperty(default=False) for t in get_dirs(cat.path)
}
}
PanelProps = type("CustomShelfPrefs", (PropertyGroup,), panel_props)
bpy.utils.register_class(PanelProps)
@ -111,10 +123,10 @@ def read_shelves() :
# search and filter
props = {
"search": StringProperty(options={"TEXTEDIT_UPDATE"}),
"filter" : BoolProperty(default = True)
"filter": BoolProperty(default=True),
}
# Props = type("props",(PropertyGroup,), props)# annotation error
Props = type("props",(PropertyGroup,),{'__annotations__': props})
Props = type("props", (PropertyGroup,), {"__annotations__": props})
bpy.utils.register_class(Props)
setattr(CustomShelfSettings, cat.name, PointerProperty(type=Props))
@ -127,26 +139,36 @@ def read_shelves() :
for tab in get_dirs(cat.path):
# get_settings
settings_json = join(tab.path,'settings.json')
settings_json = join(tab.path, "settings.json")
tab_settings = {}
if exists(settings_json):
tab_settings = read_json(settings_json)
if not tab_settings or not tab_settings.get('tags') : continue
for tag in tab_settings['tags'] :
if not tab_settings or not tab_settings.get("tags"):
continue
for tag in tab_settings["tags"]:
if tag not in tag_filter_items:
tag_filter_items.append(tag)
panel_info = {
"bl_region_type": "UI", # Added, maybe need it in 2.8+...
"bl_category": cat_name,
"bl_idname" : "%s_PT_%s_%s"%(__package__.upper(),cat.name,tab.name),#"%s.%s_%s" -> makes _PT_ warning
"bl_label" : tab.name.replace('_',' ').title(),
"bl_idname": "%s_PT_%s_%s"
% (
__package__.upper(),
cat.name,
tab.name,
), # "%s.%s_%s" -> makes _PT_ warning
"bl_label": tab.name.replace("_", " ").title(),
"cat": cat.name,
"settings" :tab_settings
"settings": tab_settings,
}
space_info = {k:v for k,v in tab_settings.items() if k in ("bl_space_type","bl_region_type")}
space_info = {
k: v
for k, v in tab_settings.items()
if k in ("bl_space_type", "bl_region_type")
}
panel_info.update(space_info)
panel = type("Panel", (CSHELF_PT_shelf_panel,), panel_info)
@ -157,7 +179,7 @@ def read_shelves() :
scripts = []
for script in scandir(tab.path):
if not script.name.endswith('.py'):
if not script.name.endswith(".py"):
continue
print(script.path)
@ -166,28 +188,31 @@ def read_shelves() :
info, lines = read_info(script.path)
info["description"] = info.get('description',"")
info["description"] = info.get("description", "")
icon = info.get('icon', "WORDWRAP_OFF")
icon = info.get("icon", "WORDWRAP_OFF")
popup = type(script_name, (CSHELF_OT_shelf_popup,),
{"script": script.path,
'bl_description': info["description"]
})
popup = type(
script_name,
(CSHELF_OT_shelf_popup,),
{"script": script.path, "bl_description": info["description"]},
)
popup.initialyse(popup, script_name, info)
bpy.utils.register_class(popup)
scripts.append({"operator" : popup.bl_idname,"text" : script_name,"icon" : icon})
scripts.append(
{"operator": popup.bl_idname, "text": script_name, "icon": icon}
)
panel.scripts = sorted(scripts,key = lambda x :x['text'])
panel.scripts = sorted(scripts, key=lambda x: x["text"])
# Register tag filter enum
# setattr(Pre, v)
prefs = bpy.context.preferences.addons[__package__].preferences
tag_filter_items.sort()
tag_filter_items.insert(0, '__clear__')
tag_filter_items.insert(0, "__clear__")
prefs['tag_filter_items'] = tag_filter_items
prefs["tag_filter_items"] = tag_filter_items
# bpy.utils.unregister_class(CustomShelfPrefs)
# bpy.utils.register_class(CustomShelfPrefs)

View File

@ -7,21 +7,22 @@ from .properties import CustomShelfSettings
class CSHELF_OT_refresh(Operator):
bl_idname = "customshelf.refresh"
bl_label = 'Refresh Shelves'
bl_label = "Refresh Shelves"
def execute(self, context):
read_shelves()
return {'FINISHED'}
return {"FINISHED"}
def get_tag_items(self, context):
prefs = bpy.context.preferences.addons[__package__].preferences
return [(i,i.title(),"") for i in prefs['tag_filter_items']]
return [(i, i.title(), "") for i in prefs["tag_filter_items"]]
class CSHELF_OT_set_tag_filter(Operator):
bl_idname = "customshelf.set_tag_filter"
bl_label = 'Refresh Shelves'
bl_label = "Refresh Shelves"
tag_filter: EnumProperty(items=get_tag_items)
@ -29,25 +30,26 @@ class CSHELF_OT_set_tag_filter(Operator):
prefs = bpy.context.preferences.addons[__package__].preferences
tag_filter = self.tag_filter
if tag_filter == '__clear__' :
if tag_filter == "__clear__":
tag_filter = ""
prefs.tag_filter = tag_filter
return {'FINISHED'}
return {"FINISHED"}
class CSHELF_OT_add_shelf_folder(Operator):
bl_idname = "customshelf.add_shelves_folder"
bl_label = 'Refresh Shelves'
bl_label = "Refresh Shelves"
def execute(self, context):
prefs = bpy.context.preferences.addons[__package__].preferences
path = prefs.additionnal_shelves.add()
return {'FINISHED'}
return {"FINISHED"}
class CSHELF_OT_remove_shelf_folder(Operator):
bl_idname = "customshelf.remove_shelves_folder"
bl_label = 'Refresh Shelves'
bl_label = "Refresh Shelves"
index: IntProperty()
@ -55,26 +57,24 @@ class CSHELF_OT_remove_shelf_folder(Operator):
prefs = bpy.context.preferences.addons[__package__].preferences
prefs.additionnal_shelves.remove(self.index)
return {'FINISHED'}
return {"FINISHED"}
class CSHELF_OT_open_shelf_folder(Operator):
bl_idname = "customshelf.open_shelf_folder"
bl_label = 'Run Function'
bl_label = "Run Function"
path: StringProperty()
def execute(self, context):
open_folder(self.path)
return {'FINISHED'}
return {"FINISHED"}
class CSHELF_OT_add_script(Operator):
bl_idname = "customshelf.add_script"
bl_label = 'Add script to a shelf'
bl_label = "Add script to a shelf"
add_category: BoolProperty()
add_tab: BoolProperty()
@ -88,7 +88,7 @@ class CSHELF_OT_add_script(Operator) :
@classmethod
def poll(cls, context):
return context.area.type == 'TEXT_EDITOR' and context.space_data.text
return context.area.type == "TEXT_EDITOR" and context.space_data.text
def add_folder(self, context, op):
folder = self.folder.add()
@ -104,61 +104,155 @@ class CSHELF_OT_add_script(Operator) :
ui_layout = bpy.types.UILayout
icons = ui_layout.bl_rna.functions["prop"].parameters["icon"].enum_items.keys()
prefixes = ('BRUSH_','MATCAP_','COLORSET_')
exception = ('BRUSH_DATA')
prefixes = ("BRUSH_", "MATCAP_", "COLORSET_")
exception = "BRUSH_DATA"
return [i for i in icons if not i.startswith(prefixes) or i in exception]
def get_icons(self, context, op):
icons = [
["SCENE_DATA","RENDERLAYERS","MATERIAL_DATA","GROUP_UVS","TEXTURE","WORLD","SPEAKER","TEXT","NODETREE","NODE_INSERT_OFF","PARTICLES","SORTALPHA"],
["MODIFIER","MOD_WAVE","MOD_SUBSURF","MOD_FLUIDSIM","MOD_OCEAN","BLANK1",
"ARMATURE_DATA","BONE_DATA","GROUP_BONE"],
["SEQUENCE","CAMERA_DATA","SCENE","BLANK1",
"FILE_NEW","CONSOLE","BLENDER","APPEND_BLEND"],
["GROUP","MESH_CUBE","MESH_PLANE","MESH_CIRCLE","MESH_UVSPHERE","MESH_GRID","EMPTY_DATA","OUTLINER_DATA_MESH",
"LIGHT_SUN","LIGHT_SPOT","LIGHT"],
["TRIA_RIGHT_BAR","REC","PLAY","PREV_KEYFRAME","NEXT_KEYFRAME","PAUSE","X","ADD","REMOVE","RESTRICT_VIEW_OFF","RESTRICT_VIEW_ON","RESTRICT_SELECT_OFF",],
["BRUSH_DATA","GREASEPENCIL","LINE_DATA","PARTICLEMODE","SCULPTMODE_HLT","WPAINT_HLT","TPAINT_HLT","VIEWZOOM","HAND","KEY_HLT","KEY_DEHLT",],
["PLUGIN","SCRIPT","PREFERENCES",
"ACTION","SOLO_OFF",
"RNDCURVE","SNAP_ON",
"FORCE_WIND","COPY_ID","EYEDROPPER","AUTO","UNLOCKED","LOCKED",
"UNPINNED","PINNED","PLUGIN","HELP","GHOST_ENABLED","GHOST_DISABLED","COLOR",
"LINKED","UNLINKED","LINKED","ZOOM_ALL",
"FREEZE","STYLUS_PRESSURE","FILE_TICK",
"QUIT","RECOVER_LAST","TIME","PREVIEW_RANGE",
"OUTLINER","NLA","EDITMODE_HLT",
"BOIDS","RNA","CAMERA_STEREO"],
[
"SCENE_DATA",
"RENDERLAYERS",
"MATERIAL_DATA",
"GROUP_UVS",
"TEXTURE",
"WORLD",
"SPEAKER",
"TEXT",
"NODETREE",
"NODE_INSERT_OFF",
"PARTICLES",
"SORTALPHA",
],
[
"MODIFIER",
"MOD_WAVE",
"MOD_SUBSURF",
"MOD_FLUIDSIM",
"MOD_OCEAN",
"BLANK1",
"ARMATURE_DATA",
"BONE_DATA",
"GROUP_BONE",
],
[
"SEQUENCE",
"CAMERA_DATA",
"SCENE",
"BLANK1",
"FILE_NEW",
"CONSOLE",
"BLENDER",
"APPEND_BLEND",
],
[
"GROUP",
"MESH_CUBE",
"MESH_PLANE",
"MESH_CIRCLE",
"MESH_UVSPHERE",
"MESH_GRID",
"EMPTY_DATA",
"OUTLINER_DATA_MESH",
"LIGHT_SUN",
"LIGHT_SPOT",
"LIGHT",
],
[
"TRIA_RIGHT_BAR",
"REC",
"PLAY",
"PREV_KEYFRAME",
"NEXT_KEYFRAME",
"PAUSE",
"X",
"ADD",
"REMOVE",
"RESTRICT_VIEW_OFF",
"RESTRICT_VIEW_ON",
"RESTRICT_SELECT_OFF",
],
[
"BRUSH_DATA",
"GREASEPENCIL",
"LINE_DATA",
"PARTICLEMODE",
"SCULPTMODE_HLT",
"WPAINT_HLT",
"TPAINT_HLT",
"VIEWZOOM",
"HAND",
"KEY_HLT",
"KEY_DEHLT",
],
[
"PLUGIN",
"SCRIPT",
"PREFERENCES",
"ACTION",
"SOLO_OFF",
"RNDCURVE",
"SNAP_ON",
"FORCE_WIND",
"COPY_ID",
"EYEDROPPER",
"AUTO",
"UNLOCKED",
"LOCKED",
"UNPINNED",
"PINNED",
"PLUGIN",
"HELP",
"GHOST_ENABLED",
"GHOST_DISABLED",
"COLOR",
"LINKED",
"UNLINKED",
"LINKED",
"ZOOM_ALL",
"FREEZE",
"STYLUS_PRESSURE",
"FILE_TICK",
"QUIT",
"RECOVER_LAST",
"TIME",
"PREVIEW_RANGE",
"OUTLINER",
"NLA",
"EDITMODE_HLT",
"BOIDS",
"RNA",
"CAMERA_STEREO",
],
]
self.icons = icons
def set_icon(self, context, op):
# bl_props = context.scene.CustomShelf
self.icon = op.icon
self.icons = []
def draw(self, context):
bl_props = context.scene.CustomShelf
layout = self.layout
row = layout.row()
row.operator("customshelf.get_icons",text= '',icon = self.icon)
row.operator("customshelf.get_icons", text="", icon=self.icon)
row.prop(self, "name", text="")
col = layout.column(align=True)
for icon_list in self.icons:
i = 0
for icon in icon_list:
if not i % 12:
row = col.row(align=True)
row.operator("customshelf.set_icon", icon=icon, emboss=False, text="").icon=icon
row.operator(
"customshelf.set_icon", icon=icon, emboss=False, text=""
).icon = icon
i += 1
row = col.row(align=True)
@ -167,20 +261,20 @@ class CSHELF_OT_add_script(Operator) :
# Category Row
folder_row = layout.row(align=True)
folder_row.label(text="", icon='FILE_FOLDER')
folder_row.label(text="", icon="FILE_FOLDER")
folder_row.separator()
if not self.add_category:
folder_row.prop(bl_props, "category_enum", expand=True)
else:
folder_row.prop(self,"new_category",text='')
folder_row.prop(self, "new_category", text="")
folder_row.prop(self,"add_category",icon = 'ADD',text='')
folder_row.prop(self, "add_category", icon="ADD", text="")
# Tabs row
tab_row = layout.row(align=True)
tab_row.label(text='', icon='MENU_PANEL')
tab_row.label(text="", icon="MENU_PANEL")
tab_row.separator()
if not self.add_tab:
category_tabs = get_category_tabs(bl_props.category_enum)
@ -188,19 +282,18 @@ class CSHELF_OT_add_script(Operator) :
tab_row.prop_enum(bl_props, "tab_enum", t)
else:
tab_row.prop(self,"new_tab",text='')
tab_row.prop(self, "new_tab", text="")
tab_row.prop(self,"add_tab",icon = 'ADD',text='')
tab_row.prop(self, "add_tab", icon="ADD", text="")
# folder_row.operator("customshelf.remove_folder",icon = 'REMOVE',text='')
def write_script(self, f):
keys = ['icon','description']
keys = ["icon", "description"]
keys += [k for k in self.info if k not in keys]
f.write("info = " + dic_to_str(self.info, keys))
print(self.lines)
f.write('\n'.join(self.lines))
f.write("\n".join(self.lines))
def execute(self, context):
preferences = context.preferences
@ -221,16 +314,14 @@ class CSHELF_OT_add_script(Operator) :
else:
tab_path = join(category_path, bl_props.tab_enum)
script_name = self.name.replace(" ", "_").replace("-", "_")
script_name = self.name.replace(' ','_').replace('-','_')
script_path = join(tab_path,script_name+'.py')
script_path = join(tab_path, script_name + ".py")
if os.path.exists(script_path):
os.remove(script_path)
self.info['icon'] = self.icon
self.info['description'] = self.description
self.info["icon"] = self.icon
self.info["description"] = self.description
with open(script_path, "w") as f:
self.write_script(f)
@ -256,11 +347,11 @@ class CSHELF_OT_add_script(Operator) :
self.info, self.lines = read_info([l.body for l in self.active_text.lines])
icon = "LONGDISPLAY"
if self.info.get('icon') :
if self.info.get("icon"):
icon = self.info["icon"]
description = "Some description"
if self.info.get('description') :
if self.info.get("description"):
description = self.info["description"]
self.icon = icon
@ -272,16 +363,23 @@ class CSHELF_OT_add_script(Operator) :
self.new_shelf = ""
self.icons = []
operator("get_icons", {'execute': self.get_icons})
operator("set_icon", {'execute': self.set_icon, '__annotations__': {'icon' : StringProperty()}})
operator("add_folder", {'execute': self.add_folder})
operator("get_icons", {"execute": self.get_icons})
operator(
"set_icon",
{"execute": self.set_icon, "__annotations__": {"icon": StringProperty()}},
)
operator("add_folder", {"execute": self.add_folder})
# operator("remove_folder",{'execute':self.remove_folder})
self.tabs = get_tabs()
self.categories = get_categories()
CustomShelfSettings.category_enum = EnumProperty(items=[(i,i,"") for i in self.categories])
CustomShelfSettings.tab_enum = EnumProperty(items=[(i,i,"") for i in self.tabs])
CustomShelfSettings.category_enum = EnumProperty(
items=[(i, i, "") for i in self.categories]
)
CustomShelfSettings.tab_enum = EnumProperty(
items=[(i, i, "") for i in self.tabs]
)
if self.active_text.filepath:
tab_path = dirname(self.active_text.filepath)

View File

@ -2,7 +2,6 @@ from .utils import *
from bpy.props import *
class CustomShelfSettings(bpy.types.PropertyGroup):
panel_list = []
# category_enum = EnumProperty(items = )
@ -13,22 +12,25 @@ class CustomShelfSettings(bpy.types.PropertyGroup) :
# folders = PointerProperty(type= CustomShelfFolders)
# variables = bpy.props.PointerProperty(type= CustomShelfVariables)
class CustomShelfProps(bpy.types.PropertyGroup):
pass
class AdditionnalShelves(bpy.types.PropertyGroup):
path: StringProperty(name="Path",subtype = 'FILE_PATH')
path: StringProperty(name="Path", subtype="FILE_PATH")
class CustomShelfPrefs(bpy.types.AddonPreferences):
bl_idname = __package__
global_path = join(dirname(__file__),'shelves')
global_shelves : StringProperty(name="Shelves Path",subtype = 'DIR_PATH',default =global_path)
global_path = join(dirname(__file__), "shelves")
global_shelves: StringProperty(
name="Shelves Path", subtype="DIR_PATH", default=global_path
)
additionnal_shelves: CollectionProperty(type=AdditionnalShelves)
tag_filter: StringProperty()
def draw(self, context):
prefs = bpy.context.preferences.addons[__package__].preferences
layout = self.layout
@ -36,13 +38,17 @@ class CustomShelfPrefs(bpy.types.AddonPreferences):
row = layout.row(align=True)
row.operator("customshelf.add_shelves_folder", icon="ADD", text="")
row.label(text='Additionnal Shelves folder')
row.label(text="Additionnal Shelves folder")
for i, shelf in enumerate(prefs.additionnal_shelves):
row = layout.row(align=True)
row.prop(shelf, "path")
row.operator("customshelf.remove_shelves_folder",icon="REMOVE",text="").index = i
row.operator(
"customshelf.remove_shelves_folder", icon="REMOVE", text=""
).index = i
row = layout.row(align=True)
row.prop(self, "tag_filter")
row.operator_menu_enum("customshelf.set_tag_filter",'tag_filter',icon= "DOWNARROW_HLT",text='')
row.operator_menu_enum(
"customshelf.set_tag_filter", "tag_filter", icon="DOWNARROW_HLT", text=""
)

12
pyproject.toml Normal file
View File

@ -0,0 +1,12 @@
[project]
name = "custom-shelf"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"black>=25.11.0",
]

5
ui.py
View File

@ -1,4 +1,3 @@
import bpy
@ -7,9 +6,7 @@ class CSHELF_MT_text_editor(bpy.types.Menu):
def draw(self, context):
layout = self.layout
layout.operator("customshelf.add_script", text='Add Script', icon="FILE_NEW")
layout.operator("customshelf.add_script", text="Add Script", icon="FILE_NEW")
"""

107
utils.py
View File

@ -8,74 +8,105 @@ import subprocess
import json
id_type = {"Action" : "actions", "Armature":"armatures", "Brush":"brushes", "CacheFile":"cache_files", "Camera":"cameras",
"Curve":"curves", "FreestyleLineStyle":"linestyles", "GreasePencil":"grease_pencil", "Group":"groups",
"Image":"images", "Key":"shape_keys", "Light":"lights", "Lattice":"lattices", "Library":"librairies", "Mask":"masks",
"Material":"materials", "Mesh":"meshes", "MetaBall":"metaballs", "MovieClip":"movieclips", "NodeTree":"node_groups",
"Object":"objects", "PaintCurve":"paint_curves", "Palette":"palettes", "ParticleSettings":"particles",
"Scene":"scenes", "Screen":"screens", "Sound":"sounds", "Speaker":"speakers", "Text":"texts", "Texture":"textures",
"Collection":"collections", "VectorFont":"fonts", "WindowManager":"window_managers", "World":"worlds"}
id_type = {
"Action": "actions",
"Armature": "armatures",
"Brush": "brushes",
"CacheFile": "cache_files",
"Camera": "cameras",
"Curve": "curves",
"FreestyleLineStyle": "linestyles",
"GreasePencil": "grease_pencil",
"Group": "groups",
"Image": "images",
"Key": "shape_keys",
"Light": "lights",
"Lattice": "lattices",
"Library": "librairies",
"Mask": "masks",
"Material": "materials",
"Mesh": "meshes",
"MetaBall": "metaballs",
"MovieClip": "movieclips",
"NodeTree": "node_groups",
"Object": "objects",
"PaintCurve": "paint_curves",
"Palette": "palettes",
"ParticleSettings": "particles",
"Scene": "scenes",
"Screen": "screens",
"Sound": "sounds",
"Speaker": "speakers",
"Text": "texts",
"Texture": "textures",
"Collection": "collections",
"VectorFont": "fonts",
"WindowManager": "window_managers",
"World": "worlds",
}
def report(var,message,type='INFO') :
def report(var, message, type="INFO"):
print([a for a in locals()])
print([a for a in globals()])
if 'self' in var :
var['self'].report({type}, message)
if "self" in var:
var["self"].report({type}, message)
else:
print('')
print("")
print(type)
print(message)
def read_json(path):
jsonFile = path
if os.path.exists(jsonFile):
try:
with open(jsonFile) as data_file:
return (json.load(data_file))
return json.load(data_file)
except:
print("the file %s not json readable" % jsonFile)
return
def search_filter(search, str):
return search.lower() in str.lower()
def title(string):
return string.replace('_',' ').replace('-',' ').title()
return string.replace("_", " ").replace("-", " ").title()
def arg_name(string):
return string.replace(' ','_').replace('-','_').lower().strip('_')
return string.replace(" ", "_").replace("-", "_").lower().strip("_")
def open_folder(path):
try:
os.startfile(path)
except:
subprocess.Popen(['xdg-open', path])
subprocess.Popen(["xdg-open", path])
def dic_to_args(dic):
import collections
args = collections.OrderedDict()
prop_type = {
str: StringProperty,
bool: BoolProperty,
float: FloatProperty,
int : IntProperty
int: IntProperty,
}
for k, v in dic.items():
if k in ('icon','description') or not isinstance(k,str):
if k in ("icon", "description") or not isinstance(k, str):
continue
if isinstance(v, str):
if '/' in v or '\\' in v :
if "/" in v or "\\" in v:
args[k] = StringProperty(default=v, subtype="FILE_PATH")
else:
args[k] = StringProperty(default=v)
@ -95,14 +126,17 @@ def dic_to_args(dic):
elif v.get("collection") and isinstance(v["collection"], str):
if v["collection"] not in id_type:
print("Collection %s not supported must be in %s"%(v["collection"],id_type))
print(
"Collection %s not supported must be in %s"
% (v["collection"], id_type)
)
# args[k] = PointerProperty(type = getattr(bpy.types,v["collection"]))
else:
args[k] = PointerProperty(type = getattr(bpy.types,v["collection"]),poll = v.get('poll'))
args[k] = PointerProperty(
type=getattr(bpy.types, v["collection"]), poll=v.get("poll")
)
else:
args[k] = prop_type[type(v['default'])](**v)
args[k] = prop_type[type(v["default"])](**v)
return args
@ -125,18 +159,22 @@ def read_info(script_path) :
info_start = i
info_str = l
while info_str.endswith(',') or not info_str.endswith('}') or i == len(lines)-1:
while (
info_str.endswith(",")
or not info_str.endswith("}")
or i == len(lines) - 1
):
i += 1
info_str += lines[i]
info_end = i
opening_brace_index = info_str.find('{')
opening_brace_index = info_str.find("{")
try:
info = eval(info_str[opening_brace_index:])
except:
print('The info header in the file %s cannot be read'%script_path)
print("The info header in the file %s cannot be read" % script_path)
break
info = [(k, v) for k, v in info.items()]
@ -163,30 +201,31 @@ def read_info(script_path) :
return info, lines
def dic_to_str(dic, keys=None, spaces=4):
if not keys:
keys = sorted(dic)
line = '{\n'
line = "{\n"
for k in keys:
v = dic[k]
line += ' '*spaces
line += " " * spaces
if isinstance(k, str):
line += "'%s'" % k
else:
line += "%s" % k
line += ' : '
line += " : "
if isinstance(v, str):
line += "'%s'" % v
else:
line += "%s" % v
line += ',\n'
line += ",\n"
line += '}\n\n'
line += "}\n\n"
return line

113
uv.lock generated Normal file
View File

@ -0,0 +1,113 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "black"
version = "25.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
{ name = "pytokens" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" },
{ url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" },
{ url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" },
{ url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" },
{ url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" },
{ url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" },
{ url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" },
{ url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" },
{ url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" },
{ url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" },
{ url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "custom-shelf"
version = "0.1.0"
source = { virtual = "." }
[package.dev-dependencies]
dev = [
{ name = "black" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [{ name = "black", specifier = ">=25.11.0" }]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
]
[[package]]
name = "pytokens"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" },
]