"""Functions for changing the installed version of Ladybug Tools."""
import os
import json
import subprocess
try:
from ladybug.config import folders
from ladybug.futil import nukedir, copy_file_tree, \
download_file_by_name, unzip_file
except ImportError as e:
raise ImportError('\nFailed to import ladybug:\n\t{}'.format(e))
from ..config import folders as lbr_folders
from ..config import rhino_version
from ..pythonpath import iron_python_search_path, create_python_package_dir, \
clean_rhino_scripts
# find the location where the Grasshopper user objects are stored
UO_DIRECTORY = lbr_folders.uo_folder
GHA_DIRECTORY = lbr_folders.gha_folder
[docs]
def get_gem_directory():
"""Get the directory where measures distributed with Ladybug Tools are installed."""
measure_folder = os.path.join(folders.ladybug_tools_folder, 'resources', 'measures')
if not os.path.isdir(measure_folder):
os.makedirs(measure_folder)
return measure_folder
[docs]
def get_standards_directory():
"""Get the directory where Honeybee standards are installed."""
hb_folder = os.path.join(folders.ladybug_tools_folder, 'resources', 'standards')
if not os.path.isdir(hb_folder):
os.makedirs(hb_folder)
return hb_folder
[docs]
def remove_dist_info_files(directory):
"""Remove all of the PyPI .dist-info folders from a given directory.
Args:
directory: A directory containing .dist-info folders to delete.
"""
for fold in os.listdir(directory):
if fold.endswith('.dist-info'):
nukedir(os.path.join(directory, fold), rmdir=True)
[docs]
def get_config_dict():
"""Get a dictionary of the ladybug configurations.
This is needed in order to put the configurations back after update.
"""
with open(folders.config_file, 'r') as cfg:
config_dict = json.load(cfg)
return config_dict
[docs]
def set_config_dict(config_dict):
"""Set the configurations using a dictionary.
Args:
config_dict: A dictionary of configuration paths.
"""
with open(folders.config_file, 'w') as fp:
json.dump(config_dict, fp, indent=4)
[docs]
def full_access_permission(directory):
"""Give a directory any all of its files full permissions.
Args:
directory: A directory containing for which full access will be given.
"""
for root, dirs, files in os.walk(directory):
for d in dirs:
os.chmod(os.path.join(root, d), 0o777)
for f in files:
os.chmod(os.path.join(root, f), 0o777)
[docs]
def update_libraries_pip(python_exe, package_name, version=None, target=None):
"""Change python libraries to be of a specific version using pip.
Args:
python_exe: The path to the Python executable to be used for installation.
package_name: The name of the PyPI package to install.
version: An optional string for the version of the package to install.
If None, the library will be updated to the latest version with -U.
target: An optional target directory into which the package will be installed.
"""
# build up the command using the inputs
if version is not None:
package_name = '{}=={}'.format(package_name, version)
cmds = [python_exe, '-m', 'pip', 'install', package_name]
if version is None:
cmds.append('-U')
if target is not None:
cmds.extend(['--target', target, '--upgrade'])
# execute the command and print any errors
print('Installing "{}" version via pip'.format(package_name))
use_shell = True if os.name == 'nt' else False
process = subprocess.Popen(
cmds, shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = process.communicate()
stdout, stderr = output
error_msg = 'Package "{}" may not have been updated correctly\n' \
'or its usage in the plugin may have changed. See pip stderr below:\n' \
'{}'.format(package_name, stderr)
return error_msg
[docs]
def download_repo_github(repo, target_directory, version=None):
"""Download a repo of a particular version from from github.
Args:
repo: The name of a repo to be downloaded (eg. 'lbt-grasshopper').
target_directory: The directory where the library should be downloaded.
version: The version of the repository to download. If None, the most
recent version will be downloaded. (Default: None)
"""
# download files
if version is None:
url = 'https://github.com/ladybug-tools/{}/archive/master.zip'.format(repo)
else:
url = 'https://github.com/ladybug-tools/{}/archive/v{}.zip'.format(repo, version)
zip_file = os.path.join(target_directory, '%s.zip' % repo)
print('Downloading "{}" github repository to: {}'.format(repo, target_directory))
try:
download_file_by_name(url, target_directory, zip_file)
except ValueError:
msg = 'Access is denied to: {}\nMake sure that you are running this command as' \
' an Administrator by right-clicking on\nRhino and selecting "Run As ' \
'Administrator" before opening Grasshopper and\n running this ' \
'component.'.format(target_directory)
print(msg)
raise ValueError(msg)
# unzip the file
unzip_file(zip_file, target_directory)
# try to clean up the downloaded zip file
try:
os.remove(zip_file)
except Exception:
print('Failed to remove downloaded zip file: {}.'.format(zip_file))
# return the directory where the unzipped files live
if version is None:
return os.path.join(target_directory, '{}-master'.format(repo))
else:
return os.path.join(target_directory, '{}-{}'.format(repo, version))
[docs]
def latest_github_version(repo, target_directory):
"""Get the latest version tag of a particular repo on github.
Args:
repo: The name of a repo to be downloaded (eg. 'lbt-grasshopper').
target_directory: The directory where the HTML Tags page should be downloaded.
"""
# download the latest page with all of the tags
url = 'https://github.com/ladybug-tools/{}/tags'.format(repo)
tag_file = os.path.join(target_directory, '%s.html' % repo)
try:
download_file_by_name(url, target_directory, tag_file)
except ValueError:
msg = 'Access is denied to: {}\nMake sure that you are running this command as' \
' an Administrator.'.format(target_directory)
print(msg)
raise ValueError(msg)
# parse the page to find the correct tag
version_str, key_word = None, '/ladybug-tools/lbt-grasshopper/releases/tag/'
try:
with open(tag_file) as tf:
for row in tf:
if key_word in row:
version_str = row.split(key_word)[-1].split('"')[0].replace('v', '')
break
except UnicodeDecodeError:
pass # machine lacks the encoding to read the HTML
# try to clean up the downloaded html file
try:
os.remove(tag_file)
except Exception:
print('Failed to remove downloaded HTML file: {}.'.format(tag_file))
return version_str
[docs]
def update_requirements_version(uo_folder, lbt_version):
"""Update the version of lbt_grasshopper in the user object requirements.txt file.
Args:
uo_folder: The directory where the user objects currently exist.
lbt_version: The version of LBT-grasshopper to be updated in the
requirements.txt file.
"""
req_file = os.path.join(uo_folder, 'requirements.txt')
all_rows = []
if os.path.isfile(req_file):
with open(req_file) as rf:
for row in rf:
if row.startswith('lbt-grasshopper=='):
all_rows.append('lbt-grasshopper=={}\n'.format(lbt_version))
else:
all_rows.append(row)
else:
all_rows.append('lbt-grasshopper=={}\n'.format(lbt_version))
with open(req_file, 'w') as rf:
rf.write(''.join(all_rows))
[docs]
def parse_lbt_gh_versions(lbt_gh_folder):
"""Parse versions of compatible libs from a clone of the lbt-grasshopper repo.
Args:
lbt_gh_folder: Path to the clone of the lbt-grasshopper repo
Returns:
A dictionary of library versions formatted like so (but with actual version
numbers in place of '0.0.0').
{
'lbt-dragonfly' = '0.0.0',
'ladybug-rhino' = '0.0.0',
'lbt-recipes' = '0.0.0',
'honeybee-openstudio-gem' = '0.0.0',
'lbt-measures' = '0.0.0',
'honeybee-standards' = '0.0.0',
'honeybee-energy-standards' = '0.0.0',
'ladybug-grasshopper': '0.0.0',
'honeybee-grasshopper-core': '0.0.0',
'honeybee-grasshopper-radiance': '0.0.0',
'honeybee-grasshopper-energy': '0.0.0',
'dragonfly-grasshopper': '0.0.0',
'ladybug-grasshopper-dotnet': '0.0.0'
}
"""
# set the names of the libraries to collect and the version dict
version_dict = {
'lbt-dragonfly': None,
'ladybug-rhino': None,
'lbt-recipes': None,
'honeybee-standards': None,
'honeybee-energy-standards': None,
'honeybee-openstudio-gem': None,
'lbt-measures': None,
'ladybug-grasshopper': None,
'honeybee-grasshopper-core': None,
'honeybee-grasshopper-radiance': None,
'honeybee-grasshopper-energy': None,
'dragonfly-grasshopper': None,
'ladybug-grasshopper-dotnet': None
}
libs_to_collect = list(version_dict.keys())
def search_versions(version_file):
"""Search for version numbers within a .txt file."""
with open(version_file) as ver_file:
for row in ver_file:
try:
library, version = row.strip().split('==')
if library in libs_to_collect:
version_dict[library] = version
except Exception: # not a row with a ladybug tools library
pass
# search files for versions
requirements = os.path.join(lbt_gh_folder, 'requirements.txt')
dev_requirements = os.path.join(lbt_gh_folder, 'dev-requirements.txt')
ruby_requirements = os.path.join(lbt_gh_folder, 'ruby-requirements.txt')
search_versions(requirements)
search_versions(dev_requirements)
search_versions(ruby_requirements)
return version_dict
[docs]
def change_installed_version(version_to_install=None):
"""Change the currently installed version of Ladybug Tools.
This requires an internet connection and will update all core libraries and
Grasshopper components to the specified version_to_install.
Args:
version_to_install: An optional text string for the version of the LBT
plugin to be installed. The input should contain only integers separated
by two periods (eg. 1.0.0). If None, the Ladybug Tools plugin shall
be updated to the latest available version. The version specified here
does not need to be newer than the current installation and can be older
but grasshopper plugin versions less than 0.3.0 are not supported.
A list of all versions of the Grasshopper plugin can be found
here - https://github.com/ladybug-tools/lbt-grasshopper/releases
"""
# ensure that Python has been installed in the ladybug_tools folder
py_exe, py_lib = folders.python_exe_path, folders.python_package_path
assert py_exe is not None and py_lib is not None, \
'No valid Python installation was found at: {}.\nThis is a requirement in ' \
'order to continue with installation'.format(
os.path.join(folders.ladybug_tools_folder, 'python'))
# get the compatible versions of all the dependencies
temp_folder = os.path.join(folders.ladybug_tools_folder, 'temp')
lbt_gh_folder = download_repo_github(
'lbt-grasshopper', temp_folder, version_to_install)
ver_dict = parse_lbt_gh_versions(lbt_gh_folder)
if version_to_install is None:
version_to_install = latest_github_version('lbt-grasshopper', temp_folder)
ver_dict['lbt-grasshopper'] = version_to_install
# install the core libraries
print('Installing Ladybug Tools core Python libraries.')
config_dict = get_config_dict() # load configs so they can be put back after update
df_ver = ver_dict['lbt-dragonfly']
stderr = update_libraries_pip(py_exe, 'lbt-dragonfly', df_ver)
if os.path.isdir(os.path.join(py_lib, 'lbt_dragonfly-{}.dist-info'.format(df_ver))):
print('Ladybug Tools core Python libraries successfully installed!\n ')
else:
print(stderr)
set_config_dict(config_dict) # restore the previous configurations
# install the lbt_recipes package
print('Installing Ladybug Tools Recipes.')
rec_ver = ver_dict['lbt-recipes']
stderr = update_libraries_pip(py_exe, 'lbt-recipes', rec_ver)
if os.path.isdir(os.path.join(py_lib, 'lbt_recipes-{}.dist-info'.format(rec_ver))):
print('Ladybug Tools Recipes successfully installed!\n ')
else:
print(stderr)
# install the library needed for interaction with Rhino
print('Installing ladybug-rhino Python library.')
rh_ver = ver_dict['ladybug-rhino']
stderr = update_libraries_pip(py_exe, 'ladybug-rhino', rh_ver)
if os.path.isdir(os.path.join(py_lib, 'ladybug_rhino-{}.dist-info'.format(rh_ver))):
print('Ladybug-rhino Python library successfully installed!\n ')
else:
print(stderr)
# make sure libraries are copied to the rhino scripts folder on Mac
if os.name != 'nt':
if rhino_version[0] < 7: # remove and replace all packages
iron_python_search_path(create_python_package_dir())
else: # just remove any possible conflicting packages
clean_rhino_scripts()
# install the grasshopper components
print('Installing Ladybug Tools Grasshopper components.')
gh_ver = ver_dict['lbt-grasshopper']
stderr = update_libraries_pip(py_exe, 'lbt-grasshopper', gh_ver, UO_DIRECTORY)
lb_gh_ver = ver_dict['ladybug-grasshopper']
lb_gh_info = 'ladybug_grasshopper-{}.dist-info'.format(lb_gh_ver)
if os.path.isdir(os.path.join(UO_DIRECTORY, lb_gh_info)):
print('Ladybug Tools Grasshopper components successfully installed!\n ')
remove_dist_info_files(UO_DIRECTORY) # remove the .dist-info files
full_access_permission(UO_DIRECTORY)
else:
print(stderr)
if version_to_install is not None:
update_requirements_version(UO_DIRECTORY, version_to_install)
# install the .gha Grasshopper components
gha_location = os.path.join(GHA_DIRECTORY, 'ladybug_grasshopper_dotnet')
if os.path.isdir(gha_location):
msg = '.gha files already exist in your Components folder and cannot be ' \
'deleted while Grasshopper is open.\nThese .gha files rarely change ' \
'and so it is not critical that they be updated. However, if you ' \
'want to be sure that you have the latest version installed, then close ' \
'Grasshopper, delete the ladybug_grasshopper_dotnet folder at\n{}\nand ' \
'rerun this versioner component to get the new .gha files.\n'.format(
gha_location)
print(msg)
else:
gha_ver = ver_dict['ladybug-grasshopper-dotnet']
stderr = update_libraries_pip(
py_exe, 'ladybug-grasshopper-dotnet', gha_ver, GHA_DIRECTORY)
package_dir = os.path.join(
GHA_DIRECTORY, 'ladybug_grasshopper_dotnet-{}.dist-info'.format(gha_ver))
if os.path.isdir(package_dir):
print('Ladybug Tools .gha Grasshopper components successfully installed!\n ')
remove_dist_info_files(GHA_DIRECTORY) # remove the dist-info files
full_access_permission(GHA_DIRECTORY)
else:
print(stderr)
# install the honeybee-openstudio ruby gem
gem_ver = ver_dict['honeybee-openstudio-gem']
print('Installing Honeybee-OpenStudio gem version {}.'.format(gem_ver))
gem_dir = get_gem_directory()
base_folder = download_repo_github('honeybee-openstudio-gem', gem_dir, gem_ver)
source_folder = os.path.join(base_folder, 'lib')
lib_folder = os.path.join(gem_dir, 'honeybee_openstudio_gem', 'lib')
print('Copying "honeybee_openstudio_gem" source code to {}\n '.format(lib_folder))
copy_file_tree(source_folder, lib_folder)
nukedir(base_folder, True)
# install the lbt-measures ruby gem
mea_ver = ver_dict['lbt-measures']
print('Installing Ladybug Tools Measures version {}.'.format(mea_ver))
base_folder = download_repo_github('lbt-measures', gem_dir, mea_ver)
source_folder = os.path.join(base_folder, 'lib')
print('Copying "lbt_measures" source code to {}\n '.format(gem_dir))
copy_file_tree(source_folder, gem_dir)
nukedir(base_folder, True)
# always update the honeybee-energy-standards package
print('Installing Honeybee energy standards.')
stand_dir = get_standards_directory()
hes_ver = ver_dict['honeybee-energy-standards']
if os.path.isdir(os.path.join(stand_dir, 'honeybee_energy_standards')):
nukedir(os.path.join(stand_dir, 'honeybee_energy_standards'), True)
stderr = update_libraries_pip(
py_exe, 'honeybee-energy-standards', hes_ver, stand_dir)
hes_info = 'honeybee_energy_standards-{}.dist-info'.format(hes_ver)
if os.path.isdir(os.path.join(stand_dir, hes_info)):
print('Honeybee energy standards successfully installed!\n ')
remove_dist_info_files(stand_dir) # remove the dist-info files
full_access_permission(stand_dir)
else:
print(stderr)
# delete the temp folder and give a completion message
nukedir(temp_folder, True)
version = 'LATEST' if version_to_install is None else version_to_install
success_msg = 'Change to Version {} Successful!'.format(version)
restart_msg = 'RESTART RHINO to load the new components + library.'
sync_msg = 'The "LB Sync Grasshopper File" component can be used\n' \
'to sync Grasshopper definitions with your new installation.'
for msg in (success_msg, restart_msg, sync_msg):
print(msg)