-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathAddonCatalog.py
143 lines (126 loc) · 7.05 KB
/
AddonCatalog.py
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
# 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}'")