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()