MaterialXUSD 0.25.05
Utilities for using MaterialX with USD
Loading...
Searching...
No Matches
MaterialXDownloader.py
Go to the documentation of this file.
1
2"""
3@file MaterialXDownloader.py
4@brief MaterialX release downloader utility for fetching releases and libraries from GitHub.
5"""
6import os
7import requests
8import zipfile
9import argparse
10import json
11
13 """Class to handle downloading MaterialX releases and libraries."""
14
15 def __init__(self) -> None:
16 """Initialize the MaterialX release downloader with default settings.
17
18 Sets up default configuration for downloading MaterialX releases including
19 version count, target file names, chunk size, and download directory.
20 """
21 self.GITHUB_TOKEN = None
22 self.REPO_OWNER = "AcademySoftwareFoundation"
23 self.REPO_NAME = "MaterialX"
24
25 self._version_count = 15 # Number of latest releases to download
26 self._version_number = [1, 39, 4] # Default version to download
27 self._download_directory = "downloaded_release_libraries" # Directory to save downloaded files
28 self._target_file_names = ["Windows"] # Default to Windows assets
29 self._chunk_size = (65536*65536*16) # 16 MB chunk size for downloading
30 self._remove_zip_after_extract_remove_zip_after_extract = False # Whether to remove the zip file after extraction
31 self._folder_filter = ["libraries"]
32
33 # GitHub API URL for releases
34 self.RELEASES_API_URL = f"https://api.github.com/repos/{self.REPO_OWNER}/{self.REPO_NAME}/releases"
35
36 def __setattr__(self, name: str, value) -> None:
37 """Custom attribute setter with validation.
38
39 @param name: The name of the attribute to set
40 @param value: The value to set for the attribute
41 @return: None
42 """
43 if name == 'chunk_size':
44 if value <= 0:
45 raise ValueError("Chunk size must be a positive integer.")
46 super().__setattr__('_chunk_size', value)
47 elif name == 'version_number':
48 if isinstance(value, list) and len(value) == 3:
49 super().__setattr__('_version_number', value)
50 else:
51 raise ValueError("Version number must be a list of three integers [major, minor, patch].")
52 elif name == 'download_directory':
53 super().__setattr__('_download_directory', value)
54 if hasattr(self, 'create_download_directory'):
56 elif name in ['version_count', 'target_file_names', 'folder_filter']:
57 super().__setattr__(f'_{name}', value)
58 else:
59 super().__setattr__(name, value)
60
61 def __getattr__(self, name: str):
62 """Custom attribute getter for private attributes.
63
64 @param name: The name of the attribute to get
65 @return: The value of the requested attribute
66 """
67 if name in ['version_count', 'version_number', 'download_directory', 'target_file_names', 'chunk_size', 'folder_filter']:
68 return getattr(self, f'_{name}')
69 elif name == 'version_string':
70 return ".".join(map(str, self._version_number))
71 raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
72
73 def create_download_directory(self) -> None:
74 """Create the download directory if it doesn't exist.
75
76 @return: None
77 """
78 os.makedirs(self._download_directory, exist_ok=True)
79
80 def download_file(self, url: str, dest_path: str) -> None:
81 """Download a file from a given URL to the specified destination.
82
83 @param url: The URL to download the file from
84 @param dest_path: The local path where the file should be saved
85 @return: None
86 """
87 headers = {"Authorization": f"token {self.GITHUB_TOKEN}"} if self.GITHUB_TOKEN else {}
88 response = requests.get(url, headers=headers, stream=True)
89 response.raise_for_status()
90
91 download_chunk_size = self._chunk_size
92 with open(dest_path, "wb") as file:
93 for chunk in response.iter_content(chunk_size=download_chunk_size):
94 #print(f"...Downloading {len(chunk)} bytes")
95 print('*', end='', flush=True) # Print a dot for each chunk downloaded
96 file.write(chunk)
97
98 def download_releases(self, by_count: bool = True) -> None:
99 """Download the assets of the latest N releases.
100
101 @param by_count: If True, download by count limit; if False, download specific version
102 @return: None
103 """
104 headers = {"Authorization": f"token {self.GITHUB_TOKEN}"} if self.GITHUB_TOKEN else {}
105 response = requests.get(self.RELEASES_API_URL, headers=headers)
106 response.raise_for_status()
107
108 releases = response.json()
109 index = 0
110 version_string = None
111 if not by_count:
112 version_string = self.version_string
113 stop_search = False
114
115 for release in releases:
116 release_name = release["name"]
117
118 # Look by version
119 if version_string:
120 if version_string in release_name:
121 stop_search = True
122 else:
123 print(f"Skipping release: {release_name} (does not match version string {version_string})")
124 continue
125
126 asset_urls = []
127 asset_names = []
128 tag_names = []
129 print(f"Checking assets for release: {release_name}. Tag: {release['tag_name']}")
130 for asset in release["assets"]:
131 asset_name = asset["name"]
132 # Check if the asset name contains any of the target file names
133 if any(target in asset_name for target in self._target_file_names):
134 print("- Found asset:", asset_name)
135 tag_names.append(release["tag_name"]) #.replace(" ", "_").replace(".", "_"))
136 asset_url = asset["browser_download_url"]
137 asset_urls.append(asset_url)
138
139 # Sort asset_urls and tag_names by value of tag_names
140 #asset_urls, tag_names = zip(*sorted(zip(asset_urls, tag_names), key=lambda x: x[1], reverse=True))
141 asset_urls = list(asset_urls)
142 tag_names = list(tag_names)
143 #print(f"Sorted asset URLs and tag names: {asset_urls}\n, {tag_names}")
144 for asset_url, tag_name in zip(asset_urls, tag_names):
145 print(f"- Downloading {asset_name} from {asset_url}")
146 dest_path = os.path.join(self._download_directory, tag_name + ".zip")
147 self.download_file(asset_url, dest_path)
148 print(f"Saved to {dest_path}")
149
150 # Unzip the downloaded file
151 print(f"Extracting {dest_path} to {os.path.join(self._download_directory, tag_name)}")
152 with zipfile.ZipFile(dest_path, 'r') as zip_ref:
153 zip_ref.extractall(os.path.join(self._download_directory, tag_name))
154 print(f"Extracted to {os.path.join(self._download_directory, tag_name)}")
155
156 # TODO Just pick one for now.
157 break
158 print(f"Finished checking assets for release: {release_name}")
159
160 if by_count:
161 if index >= self._version_count - 1:
162 stop_search = True
163 index += 1
164
165 if stop_search:
166 break
167
168 def download_libraries(self, by_count: bool = True) -> None:
169 """Download the 'Source code (zip)' for the latest N releases and extract the standard 'libraries' folder.
170
171 @param by_count: If True, download by count limit; if False, download specific version
172 @return: None
173 """
174 headers = {"Authorization": f"token {self.GITHUB_TOKEN}"} if self.GITHUB_TOKEN else {}
175 response = requests.get(self.RELEASES_API_URL, headers=headers)
176 response.raise_for_status()
177
178 releases = response.json()
179 index = 0
180 version_string = None
181 if not by_count:
182 version_string = self.version_string
183 stop_search = False
184
185 for release in releases:
186 tag_name = release["tag_name"]
187 release_name = release["name"]
188 if version_string:
189 if version_string not in release_name:
190 print(f"Skipping release: {release_name} (does not match version string {version_string})")
191 continue
192 else:
193 stop_search = True
194
195 print(f"Downloading 'Source code (zip)' for release: {release_name} (tag: {tag_name})")
196
197 source_zip_url = f"https://github.com/{self.REPO_OWNER}/{self.REPO_NAME}/archive/refs/tags/{tag_name}.zip"
198 dest_path = os.path.join(self._download_directory, f"{tag_name}.zip")
199 self.download_file(source_zip_url, dest_path)
200 print(f"\nSaved ZIP to {dest_path}")
201
202 # Extract out the "libraries" folder from the zip file
203 print(f"Extracing ZIP files from {dest_path} to {os.path.join(self._download_directory, tag_name)}")
204 with zipfile.ZipFile(dest_path, 'r') as zip_ref:
205 for file in zip_ref.namelist():
206
207 # Strip the top-level folder first to check the actual path structure
208 path_parts = file.split('/')
209 if len(path_parts) > 1:
210
211 # Get path without the top-level folder (e.g., MaterialX-1.39.3/)
212 relative_path = '/'.join(path_parts[1:])
213
214 # Check if the path starts with any of the folder_filter entries
215 if any(relative_path.startswith(f"{folder}/") for folder in self._folder_filter):
216
217 # Create new path without the top-level folder
218 new_file_path = relative_path
219
220 # Create a ZipInfo object with the modified path
221 info = zip_ref.getinfo(file)
222 info.filename = new_file_path
223
224 # Extract with the modified path
225 zip_ref.extract(info, os.path.join(self._download_directory, tag_name))
226
227 print('+', end='', flush=True) # Print a dot for each chunk downloaded
228 #print(f"Extracted {new_file_path} to {os.path.join(self._download_directory, tag_name, new_file_path)}")
229
230 print("\nFinished extracting libraries folder from zip file.")
231
232 # Optionally, remove the zip file after extraction
234 os.remove(dest_path)
235 print(f"Removed zip file: {dest_path}")
236
237 if by_count:
238 if index >= self._version_count - 1:
239 stop_search = True
240 index += 1
241
242 if stop_search:
243 break
244
Class to handle downloading MaterialX releases and libraries.
None download_libraries(self, bool by_count=True)
Download the 'Source code (zip)' for the latest N releases and extract the standard 'libraries' folde...
None __init__(self)
Initialize the MaterialX release downloader with default settings.
None __setattr__(self, str name, value)
Custom attribute setter with validation.
None download_releases(self, bool by_count=True)
Download the assets of the latest N releases.
None download_file(self, str url, str dest_path)
Download a file from a given URL to the specified destination.
__getattr__(self, str name)
Custom attribute getter for private attributes.
None create_download_directory(self)
Create the download directory if it doesn't exist.