diff --git a/package/LICENSE b/package/LICENSE new file mode 100644 index 00000000..335ea9d0 --- /dev/null +++ b/package/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 The Python Packaging Authority + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/package/README.md b/package/README.md new file mode 100644 index 00000000..b3918df8 --- /dev/null +++ b/package/README.md @@ -0,0 +1,29 @@ +# PsyNeuLinkView Package Building + +To build pip package +``` +cd package +python3 -m build +``` + +To upload to distribution server. You will need token shared privately. +``` +twine upload dist/* +``` + +To pip install local package created in previous steps +``` +python3 -m pip install --no-index --find-links=package_directory_path + "/dist" psyneulinkview +``` + +# PsyNeuLinkView Installing from PyPI + +To install from PyPi +``` +pip install psyneulinkview --extra-index-url https://pypi.org/project/psyneulinkview +``` + +To run psyneulinkviewer +``` +psyneulinkviewer +``` diff --git a/package/configuration.py b/package/configuration.py new file mode 100644 index 00000000..77c57523 --- /dev/null +++ b/package/configuration.py @@ -0,0 +1,21 @@ +graphviz = "graphviz" +psyneulink = "psyneulink" +conda_required_version = "4.9.1" + +releases_url = 'https://api.github.com/repos/MetaCell/PsyNeuLinkView/releases' +application_url = "psyneulinkviewer-linux-x64/psyneulinkviewer" +symlink = "/usr/local/bin/psyneulinkviewer" +extract_location = "/usr/local/bin" + +linux_conda_bash = "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh" +mac_conda_bash = "https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh" + +env_name = "psyneulinkviewer" +create_env = "conda create --name " + env_name + " python=3.11" +activate_env = "conda activate " + env_name + +rosetta_installation = "softwareupdate --install-rosetta --agree-to-license" + +conda_forge = "conda config --add channels conda-forge" +node_installation = "conda install nodejs" +node_required_version = "4.19.0" diff --git a/package/dist/psyneulinkview-0.0.5-py3-none-any.whl b/package/dist/psyneulinkview-0.0.5-py3-none-any.whl new file mode 100644 index 00000000..2e21180b Binary files /dev/null and b/package/dist/psyneulinkview-0.0.5-py3-none-any.whl differ diff --git a/package/dist/psyneulinkview-0.0.5.tar.gz b/package/dist/psyneulinkview-0.0.5.tar.gz new file mode 100644 index 00000000..bd97d99c Binary files /dev/null and b/package/dist/psyneulinkview-0.0.5.tar.gz differ diff --git a/package/psyneulinkviewer/__init__.py b/package/psyneulinkviewer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/package/psyneulinkviewer/conda.py b/package/psyneulinkviewer/conda.py new file mode 100644 index 00000000..5f52809b --- /dev/null +++ b/package/psyneulinkviewer/conda.py @@ -0,0 +1,88 @@ +import platform +import os +import sys +import subprocess +import logging +import configuration +import wget +import platform +from setuptools import setup, find_packages +from setuptools.command.install import install +from packaging.version import Version + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +def activate_env(): + logging.info("Creating conda environment ") + subprocess.run(configuration.create_env, shell=True) + + logging.info("Activating conda ") + subprocess.run(configuration.activate_env, shell=True) + +def install_conda(): + if os.name == 'posix': + bash_file = wget.download(configuration.linux_conda_bash, out="psyneulinkviewer") + elif platform.system() == 'Darwin': + bash_file = wget.download(configuration.mac_conda_bashf) + + logging.info("Installing conda %s", bash_file) + logging.info(bash_file) + subprocess.run("chmod +x " + bash_file, shell=True) + subprocess.run(bash_file + " -b -u -p ~/miniconda3", shell=True) + + activate_env() + logging.info("Clean up ") + subprocess.run("rm -rf " + bash_file, shell=True) + + +def check_conda_installation(): + conda_version = None + try: + conda_version = subprocess.run( + ["conda", "--version"], + capture_output = True, + text = True + ).stdout + if conda_version: + conda_version = conda_version.split(" ")[1] + logging.info("Conda version detected : %s", conda_version) + except Exception as error: + if not isinstance(error, FileNotFoundError): + logging.error("Error with conda installation, exiting setup: %s ", error) + sys.exit() + + if conda_version is None: + logging.info("Conda is not installed") + user_input = input("Do you want to continue with conda installation? (yes/no): ") + if user_input.lower() in ["yes", "y"]: + logging.info("Continuing with conda installation...") + install_conda() + else: + logging.error("Exiting, conda must be installed to continue...") + sys.exit() + + if Version(conda_version) > Version(configuration.conda_required_version): + logging.info("Conda version exists and valid, %s", conda_version) + else: + logging.error("Conda version not up to date, update required") + install_conda() + + env_name = None + try: + envs = subprocess.run( + ["conda", "env","list"], + capture_output = True, + text = True + ).stdout + if envs is not None: + envs = envs.splitlines() + active_env = list(filter(lambda s: '*' in str(s), envs))[0] + env_name = str(active_env).split()[0] + except Exception as error: + logging.error("Environment not found active: %s ", error) + + if env_name is not None: + logging.info("Conda environment found and activated %s", env_name) + else: + activate_env() \ No newline at end of file diff --git a/package/psyneulinkviewer/node.py b/package/psyneulinkviewer/node.py new file mode 100644 index 00000000..7baf555a --- /dev/null +++ b/package/psyneulinkviewer/node.py @@ -0,0 +1,43 @@ +import sys +import subprocess +import logging +import configuration +from setuptools.command.install import install +from packaging.version import Version + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +def install_node(): + logging.info("Installing node") + subprocess.run(configuration.conda_forge, shell=True) + subprocess.run(configuration.node_installation, shell=True) + +def check_node_installation(): + node_version = None + try: + node_version = subprocess.run( + ["node", "--version"], + capture_output = True, + text = True + ).stdout + except Exception as error: + if not isinstance(error, FileNotFoundError): + logging.error("Error with node installation, exiting setup: %s ", error) + sys.exit() + + if node_version is None: + logging.info("Node is not installed") + user_input = input("Do you want to continue with node installation? (yes/no): ") + if user_input.lower() in ["yes", "y"]: + logging.info("Continuing with node installation...") + install_node() + else: + logging.error("Exiting, node must be installed to continue...") + sys.exit() + + if Version(node_version) > Version(configuration.node_required_version): + logging.info("Node version exists and valid, %s", node_version) + else: + logging.error("Node version not up to date, update required") + install_node() \ No newline at end of file diff --git a/package/psyneulinkviewer/rosetta.py b/package/psyneulinkviewer/rosetta.py new file mode 100644 index 00000000..0e882b91 --- /dev/null +++ b/package/psyneulinkviewer/rosetta.py @@ -0,0 +1,44 @@ +import platform +import subprocess +import logging +import configuration +import platform +import sys +from setuptools.command.install import install +from packaging.version import Version + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +def install_rosetta(): + if platform.system() == 'Darwin': + logging.info("Installing rosetta") + subprocess.run(configuration.rosetta_installation, shell=True) + +def check_rosetta_installation(): + rosetta_version = None + try: + if platform.system() == 'Darwin': + rosetta_version = subprocess.run( + ["rosseta", "--version"], + capture_output = True, + text = True + ).stdout + if rosetta_version: + rosetta_version = rosetta_version.split(" ")[1] + logging.info("Rosseta version detected : %s", rosetta_version) + except Exception as error: + if not isinstance(error, FileNotFoundError): + logging.error("Error with rosetta installation, exiting setup: %s ", error) + sys.exit() + + if rosetta_version is None: + logging.info("Rosetta ist not installed") + user_input = input("Do you want to continue with rosetta installation? (yes/no): ") + if user_input.lower() in ["yes", "y"]: + logging.info("Continuing with rosetta installation...") + install_rosetta() + else: + logging.error("Exiting, rosetta must be installed to continue...") + sys.exit() + diff --git a/package/psyneulinkviewer/start.py b/package/psyneulinkviewer/start.py new file mode 100644 index 00000000..e79d499d --- /dev/null +++ b/package/psyneulinkviewer/start.py @@ -0,0 +1,130 @@ +import re +import json +import platform +import os +import sys +import subprocess +import logging +import importlib.util +import tarfile +import atexit +from setuptools import setup, find_packages +from setuptools.command.install import install +from packaging.version import Version +from psyneulinkviewer.conda import check_conda_installation +from psyneulinkviewer.rosetta import check_rosetta_installation +from psyneulinkviewer.node import check_node_installation +import configuration + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +def check_os(): + if os.name == 'nt': + sys.exit('Windows is not supported') + else: + logging.info("OS version supported") + +def check_python(): + if not sys.version_info.major == 3 and not sys.version_info.minor == 6 : + logging.error('Python version not supported, 3.11 is required. %f' , sys.version_info) + sys.exit('Python version not supported, 3.11 is required.') + else: + logging.info("Python version is supported") + +def check_graphviz(): + if importlib.util.find_spec(configuration.graphviz) is None: + logging.error(configuration.graphviz +" is not installed, installing") + result = subprocess.run( + ["pip", "install", "graphviz"], + capture_output = True, + text = True + ) + else: + logging.info(configuration.graphviz +" is installed") + +def check_psyneulink(): + if importlib.util.find_spec(configuration.psyneulink) is None: + logging.error(configuration.psyneulink +" is not installed, installing") + result = subprocess.run( + ["pip", "install", "psyneulink"], + capture_output = True, + text = True + ) + else: + logging.info(configuration.psyneulink +" is installed") + + +def get_filename_from_cd(cd): + """ + Get filename from content-disposition + """ + if not cd: + return None + fname = re.findall('filename=(.+)', cd) + if len(fname) == 0: + return None + return fname[0] + +def get_latest_release(installation_path): + import requests + + headers = {'Accept': 'application/vnd.github+json','Authorization': 'Bearer JWT', 'X-GitHub-Api-Version' : '2022-11-28'} + r = requests.get(configuration.releases_url, allow_redirects=True) + releases = json.loads(r.text) + assets = releases[0]["assets"] + + target_release = None + for asset in assets : + if platform.system().lower() in asset['name'] : + target_release = asset["browser_download_url"] + + logging.info("System detected %s :", platform.system()) + logging.info("Target release url found %s :", target_release) + logging.info("Downloading release to %s...", installation_path) + release_download = requests.get(target_release, allow_redirects=True) + + filename = get_filename_from_cd(release_download.headers.get('content-disposition')) + tar_location = os.path.join(installation_path, filename) + logging.info("Writing release to %s...", tar_location) + open(tar_location, 'wb').write(release_download.content) + + logging.info("Opening compressed file %s", filename) + tar = tarfile.open(tar_location) + extract_location = configuration.extract_location + tar.extractall(path=extract_location) + tar.close() + logging.info("Release file uncompressed at : %s", extract_location) + + application = os.path.join(extract_location, configuration.application_url) + symlink = configuration.symlink + logging.info("Creating symlink at : %s", symlink) + logging.info("Application at : %s", application) + try: + if os.path.islink(symlink): + os.remove(symlink) + os.symlink(application, symlink) + except OSError as e: + logging.error("Error applying symlin %f ", e) + + logging.info("Symlink created") + + logging.info("*** To launch the application run : **** ") + logging.info(" %s " ,symlink) + +def prerequisites(): + check_os() + check_python() + check_conda_installation() + #Install package requirements here + check_rosetta_installation() + check_node_installation() + check_graphviz() + check_psyneulink() + get_latest_release(os.path.dirname(os.path.realpath(__file__))) + +def main(): + prerequisites() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/package/requirements.txt b/package/requirements.txt new file mode 100644 index 00000000..a379fde6 --- /dev/null +++ b/package/requirements.txt @@ -0,0 +1,2 @@ +wget==3.2 +packaging \ No newline at end of file diff --git a/package/setup.py b/package/setup.py new file mode 100644 index 00000000..8339933a --- /dev/null +++ b/package/setup.py @@ -0,0 +1,43 @@ +import logging +import os +from setuptools import setup, find_packages +from setuptools.command.install import install +from psyneulinkviewer.start import prerequisites + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +class InstallCommand(install): + user_options = install.user_options + [ + ('path=', None, 'an option that takes a value') + ] + + def initialize_options(self): + install.initialize_options(self) + self.path = None + + def finalize_options(self): + # Validate options + if self.path is None: + self.path = os.path.dirname(os.path.realpath(__file__)) + super().finalize_options() + + + def run(self): + global path + prerequisites() + print("pre") + path = self.path # will be 1 or None + install.run(self) + prerequisites() + +setup( + name="psyneulinkview", + version="0.0.1", + install_requires=['requests', + 'wget' + 'packaging'], + cmdclass={ + 'install': InstallCommand, + } +) \ No newline at end of file