Skip to content

Commit dfc659b

Browse files
chriswmackeyChris Mackey
authored andcommitted
feat(versioner): Add versioner component for updating installed version
I also made some tweaks to the wind rose component, one of which was changing the name from "Wind Rose Plot" to just "Wind Rose". If the word "Plot" was helping us distinguish the component from another, then I would have kept but but, otherwise, brevity is the soul of wit.
1 parent 5cb0fdc commit dfc659b

File tree

6 files changed

+362
-43
lines changed

6 files changed

+362
-43
lines changed
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# Dragonfly: A Plugin for Environmental Analysis (GPL)
2+
# This file is part of Dragonfly.
3+
#
4+
# Copyright (c) 2019, Ladybug Tools.
5+
# You should have received a copy of the GNU General Public License
6+
# along with Dragonfly; If not, see <http://www.gnu.org/licenses/>.
7+
#
8+
# @license GPL-3.0+ <http://spdx.org/licenses/GPL-3.0+>
9+
10+
"""
11+
This component updates the Ladybug Tools core libraries and grasshopper components
12+
to either the latest development version available (default) or to a specific
13+
version of the grasshopper plugin.
14+
_
15+
The input version_ does not need to be newer than the current installation and can
16+
be older but grasshopper plugin versions less than 0.3.0 are not supported.
17+
A list of all versions of the Grasshopper plugin and corresponding release notes
18+
can be found at: https://github.com/ladybug-tools/lbt-grasshopper/releases
19+
_
20+
This component can also overwrite the user libraries of standards (constructions,
21+
schedules, modifiers) with a completely fresh copy if clean_standards_ is set to True.
22+
-
23+
24+
Args:
25+
_update: Set to True to update your installation of Ladybug Tools to the
26+
latest development version or to be at the version specified below.
27+
version_: An optional text string for the version of the grasshopper
28+
plugin which you would like to update to. The input should contain
29+
only integers separated by two periods (eg. 1.0.0). The version does
30+
not need to be newer than the current installation and can be older
31+
but grasshopper plugin versions less than 0.3.0 are not supported.
32+
A list of all versions of the Grasshopper plugin can be found
33+
here - https://github.com/ladybug-tools/lbt-grasshopper/releases
34+
clean_standards_: Set to True to have any user libraries of standards
35+
(constructions, schedules, modifiers) overwritten with a
36+
completely fresh copy. If False or None, any existing standards
37+
will be left alone.
38+
39+
Returns:
40+
Vviiiiiz!: !!!
41+
"""
42+
43+
ghenv.Component.Name = 'LB Versioner'
44+
ghenv.Component.NickName = 'Versioner'
45+
ghenv.Component.Message = '0.1.0'
46+
ghenv.Component.Category = 'Ladybug'
47+
ghenv.Component.SubCategory = '5 :: Version'
48+
ghenv.Component.AdditionalHelpFromDocStrings = '1'
49+
50+
try:
51+
from ladybug.futil import preparedir, nukedir, copy_file_tree, unzip_file
52+
from ladybug.config import folders
53+
except ImportError as e:
54+
raise ImportError('\nFailed to import ladybug:\n\t{}'.format(e))
55+
56+
try:
57+
from ladybug_rhino.download import download_file_by_name
58+
from ladybug_rhino.grasshopper import all_required_inputs, give_warning
59+
except ImportError as e:
60+
raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e))
61+
62+
import os
63+
import subprocess
64+
from Grasshopper.Folders import UserObjectFolders
65+
66+
67+
def get_python_exe():
68+
"""Get the path to the Python installed in the ladybug_tools folder.
69+
70+
Will be None if Python is not installed.
71+
"""
72+
py_update = os.path.join(folders.ladybug_tools_folder, 'python')
73+
py_exe_file = os.path.join(py_update, 'python.exe') if os.name == 'nt' else \
74+
os.path.join(py_update, 'bin', 'python3')
75+
py_site_pack = os.path.join(py_update, 'Lib', 'site-packages') if os.name == 'nt' else \
76+
os.path.join(py_update, 'lib', 'python3.8', 'site-packages')
77+
if os.path.isfile(py_exe_file):
78+
return py_exe_file, py_site_pack
79+
return None, None
80+
81+
82+
def get_gem_directory():
83+
"""Get the directory where measures distributed with Ladybug Tools are installed."""
84+
measure_folder = os.path.join(folders.ladybug_tools_folder, 'resources', 'measures')
85+
if not os.path.isdir(measure_folder):
86+
os.makedirs(measure_folder)
87+
return measure_folder
88+
89+
90+
def get_standards_directory():
91+
"""Get the directory where Honeybee standards are installed."""
92+
hb_folder = os.path.join(folders.ladybug_tools_folder, 'resources', 'standards')
93+
if not os.path.isdir(hb_folder):
94+
os.makedirs(hb_folder)
95+
return hb_folder
96+
97+
98+
def update_libraries_pip(python_exe, package_name, version=None, target=None):
99+
"""Change python libraries to be of a specific version using pip.
100+
101+
Args:
102+
python_exe: The path to the Python executable to be used for installation.
103+
package_name: The name of the PyPI package to install.
104+
version: An optional string for the version of the package to install.
105+
If None, the library will be updated to the latest version with -U.
106+
target: An optional target directory into which the package will be installed.
107+
"""
108+
# build up the command using the inputs
109+
if version is not None:
110+
package_name = '{}=={}'.format(package_name, version)
111+
cmds = [python_exe, '-m', 'pip', 'install', package_name]
112+
if version is None:
113+
cmds.append('-U')
114+
if target is not None:
115+
cmds.extend(['--target', target, '--upgrade'])
116+
117+
# execute the command and print any errors
118+
print('Installing "{}" version via pip'.format(package_name))
119+
use_shell = True if os.name == 'nt' else False
120+
process = subprocess.Popen(
121+
cmds, shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
122+
output = process.communicate()
123+
stdout, stderr = output
124+
return stderr
125+
126+
127+
def download_repo_github(repo, target_directory, version=None):
128+
"""Download a repo of a particular version from from github.
129+
130+
Args:
131+
repo: The name of a repo to be downloaded (eg. 'lbt-grasshopper').
132+
target_directory: the directory where the library should be downloaded to.
133+
version: The version of the repository to download. If None, the most
134+
recent version will be downloaded. (Default: None)
135+
"""
136+
# download files
137+
if version is None:
138+
url = "https://github.com/ladybug-tools/{}/archive/master.zip".format(repo)
139+
else:
140+
url = "https://github.com/ladybug-tools/{}/archive/v{}.zip".format(repo, version)
141+
zip_file = os.path.join(target_directory, '%s.zip' % repo)
142+
print 'Downloading "{}" github repository to: {}'.format(repo, target_directory)
143+
download_file_by_name(url, target_directory, zip_file)
144+
145+
#unzip the file
146+
unzip_file(zip_file, target_directory)
147+
148+
# try to clean up the downloaded zip file
149+
try:
150+
os.remove(zip_file)
151+
except:
152+
print 'Failed to remove downloaded zip file: {}.'.format(zip_file)
153+
154+
# return the directory where the unzipped files live
155+
if version is None:
156+
return os.path.join(target_directory, '{}-master'.format(repo))
157+
else:
158+
return os.path.join(target_directory, '{}-{}'.format(repo, version))
159+
160+
161+
def parse_lbt_gh_versions(lbt_gh_folder):
162+
"""Parse versions of compatible libs from a clone of the lbt-grasshopper repo.
163+
164+
Args:
165+
lbt_gh_folder: Path to the clone of the lbt-grasshopper repo
166+
167+
Returns:
168+
A dictionary of library versions formatted like so (but with actual version
169+
numbers in place of '0.0.0':
170+
171+
{
172+
'lbt-dragonfly' = '0.0.0',
173+
'ladybug-rhino' = '0.0.0',
174+
'lbt-grasshopper' = '0.0.0',
175+
'honeybee-openstudio-gem' = '0.0.0',
176+
'honeybee-standards' = '0.0.0',
177+
'honeybee-energy-standards' = '0.0.0',
178+
'ladybug-grasshopper': '0.0.0',
179+
'honeybee-grasshopper-core': '0.0.0',
180+
'honeybee-grasshopper-radiance': '0.0.0',
181+
'honeybee-grasshopper-energy': '0.0.0',
182+
'dragonfly-grasshopper': '0.0.0'
183+
}
184+
"""
185+
# set the names of the libraries to collect and the version dict
186+
version_dict = {
187+
'lbt-dragonfly': None,
188+
'ladybug-rhino': None,
189+
'honeybee-standards': None,
190+
'honeybee-energy-standards': None,
191+
'honeybee-openstudio-gem': None,
192+
'ladybug-grasshopper': None,
193+
'honeybee-grasshopper-core': None,
194+
'honeybee-grasshopper-radiance': None,
195+
'honeybee-grasshopper-energy': None,
196+
'dragonfly-grasshopper': None
197+
}
198+
libs_to_collect = list(version_dict.keys())
199+
200+
def search_versions(version_file):
201+
"""Search for version numbers within a .txt file."""
202+
with open(version_file) as ver_file:
203+
for row in ver_file:
204+
try:
205+
library, version = row.strip().split('==')
206+
if library in libs_to_collect:
207+
version_dict[library] = version
208+
except Exception : # not a row with a ladybug tools library
209+
pass
210+
211+
# search files for versions
212+
requirements = os.path.join(lbt_gh_folder, 'requirements.txt')
213+
dev_requirements = os.path.join(lbt_gh_folder, 'dev-requirements.txt')
214+
ruby_requirements = os.path.join(lbt_gh_folder, 'ruby-requirements.txt')
215+
search_versions(requirements)
216+
search_versions(dev_requirements)
217+
search_versions(ruby_requirements)
218+
return version_dict
219+
220+
221+
if all_required_inputs(ghenv.Component) and _update is True:
222+
# ensure that Python has been installed in the ladybug_tools folder
223+
py_exe, py_lib = get_python_exe()
224+
assert py_exe is not None, \
225+
'No Python instalation was found at: {}.\nThis is a requirement in ' \
226+
'order to contine with installation'.format(
227+
os.path.join(folders.ladybug_tools_folder, 'python'))
228+
229+
# get the compatiable versions of all the dependencies
230+
temp_folder = os.path.join(folders.ladybug_tools_folder, 'temp')
231+
lbt_gh_folder = download_repo_github('lbt-grasshopper', temp_folder, version_)
232+
ver_dict = parse_lbt_gh_versions(lbt_gh_folder)
233+
ver_dict['lbt-grasshopper'] = version_
234+
235+
# install the core libraries
236+
print 'Installing Ladybug Tools core Python libraries.'
237+
df_ver = ver_dict['lbt-dragonfly']
238+
stderr = update_libraries_pip(py_exe, 'lbt-dragonfly[cli]', df_ver)
239+
if os.path.isdir(os.path.join(py_lib, 'lbt_dragonfly-{}.dist-info'.format(df_ver))):
240+
print 'Ladybug Tools core Python libraries successfully installed!\n '
241+
else:
242+
give_warning(ghenv.Component, stderr)
243+
print stderr
244+
245+
# install the library needed for interaction with Rhino
246+
print 'Installing ladybug-rhino Python library.'
247+
rh_ver = ver_dict['ladybug-rhino']
248+
stderr = update_libraries_pip(py_exe, 'ladybug-rhino[cli]', rh_ver)
249+
if os.path.isdir(os.path.join(py_lib, 'ladybug_rhino-{}.dist-info'.format(rh_ver))):
250+
print 'Ladybug-rhino Python library successfully installed!\n '
251+
else:
252+
give_warning(ghenv.Component, stderr)
253+
print stderr
254+
255+
# install the grasshopper components
256+
print 'Installing Ladybug Tools Grasshopper components.'
257+
gh_ver = ver_dict['lbt-grasshopper']
258+
uo_folder = UserObjectFolders[0]
259+
stderr = update_libraries_pip(py_exe, 'lbt-grasshopper', gh_ver, uo_folder)
260+
lbgh_ver = ver_dict['ladybug-grasshopper']
261+
if os.path.isdir(os.path.join(uo_folder, 'ladybug_grasshopper-{}.dist-info'.format(lbgh_ver))):
262+
print 'Ladybug Tools Grasshopper components successfully installed!\n '
263+
else:
264+
give_warning(ghenv.Component, stderr)
265+
print stderr
266+
267+
# install the honeybee-openstudio ruby gem
268+
gem_ver = ver_dict['honeybee-openstudio-gem']
269+
print 'Installing Honeybee-OpenStudio gem version {}.'.format(gem_ver)
270+
gem_dir = get_gem_directory()
271+
base_folder = download_repo_github('honeybee-openstudio-gem', gem_dir, gem_ver)
272+
source_folder = os.path.join(base_folder, 'lib')
273+
lib_folder = os.path.join(gem_dir, 'honeybee_openstudio_gem', 'lib')
274+
print 'Copying "honeybee_openstudio_gem" source code to {}\n '.format(lib_folder)
275+
copy_file_tree(source_folder, lib_folder)
276+
nukedir(base_folder, True)
277+
278+
# install the standards libraries
279+
standards = ['honeybee-standards', 'honeybee-energy-standards']
280+
stand_versions = [ver_dict['honeybee-standards'], ver_dict['honeybee-energy-standards']]
281+
stand_dir = get_standards_directory()
282+
if not clean_standards_:
283+
new_standards, new_stand_versions = [], []
284+
for i, pkg_name in enumerate(standards):
285+
lib_folder = os.path.join(stand_dir, pkg_name.replace('-', '_'))
286+
if not os.path.isdir(lib_folder):
287+
new_standards.append(standards[i])
288+
new_stand_versions.append(stand_versions[i])
289+
standards, stand_versions = new_standards, new_stand_versions
290+
if len(standards) != 0:
291+
print 'Installing Ladybug Tools standards libraries (constructions, schedules, etc.).'
292+
for pkg_name, ver in zip(standards, stand_versions):
293+
stderr = update_libraries_pip(py_exe, pkg_name, ver, stand_dir)
294+
if os.path.isdir(os.path.join(stand_dir, '{}-{}.dist-info'.format(pkg_name.replace('-', '_'), ver))):
295+
print 'Ladybug Tools Grasshopper standards libraries successfully installed!\n '
296+
else:
297+
give_warning(ghenv.Component, stderr)
298+
print stderr
299+
300+
# delete the temp folder and give a completion message
301+
nukedir(temp_folder, True)
302+
print 'Update successful!'
303+
print 'Restart Grasshopper and Rhino to load the new components + library.'
304+
else: # give a message to the user about what to do
305+
print 'Make sure you are connected to the internet and set _update to True!'

0 commit comments

Comments
 (0)