diff --git a/.circleci/config.yml b/.circleci/config.yml index e295b84..9cddc1a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,6 +28,29 @@ workflows: - test-installation-deb: requires: - build-deb + binary: + jobs: + - build-binary + - test-installation-binary: + requires: + - build-binary + - upload-binary: + name: upload-binary-staging + context: aws-staging + requires: + - test-installation-binary + - upload-binary: + name: upload-binary-production + context: aws + requires: + - upload-binary-staging + filters: + # Ignore any commit on any branch by default + branches: + ignore: /.*/ + # Run only when a tag is created + tags: + only: /^v.+\..+\..+/ deploy: jobs: - upload-to-pypi: @@ -61,7 +84,7 @@ jobs: executor: << parameters.os >> steps: - run: sudo apt-get update - - run: sudo apt-get install -y software-properties-common dpkg-dev devscripts equivs + - run: sudo apt-get install -y software-properties-common dpkg-dev devscripts equivs python3-pip - run: sudo add-apt-repository ppa:jyrki-pulliainen/dh-virtualenv - run: sudo apt-get install -y dh-virtualenv - install-poetry @@ -112,7 +135,7 @@ jobs: image: ubuntu-2004:202010-01 steps: - run: sudo apt-get update - - run: sudo apt-get install python3 python3-dev curl git -y + - run: sudo apt-get -y install python3-pip python3-dev curl git - checkout - install-poetry - run: sudo $(which poetry) install @@ -122,10 +145,54 @@ jobs: - assert-core-responds-to-http - run: sudo $(which poetry) run brainframe compose down + build-binary: + docker: + # CentOS 7 is the oldest Linux distribution we unofficially support. + # Building on here allows us to link to a very old version of libc, + # which will work on all more modern distributions. + - image: centos:7 + environment: + PYTHONIOENCODING: utf8 + steps: + - checkout + - run: yum -y install python3 + - install-poetry + - run: poetry install --no-root + - run: PYTHONPATH=$PYTHONPATH:. poetry run pyinstaller package/main.spec + - store_artifacts: + path: dist + - persist_to_workspace: + root: . + paths: + - dist/* + + upload-binary: + docker: + - image: cimg/python:3.6 + steps: + - attach_workspace: + at: /tmp/workspace + - checkout + - run: poetry install + - run: poetry run python package/upload_binary.py --binary-path /tmp/workspace/dist/brainframe + + test-installation-binary: + machine: + image: ubuntu-2004:202010-01 + steps: + - run: sudo apt-get update + - attach_workspace: + at: /tmp/workspace + - run: sudo cp /tmp/workspace/dist/brainframe /usr/local/bin/brainframe + - run: sudo brainframe install --noninteractive + - run: sudo brainframe compose up -d + - assert-core-container-running + - assert-core-responds-to-http + - run: sudo brainframe compose down + commands: install-poetry: steps: - - run: sudo apt-get update && sudo apt-get install -y python3-pip - run: > curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 - --version << pipeline.parameters.poetry-version >> diff --git a/.gitignore b/.gitignore index d511d9e..ac2cc01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ +# Tooling .mypy_cache/ -dist/ + +# IDEs .idea/ + +# Python __pycache__ *.pyc *.egg-info/ + +# PyInstaller +dist/ +build/ diff --git a/brainframe/cli/__init__.py b/brainframe/cli/__init__.py new file mode 100644 index 0000000..d3ec452 --- /dev/null +++ b/brainframe/cli/__init__.py @@ -0,0 +1 @@ +__version__ = "0.2.0" diff --git a/brainframe/cli/commands/__init__.py b/brainframe/cli/commands/__init__.py index 971e588..1fa0d36 100644 --- a/brainframe/cli/commands/__init__.py +++ b/brainframe/cli/commands/__init__.py @@ -2,5 +2,6 @@ from .compose import compose from .info import info from .install import install +from .self_update import self_update from .update import update from .utils import by_name diff --git a/brainframe/cli/commands/install.py b/brainframe/cli/commands/install.py index 15100ee..c42f496 100644 --- a/brainframe/cli/commands/install.py +++ b/brainframe/cli/commands/install.py @@ -1,3 +1,4 @@ +import shutil from argparse import ArgumentParser from pathlib import Path @@ -6,6 +7,7 @@ config, dependencies, docker_compose, + frozen_utils, os_utils, print_utils, ) @@ -30,10 +32,14 @@ def install(): print() # Check all dependencies - dependencies.curl.ensure(args.noninteractive, args.install_curl) dependencies.docker.ensure(args.noninteractive, args.install_docker) + # We only require the Docker Compose command in frozen distributions + if frozen_utils.is_frozen() and shutil.which("docker-compose") is None: + print_utils.fail_translate( + "install.install-dependency-manually", dependency="docker-compose", + ) - _, _, download_version = docker_compose.check_download_version() + download_version = docker_compose.get_latest_version() print_utils.translate("install.install-version", version=download_version) if not os_utils.added_to_group("docker"): @@ -123,8 +129,8 @@ def install(): print_utils.translate("install.set-custom-directory-env-vars") print( f"\n" - f'export {config.install_path.name}="{install_path}"\n' - f'export {config.data_path.name}="{data_path}"\n' + f'export {config.install_path.env_var_name}="{install_path}"\n' + f'export {config.data_path.env_var_name}="{data_path}"\n' ) @@ -156,11 +162,6 @@ def _parse_args(): action="store_true", help=i18n.t("install.install-docker-help"), ) - parser.add_argument( - "--install-curl", - action="store_true", - help=i18n.t("install.install-curl-help"), - ) parser.add_argument( "--add-to-group", action="store_true", diff --git a/brainframe/cli/commands/self_update.py b/brainframe/cli/commands/self_update.py new file mode 100644 index 0000000..526bd38 --- /dev/null +++ b/brainframe/cli/commands/self_update.py @@ -0,0 +1,117 @@ +import os +import shutil +import stat +import sys +from argparse import ArgumentParser +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import Optional, Tuple, Union + +import i18n +import requests +from brainframe.cli import __version__, config, frozen_utils, print_utils +from packaging import version + +from .utils import command + +_RELEASES_URL_PREFIX = "https://{subdomain}aotu.ai" +_BINARY_URL = "{prefix}/releases/brainframe-cli/brainframe" +_LATEST_TAG_URL = "{prefix}/releases/brainframe-cli/latest" + + +@command("self-update") +def self_update(): + _parse_args() + + if not frozen_utils.is_frozen(): + print_utils.fail_translate("self-update.not-frozen") + + executable_path = Path(sys.executable) + + # Check if the user has permissions to overwrite the executable in its + # current location + if not os.access(executable_path, os.W_OK): + error_message = i18n.t( + "general.file-bad-write-permissions", path=executable_path + ) + error_message += "\n" + error_message += i18n.t("general.retry-as-root") + print_utils.fail(error_message) + + credentials = config.staging_credentials() + + prefix = _RELEASES_URL_PREFIX.format( + subdomain="staging." if config.is_staging.value else "" + ) + binary_url = _BINARY_URL.format(prefix=prefix) + + current_version = version.parse(__version__) + latest_version = _latest_version(prefix, credentials) + + if current_version >= latest_version: + print_utils.fail_translate( + "self-update.already-up-to-date", + current_version=current_version, + latest_version=latest_version, + ) + + # Get the updated executable + print_utils.translate("self-update.downloading") + response = requests.get(binary_url, auth=credentials, stream=True) + if not response.ok: + print_utils.fail_translate( + "self-update.error-downloading", + status_code=response.status_code, + error_message=response.text, + ) + + with NamedTemporaryFile("wb") as new_executable: + for block in response.iter_content(_BLOCK_SIZE): + new_executable.write(block) + new_executable.flush() + + # Set the result as executable + current_stat = executable_path.stat() + executable_path.chmod( + current_stat.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + + # Overwrite the existing executable with the new one + # Really excited for copy3, coming this summer + shutil.copy2(new_executable.name, executable_path) + + print() + print_utils.translate( + "self-update.complete", color=print_utils.Color.GREEN + ) + + +def _latest_version( + url_prefix: str, credentials: Optional[Tuple[str, str]], +) -> Union[version.LegacyVersion, version.Version]: + latest_tag_url = _LATEST_TAG_URL.format(prefix=url_prefix) + response = requests.get(latest_tag_url, auth=credentials) + + if not response.ok: + print_utils.fail_translate( + "self-update.error-getting-latest-version", + status_code=response.status_code, + error_message=response.text, + ) + + return version.parse(response.text) + + +def _parse_args(): + parser = ArgumentParser( + description=i18n.t("self-update.description"), + usage=i18n.t("self-update.usage"), + ) + + return parser.parse_args(sys.argv[2:]) + + +_BLOCK_SIZE = 1024000 +"""The block size to read files at. Chosen from this answer: +https://stackoverflow.com/a/3673731 +""" diff --git a/brainframe/cli/commands/update.py b/brainframe/cli/commands/update.py index 97f8ff2..bf3e7f3 100644 --- a/brainframe/cli/commands/update.py +++ b/brainframe/cli/commands/update.py @@ -16,7 +16,9 @@ def update(): docker_compose.assert_installed(install_path) if args.version == "latest": - _, _, requested_version_str = docker_compose.check_download_version() + requested_version_str = docker_compose.check_existing_version( + install_path + ) else: requested_version_str = args.version diff --git a/brainframe/cli/config.py b/brainframe/cli/config.py index d02e7ad..7bc66a9 100644 --- a/brainframe/cli/config.py +++ b/brainframe/cli/config.py @@ -1,14 +1,11 @@ import os from distutils.util import strtobool from pathlib import Path -from typing import Callable, Dict, Generic, Optional, TypeVar, Union +from typing import Callable, Dict, Generic, Optional, Tuple, TypeVar, Union import yaml -from . import print_utils - -_DEFAULTS_FILE_PATH = Path(__file__).parent / "defaults.yaml" - +from . import frozen_utils, print_utils T = TypeVar("T") @@ -55,13 +52,7 @@ def load( def load() -> None: """Initializes configuration options""" - if not _DEFAULTS_FILE_PATH.is_file(): - print_utils.fail_translate( - "general.missing-defaults-file", - defaults_file_path=_DEFAULTS_FILE_PATH, - ) - - with _DEFAULTS_FILE_PATH.open("r") as defaults_file: + with frozen_utils.DEFAULTS_FILE_PATH.open("r") as defaults_file: defaults = yaml.load(defaults_file, Loader=yaml.FullLoader) install_path.load(Path, defaults) @@ -72,6 +63,24 @@ def load() -> None: staging_password.load(str, defaults) +def staging_credentials() -> Optional[Tuple[str, str]]: + if not is_staging.value: + return None + + username = staging_username.value + password = staging_password.value + if username is None or password is None: + print_utils.fail_translate( + "general.staging-missing-credentials", + username_env_var=staging_username.env_var_name, + password_env_var=staging_password.env_var_name, + ) + + # Mypy doesn't understand that fail_translate exits this function, so it + # thinks the return type should be Tuple[Optional[str], Optional[str]] + return username, password # type: ignore + + def _bool_converter(value: Union[str, bool]) -> bool: if isinstance(value, bool): return value diff --git a/brainframe/cli/dependencies.py b/brainframe/cli/dependencies.py index f40962d..d78463a 100644 --- a/brainframe/cli/dependencies.py +++ b/brainframe/cli/dependencies.py @@ -1,4 +1,7 @@ import shutil +from tempfile import NamedTemporaryFile + +import requests from . import os_utils, print_utils @@ -55,10 +58,12 @@ def ensure(self, noninteractive: bool, install_requested: bool): def _install_docker(): - os_utils.run( - ["curl", "-fsSL", "https://get.docker.com", "-o", "/tmp/get-docker.sh"] - ) - os_utils.run(["sh", "/tmp/get-docker.sh"]) + response = requests.get("https://get.docker.com") + + with NamedTemporaryFile("w") as get_docker_script: + get_docker_script.write(response.text) + get_docker_script.flush() + os_utils.run(["sh", get_docker_script.name]) docker = Dependency("docker", "install.ask-install-docker", _install_docker,) @@ -68,9 +73,3 @@ def _install_docker(): "install.ask-install-rsync", lambda: os_utils.run(["apt-get", "install", "-y", "rsync"]), ) - -curl = Dependency( - "curl", - "install.ask-install-curl", - lambda: os_utils.run(["apt-get", "install", "-y", "curl"]), -) diff --git a/brainframe/cli/docker_compose.py b/brainframe/cli/docker_compose.py index e7fcc00..790abd4 100644 --- a/brainframe/cli/docker_compose.py +++ b/brainframe/cli/docker_compose.py @@ -1,13 +1,13 @@ import os -import subprocess import sys from pathlib import Path -from typing import List, TextIO, Tuple, cast +from typing import List, Optional, Tuple import i18n +import requests import yaml -from . import config, os_utils, print_utils +from . import config, frozen_utils, os_utils, print_utils # The URL to the docker-compose.yml BRAINFRAME_DOCKER_COMPOSE_URL = "https://{subdomain}aotu.ai/releases/brainframe/{version}/docker-compose.yml" @@ -33,10 +33,19 @@ def run(install_path: Path, commands: List[str]) -> None: compose_path = install_path / "docker-compose.yml" - full_command = [ - sys.executable, - "-m", - "compose", + if frozen_utils.is_frozen(): + # Rely on the system's Docker Compose, since Compose can't be easily embedded + # into a PyInstaller executable + full_command = ["docker-compose"] + else: + # Use the included Docker Compose + full_command = [ + sys.executable, + "-m", + "compose", + ] + + full_command += [ "--file", str(compose_path), ] @@ -57,14 +66,24 @@ def run(install_path: Path, commands: List[str]) -> None: def download(target: Path, version: str = "latest") -> None: _assert_has_write_permissions(target.parent) - subdomain, auth_flags, version = check_download_version(version=version) + if version == "latest": + version = get_latest_version() + + credentials = config.staging_credentials() url = BRAINFRAME_DOCKER_COMPOSE_URL.format( - subdomain=subdomain, version=version - ) - os_utils.run( - ["curl", "-o", str(target), "--fail", "--location", url] + auth_flags, + subdomain="staging." if config.is_staging.value else "", + version=version, ) + response = requests.get(url, auth=credentials, stream=True) + if not response.ok: + print_utils.fail_translate( + "general.error-downloading-docker-compose", + status_code=response.status_code, + error_message=response.text, + ) + + target.write_text(response.text) if os_utils.is_root(): # Fix the permissions of the docker-compose.yml so that the BrainFrame @@ -72,43 +91,19 @@ def download(target: Path, version: str = "latest") -> None: os_utils.give_brainframe_group_rw_access([target]) -def check_download_version( - version: str = "latest", -) -> Tuple[str, List[str], str]: - subdomain = "" - auth_flags = [] - +def get_latest_version() -> str: + """ + :return: The latest available version in the format "vX.Y.Z" + """ # Add the flags to authenticate with staging if the user wants to download # from there - if config.is_staging.value: - subdomain = "staging." - - username = config.staging_username.value - password = config.staging_password.value - if username is None or password is None: - print_utils.fail_translate( - "general.staging-missing-credentials", - username_env_var=config.staging_username.name, - password_env_var=config.staging_password.name, - ) - - auth_flags = ["--user", f"{username}:{password}"] - - if version == "latest": - # Check what the latest version is - url = BRAINFRAME_LATEST_TAG_URL.format(subdomain=subdomain) - result = os_utils.run( - ["curl", "--fail", "-s", "--location", url] + auth_flags, - print_command=False, - stdout=subprocess.PIPE, - encoding="utf-8", - ) - # stdout is a file-like object opened in text mode when the encoding - # argument is "utf-8" - stdout = cast(TextIO, result.stdout) - version = stdout.readline().strip() + subdomain = "staging." if config.is_staging.value else "" + credentials = config.staging_credentials() - return subdomain, auth_flags, version + # Check what the latest version is + url = BRAINFRAME_LATEST_TAG_URL.format(subdomain=subdomain) + response = requests.get(url, auth=credentials) + return response.text def check_existing_version(install_path: Path) -> str: diff --git a/brainframe/cli/frozen_utils.py b/brainframe/cli/frozen_utils.py new file mode 100644 index 0000000..186bacd --- /dev/null +++ b/brainframe/cli/frozen_utils.py @@ -0,0 +1,47 @@ +import sys +from pathlib import Path +from typing import Union + + +def is_frozen() -> bool: + """ + :return: True if the application is running as a frozen executable + """ + return getattr(sys, "frozen", False) + + +def _get_absolute_path(*args: Union[str, Path]) -> Path: + """Gets the absolute path of a resource. + + :param args: Arguments to pass to the Path constructor + :return: The absolute path of the resource + """ + if is_frozen(): + path = Path(_pyinstaller_tmp_path(), *args) + + if not path.exists(): + raise RuntimeError( + f"Missing resource in PyInstaller bundle: {args}" + ) + + return path + else: + for parent in Path(__file__).absolute().parents: + path = Path(parent, *args) + if path.exists(): + return path + + raise RuntimeError( + f"Could not find the absolute path for resource: {args}" + ) + + +def _pyinstaller_tmp_path() -> Path: + # _MEIPASS is an attribute defined by PyInstaller at runtime + return Path(sys._MEIPASS) # type: ignore[attr-defined] + + +RELATIVE_TRANSLATIONS_PATH = Path("brainframe/cli/translations") +TRANSLATIONS_PATH = _get_absolute_path(RELATIVE_TRANSLATIONS_PATH) +RELATIVE_DEFAULTS_FILE_PATH = Path("brainframe/cli/defaults.yaml") +DEFAULTS_FILE_PATH = _get_absolute_path(RELATIVE_DEFAULTS_FILE_PATH) diff --git a/brainframe/cli/main.py b/brainframe/cli/main.py index 0743cf8..ffdd3bd 100644 --- a/brainframe/cli/main.py +++ b/brainframe/cli/main.py @@ -9,14 +9,14 @@ from brainframe.cli import ( commands, config, + frozen_utils, os_utils, print_utils, - translations, ) def main(): - i18n.load_path.append(str(translations.PATH)) + i18n.load_path.append(str(frozen_utils.TRANSLATIONS_PATH)) parser = ArgumentParser( description=i18n.t("portal.description"), usage=i18n.t("portal.usage") diff --git a/brainframe/cli/print_utils.py b/brainframe/cli/print_utils.py index d478242..a271241 100644 --- a/brainframe/cli/print_utils.py +++ b/brainframe/cli/print_utils.py @@ -3,6 +3,7 @@ import sys from enum import Enum from pathlib import Path +from typing import NoReturn import i18n @@ -72,11 +73,11 @@ def warning_translate(message_id, color=Color.YELLOW, **kwargs): print_color(i18n.t(message_id, **kwargs), color) -def fail_translate(message_id, **kwargs): +def fail_translate(message_id, **kwargs) -> NoReturn: fail(i18n.t(message_id, **kwargs)) -def fail(message, **kwargs): +def fail(message, **kwargs) -> NoReturn: print_color(message, Color.RED, file=sys.stderr, **kwargs) sys.exit(1) diff --git a/brainframe/cli/translations/__init__.py b/brainframe/cli/translations/__init__.py deleted file mode 100644 index b509612..0000000 --- a/brainframe/cli/translations/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from pathlib import Path - -PATH = Path(__file__).parent diff --git a/brainframe/cli/translations/general.en.yml b/brainframe/cli/translations/general.en.yml index 97235f8..b75c8f4 100644 --- a/brainframe/cli/translations/general.en.yml +++ b/brainframe/cli/translations/general.en.yml @@ -4,6 +4,8 @@ en: invalid-yes-no-input: "Please enter either a \"y\" or an \"n\"." default: "Default" downloading-docker-compose: "Downloading docker-compose.yml..." + error-downloading-docker-compose: "Error while downloading + docker-compose.yml: HTTP %{status_code} - %{error_message}" noninteractive-help: "Prevents this command from prompting for input and pulls configuration from flags instead. This can be useful for scripts." adding-to-group: "Adding user to the %{group} group" @@ -21,6 +23,7 @@ en: restart-for-group-access: "Your user has been added to the \"%{group}\" group but you must restart to apply this change. Alternatively, you can try again as root." + retry-as-root: "Please try again as root." retry-as-root-or-group: "Please try again as root or consider adding your user to the \"%{group}\" group." file-bad-write-permissions: "Your user does not have write access to @@ -28,6 +31,5 @@ en: unexpected-group-for-file: "File \"%{path}\" is not owned by the \"%{group}\" group, but probably should be. Please add this file to the group or try again as root." - missing-defaults-file: "This distribution is missing a defaults file, - expected at \"%{defaults_file_path}\". Please try re-installing the latest - version of the BrainFrame CLI and trying again." + missing-defaults-file: "This distribution is missing a defaults file. Please + re-install the latest version of the BrainFrame CLI and try again." diff --git a/brainframe/cli/translations/install.en.yml b/brainframe/cli/translations/install.en.yml index d09f618..1faf0ea 100644 --- a/brainframe/cli/translations/install.en.yml +++ b/brainframe/cli/translations/install.en.yml @@ -22,8 +22,6 @@ en: data-path-help: "The path where BrainFrame will write data to at runtime" install-docker-help: "If provided, Docker will be automatically installed if it is not present. This is only available for supported operating systems." - install-curl-help: "If provided, curl will be automatically installed if not - present. This is only available for supported operating systems." add-to-group-help: "If provided, the current user will be added to the \"brainframe\" group" add-to-docker-group-help: "If provided, the current user will be added to the @@ -62,10 +60,6 @@ en: through the script at get.docker.com. Would you like to do this?" get-docker-script-failed: "Failed to download the Docker installation script: {error}" - ask-install-docker-compose: "This script can attempt to install Docker - Compose for you using pip. Would you like to do this?" - ask-install-curl: "This script can install curl for you through apt. Would - you like to do this?" ask-docker-compose-path: "Where should Docker Compose be installed to?" get-docker-compose-failed: "Failed to download Docker Compose: {error}" unsupported-os: "Warning: You are using an unsupported operating system. diff --git a/brainframe/cli/translations/self-update.en.yml b/brainframe/cli/translations/self-update.en.yml new file mode 100644 index 0000000..ae61e09 --- /dev/null +++ b/brainframe/cli/translations/self-update.en.yml @@ -0,0 +1,17 @@ +en: + description: "Updates the BrainFrame CLI to a new version" + usage: "brainframe self-update" + not-frozen: "The self-update command is only used for binary installations. + If you installed the BrainFrame CLI through a package manager, use its update + mechanism instead." + + downloading: "Downloading new version..." + error-getting-latest-version: "Error while checking the latest CLI version: + HTTP %{status_code} - %{error_message}" + error-downloading: "Error while downloading the new version: HTTP + %{status_code} - %{error_message}" + + already-up-to-date: "The CLI is already up-to-date. (Current version: + %{current_version}, latest version: %{latest_version})" + + complete: "The BrainFrame CLI has been updated!" diff --git a/brainframe/cli/translations/update.en.yml b/brainframe/cli/translations/update.en.yml index c8537d3..ce30629 100644 --- a/brainframe/cli/translations/update.en.yml +++ b/brainframe/cli/translations/update.en.yml @@ -13,6 +13,11 @@ en: complete: "BrainFrame has been updated!" upgrade-version: "BrainFrame:%{existing_version} detected under your installation location, we will replace it with BrainFrame:%{requested_version}" + version-failing: "You have BrainFrame:%{existing_version} installed already. + The latest available version is BrainFrame:%{upgrade_version}. We don't + recommend downgrading BrainFrame for data integrity reasons. This request + will be ignored. You can force a downgrade by also supplying the --downgrade + flag." version-already-installed: "BrainFrame:%{existing_version} is already installed. This request will be ignored. You can force this release to be re-downloaded using the --force flag." @@ -20,7 +25,3 @@ en: than the currently installed version, BrainFrame:%{existing_version}. We don't recommend downgrading BrainFrame for data integrity reasons. This request will be ignored. You can force a downgrade using the --force flag." - ask-force-downgrade: "BrainFrame:%{requested_version} does not appear to - be a newer version then the current version, BrainFrame:%{existing_version}. - Downgrading BrainFrame is not officially supported and may lead to data loss. - Are you sure you want to continue?" diff --git a/package/debian/control b/package/debian/control index cb46005..ffad132 100644 --- a/package/debian/control +++ b/package/debian/control @@ -8,5 +8,5 @@ Standards-Version: 3.9.5 Package: brainframe-cli Architecture: any Pre-Depends: -Depends: python3-distutils, rsync, curl +Depends: python3-distutils, rsync Description: A CLI that makes installing and managing a BrainFrame server easy diff --git a/package/main.spec b/package/main.spec new file mode 100644 index 0000000..d7dc577 --- /dev/null +++ b/package/main.spec @@ -0,0 +1,50 @@ +# -*- mode: python ; coding: utf-8 -*- + +from pathlib import Path + +from brainframe.cli.frozen_utils import RELATIVE_TRANSLATIONS_PATH, RELATIVE_DEFAULTS_FILE_PATH + +if not Path("brainframe").is_dir(): + raise RuntimeError("PyInstaller must be run from the root of the project") + +# Package data files in with the executable +data_files = [ + (str(RELATIVE_DEFAULTS_FILE_PATH.absolute()), + str(RELATIVE_DEFAULTS_FILE_PATH.parent)), +] +for file_ in RELATIVE_TRANSLATIONS_PATH.glob("*.yml"): + data_files.append((str(file_.absolute()), str(file_.parent))) + +a = Analysis( + ["../brainframe/cli/main.py"], + pathex=[".."], + binaries=[], + datas=data_files, + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=None, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=None) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name="brainframe", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, +) diff --git a/package/upload_binary.py b/package/upload_binary.py new file mode 100644 index 0000000..490b614 --- /dev/null +++ b/package/upload_binary.py @@ -0,0 +1,87 @@ +"""Uploads the PyInstaller binary to our S3 bucket for storing releases, then creates +a CloudFront invalidation to make those changes live. +""" + +import argparse +import os +import uuid +from io import BytesIO +from pathlib import Path + +import boto3 +from brainframe.cli import __version__ +from brainframe.cli.print_utils import fail + +ssm = boto3.client("ssm") +s3 = boto3.client("s3") +cloudfront = boto3.client("cloudfront") + + +_COMPANY_NAMES = ["aotu", "dilililabs"] + + +def main(): + stage = os.environ.get("STAGE", "dev") + + args = _parse_args() + + if not args.binary_path.exists(): + fail(f"Missing binary at '{args.binary_path}'. Has a build been run?") + + bucket_name = _get_parameter( + f"/content-delivery/bucket/releases/{stage}/bucket-name" + ) + + # Upload the binary + s3.upload_file( + Filename=str(args.binary_path), + Bucket=bucket_name, + Key="releases/brainframe-cli/brainframe", + ) + + # Upload a latest tag, containing the latest version number + version_tag_file = BytesIO(__version__.encode("utf-8")) + s3.upload_fileobj( + Fileobj=version_tag_file, + Bucket=bucket_name, + Key="releases/brainframe-cli/latest", + ) + + for company_name in _COMPANY_NAMES: + distribution_id = _get_parameter( + f"/content-delivery/cdn/{company_name}/{stage}/distribution-id" + ) + cloudfront.create_invalidation( + DistributionId=distribution_id, + InvalidationBatch={ + "Paths": { + "Quantity": 1, + "Items": ["/releases/brainframe-cli/*"], + }, + "CallerReference": str(uuid.uuid4()), + }, + ) + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Uploads the PyInstaller binary to the releases bucket and makes " + "the change live" + ) + parser.add_argument( + "--binary-path", + help="A path to the binary to upload", + type=Path, + default=Path("dist/brainframe"), + ) + + return parser.parse_args() + + +def _get_parameter(name: str) -> str: + response = ssm.get_parameter(Name=name) + return response["Parameter"]["Value"] + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index abbf466..c225324 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +name = "altgraph" +version = "0.17" +description = "Python graph (network) package" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "appdirs" version = "1.4.4" @@ -56,6 +64,35 @@ typed-ast = ">=1.4.0" [package.extras] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +[[package]] +name = "boto3" +version = "1.17.83" +description = "The AWS SDK for Python" +category = "dev" +optional = false +python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +botocore = ">=1.20.83,<1.21.0" +jmespath = ">=0.7.1,<1.0.0" +s3transfer = ">=0.4.0,<0.5.0" + +[[package]] +name = "botocore" +version = "1.20.83" +description = "Low-level, data-driven core of boto 3." +category = "dev" +optional = false +python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +jmespath = ">=0.7.1,<1.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.11.15)"] + [[package]] name = "cached-property" version = "1.5.2" @@ -66,7 +103,7 @@ python-versions = "*" [[package]] name = "certifi" -version = "2020.12.5" +version = "2020.6.20" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -85,11 +122,11 @@ pycparser = "*" [[package]] name = "chardet" -version = "4.0.0" +version = "3.0.4" description = "Universal encoding detector for Python 2 and 3" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "*" [[package]] name = "click" @@ -154,7 +191,7 @@ tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] [[package]] name = "docker-compose" -version = "1.29.1" +version = "1.29.2" description = "Multi-container orchestration for Docker" category = "main" optional = false @@ -197,6 +234,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "idna" version = "2.10" @@ -234,6 +279,14 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +[[package]] +name = "jmespath" +version = "0.10.0" +description = "JSON Matching Expressions" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "jsonschema" version = "3.2.0" @@ -252,6 +305,17 @@ six = ">=1.11.0" format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +[[package]] +name = "macholib" +version = "1.14" +description = "Mach-O header analysis and editing" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +altgraph = ">=0.15" + [[package]] name = "mypy" version = "0.790" @@ -315,6 +379,17 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pefile" +version = "2019.4.18" +description = "Python PE parsing module" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +future = "*" + [[package]] name = "pycparser" version = "2.20" @@ -323,6 +398,33 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyinstaller" +version = "4.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2020.6" +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} + +[package.extras] +encryption = ["tinyaes (>=1.0.0)"] +hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2020.9" +description = "Community maintained hooks for PyInstaller" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pynacl" version = "1.4.0" @@ -355,6 +457,17 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "0.17.1" @@ -385,6 +498,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "5.3.1" @@ -403,7 +524,7 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.1" +version = "2.24.0" description = "Python HTTP for Humans." category = "main" optional = false @@ -411,14 +532,28 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" +chardet = ">=3.0.2,<4" idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +[[package]] +name = "s3transfer" +version = "0.4.2" +description = "An Amazon S3 Transfer Manager" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + [[package]] name = "six" version = "1.15.0" @@ -461,20 +596,20 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.4" +version = "1.25.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] +brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "websocket-client" -version = "0.58.0" +version = "0.59.0" description = "WebSocket client for Python with low level API options" category = "main" optional = false @@ -498,9 +633,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = ">=3.6,<4.0" -content-hash = "521e77aa15f42ec0bba3cce7d1b8238eaad99e5e27ce72f71f3b755f6e7843fb" +content-hash = "71976d976f0a0188964678b387b58727afa173cc0eec23d7e83365f001c66d8a" [metadata.files] +altgraph = [ + {file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"}, + {file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -522,13 +661,21 @@ black = [ {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, ] +boto3 = [ + {file = "boto3-1.17.83-py2.py3-none-any.whl", hash = "sha256:40ccb6ec2d7e5e4d250d630a245aae7aa1fcd43c3519e9808b444f083ca4014a"}, + {file = "boto3-1.17.83.tar.gz", hash = "sha256:e6fa13cd8f16a6c222104ab17e0439e24b6974f60e7af113a38a80f252457cb0"}, +] +botocore = [ + {file = "botocore-1.20.83-py2.py3-none-any.whl", hash = "sha256:b36c14cfe208969ee9f658b645cfc718c1700c593313787a3fd59b335f7a6e2c"}, + {file = "botocore-1.20.83.tar.gz", hash = "sha256:55d450b6bf0df642809fe88a6840d90dab6b6ad5ff3dccaa1faf9e085dfd864a"}, +] cached-property = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, ] cffi = [ {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, @@ -570,8 +717,8 @@ cffi = [ {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, ] chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -603,8 +750,8 @@ docker = [ {file = "docker-5.0.0.tar.gz", hash = "sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5"}, ] docker-compose = [ - {file = "docker-compose-1.29.1.tar.gz", hash = "sha256:d2064934f5084db8a0c4805e226447bf1fd0c928419be95afb6bd1866838c1f1"}, - {file = "docker_compose-1.29.1-py2.py3-none-any.whl", hash = "sha256:6510302727fe20faff5177f493b0c9897c73132539eaf55709a195c914c1a012"}, + {file = "docker-compose-1.29.2.tar.gz", hash = "sha256:4c8cd9d21d237412793d18bd33110049ee9af8dab3fe2c213bbd0733959b09b7"}, + {file = "docker_compose-1.29.2-py2.py3-none-any.whl", hash = "sha256:8d5589373b35c8d3b1c8c1182c6e4a4ff14bffa3dd0b605fcd08f73c94cef809"}, ] dockerpty = [ {file = "dockerpty-0.4.1.tar.gz", hash = "sha256:69a9d69d573a0daa31bcd1c0774eeed5c15c295fe719c61aca550ed1393156ce"}, @@ -612,6 +759,9 @@ dockerpty = [ docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, @@ -624,10 +774,18 @@ isort = [ {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, ] +jmespath = [ + {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, + {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, +] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, ] +macholib = [ + {file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"}, + {file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"}, +] mypy = [ {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, @@ -660,10 +818,20 @@ pathspec = [ {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, ] +pefile = [ + {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, +] pycparser = [ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] +pyinstaller = [ + {file = "pyinstaller-4.0.tar.gz", hash = "sha256:970beb07115761d5e4ec317c1351b712fd90ae7f23994db914c633281f99bab0"}, +] +pyinstaller-hooks-contrib = [ + {file = "pyinstaller-hooks-contrib-2020.9.tar.gz", hash = "sha256:a5fd45a920012802e3f2089e1d3501ef2f49265dfea8fc46c3310f18e3326c91"}, + {file = "pyinstaller_hooks_contrib-2020.9-py2.py3-none-any.whl", hash = "sha256:c382f3ac1a42b45cfecd581475c36db77da90e479b2f5bcb6d840d21fa545114"}, +] pynacl = [ {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, @@ -691,6 +859,10 @@ pyparsing = [ pyrsistent = [ {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, ] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] python-dotenv = [ {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, @@ -713,6 +885,10 @@ pywin32 = [ {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, ] +pywin32-ctypes = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, @@ -756,8 +932,12 @@ regex = [ {file = "regex-2020.10.23.tar.gz", hash = "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +s3transfer = [ + {file = "s3transfer-0.4.2-py2.py3-none-any.whl", hash = "sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc"}, + {file = "s3transfer-0.4.2.tar.gz", hash = "sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, @@ -803,12 +983,12 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] urllib3 = [ - {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, - {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, + {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, + {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, ] websocket-client = [ - {file = "websocket_client-0.58.0-py2.py3-none-any.whl", hash = "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663"}, - {file = "websocket_client-0.58.0.tar.gz", hash = "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"}, + {file = "websocket-client-0.59.0.tar.gz", hash = "sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c"}, + {file = "websocket_client-0.59.0-py2.py3-none-any.whl", hash = "sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32"}, ] zipp = [ {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, diff --git a/pyproject.toml b/pyproject.toml index 4e1c851..f23a4cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "brainframe-cli" version = "0.2.0" license = "BSD-3-Clause" -authors = ["Aotu "] +authors = ["Aotu.ai "] description = "Makes installing and managing a BrainFrame server easy" readme = "README.rst" @@ -21,12 +21,15 @@ python-i18n = "^0.3" pyyaml = "^5.3" distro = "^1.5" packaging = "^20.4" +requests = "^2.24.0" docker-compose = "^1.29.1" [tool.poetry.dev-dependencies] black = "^19.10b0" mypy = "*" +pyinstaller = "^4.0" isort = "^5.6" +boto3 = "^1.17.83" [tool.poetry.scripts] brainframe = "brainframe.cli.main:main"