144 lines
7.1 KiB
Python
144 lines
7.1 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2025 The FreeCAD project association AISBL *
|
|
# * *
|
|
# * This file is part of FreeCAD. *
|
|
# * *
|
|
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
|
# * under the terms of the GNU Lesser General Public License as *
|
|
# * published by the Free Software Foundation, either version 2.1 of the *
|
|
# * License, or (at your option) any later version. *
|
|
# * *
|
|
# * FreeCAD 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 *
|
|
# * Lesser General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Lesser General Public *
|
|
# * License along with FreeCAD. If not, see *
|
|
# * <https://www.gnu.org/licenses/>. *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
"""The Addon Catalog is the main list of all Addons along with their various
|
|
sources and compatible versions. Added in FreeCAD 1.1 to replace .gitmodules."""
|
|
|
|
from dataclasses import dataclass
|
|
from hashlib import sha256
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
from addonmanager_metadata import Version
|
|
from Addon import Addon
|
|
|
|
import addonmanager_freecad_interface as fci
|
|
|
|
|
|
@dataclass
|
|
class AddonCatalogEntry:
|
|
"""Each individual entry in the catalog, storing data about a particular version of an
|
|
Addon. Note that this class needs to be identical to the one that is used in the remote cache
|
|
generation, so don't make changes here without ensuring that the classes are synchronized."""
|
|
|
|
freecad_min: Optional[Version] = None
|
|
freecad_max: Optional[Version] = None
|
|
repository: Optional[str] = None
|
|
git_ref: Optional[str] = None
|
|
zip_url: Optional[str] = None
|
|
note: Optional[str] = None
|
|
branch_display_name: Optional[str] = None
|
|
|
|
def __init__(self, raw_data: Dict[str, str]) -> None:
|
|
"""Create an AddonDictionaryEntry from the raw JSON data"""
|
|
super().__init__()
|
|
for key, value in raw_data.items():
|
|
if hasattr(self, key):
|
|
if key in ("freecad_min", "freecad_max"):
|
|
value = Version(from_string=value)
|
|
setattr(self, key, value)
|
|
|
|
def is_compatible(self) -> bool:
|
|
"""Check whether this AddonCatalogEntry is compatible with the current version of FreeCAD"""
|
|
if self.freecad_min is None and self.freecad_max is None:
|
|
return True
|
|
current_version = Version(from_list=fci.Version())
|
|
if self.freecad_min is None:
|
|
return current_version <= self.freecad_max
|
|
if self.freecad_max is None:
|
|
return current_version >= self.freecad_min
|
|
return self.freecad_min <= current_version <= self.freecad_max
|
|
|
|
def unique_identifier(self) -> str:
|
|
"""Return a unique identifier of the AddonCatalogEntry, guaranteed to be repeatable: when
|
|
given the same basic information, the same ID is created. Used as the key when storing
|
|
the metadata for a given AddonCatalogEntry."""
|
|
sha256_hash = sha256()
|
|
sha256_hash.update(str(self).encode("utf-8"))
|
|
return sha256_hash.hexdigest()
|
|
|
|
|
|
class AddonCatalog:
|
|
"""A catalog of addons grouped together into sets representing versions that are
|
|
compatible with different versions of FreeCAD and/or represent different available branches
|
|
of a given addon (e.g. a Development branch that users are presented)."""
|
|
|
|
def __init__(self, data: Dict[str, Any]):
|
|
self._original_data = data
|
|
self._dictionary = {}
|
|
self._parse_raw_data()
|
|
|
|
def _parse_raw_data(self):
|
|
self._dictionary = {} # Clear pre-existing contents
|
|
for key, value in self._original_data.items():
|
|
if key == "_meta": # Don't add the documentation object to the tree
|
|
continue
|
|
self._dictionary[key] = []
|
|
for entry in value:
|
|
self._dictionary[key].append(AddonCatalogEntry(entry))
|
|
|
|
def load_metadata_cache(self, cache: Dict[str, Any]):
|
|
"""Given the raw dictionary, couple that with the remote metadata cache to create the
|
|
final working addon dictionary. Only create Addons that are compatible with the current
|
|
version of FreeCAD."""
|
|
for value in self._dictionary.values():
|
|
for entry in value:
|
|
sha256_hash = entry.unique_identifier()
|
|
print(sha256_hash)
|
|
if sha256_hash in cache and entry.is_compatible():
|
|
entry.addon = Addon.from_cache(cache[sha256_hash])
|
|
|
|
def get_available_addon_ids(self) -> List[str]:
|
|
"""Get a list of IDs that have at least one entry compatible with the current version of
|
|
FreeCAD"""
|
|
id_list = []
|
|
for key, value in self._dictionary.items():
|
|
for entry in value:
|
|
if entry.is_compatible():
|
|
id_list.append(key)
|
|
break
|
|
return id_list
|
|
|
|
def get_available_branches(self, addon_id: str) -> List[Tuple[str, str]]:
|
|
"""For a given ID, get the list of available branches compatible with this version of
|
|
FreeCAD along with the branch display name. Either field may be empty, but not both. The
|
|
first entry in the list is expected to be the "primary"."""
|
|
if addon_id not in self._dictionary:
|
|
return []
|
|
result = []
|
|
for entry in self._dictionary[addon_id]:
|
|
if entry.is_compatible():
|
|
result.append((entry.git_ref, entry.branch_display_name))
|
|
return result
|
|
|
|
def get_addon_from_id(self, addon_id: str, branch: Optional[Tuple[str, str]] = None) -> Addon:
|
|
"""Get the instantiated Addon object for the given ID and optionally branch. If no
|
|
branch is provided, whichever branch is the "primary" branch will be returned (i.e. the
|
|
first branch that matches). Raises a ValueError if no addon matches the request."""
|
|
if addon_id not in self._dictionary:
|
|
raise ValueError(f"Addon '{addon_id}' not found")
|
|
for entry in self._dictionary[addon_id]:
|
|
if not entry.is_compatible():
|
|
continue
|
|
if not branch or entry.branch_display_name == branch:
|
|
return entry.addon
|
|
raise ValueError(f"Addon '{addon_id}' has no compatible branches named '{branch}'")
|