From 6719d85807dd372ed7d0b0403b792e29664a163e Mon Sep 17 00:00:00 2001 From: Bracey Summers Date: Sun, 17 Sep 2023 11:42:14 -0500 Subject: [PATCH] APP-4171 - [DEPS] Updated deps command to add a link to lib_latest for current Python version for App Builder APP-4172 - [CLI] Minor enhancement to output of multiple commands APP-4773 - [SUBMODULE] Minor update to config submodule --- release_notes.md | 10 +++- tcex_cli/__metadata__.py | 2 +- tcex_cli/app/config | 2 +- tcex_cli/cli/deploy/deploy_cli.py | 5 +- tcex_cli/cli/deps/deps_cli.py | 57 +++++++------------ tcex_cli/cli/run/launch_service_common_abc.py | 6 +- .../cli/run/launch_service_webhook_trigger.py | 2 +- tcex_cli/cli/run/run_cli.py | 18 ++++++ tcex_cli/cli/spec_tool/gen_app_input.py | 2 +- .../cli/spec_tool/gen_app_input_static.py | 2 +- tcex_cli/cli/spec_tool/gen_app_spec_yml.py | 1 + tcex_cli/cli/spec_tool/spec_tool_cli.py | 3 + tcex_cli/cli/template/template_cli.py | 5 +- tests/deps/test_tcex_deps.py | 29 +++++++++- 14 files changed, 96 insertions(+), 48 deletions(-) diff --git a/release_notes.md b/release_notes.md index 69233b1..79d4cae 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,6 +1,12 @@ # Release Notes -### 1.0.1 +## 1.0.2 + +- APP-4171 - [DEPS] Updated deps command to add a link to lib_latest for current Python version for App Builder +- APP-4172 - [CLI] Minor enhancement to output of multiple commands +- APP-4773 - [SUBMODULE] Minor update to config submodule + +## 1.0.1 - APP-3915 - [CONFIG] Added validation to ensure displayPath is always in the install.json for API Services - APP-4060 - [CLI] Updated proxy inputs to use environment variables @@ -9,7 +15,7 @@ - APP-4113 - [CONFIG] Updated App Spec model to normalize App features -### 1.0.0 +## 1.0.0 - APP-3926 - Split CLI module of TcEx into tcex-cli project - APP-3912 - [CLI] Updated `tcex` command to use "project.scripts" setting in pyproject.toml diff --git a/tcex_cli/__metadata__.py b/tcex_cli/__metadata__.py index 3327b70..e789888 100644 --- a/tcex_cli/__metadata__.py +++ b/tcex_cli/__metadata__.py @@ -1,3 +1,3 @@ """TcEx Framework metadata.""" __license__ = 'Apache-2.0' -__version__ = '1.0.1' +__version__ = '1.0.2' diff --git a/tcex_cli/app/config b/tcex_cli/app/config index 7c596ca..249a4cf 160000 --- a/tcex_cli/app/config +++ b/tcex_cli/app/config @@ -1 +1 @@ -Subproject commit 7c596ca553f816f3a63c83277054644615f06a3d +Subproject commit 249a4cfc71b21e3d0db70a30ba67062499076296 diff --git a/tcex_cli/cli/deploy/deploy_cli.py b/tcex_cli/cli/deploy/deploy_cli.py index 7b344b6..42c4ee1 100644 --- a/tcex_cli/cli/deploy/deploy_cli.py +++ b/tcex_cli/cli/deploy/deploy_cli.py @@ -93,8 +93,11 @@ def deploy_app(self): except Exception as err: Render.panel.failure(f'Failed Deploying App: {err}') - if not response.ok: + # TC will respond with a 200 even if the deploy fails with content of "[]" + if not response.ok or response.text in ('[]', None): reason = response.text or response.reason + if response.text == '[]': + reason = 'TC responded with an empty array ([]), which indicates a failure.' Render.table.key_value( 'Failed To Deploy App', { diff --git a/tcex_cli/cli/deps/deps_cli.py b/tcex_cli/cli/deps/deps_cli.py index 0b2e856..eed09b8 100644 --- a/tcex_cli/cli/deps/deps_cli.py +++ b/tcex_cli/cli/deps/deps_cli.py @@ -6,6 +6,7 @@ import subprocess # nosec import sys from functools import cached_property +from importlib.metadata import version as get_version from pathlib import Path from urllib.parse import quote @@ -154,36 +155,6 @@ def create_requirements_lock(self, contents: str, requirements_file: Path): fh.write(contents) fh.write('') - # def create_temp_requirements(self): - # """Create a temporary requirements.txt. - - # This allows testing against a git branch instead of pulling from pypi. - # """ - # _requirements_fqfn = Path('requirements.txt') - # if self.has_requirements_lock: - # _requirements_fqfn = Path('requirements.lock') - - # # Replace tcex version with develop branch of tcex - # with _requirements_fqfn.open(encoding='utf-8') as fh: - # current_requirements = fh.read().strip().split('\n') - - # self.requirements_fqfn_branch = Path(f'temp-{_requirements_fqfn}') - # with self.requirements_fqfn_branch.open(mode='w', encoding='utf-8') as fh: - # requirements = [] - # for line in current_requirements: - # if not line: - # continue - # if line.startswith('tcex'): - # line = ( - # 'git+https://github.com/ThreatConnect-Inc/tcex.git@' - # f'{self.branch}#egg=tcex' - # ) - # requirements.append(line) - # fh.write('\n'.join(requirements)) - - # # display branch setting - # self.output.append(KeyValueModel(key='Using Branch', value=self.branch)) - def download_deps(self, exe_command: list[str]): """Download the dependencies (run pip).""" # recommended -> https://pip.pypa.io/en/latest/user_guide/#using-pip-from-your-program @@ -218,11 +189,6 @@ def install_deps(self): # support temp (branch) requirements.txt file exe_command = self._build_command(self.deps_dir, self.requirements_fqfn) - # display tcex version - self.output.append( - KeyValueModel(key='App TcEx Version', value=str(self.app.ij.model.sdk_version)) - ) - # display command setting self.output.append(KeyValueModel(key='Pip Command', value=f'''{' '.join(exe_command)}''')) @@ -245,6 +211,27 @@ def install_deps(self): contents = self.requirements_lock_contents(self.deps_dir) self.create_requirements_lock(contents, self.requirements_lock) + if self.app_builder is True and self.app.ij.model.sdk_version < Version('4.0.0'): + # the lib_version directory + python_version = self.target_python_version or '3.6.15' + lib_version = Path(f'lib_{python_version}') + + # remove previous build director + if lib_version.is_symlink(): + lib_version.unlink() + elif lib_version.is_dir(): + shutil.rmtree(lib_version) + + # create symlink: lib_latest -> lib_version + lib_version.symlink_to(self.deps_dir, target_is_directory=True) + + # display tcex version + try: + self.output.append(KeyValueModel(key='App TcEx Version', value=get_version('tcex'))) + except Exception: # nosec + # best effort to log tcex version + pass + def install_deps_tests(self): """Install tests dependencies.""" if self.requirements_txt_tests.exists(): diff --git a/tcex_cli/cli/run/launch_service_common_abc.py b/tcex_cli/cli/run/launch_service_common_abc.py index b8debf0..bbebb56 100644 --- a/tcex_cli/cli/run/launch_service_common_abc.py +++ b/tcex_cli/cli/run/launch_service_common_abc.py @@ -83,7 +83,11 @@ def keyboard_listener_get_key(self): def keyboard_listener_reset(self): """Reset the keyboard""" - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.stored_keyboard_settings) + try: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.stored_keyboard_settings) + except Exception: # nosec + # ignore errors for now + pass def live_data_commands(self): """Display live data.""" diff --git a/tcex_cli/cli/run/launch_service_webhook_trigger.py b/tcex_cli/cli/run/launch_service_webhook_trigger.py index b008c17..6dc78e8 100644 --- a/tcex_cli/cli/run/launch_service_webhook_trigger.py +++ b/tcex_cli/cli/run/launch_service_webhook_trigger.py @@ -66,8 +66,8 @@ def live_data_display(self): def live_data_header(self) -> Panel: """Display live header.""" + panel_data = [] if self.model.trigger_inputs: - panel_data = [] for trigger_id, _ in enumerate(self.model.trigger_inputs): panel_data.append( f'Running server: [{self.accent}]http://{self.model.inputs.api_service_host}' diff --git a/tcex_cli/cli/run/run_cli.py b/tcex_cli/cli/run/run_cli.py index 65ea2ca..70c2d3b 100644 --- a/tcex_cli/cli/run/run_cli.py +++ b/tcex_cli/cli/run/run_cli.py @@ -11,6 +11,8 @@ from tcex_cli.cli.run.launch_service_api import LaunchServiceApi from tcex_cli.cli.run.launch_service_custom_trigger import LaunchServiceCustomTrigger from tcex_cli.cli.run.launch_service_webhook_trigger import LaunchServiceWebhookTrigger +from tcex_cli.cli.run.model.app_api_service_model import AppApiServiceModel +from tcex_cli.cli.run.model.app_webhook_trigger_service_model import AppWebhookTriggerServiceModel from tcex_cli.render.render import Render @@ -28,6 +30,20 @@ def __init__(self): # validate in App directory self._validate_in_app_directory() + def _display_api_settings(self, api_inputs: AppApiServiceModel | AppWebhookTriggerServiceModel): + """Display API settings.""" + Render.panel.info( + ( + 'Current API Service Settings:\n' + f'host: [{self.accent}]{api_inputs.api_service_host}[/{self.accent}]\n' + f'port: [{self.accent}]{api_inputs.api_service_port}[/{self.accent}]\n\n' + 'API default settings can be overridden with these environment variables:\n' + f' - [{self.accent}]API_SERVICE_HOST[/{self.accent}]\n' + f' - [{self.accent}]API_SERVICE_PORT[/{self.accent}]' + ), + 'API Settings', + ) + def _validate_in_app_directory(self): """Return True if in App directory.""" if not Path('app.py').is_file() or not Path('run.py').is_file(): @@ -58,6 +74,7 @@ def run(self, config_json: Path): case 'apiservice': Render.panel.info('Launching API Service', f'[{self.panel_title}]Running App[/]') launch_app = LaunchServiceApi(config_json) + self._display_api_settings(launch_app.model.inputs) launch_app.setup() exit_code = launch_app.launch() self.exit_cli(exit_code) @@ -101,6 +118,7 @@ def run(self, config_json: Path): 'Launching Webhook Trigger Service', f'[{self.panel_title}]Running App[/]' ) launch_app = LaunchServiceWebhookTrigger(config_json) + self._display_api_settings(launch_app.model.inputs) launch_app.setup() exit_code = launch_app.launch() self.exit_cli(exit_code) diff --git a/tcex_cli/cli/spec_tool/gen_app_input.py b/tcex_cli/cli/spec_tool/gen_app_input.py index f71c26d..b886343 100644 --- a/tcex_cli/cli/spec_tool/gen_app_input.py +++ b/tcex_cli/cli/spec_tool/gen_app_input.py @@ -31,7 +31,7 @@ def __init__(self): self.input_static = GenAppInputStatic() self.log = _logger self.typing_modules = set() - self.pydantic_modules = {'BaseModel'} + self.pydantic_modules = set() self.report_mismatch = [] def _add_action_classes(self): diff --git a/tcex_cli/cli/spec_tool/gen_app_input_static.py b/tcex_cli/cli/spec_tool/gen_app_input_static.py index 9582b21..44b9793 100644 --- a/tcex_cli/cli/spec_tool/gen_app_input_static.py +++ b/tcex_cli/cli/spec_tool/gen_app_input_static.py @@ -128,7 +128,7 @@ def template_app_imports( # add import for service trigger Apps if self.ij.model.is_trigger_app: - _imports.append('from tcex.input.model import CreateConfigModel') + _imports.append('from tcex.input.model.create_config_model import CreateConfigModel') # add new lines _imports.extend( diff --git a/tcex_cli/cli/spec_tool/gen_app_spec_yml.py b/tcex_cli/cli/spec_tool/gen_app_spec_yml.py index 1afd395..ab40eaf 100644 --- a/tcex_cli/cli/spec_tool/gen_app_spec_yml.py +++ b/tcex_cli/cli/spec_tool/gen_app_spec_yml.py @@ -44,6 +44,7 @@ def _add_standard_fields(self, app_spec_yml_data: dict): 'programMain': self.app.ij.model.program_main, 'programVersion': str(self.app.ij.model.program_version), 'runtimeLevel': self.app.ij.model.runtime_level, + 'schemaVersion': '1.1.0', 'sdkVersion': self.app.ij.model.sdk_version, } ) diff --git a/tcex_cli/cli/spec_tool/spec_tool_cli.py b/tcex_cli/cli/spec_tool/spec_tool_cli.py index ddc6cc7..9569338 100644 --- a/tcex_cli/cli/spec_tool/spec_tool_cli.py +++ b/tcex_cli/cli/spec_tool/spec_tool_cli.py @@ -102,6 +102,9 @@ def generate_app_spec(self): self.write_app_file(gen.filename, f'{config}\n') + # for reload/rewrite/fix of app_spec.yml + self.asy.contents # pylint: disable=pointless-statement + def generate_install_json(self): """Generate the install.json file.""" gen = GenInstallJson(self.asy) diff --git a/tcex_cli/cli/template/template_cli.py b/tcex_cli/cli/template/template_cli.py index 6ff3914..3643ee9 100644 --- a/tcex_cli/cli/template/template_cli.py +++ b/tcex_cli/cli/template/template_cli.py @@ -160,8 +160,11 @@ def download_template_file(self, item: FileMetadataModel): if item.download_url is None: return + # neither of the following options seem to work, but leaving here for future reference: + # - headers={'Cache-Control': 'no-cache'} + # - headers={'Cache-Control': 'max-age=0'} r = self.session.get( - item.download_url, allow_redirects=True, headers={'Cache-Control': 'no-cache'} + item.download_url, allow_redirects=True, headers={'Cache-Control': 'max-age=0'} ) if not r.ok: self.log.error( diff --git a/tests/deps/test_tcex_deps.py b/tests/deps/test_tcex_deps.py index 4622439..f35985e 100644 --- a/tests/deps/test_tcex_deps.py +++ b/tests/deps/test_tcex_deps.py @@ -26,6 +26,15 @@ def teardown_method(self): deps_dir = Path('deps') shutil.rmtree(deps_dir, ignore_errors=True) + def _remove_proxy_env_vars(self): + """Remove proxy env vars""" + os.environ.pop('TC_PROXY_HOST', None) + os.environ.pop('TC_PROXY_PORT', None) + os.environ.pop('TC_PROXY_USER', None) + os.environ.pop('TC_PROXY_USERNAME', None) + os.environ.pop('TC_PROXY_PASS', None) + os.environ.pop('TC_PROXY_PASSWORD', None) + def _run_command( self, args: list[str], @@ -52,11 +61,17 @@ def _run_command( def test_tcex_deps_std(self, monkeypatch: pytest.MonkeyPatch, request: FixtureRequest): """Test Case""" + # remove proxy env vars + self._remove_proxy_env_vars() + result = self._run_command(['deps'], 'app_std', monkeypatch, request) assert result.exit_code == 0 def test_tcex_deps_branch(self, monkeypatch: pytest.MonkeyPatch, request: FixtureRequest): """Test Case""" + # remove proxy env vars + self._remove_proxy_env_vars() + branch = 'develop' result = self._run_command(['deps', '--branch', branch], 'app_branch', monkeypatch, request) assert result.exit_code == 0 @@ -71,12 +86,20 @@ def test_tcex_deps_branch(self, monkeypatch: pytest.MonkeyPatch, request: Fixtur if 'Running' in line: assert 'temp-requirements.txt' in line - def test_tcex_deps_proxy(self, monkeypatch: pytest.MonkeyPatch, request: FixtureRequest): + def test_tcex_deps_proxy_env(self, monkeypatch: pytest.MonkeyPatch, request: FixtureRequest): + """Test Case""" + # proxy settings will be pulled from env vars + result = self._run_command(['deps'], 'app_std', monkeypatch, request) + assert result.exit_code == 0 + + def test_tcex_deps_proxy_explicit( + self, monkeypatch: pytest.MonkeyPatch, request: FixtureRequest + ): """Test Case""" proxy_host = os.getenv('TC_PROXY_HOST') proxy_port = os.getenv('TC_PROXY_PORT') - proxy_user = os.getenv('TC_PROXY_USERNAME') - proxy_pass = os.getenv('TC_PROXY_PASSWORD') + proxy_user = os.getenv('TC_PROXY_USERNAME') or os.getenv('TC_PROXY_USER') + proxy_pass = os.getenv('TC_PROXY_PASSWORD') or os.getenv('TC_PROXY_PASS') command = ['deps', '--proxy-host', proxy_host, '--proxy-port', proxy_port] if proxy_user and proxy_pass: