File size: 7,761 Bytes
985c397 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | # SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2025 Samuel Abels <knipknap@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import uuid
import pathlib
from typing import Mapping, Union, Optional, List, Dict, cast
import Path
from ...assets import Asset, AssetUri
from ...toolbit import ToolBit
class Library(Asset):
asset_type: str = "toolbitlibrary"
API_VERSION = 1
def __init__(self, label, id=None):
self.id = id if id is not None else str(uuid.uuid4())
self._label = label
self._bits: List[ToolBit] = []
self._bit_nos: Dict[int, ToolBit] = {}
self._bit_urls: Dict[AssetUri, ToolBit] = {}
@property
def label(self) -> str:
return self._label
def get_id(self) -> str:
"""Returns the unique identifier for the Library instance."""
return self.id
@classmethod
def resolve_name(cls, identifier: Union[str, AssetUri, pathlib.Path]) -> AssetUri:
"""
Resolves various forms of library identifiers to a canonical AssetUri string.
Handles direct AssetUri objects, URI strings, asset IDs, or legacy filenames.
Returns the canonical URI string or None if resolution fails.
"""
if isinstance(identifier, AssetUri):
return identifier
if isinstance(identifier, str) and AssetUri.is_uri(identifier):
return AssetUri(identifier)
if isinstance(identifier, pathlib.Path): # Handle direct Path objects (legacy filenames)
identifier = identifier.stem # Use the filename stem as potential ID
if not isinstance(identifier, str):
raise ValueError("Failed to resolve {identifier} to a Uri")
return AssetUri.build(asset_type=Library.asset_type, asset_id=identifier)
def to_dict(self) -> dict:
"""Returns a dictionary representation of the Library in the specified format."""
tools_list = []
for tool_no, tool in self._bit_nos.items():
tools_list.append(
{"nr": tool_no, "path": f"{tool.get_id()}.fctb"} # Tool ID with .fctb extension
)
return {"label": self.label, "tools": tools_list, "version": self.API_VERSION}
@classmethod
def from_dict(
cls,
data_dict: dict,
id: str,
dependencies: Optional[Mapping[AssetUri, Asset]],
) -> "Library":
"""
Creates a Library instance from a dictionary and resolved dependencies.
If dependencies is None, it's a shallow load, and tools are not populated.
"""
library = cls(data_dict.get("label", id or "Unnamed Library"), id=id)
if dependencies is None:
Path.Log.debug(
f"Library.from_dict: Shallow load for library '{library.label}' (id: {id}). Tools not populated."
)
return library # Only process tools if dependencies were resolved
tools_list = data_dict.get("tools", [])
for tool_data in tools_list:
tool_no = tool_data["nr"]
tool_id = pathlib.Path(tool_data["path"]).stem # Extract tool ID
tool_uri = AssetUri(f"toolbit://{tool_id}")
bit = cast(ToolBit, dependencies.get(tool_uri))
if bit:
library.add_bit(bit, bit_no=tool_no)
else:
raise ValueError(f"Tool with id {tool_id} not found in dependencies")
return library
def __str__(self):
return '{} "{}"'.format(self.id, self.label)
def __eq__(self, other):
return self.id == other.id
def __iter__(self):
return self._bits.__iter__()
def get_next_bit_no(self):
bit_nolist = sorted(self._bit_nos, reverse=True)
return bit_nolist[0] + 1 if bit_nolist else 1
def get_bit_no_from_bit(self, bit: ToolBit) -> Optional[int]:
for bit_no, thebit in self._bit_nos.items():
if bit == thebit:
return bit_no
return None
def get_tool_by_uri(self, uri: AssetUri) -> Optional[ToolBit]:
for tool in self._bit_nos.values():
if tool.get_uri() == uri:
return tool
return None
def assign_new_bit_no(self, bit: ToolBit, bit_no: Optional[int] = None) -> int:
if bit not in self._bits:
raise ValueError(f"given bit {bit} not in library; cannot assign tool number")
# If no specific bit_no was requested, assign a new one.
if bit_no is None:
bit_no = self.get_next_bit_no()
elif self._bit_nos.get(bit_no) == bit:
return bit_no
# Otherwise, add the bit. Since the requested bit_no may already
# be in use, we need to account for that. In this case, we will
# add the removed bit into a new bit_no.
old_bit = self._bit_nos.pop(bit_no, None)
old_bit_no = self.get_bit_no_from_bit(bit)
if old_bit_no:
del self._bit_nos[old_bit_no]
self._bit_nos[bit_no] = bit
if old_bit:
self.assign_new_bit_no(old_bit)
return bit_no
def add_bit(self, bit: ToolBit, bit_no: Optional[int] = None) -> int:
if bit not in self._bits:
self._bits.append(bit)
return self.assign_new_bit_no(bit, bit_no)
def get_bits(self) -> List[ToolBit]:
return self._bits
def has_bit(self, bit: ToolBit) -> bool:
for t in self._bits:
if bit.id == t.id:
return True
return False
def remove_bit(self, bit: ToolBit):
self._bits = [t for t in self._bits if t.id != bit.id]
self._bit_nos = {k: v for (k, v) in self._bit_nos.items() if v.id != bit.id}
def remove_bit_by_uri(self, uri: AssetUri | str):
if isinstance(uri, str):
uri = AssetUri(uri)
self._bits = [t for t in self._bits if t.get_uri() != uri]
self._bit_nos = {k: v for (k, v) in self._bit_nos.items() if v.get_uri() != uri}
def dump(self, summarize: bool = False):
title = 'Library "{}" ({}) (instance {})'.format(self.label, self.id, id(self))
print("-" * len(title))
print(title)
print("-" * len(title))
for bit in self._bits:
print(f"- {bit.label} ({bit.get_id()})")
print()
|