-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #266 from specklesystems/jrm/one-installer-for-all
Added Installer
- Loading branch information
Showing
1 changed file
with
207 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
""" | ||
Provides uniform and consistent path helpers for `specklepy` | ||
""" | ||
import os | ||
import sys | ||
from pathlib import Path | ||
from typing import Optional | ||
from importlib import import_module, invalidate_caches | ||
|
||
_user_data_env_var = "SPECKLE_USERDATA_PATH" | ||
|
||
|
||
def _path() -> Optional[Path]: | ||
"""Read the user data path override setting.""" | ||
path_override = os.environ.get(_user_data_env_var) | ||
if path_override: | ||
return Path(path_override) | ||
return None | ||
|
||
|
||
_application_name = "Speckle" | ||
|
||
|
||
def override_application_name(application_name: str) -> None: | ||
"""Override the global Speckle application name.""" | ||
global _application_name | ||
_application_name = application_name | ||
|
||
|
||
def override_application_data_path(path: Optional[str]) -> None: | ||
""" | ||
Override the global Speckle application data path. | ||
If the value of path is `None` the environment variable gets deleted. | ||
""" | ||
if path: | ||
os.environ[_user_data_env_var] = path | ||
else: | ||
os.environ.pop(_user_data_env_var, None) | ||
|
||
|
||
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path: | ||
path = base_path.joinpath(folder_name) | ||
path.mkdir(exist_ok=True, parents=True) | ||
return path | ||
|
||
|
||
def user_application_data_path() -> Path: | ||
"""Get the platform specific user configuration folder path""" | ||
path_override = _path() | ||
if path_override: | ||
return path_override | ||
|
||
try: | ||
if sys.platform.startswith("win"): | ||
app_data_path = os.getenv("APPDATA") | ||
if not app_data_path: | ||
raise Exception( | ||
"Cannot get appdata path from environment." | ||
) | ||
return Path(app_data_path) | ||
else: | ||
# try getting the standard XDG_DATA_HOME value | ||
# as that is used as an override | ||
app_data_path = os.getenv("XDG_DATA_HOME") | ||
if app_data_path: | ||
return Path(app_data_path) | ||
else: | ||
return _ensure_folder_exists(Path.home(), ".config") | ||
except Exception as ex: | ||
raise Exception( | ||
"Failed to initialize user application data path.", ex | ||
) | ||
|
||
|
||
def user_speckle_folder_path() -> Path: | ||
"""Get the folder where the user's Speckle data should be stored.""" | ||
return _ensure_folder_exists(user_application_data_path(), _application_name) | ||
|
||
|
||
def user_speckle_connector_installation_path(host_application: str) -> Path: | ||
""" | ||
Gets a connector specific installation folder. | ||
In this folder we can put our connector installation and all python packages. | ||
""" | ||
return _ensure_folder_exists( | ||
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"), | ||
host_application, | ||
) | ||
|
||
|
||
|
||
|
||
|
||
|
||
print("Starting module dependency installation") | ||
print(sys.executable) | ||
|
||
PYTHON_PATH = sys.executable | ||
|
||
|
||
|
||
def connector_installation_path(host_application: str) -> Path: | ||
connector_installation_path = user_speckle_connector_installation_path(host_application) | ||
connector_installation_path.mkdir(exist_ok=True, parents=True) | ||
|
||
# set user modules path at beginning of paths for earlier hit | ||
if sys.path[0] != connector_installation_path: | ||
sys.path.insert(0, str(connector_installation_path)) | ||
|
||
print(f"Using connector installation path {connector_installation_path}") | ||
return connector_installation_path | ||
|
||
|
||
|
||
def is_pip_available() -> bool: | ||
try: | ||
import_module("pip") # noqa F401 | ||
return True | ||
except ImportError: | ||
return False | ||
|
||
|
||
def ensure_pip() -> None: | ||
print("Installing pip... ") | ||
|
||
from subprocess import run | ||
|
||
completed_process = run([PYTHON_PATH, "-m", "ensurepip"]) | ||
|
||
if completed_process.returncode == 0: | ||
print("Successfully installed pip") | ||
else: | ||
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code") | ||
|
||
|
||
def get_requirements_path() -> Path: | ||
# we assume that a requirements.txt exists next to the __init__.py file | ||
path = Path(Path(__file__).parent, "requirements.txt") | ||
assert path.exists() | ||
return path | ||
|
||
|
||
def install_requirements(host_application: str) -> None: | ||
# set up addons/modules under the user | ||
# script path. Here we'll install the | ||
# dependencies | ||
path = connector_installation_path(host_application) | ||
print(f"Installing Speckle dependencies to {path}") | ||
|
||
from subprocess import run | ||
|
||
completed_process = run( | ||
[ | ||
PYTHON_PATH, | ||
"-m", | ||
"pip", | ||
"install", | ||
"-t", | ||
str(path), | ||
"-r", | ||
str(get_requirements_path()), | ||
], | ||
capture_output=True, | ||
text=True, | ||
) | ||
|
||
if completed_process.returncode != 0: | ||
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code" | ||
print(m) | ||
raise Exception(m) | ||
|
||
|
||
def install_dependencies(host_application: str) -> None: | ||
if not is_pip_available(): | ||
ensure_pip() | ||
|
||
install_requirements(host_application) | ||
|
||
|
||
def _import_dependencies() -> None: | ||
import_module("specklepy") | ||
# the code above doesn't work for now, it fails on importing graphql-core | ||
# despite that, the connector seams to be working as expected | ||
# But it would be nice to make this solution work | ||
# it would ensure that all dependencies are fully loaded | ||
# requirements = get_requirements_path().read_text() | ||
# reqs = [ | ||
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_") | ||
# for req in requirements.split("\n") | ||
# if req and not req.startswith(" ") | ||
# ] | ||
# for req in reqs: | ||
# print(req) | ||
# import_module("specklepy") | ||
|
||
def ensure_dependencies(host_application: str) -> None: | ||
try: | ||
install_dependencies(host_application) | ||
invalidate_caches() | ||
_import_dependencies() | ||
print("Successfully found dependencies") | ||
except ImportError: | ||
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!") | ||
|
||
|