From a8c8300d6fc58e72c3e1e9e35230db481e910823 Mon Sep 17 00:00:00 2001 From: Ilya Krukov Date: Fri, 25 Dec 2020 17:59:00 +0300 Subject: [PATCH] IDEKind selection before ide install --- projector_installer/actions.py | 22 +-- projector_installer/apps.py | 27 +--- projector_installer/dialogs.py | 77 +++++++--- projector_installer/global_config.py | 16 -- projector_installer/installable_app.py | 106 ------------- projector_installer/products.py | 199 +++++++++++++++++++++++++ 6 files changed, 263 insertions(+), 184 deletions(-) delete mode 100644 projector_installer/installable_app.py create mode 100644 projector_installer/products.py diff --git a/projector_installer/actions.py b/projector_installer/actions.py index 180155b..8605a5c 100644 --- a/projector_installer/actions.py +++ b/projector_installer/actions.py @@ -11,7 +11,7 @@ from typing import Optional, List from os import path, system, uname -from .apps import get_compatible_apps, get_app_path, get_installed_apps, get_product_info, \ +from .apps import get_app_path, get_installed_apps, get_product_info, \ unpack_app, get_java_path, get_path_to_latest_app from .log_utils import init_log, shutdown_log, get_path_to_log from .secure_config import get_ca_crt_file, parse_custom_fqdns @@ -316,26 +316,12 @@ def do_list_app(pattern: Optional[str] = None) -> None: def do_install_app(app_name: Optional[str], auto_run: bool = False, allow_updates: bool = False, run_browser: bool = True) -> None: """Installs specified app.""" - apps = get_compatible_apps(app_name) + app = select_compatible_app(app_name) - if len(apps) == 0: - print('There are no known IDEs matched to the given name.') - if app_name is None: - print('Try to reinstall Projector.') - print('Exiting...') + if app is None: + print('IDE was not selected, exiting...') sys.exit(1) - if len(apps) > 1: - app_name = select_compatible_app(app_name) - - if app_name is None: - print('IDE was not selected, exiting...') - sys.exit(1) - else: - app_name = apps[0].name - - apps = get_compatible_apps(app_name) - app = apps[0] config_name_hint = make_config_name(app.name) user_input = get_user_install_input(config_name_hint, auto_run) diff --git a/projector_installer/apps.py b/projector_installer/apps.py index 0830cb1..ba45389 100644 --- a/projector_installer/apps.py +++ b/projector_installer/apps.py @@ -10,9 +10,7 @@ from typing import Optional, List import json -from . import global_config -from .global_config import get_apps_dir, init_compatible_apps -from .installable_app import InstallableApp +from .global_config import get_apps_dir from .utils import unpack_tar_file IDEA_PATH_SELECTOR = 'idea.paths.selector' @@ -26,29 +24,6 @@ def get_installed_apps(pattern: Optional[str] = None) -> List[str]: return res -def get_compatible_apps(pattern: Optional[str] = None) -> List[InstallableApp]: - """Returns list of compatible apps, matched given pattern.""" - if not global_config.COMPATIBLE_APPS: - global_config.COMPATIBLE_APPS = init_compatible_apps() - - apps = [app for app in global_config.COMPATIBLE_APPS if - pattern is None or app.name.lower().find(pattern.lower()) != -1] - - if pattern: - for app in apps: - if pattern.lower() == app.name.lower(): - return [app] - - return apps - - -def get_compatible_app_names(pattern: Optional[str] = None) -> List[str]: - """Get sorted list of projector-compatible applications, matches given pattern.""" - res = [app.name for app in get_compatible_apps(pattern)] - res.sort() - return res - - def get_app_path(app_name: str) -> str: """Returns full path to given app.""" return join(get_apps_dir(), app_name) diff --git a/projector_installer/dialogs.py b/projector_installer/dialogs.py index 8b32285..4bda2c1 100644 --- a/projector_installer/dialogs.py +++ b/projector_installer/dialogs.py @@ -12,12 +12,18 @@ import click from .run_config import get_run_configs, RunConfig, get_run_config_names, get_used_projector_ports -from .apps import get_installed_apps, get_app_path, get_compatible_app_names, \ - is_path_to_app, is_toolbox_path - -from .global_config import DEF_PROJECTOR_PORT +from .apps import get_installed_apps, get_app_path, is_path_to_app, is_toolbox_path from .secure_config import generate_token from .utils import get_local_addresses +from .products import get_compatible_apps, IDEKind, Product + +DEF_PROJECTOR_PORT: int = 9999 + + +def get_compatible_app_names(kind: IDEKind, pattern: Optional[str] = None) -> List[Product]: + """Get sorted list of projector-compatible applications, matches given pattern.""" + res = get_compatible_apps(kind, pattern) + return sorted(res, key=lambda x: x.name) def display_run_configs_names(config_names: List[str]) -> None: @@ -39,17 +45,26 @@ def display_run_configs(run_configs: Dict[str, RunConfig]) -> None: display_run_configs_names(config_names) +def print_selection_list(names: List[str]) -> None: + """Pretty list for selection.""" + for i, name in enumerate(names): + print(f'\t{i + 1:4}. {name}') + + def find_apps(pattern: Optional[str] = None) -> None: """Pretty-print projector-compatible applications, matched to given pattern.""" - app_names = get_compatible_app_names(pattern) - for i, app_name in enumerate(app_names): - print(f'\t{i + 1:4}. {app_name}') + apps: List[Product] = [] + for kind in IDEKind: + apps = apps + get_compatible_app_names(kind, pattern) -def list_apps(pattern: Optional[str] = None) -> None: - """Pretty print the list of installed ide.""" - for i, app in enumerate(get_installed_apps(pattern)): - print(f'\t{i + 1:4}. {app}') + print_selection_list(list(map(lambda x: x.name, apps))) + + +def list_apps(pattern: Optional[str]) -> None: + """Print list of installed apps""" + apps = get_installed_apps(pattern) + print_selection_list(apps) def select_installed_app(pattern: Optional[str] = None) -> Optional[str]: @@ -57,7 +72,7 @@ def select_installed_app(pattern: Optional[str] = None) -> Optional[str]: apps: List[str] = get_installed_apps(pattern) while True: - list_apps(pattern) + print_selection_list(apps) prompt = f'Choose an IDE number to uninstall or 0 to exit: [0-{len(apps)}]' app_number: int = click.prompt(prompt, type=int) @@ -71,13 +86,39 @@ def select_installed_app(pattern: Optional[str] = None) -> Optional[str]: return apps[app_number - 1] -def select_compatible_app(pattern: Optional[str] = None) -> Optional[str]: +def select_ide_kind() -> Optional[IDEKind]: + """Interactively selects desired IDE kind""" + kinds = [k for k in IDEKind if k != IDEKind.Unknown] + kind_names = list(map(lambda x: x.name, kinds)) + prompt = f'Choose IDE type to install or 0 to exit: [0-{len(kind_names)}]' + + while True: + print_selection_list(kind_names) + pos: int = click.prompt(prompt, type=int) + + if pos < 0 or pos > len(kinds): + print('Invalid number.') + continue + + if pos == 0: + return None + + return kinds[pos - 1] + + +def select_compatible_app(pattern: Optional[str] = None) -> Optional[Product]: """Interactively selects app name from list of projector-compatible applications.""" - app_names: List[str] = get_compatible_app_names(pattern) + kind = select_ide_kind() + + if kind is None: + return None + + apps = get_compatible_app_names(kind, pattern) + app_names: List[str] = list(map(lambda x: x.name, apps)) + prompt = f'Choose IDE number to install or 0 to exit: [0-{len(app_names)}]' while True: - find_apps(pattern) - prompt = f'Choose IDE number to install or 0 to exit: [0-{len(app_names)}]' + print_selection_list(app_names) app_number: int = click.prompt(prompt, type=int) if app_number < 0 or app_number > len(app_names): @@ -87,7 +128,7 @@ def select_compatible_app(pattern: Optional[str] = None) -> Optional[str]: if app_number == 0: return None - return app_names[app_number - 1] + return apps[app_number - 1] def select_unused_config_name(hint: str) -> str: @@ -162,7 +203,7 @@ def select_installed_app_path() -> Optional[str]: apps = get_installed_apps() while True: - list_apps() + print_selection_list(apps) prompt = f'Choose IDE number to install or 0 to exit: [0-{len(apps)}]' app_number = click.prompt(prompt, type=int) diff --git a/projector_installer/global_config.py b/projector_installer/global_config.py index 2694d4a..072edbc 100644 --- a/projector_installer/global_config.py +++ b/projector_installer/global_config.py @@ -8,17 +8,13 @@ """ import sys -from typing import List from shutil import rmtree from os.path import dirname, join, expanduser, abspath -from .installable_app import InstallableApp, load_compatible_apps from .utils import create_dir_if_not_exist USER_HOME: str = expanduser('~') INSTALL_DIR: str = dirname(abspath(__file__)) -DEF_PROJECTOR_PORT: int = 9999 -COMPATIBLE_IDE_FILE: str = join(INSTALL_DIR, 'compatible_ide.json') DEF_CONFIG_DIR: str = '.projector' SSL_PROPERTIES_FILE = 'ssl.properties' BUNDLED_DIR: str = 'bundled' @@ -68,18 +64,6 @@ def get_ssl_dir() -> str: return join(config_dir, 'ssl') -COMPATIBLE_APPS: List[InstallableApp] = [] - - -def init_compatible_apps() -> List[InstallableApp]: - """Initializes compatible apps list.""" - try: - return load_compatible_apps(COMPATIBLE_IDE_FILE) - except IOError as error: - print(f'Cannot load compatible ide file: {str(error)}. Exiting...') - sys.exit(2) - - def get_projector_server_dir() -> str: """Returns directory with projector server jar""" return join(INSTALL_DIR, BUNDLED_DIR, SERVER_DIR) diff --git a/projector_installer/installable_app.py b/projector_installer/installable_app.py deleted file mode 100644 index 9d2b897..0000000 --- a/projector_installer/installable_app.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2000-2020 JetBrains s.r.o. -# Use of this source code is governed by the Apache 2.0 license that can be found -# in the LICENSE file. - -"""InstallableApp class and related stuff""" -import json -import socket -from os import remove -from os.path import join -from tempfile import gettempdir -from typing import List, Tuple, Any -from enum import Enum, auto -from urllib.error import URLError - -from .utils import download_file, get_file_name_from_url - -COMPATIBLE_IDE_FILE_URL: str = \ - 'https://raw.githubusercontent.com/JetBrains/projector-installer/master/' \ - 'projector_installer/compatible_ide.json' - - -class IDEKind(Enum): - """Known IDE kinds""" - Unknown = auto() - Idea_Community = auto() - Idea_Ultimate = auto() - PyCharm_Community = auto() - PyCharm_Professional = auto() - CLion = auto() - GoLand = auto() - DataGrip = auto() - PhpStorm = auto() - WebStorm = auto() - RubyMine = auto() - - -class InstallableApp: - """Installable application entry.""" - - def __init__(self, name: str, url: str, kind: IDEKind) -> None: - self.name: str = name - self.url: str = url - self.kind = kind - - def __key__(self) -> Tuple[str, str]: - return self.name, self.url - - def __eq__(self, other: object) -> bool: - if isinstance(other, InstallableApp): - return self.__key__() == other.__key__() - - return False - - def __hash__(self) -> int: - return hash(self.__key__()) - - -def _parse_entry(entry: Any) -> InstallableApp: - """Get InstallableApp from JSON array entry""" - - try: - kind = IDEKind[entry['kind']] - except KeyError: - kind = IDEKind.Unknown - - return InstallableApp(entry['name'], entry['url'], kind) - - -def load_installable_apps_from_file(file_name: str) -> List[InstallableApp]: - """Loads installable app list from json file.""" - with open(file_name, 'r') as file: - data = json.load(file) - - return [_parse_entry(entry) for entry in data] - - -def download_compatible_apps() -> str: - """Downloads compatible ide json file from github repository.""" - - try: - download_file(COMPATIBLE_IDE_FILE_URL, gettempdir(), timeout=3, silent=True) - name = get_file_name_from_url(COMPATIBLE_IDE_FILE_URL) - file_name = join(gettempdir(), name) - - return file_name - except (URLError, socket.timeout): - return '' - - -def load_compatible_apps_from_github() -> List[InstallableApp]: - """Loads compatible app list from github repo""" - file_name = download_compatible_apps() - res = load_installable_apps_from_file(file_name) if file_name else [] - remove(file_name) - return res - - -def load_compatible_apps(file_name: str) -> List[InstallableApp]: - """Loads from file and from github and merges results""" - local_list = load_installable_apps_from_file(file_name) - github_list = load_compatible_apps_from_github() - - return list(set(local_list) | set(github_list)) - -# # https://data.services.jetbrains.com/products?code=IIU%2CIIC&release.type=release -# PRODUCTS_URL = 'https://data.services.jetbrains.com/products' diff --git a/projector_installer/products.py b/projector_installer/products.py new file mode 100644 index 0000000..6b33aa3 --- /dev/null +++ b/projector_installer/products.py @@ -0,0 +1,199 @@ +# Copyright 2000-2020 JetBrains s.r.o. +# Use of this source code is governed by the Apache 2.0 license that can be found +# in the LICENSE file. + +"""Product class and related stuff""" +import json +import socket +import sys +from os import remove +from os.path import join, dirname, abspath +from tempfile import gettempdir +from typing import List, Tuple, Any, Optional +from enum import Enum, auto +from urllib.error import URLError +from distutils.version import LooseVersion + +from .utils import download_file, get_file_name_from_url, get_json + +COMPATIBLE_IDE_FILE: str = join(dirname(abspath(__file__)), 'compatible_ide.json') + +COMPATIBLE_IDE_FILE_URL: str = \ + 'https://raw.githubusercontent.com/JetBrains/projector-installer/master/' \ + 'projector_installer/compatible_ide.json' + + +class IDEKind(Enum): + """Known IDE kinds""" + Unknown = auto() + Idea_Community = auto() + Idea_Ultimate = auto() + PyCharm_Community = auto() + PyCharm_Professional = auto() + CLion = auto() + GoLand = auto() + DataGrip = auto() + PhpStorm = auto() + WebStorm = auto() + RubyMine = auto() + + +class Product: + """Installable application entry.""" + + def __init__(self, name: str, url: str, kind: IDEKind) -> None: + self.name: str = name + self.url: str = url + self.kind = kind + + def __key__(self) -> Tuple[str, str]: + return self.name, self.url + + def __eq__(self, other: object) -> bool: + if isinstance(other, Product): + return self.__key__() == other.__key__() + + return False + + def __hash__(self) -> int: + return hash(self.__key__()) + + def __str__(self) -> str: + return self.__repr__() + + def __repr__(self) -> str: + return f'Product({self.name}, {self.url}, {self.kind})' + + +COMPATIBLE_APPS: List[Product] = [] + + +def _parse_entry(entry: Any) -> Product: + """Get Product from JSON array entry""" + + try: + kind = IDEKind[entry['kind']] + except KeyError: + kind = IDEKind.Unknown + + return Product(entry['name'], entry['url'], kind) + + +def load_installable_apps_from_file(file_name: str) -> List[Product]: + """Loads installable app list from json file.""" + with open(file_name, 'r') as file: + data = json.load(file) + + return [_parse_entry(entry) for entry in data] + + +def download_compatible_apps() -> str: + """Downloads compatible ide json file from github repository.""" + + try: + download_file(COMPATIBLE_IDE_FILE_URL, gettempdir(), timeout=3, silent=True) + name = get_file_name_from_url(COMPATIBLE_IDE_FILE_URL) + file_name = join(gettempdir(), name) + + return file_name + except (URLError, socket.timeout): + return '' + + +def load_compatible_apps_from_github() -> List[Product]: + """Loads compatible app list from github repo""" + file_name = download_compatible_apps() + res = load_installable_apps_from_file(file_name) if file_name else [] + remove(file_name) + return res + + +def load_compatible_apps(file_name: str) -> List[Product]: + """Loads from file and from github and merges results""" + local_list = load_installable_apps_from_file(file_name) + github_list = load_compatible_apps_from_github() + + return list(set(local_list) | set(github_list)) + + +def init_compatible_apps() -> List[Product]: + """Initializes compatible apps list.""" + try: + return load_compatible_apps(COMPATIBLE_IDE_FILE) + except IOError as error: + print(f'Cannot load compatible ide file: {str(error)}. Exiting...') + sys.exit(2) + + +PRODUCTS_URL = 'https://data.services.jetbrains.com/products' + +KIND2CODE = { + IDEKind.Idea_Community: 'IIC', + IDEKind.Idea_Ultimate: 'IIU', + IDEKind.PyCharm_Community: 'PCC', + IDEKind.PyCharm_Professional: 'PCP', + IDEKind.CLion: 'CL', + IDEKind.GoLand: 'GO', + IDEKind.DataGrip: 'DG', + IDEKind.PhpStorm: 'PS', + IDEKind.WebStorm: 'WS', + IDEKind.RubyMine: 'RM', +} + +# All releases before this version considered as unsupported +EARLIEST_COMPATIBLE_VERSION = LooseVersion('2019.3') + + +def get_product_releases(kind: IDEKind) -> List[Product]: + """Retrieves list of product releases from JB products service""" + url = f'{PRODUCTS_URL}?code={KIND2CODE[kind]}&release.type=release' + data = get_json(url, timeout=1) + res = [] + + for entry in data: + name = entry['name'] + releases = entry['releases'] + + for release in releases: + ver = release['version'] + + if LooseVersion(ver) < EARLIEST_COMPATIBLE_VERSION: + continue + + downloads = release['downloads'] + + if 'linux' not in downloads: + continue + + link = downloads['linux']['link'] + res.append(Product(f'{name} {ver}', link, kind)) + + return res + + +def get_products(kind: IDEKind) -> List[Product]: + """Returns list of all compatible apps with given kind""" + global COMPATIBLE_APPS # pylint: disable=W0603 + if not COMPATIBLE_APPS: + COMPATIBLE_APPS = init_compatible_apps() + + return [app for app in COMPATIBLE_APPS if app.kind == kind] + + +def get_compatible_apps(kind: IDEKind, pattern: Optional[str] = None) -> List[Product]: + """Returns list of compatible apps, matched given pattern.""" + + apps = [app for app in get_products(kind) + if pattern is None or app.name.lower().find(pattern.lower()) != -1] + + if pattern: + for app in apps: + if pattern.lower() == app.name.lower(): + return [app] + + return apps + +# class ProductProvider: +# +# def get_product_list(self) -> List[Product]: +# pass