diff --git a/.changelog/4637.yml b/.changelog/4637.yml new file mode 100644 index 00000000000..7441c7673a4 --- /dev/null +++ b/.changelog/4637.yml @@ -0,0 +1,4 @@ +changes: +- description: The Demisto-SDK CLI has been upgraded to use Typer for command-line interface (CLI) management. + type: feature +pr_number: 4637 diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml index f0c3a68d4c8..1d5eb831c58 100644 --- a/.github/workflows/on-push.yml +++ b/.github/workflows/on-push.yml @@ -357,16 +357,16 @@ jobs: run: | source $(poetry env info --path)/bin/activate cd content - mkdir -p test-graph-commands + mkdir -p ./test-graph-commands/content_graph # create content graph from scratch - demisto-sdk create-content-graph + demisto-sdk graph create # clean import folder sudo rm -rf /var/lib/neo4j/import # Update content graph from the bucket - demisto-sdk update-content-graph -g -o ./test-graph-commands/content_graph + demisto-sdk graph update -g -o ./test-graph-commands/content_graph # Update content graph from the the previous content graph that was created/built - demisto-sdk update-content-graph -i ./test-graph-commands/content_graph/xsoar.zip -o ./test-graph-commands/content_graph + demisto-sdk graph update -i ./test-graph-commands/content_graph/xsoar.zip -o ./test-graph-commands/content_graph - name: Upload artifacts if: always() diff --git a/demisto_sdk/__main__.py b/demisto_sdk/__main__.py index 5bfda94b65d..4c27296cc7f 100644 --- a/demisto_sdk/__main__.py +++ b/demisto_sdk/__main__.py @@ -1,3948 +1,306 @@ -# Site packages -import platform -import sys - -import click - -from demisto_sdk.commands.common.logger import ( - handle_deprecated_args, - logger, - logging_setup, # Must remain at the top - sets up the logger -) -from demisto_sdk.commands.validate.config_reader import ConfigReader -from demisto_sdk.commands.validate.initializer import Initializer -from demisto_sdk.commands.validate.validation_results import ResultWriter -from demisto_sdk.commands.xsoar_linter.xsoar_linter import xsoar_linter_manager - -try: - import git -except ImportError: - sys.exit(click.style("Git executable cannot be found, or is invalid", fg="red")) - -import copy -import functools -import os -from pathlib import Path -from typing import IO, Any, Dict, List, Optional - -import typer -from pkg_resources import DistributionNotFound, get_distribution - -from demisto_sdk.commands.common.configuration import Configuration -from demisto_sdk.commands.common.constants import ( - DEMISTO_SDK_MARKETPLACE_XSOAR_DIST_DEV, - ENV_DEMISTO_SDK_MARKETPLACE, - INTEGRATIONS_README_FILE_NAME, - ExecutionMode, - FileType, - MarketplaceVersions, -) -from demisto_sdk.commands.common.content_constant_paths import ( - ALL_PACKS_DEPENDENCIES_DEFAULT_PATH, - CONTENT_PATH, -) -from demisto_sdk.commands.common.cpu_count import cpu_count -from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json -from demisto_sdk.commands.common.hook_validations.readme import ReadMeValidator -from demisto_sdk.commands.common.tools import ( - convert_path_to_str, - find_type, - get_last_remote_release_version, - get_release_note_entries, - is_external_repository, - is_sdk_defined_working_offline, - parse_marketplace_kwargs, -) -from demisto_sdk.commands.content_graph.commands.create import create -from demisto_sdk.commands.content_graph.commands.get_dependencies import ( - get_dependencies, -) -from demisto_sdk.commands.content_graph.commands.get_relationships import ( - get_relationships, -) -from demisto_sdk.commands.content_graph.commands.update import update -from demisto_sdk.commands.content_graph.objects.repository import ContentDTO -from demisto_sdk.commands.generate_modeling_rules import generate_modeling_rules -from demisto_sdk.commands.prepare_content.prepare_upload_manager import ( - PrepareUploadManager, -) -from demisto_sdk.commands.setup_env.setup_environment import IDEType -from demisto_sdk.commands.split.ymlsplitter import YmlSplitter -from demisto_sdk.commands.test_content.test_modeling_rule import ( - init_test_data, - test_modeling_rule, -) -from demisto_sdk.commands.upload.upload import upload_content_entity -from demisto_sdk.utils.utils import update_command_args_from_config_file - -SDK_OFFLINE_ERROR_MESSAGE = ( - "An internet connection is required for this command. If connected to the " - "internet, un-set the DEMISTO_SDK_OFFLINE_ENV environment variable." -) - - -# Third party packages - -# Common tools - - -class PathsParamType(click.Path): - """ - Defines a click options type for use with the @click.option decorator - - The type accepts a string of comma-separated values where each individual value adheres - to the definition for the click.Path type. The class accepts the same parameters as the - click.Path type, applying those arguments for each comma-separated value in the list. - See https://click.palletsprojects.com/en/8.0.x/parameters/#implementing-custom-types for - more details. - """ - - def convert(self, value, param, ctx): - if "," not in value: - return super().convert(value, param, ctx) - - split_paths = value.split(",") - # check the validity of each of the paths - _ = [ - super(PathsParamType, self).convert(path, param, ctx) - for path in split_paths - ] - return value - - -class VersionParamType(click.ParamType): - """ - Defines a click options type for use with the @click.option decorator - - The type accepts a string represents a version number. - """ - - name = "version" - - def convert(self, value, param, ctx): - version_sections = value.split(".") - if len(version_sections) == 3 and all( - version_section.isdigit() for version_section in version_sections - ): - return value - else: - self.fail( - f"Version {value} is not according to the expected format. " - f"The format of version should be in x.y.z format, e.g: <2.1.3>", - param, - ctx, - ) - - -class DemistoSDK: - """ - The core class for the SDK. - """ - - def __init__(self): - self.configuration = None - - -pass_config = click.make_pass_decorator(DemistoSDK, ensure=True) - - -def logging_setup_decorator(func, *args, **kwargs): - def get_context_arg(args): - for arg in args: - if isinstance(arg, click.core.Context): - return arg - print( # noqa: T201 - "Error: Cannot find the Context arg. Is the command configured correctly?" - ) - return None - - @click.option( - "--console-log-threshold", - help="Minimum logging threshold for the console logger." - " Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", - ) - @click.option( - "--file-log-threshold", - help="Minimum logging threshold for the file logger." - " Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", - ) - @click.option("--log-file-path", help="Path to save log files onto.") - @functools.wraps(func) - def wrapper(*args, **kwargs): - logging_setup( - console_threshold=kwargs.get("console_log_threshold") or "INFO", - file_threshold=kwargs.get("file_log_threshold") or "DEBUG", - path=kwargs.get("log_file_path"), - calling_function=func.__name__, - ) - - handle_deprecated_args(get_context_arg(args).args) - return func(*args, **kwargs) - - return wrapper - - -@click.group( - invoke_without_command=True, - no_args_is_help=True, - context_settings=dict(max_content_width=100), -) -@click.help_option("-h", "--help") -@click.option( - "-v", - "--version", - help="Get the demisto-sdk version.", - is_flag=True, - default=False, - show_default=True, -) -@click.option( - "-rn", - "--release-notes", - help="Display the release notes for the current demisto-sdk version.", - is_flag=True, - default=False, - show_default=True, -) -@pass_config -@click.pass_context -@logging_setup_decorator -def main(ctx, config, version, release_notes, **kwargs): - config.configuration = Configuration() - import dotenv - - dotenv.load_dotenv( - CONTENT_PATH / ".env", override=True - ) # load .env file from the cwd - - if platform.system() == "Windows": - logger.warning( - "Using Demisto-SDK on Windows is not supported. Use WSL2 or run in a container." - ) - - if ( - (not os.getenv("DEMISTO_SDK_SKIP_VERSION_CHECK")) or version - ) and not is_sdk_defined_working_offline(): # If the key exists/called to version - try: - __version__ = get_distribution("demisto-sdk").version - except DistributionNotFound: - __version__ = "dev" - logger.info( - "Could not find the version of the demisto-sdk. This usually happens when running in a development environment." - ) - else: - last_release = "" - if not os.environ.get( - "CI" - ): # Check only when not running in CI (e.g running locally). - last_release = get_last_remote_release_version() - logger.opt(colors=True).info( - f"demisto-sdk {__version__}" - ) # we only `opt` - if last_release and __version__ != last_release: - logger.warning( - f"A newer version ({last_release}) is available. " - f"To update, run 'pip3 install --upgrade demisto-sdk'" - ) - if release_notes: - rn_entries = get_release_note_entries(__version__) - - if not rn_entries: - logger.warning( - "\nCould not get the release notes for this version." - ) - else: - logger.info( - "\nThe following are the release note entries for the current version:\n" - ) - logger.info(rn_entries) - logger.info("") - - -# ====================== split ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option("-i", "--input", help="The yml/json file to extract from.", required=True) -@click.option( - "-o", - "--output", - help="The output dir to write the extracted code/description/image/json to.", -) -@click.option( - "--no-demisto-mock", - help="Don't add an import for demisto-mock (only for yml files).", - is_flag=True, - show_default=True, -) -@click.option( - "--no-common-server", - help="Don't add an import for CommonServerPython or CommonServerPythonShell (only for yml files).", - is_flag=True, - show_default=True, -) -@click.option( - "--no-auto-create-dir", - help="Don't auto create the directory if the target directory ends with *Integrations/*Scripts/*Dashboards" - "/*GenericModules. The auto directory created will be named according to the Integration/Script name.", - is_flag=True, - show_default=True, -) -@click.option( - "--new-module-file", - help="Create a new module file instead of editing the existing file (only for json files).", - is_flag=True, - show_default=True, -) -@pass_config -@click.pass_context -@logging_setup_decorator -def split(ctx, config, **kwargs): - """Split the code, image and description files from a Demisto integration or script yaml file - to multiple files(To a package format - https://demisto.pan.dev/docs/package-dir). - """ - from demisto_sdk.commands.split.jsonsplitter import JsonSplitter - - update_command_args_from_config_file("split", kwargs) - file_type: FileType = find_type(kwargs.get("input", ""), ignore_sub_categories=True) - if file_type not in [ - FileType.INTEGRATION, - FileType.SCRIPT, - FileType.GENERIC_MODULE, - FileType.MODELING_RULE, - FileType.PARSING_RULE, - FileType.LISTS, - FileType.ASSETS_MODELING_RULE, - ]: - logger.info( - "File is not an Integration, Script, List, Generic Module, Modeling Rule or Parsing Rule." - ) - return 1 - - if file_type in [ - FileType.INTEGRATION, - FileType.SCRIPT, - FileType.MODELING_RULE, - FileType.PARSING_RULE, - FileType.ASSETS_MODELING_RULE, - ]: - yml_splitter = YmlSplitter( - configuration=config.configuration, file_type=file_type.value, **kwargs - ) - return yml_splitter.extract_to_package_format() - - else: - json_splitter = JsonSplitter( - input=kwargs.get("input"), # type: ignore[arg-type] - output=kwargs.get("output"), # type: ignore[arg-type] - no_auto_create_dir=kwargs.get("no_auto_create_dir"), # type: ignore[arg-type] - new_module_file=kwargs.get("new_module_file"), # type: ignore[arg-type] - file_type=file_type, - ) - return json_splitter.split_json() - - -# ====================== extract-code ====================== # -@main.command(hidden=True) -@click.help_option("-h", "--help") -@click.option("--input", "-i", help="The yml file to extract from", required=True) -@click.option( - "--output", "-o", required=True, help="The output file to write the code to" -) -@click.option( - "--no-demisto-mock", - help="Don't add an import for demisto mock, false by default", - is_flag=True, - show_default=True, -) -@click.option( - "--no-common-server", - help="Don't add an import for CommonServerPython." - "If not specified will import unless this is CommonServerPython", - is_flag=True, - show_default=True, -) -@pass_config -@click.pass_context -@logging_setup_decorator -def extract_code(ctx, config, **kwargs): - """Extract code from a Demisto integration or script yaml file.""" - from demisto_sdk.commands.split.ymlsplitter import YmlSplitter - - update_command_args_from_config_file("extract-code", kwargs) - file_type: FileType = find_type(kwargs.get("input", ""), ignore_sub_categories=True) - if file_type not in [FileType.INTEGRATION, FileType.SCRIPT]: - logger.info("File is not an Integration or Script.") - return 1 - extractor = YmlSplitter( - configuration=config.configuration, file_type=file_type.value, **kwargs - ) - return extractor.extract_code(kwargs["outfile"]) - - -# ====================== prepare-content ====================== # -@main.command(name="prepare-content") -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="Comma-separated list of paths to directories or files to unify.", - required=False, - type=PathsParamType(dir_okay=True, exists=True), -) -@click.option( - "-a", - "--all", - is_flag=True, - help="Run prepare-content on all content packs. If no output path is given, will dump the result in the " - "current working path.", -) -@click.option( - "-g", - "--graph", - help="Whether to use the content graph.", - is_flag=True, - default=False, -) -@click.option( - "--skip-update", - help="Whether to skip updating the content graph (used only when graph is true).", - is_flag=True, - default=False, -) -@click.option( - "-o", "--output", help="The output dir to write the unified yml to.", required=False -) -@click.option( - "-c", - "--custom", - help="Add test label to unified yml id/name/display.", - required=False, -) -@click.option( - "-f", - "--force", - help="Forcefully overwrites the preexisting yml if one exists.", - is_flag=True, - show_default=False, -) -@click.option( - "-ini", - "--ignore-native-image", - help="Whether to ignore the addition of the nativeimage key to the yml of a script/integration.", - is_flag=True, - show_default=False, - default=False, -) -@click.option( - "-mp", - "--marketplace", - help="The marketplace content items are created for, that determines usage of marketplace " - "unique text. Default is the XSOAR marketplace.", - default="xsoar", - type=click.Choice([mp.value for mp in list(MarketplaceVersions)] + ["v2"]), -) -@click.pass_context -@logging_setup_decorator -def prepare_content(ctx, **kwargs): - """ - This command is used to prepare the content to be used in the platform. - """ - assert ( - sum([bool(kwargs["all"]), bool(kwargs["input"])]) == 1 - ), "Exactly one of the '-a' or '-i' parameters must be provided." - - if kwargs["all"]: - content_DTO = ContentDTO.from_path() - output_path = kwargs.get("output", ".") or "." - content_DTO.dump( - dir=Path(output_path, "prepare-content-tmp"), - marketplace=parse_marketplace_kwargs(kwargs), - ) - return 0 - - inputs = [] - if input_ := kwargs["input"]: - inputs = input_.split(",") - - if output_path := kwargs["output"]: - if "." in Path(output_path).name: # check if the output path is a file - if len(inputs) > 1: - raise ValueError( - "When passing multiple inputs, the output path should be a directory and not a file." - ) - else: - dest_path = Path(output_path) - dest_path.mkdir(exist_ok=True) - - for input_content in inputs: - if output_path and len(inputs) > 1: - path_name = Path(input_content).name - kwargs["output"] = str(Path(output_path, path_name)) - - if click.get_current_context().info_name == "unify": - kwargs["unify_only"] = True - - update_command_args_from_config_file("unify", kwargs) - # Input is of type Path. - kwargs["input"] = str(input_content) - file_type = find_type(kwargs["input"]) - if marketplace := kwargs.get("marketplace"): - os.environ[ENV_DEMISTO_SDK_MARKETPLACE] = marketplace.lower() - if file_type == FileType.GENERIC_MODULE: - from demisto_sdk.commands.prepare_content.generic_module_unifier import ( - GenericModuleUnifier, - ) - - # pass arguments to GenericModule unifier and call the command - generic_module_unifier = GenericModuleUnifier(**kwargs) - generic_module_unifier.merge_generic_module_with_its_dashboards() - else: - PrepareUploadManager.prepare_for_upload(**kwargs) - return 0 - - -main.add_command(prepare_content, name="unify") - - -# ====================== zip-packs ====================== # - - -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="The packs to create artifacts for. Optional values are: `all` or csv list of pack names.", - type=PathsParamType(exists=True, resolve_path=True), - required=True, -) -@click.option( - "-o", - "--output", - help="The path to the directory into which to write the zip files.", - type=click.Path(file_okay=False, resolve_path=True), - required=True, -) -@click.option( - "-v", - "--content-version", - help="The content version in CommonServerPython.", - default="0.0.0", -) -@click.option( - "-u", - "--upload", - is_flag=True, - help="Upload the unified packs to the marketplace.", - default=False, -) -@click.option( - "--zip-all", is_flag=True, help="Zip all the packs in one zip file.", default=False -) -@click.pass_context -@logging_setup_decorator -def zip_packs(ctx, **kwargs) -> int: - """Generating zipped packs that are ready to be uploaded to Cortex XSOAR machine.""" - from demisto_sdk.commands.upload.uploader import Uploader - from demisto_sdk.commands.zip_packs.packs_zipper import ( - EX_FAIL, - EX_SUCCESS, - PacksZipper, - ) - - update_command_args_from_config_file("zip-packs", kwargs) - - # if upload is true - all zip packs will be compressed to one zip file - should_upload = kwargs.pop("upload", False) - zip_all = kwargs.pop("zip_all", False) or should_upload - marketplace = parse_marketplace_kwargs(kwargs) - - packs_zipper = PacksZipper( - zip_all=zip_all, pack_paths=kwargs.pop("input"), quiet_mode=zip_all, **kwargs - ) - zip_path, unified_pack_names = packs_zipper.zip_packs() - - if should_upload and zip_path: - return Uploader( - input=Path(zip_path), pack_names=unified_pack_names, marketplace=marketplace - ).upload() - - return EX_SUCCESS if zip_path is not None else EX_FAIL - - -# ====================== validate ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "--no-conf-json", - is_flag=True, - default=False, - show_default=True, - help="Relevant only for the old validate flow and will be removed in a future release. Skip conf.json validation.", -) -@click.option( - "-s", - "--id-set", - is_flag=True, - default=False, - show_default=True, - help="Relevant only for the old validate flow and will be removed in a future release. Perform validations using the id_set file.", -) -@click.option( - "-idp", - "--id-set-path", - help="Relevant only for the old validate flow and will be removed in a future release. The path of the id-set.json used for validations.", - type=click.Path(resolve_path=True), -) -@click.option( - "-gr", - "--graph", - is_flag=True, - default=False, - show_default=True, - help="Relevant only for the old validate flow and will be removed in a future release. Perform validations on content graph.", -) -@click.option( - "--prev-ver", help="Previous branch or SHA1 commit to run checks against." -) -@click.option( - "--no-backward-comp", - is_flag=True, - show_default=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to check backward compatibility or not.", -) -@click.option( - "-g", - "--use-git", - is_flag=True, - show_default=True, - default=False, - help="Validate changes using git - this will check current branch's changes against origin/master or origin/main. " - "If the --post-commit flag is supplied: validation will run only on the current branch's changed files " - "that have been committed. " - "If the --post-commit flag is not supplied: validation will run on all changed files in the current branch, " - "both committed and not committed. ", -) -@click.option( - "-pc", - "--post-commit", - is_flag=True, - help="Whether the validation should run only on the current branch's committed changed files. " - "This applies only when the -g flag is supplied.", -) -@click.option( - "-st", - "--staged", - is_flag=True, - help="Whether the validation should ignore unstaged files." - "This applies only when the -g flag is supplied.", -) -@click.option( - "-iu", - "--include-untracked", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to include untracked files in the validation. " - "This applies only when the -g flag is supplied.", -) -@click.option( - "-a", - "--validate-all", - is_flag=True, - show_default=True, - default=False, - help="Whether to run all validation on all files or not.", -) -@click.option( - "-i", - "--input", - type=PathsParamType( - exists=True, resolve_path=True - ), # PathsParamType allows passing a list of paths - help="The path of the content pack/file to validate specifically.", -) -@click.option( - "--skip-pack-release-notes", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Skip validation of pack release notes.", -) -@click.option( - "--print-ignored-errors", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Print ignored errors as warnings.", -) -@click.option( - "--print-ignored-files", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Print which files were ignored by the command.", -) -@click.option( - "--no-docker-checks", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to run docker image validation.", -) -@click.option( - "--silence-init-prints", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to skip the initialization prints.", -) -@click.option( - "--skip-pack-dependencies", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Skip validation of pack dependencies.", -) -@click.option( - "--create-id-set", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to create the id_set.json file.", -) -@click.option( - "-j", - "--json-file", - help="The JSON file path to which to output the command results.", -) -@click.option( - "--skip-schema-check", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to skip the file schema check.", -) -@click.option( - "--debug-git", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to print debug logs for git statuses.", -) -@click.option( - "--print-pykwalify", - is_flag=True, - help="Relevant only for the old validate flow and will be removed in a future release. Whether to print the pykwalify log errors.", -) -@click.option( - "--quiet-bc-validation", - help="Relevant only for the old validate flow and will be removed in a future release. Set backwards compatibility validation's errors as warnings.", - is_flag=True, -) -@click.option( - "--allow-skipped", - help="Relevant only for the old validate flow and will be removed in a future release. Don't fail on skipped integrations or when all test playbooks are skipped.", - is_flag=True, -) -@click.option( - "--no-multiprocessing", - help="Relevant only for the old validate flow and will be removed in a future release. run validate all without multiprocessing, for debugging purposes.", - is_flag=True, - default=False, -) -@click.option( - "-sv", - "--run-specific-validations", - help="A comma separated list of validations to run stated the error codes.", - is_flag=False, -) -@click.option( - "--category-to-run", - help="Run specific validations by stating category they're listed under in the config file.", - is_flag=False, -) -@click.option( - "-f", - "--fix", - help="Wether to autofix failing validations with an available auto fix or not.", - is_flag=True, - default=False, -) -@click.option( - "--config-path", - help="Path for a config file to run, if not given - will run the default config at https://github.com/demisto/demisto-sdk/blob/master/demisto_sdk/commands/validate/default_config.toml", - is_flag=False, -) -@click.option( - "--ignore-support-level", - is_flag=True, - show_default=True, - default=False, - help="Wether to skip validations based on their support level or not.", -) -@click.option( - "--run-old-validate", - is_flag=True, - show_default=True, - default=False, - help="Wether to run the old validate flow or not. Alteratively, you can configure the RUN_OLD_VALIDATE env variable.", -) -@click.option( - "--skip-new-validate", - is_flag=True, - show_default=True, - default=False, - help="Wether to skip the new validate flow or not. Alteratively, you can configure the SKIP_NEW_VALIDATE env variable.", -) -@click.option( - "--ignore", - default=None, - multiple=True, - help="An error code to not run. Must be listed under `ignorable_errors`. To ignore more than one error, repeate this option (e.g. `--ignore AA123 --ignore BC321`)", -) -@click.argument("file_paths", nargs=-1, type=click.Path(exists=True, resolve_path=True)) -@pass_config -@click.pass_context -@logging_setup_decorator -def validate(ctx, config, file_paths: str, **kwargs): - """Validate your content files. If no additional flags are given, will validated only committed files.""" - from demisto_sdk.commands.validate.old_validate_manager import OldValidateManager - from demisto_sdk.commands.validate.validate_manager import ValidateManager - - if is_sdk_defined_working_offline(): - logger.error(SDK_OFFLINE_ERROR_MESSAGE) - sys.exit(1) - - if file_paths and not kwargs["input"]: - # If file_paths is given as an argument, use it as the file_paths input (instead of the -i flag). If both, input wins. - kwargs["input"] = ",".join(file_paths) - run_with_mp = not kwargs.pop("no_multiprocessing") - update_command_args_from_config_file("validate", kwargs) - sys.path.append(config.configuration.env_dir) - - file_path = kwargs["input"] - - if kwargs["post_commit"] and kwargs["staged"]: - logger.info( - "Could not supply the staged flag with the post-commit flag" - ) - sys.exit(1) - try: - is_external_repo = is_external_repository() - if kwargs.get("validate_all"): - execution_mode = ExecutionMode.ALL_FILES - elif kwargs.get("use_git"): - execution_mode = ExecutionMode.USE_GIT - elif file_path: - execution_mode = ExecutionMode.SPECIFIC_FILES - else: - execution_mode = ExecutionMode.USE_GIT - # default validate to -g --post-commit - kwargs["use_git"] = True - kwargs["post_commit"] = True - exit_code = 0 - run_new_validate = not kwargs["skip_new_validate"] or ( - (env_flag := os.getenv("SKIP_NEW_VALIDATE")) - and str(env_flag).lower() == "true" - ) - run_old_validate = kwargs["run_old_validate"] or ( - (env_flag := os.getenv("RUN_OLD_VALIDATE")) - and str(env_flag).lower() == "true" - ) - if not run_new_validate: - for new_validate_flag in [ - "fix", - "ignore_support_level", - "config_path", - "category_to_run", - ]: - if kwargs.get(new_validate_flag): - logger.warning( - f"The following flag {new_validate_flag.replace('_', '-')} is related only to the new validate and is being called while not running the new validate flow, therefore the flag will be ignored." - ) - if not run_old_validate: - for old_validate_flag in [ - "no_backward_comp", - "no_conf_json", - "id_set", - "graph", - "skip_pack_release_notes", - "print_ignored_errors", - "print_ignored_files", - "no_docker_checks", - "silence_init_prints", - "skip_pack_dependencies", - "id_set_path", - "create_id_set", - "skip_schema_check", - "debug_git", - "include_untracked", - "quiet_bc_validation", - "allow_skipped", - "no_multiprocessing", - ]: - if kwargs.get(old_validate_flag): - logger.warning( - f"The following flag {old_validate_flag.replace('_', '-')} is related only to the old validate and is being called while not running the old validate flow, therefore the flag will be ignored." - ) - - if run_old_validate: - if not kwargs["skip_new_validate"]: - kwargs["graph"] = False - validator = OldValidateManager( - is_backward_check=not kwargs["no_backward_comp"], - only_committed_files=kwargs["post_commit"], - prev_ver=kwargs["prev_ver"], - skip_conf_json=kwargs["no_conf_json"], - use_git=kwargs["use_git"], - file_path=file_path, - validate_all=kwargs.get("validate_all"), - validate_id_set=kwargs["id_set"], - validate_graph=kwargs.get("graph"), - skip_pack_rn_validation=kwargs["skip_pack_release_notes"], - print_ignored_errors=kwargs["print_ignored_errors"], - is_external_repo=is_external_repo, - print_ignored_files=kwargs["print_ignored_files"], - no_docker_checks=kwargs["no_docker_checks"], - silence_init_prints=kwargs["silence_init_prints"], - skip_dependencies=kwargs["skip_pack_dependencies"], - id_set_path=kwargs.get("id_set_path"), - staged=kwargs["staged"], - create_id_set=kwargs.get("create_id_set"), - json_file_path=kwargs.get("json_file"), - skip_schema_check=kwargs.get("skip_schema_check"), - debug_git=kwargs.get("debug_git"), - include_untracked=kwargs.get("include_untracked"), - quiet_bc=kwargs.get("quiet_bc_validation"), - multiprocessing=run_with_mp, - check_is_unskipped=not kwargs.get("allow_skipped", False), - specific_validations=kwargs.get("run_specific_validations"), - ) - exit_code += validator.run_validation() - if run_new_validate: - validation_results = ResultWriter( - json_file_path=kwargs.get("json_file"), - ) - config_reader = ConfigReader( - path=kwargs.get("config_path"), - category=kwargs.get("category_to_run"), - explicitly_selected=( - (kwargs.get("run_specific_validations") or "").split(",") - ), - ) - initializer = Initializer( - staged=kwargs["staged"], - committed_only=kwargs["post_commit"], - prev_ver=kwargs["prev_ver"], - file_path=file_path, - execution_mode=execution_mode, - ) - validator_v2 = ValidateManager( - file_path=file_path, - initializer=initializer, - validation_results=validation_results, - config_reader=config_reader, - allow_autofix=kwargs.get("fix"), - ignore_support_level=kwargs.get("ignore_support_level"), - ignore=kwargs.get("ignore"), - ) - exit_code += validator_v2.run_validations() - return exit_code - except (git.InvalidGitRepositoryError, git.NoSuchPathError, FileNotFoundError) as e: - logger.error(f"{e}") - logger.error( - "\nYou may not be running `demisto-sdk validate` command in the content directory.\n" - "Please run the command from content directory" - ) - sys.exit(1) - - -# ====================== create-content-artifacts ====================== # -@main.command(hidden=True) -@click.help_option("-h", "--help") -@click.option( - "-a", - "--artifacts_path", - help="Destination directory to create the artifacts.", - type=click.Path(file_okay=False, resolve_path=True), - required=True, -) -@click.option("--zip/--no-zip", help="Zip content artifacts folders", default=True) -@click.option( - "--packs", - help="Create only content_packs artifacts. " - "Used for server version 5.5.0 and earlier.", - is_flag=True, -) -@click.option( - "-v", - "--content_version", - help="The content version in CommonServerPython.", - default="0.0.0", -) -@click.option( - "-s", - "--suffix", - help="The suffix to add all yaml/json/yml files in the created artifacts.", -) -@click.option( - "--cpus", - help="Number of cpus/vcpus available - only required when os not reflect number of cpus (CircleCI" - "always show 32, but medium has 3.", - hidden=True, - default=cpu_count(), -) -@click.option( - "-idp", - "--id-set-path", - help="The full path of id_set.json", - hidden=True, - type=click.Path(exists=True, resolve_path=True), -) -@click.option( - "-p", - "--pack-names", - help=( - "Packs to create artifacts for. Optional values are: `all` or " - "csv list of packs. " - "Default is set to `all`" - ), - default="all", - hidden=True, -) -@click.option( - "-sk", - "--signature-key", - help="Base64 encoded signature key used for signing packs.", - hidden=True, -) -@click.option( - "-sd", - "--sign-directory", - help="Path to the signDirectory executable file.", - type=click.Path(exists=True, resolve_path=True), - hidden=True, -) -@click.option( - "-rt", - "--remove-test-playbooks", - is_flag=True, - help="Should remove test playbooks from content packs or not.", - default=True, - hidden=True, -) -@click.option( - "-mp", - "--marketplace", - help="The marketplace the artifacts are created for, that " - "determines which artifacts are created for each pack. " - "Default is the XSOAR marketplace, that has all of the packs " - "artifacts.", - default="xsoar", - type=click.Choice(["xsoar", "marketplacev2", "v2", "xpanse"]), -) -@click.option( - "-fbi", - "--filter-by-id-set", - is_flag=True, - help="Whether to use the id set as content items guide, meaning only include in the packs the " - "content items that appear in the id set.", - default=False, - hidden=True, -) -@click.option( - "-af", - "--alternate-fields", - is_flag=True, - help="Use the alternative fields if such are present in the yml or json of the content item.", - default=False, - hidden=True, -) -@click.pass_context -@logging_setup_decorator -def create_content_artifacts(ctx, **kwargs) -> int: - """Generating the following artifacts: - 1. content_new - Contains all content objects of type json,yaml (from_version < 6.0.0) - 2. content_packs - Contains all packs from Packs - Ignoring internal files (to_version >= 6.0.0). - 3. content_test - Contains all test scripts/playbooks (from_version < 6.0.0) - 4. content_all - Contains all from content_new and content_test. - 5. uploadable_packs - Contains zipped packs that are ready to be uploaded to Cortex XSOAR machine. - """ - from demisto_sdk.commands.create_artifacts.content_artifacts_creator import ( - ArtifactsManager, - ) - - update_command_args_from_config_file("create-content-artifacts", kwargs) - if marketplace := kwargs.get("marketplace"): - os.environ[ENV_DEMISTO_SDK_MARKETPLACE] = marketplace.lower() - artifacts_conf = ArtifactsManager(**kwargs) - return artifacts_conf.create_content_artifacts() - - -# ====================== secrets ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option("-i", "--input", help="Specify a file to check secret for.") -@click.option( - "--post-commit", - is_flag=True, - show_default=True, - help="Whether the validation secretes is done after you committed your files, " - "this will help the command to determine which files it should check in its " - "run. Before you commit the files it should not be used. Mostly for build " - "validations.", -) -@click.option( - "-ie", - "--ignore-entropy", - is_flag=True, - help="Ignore entropy algorithm that finds secret strings (passwords/api keys).", -) -@click.option( - "-wl", - "--whitelist", - default="./Tests/secrets_white_list.json", - show_default=True, - help='Full path to whitelist file, file name should be "secrets_white_list.json."', -) -@click.option("--prev-ver", help="The branch against which to run secrets validation.") -@click.argument("file_paths", nargs=-1, type=click.Path(exists=True, resolve_path=True)) -@pass_config -@click.pass_context -@logging_setup_decorator -def secrets(ctx, config, file_paths: str, **kwargs): - """Run Secrets validator to catch sensitive data before exposing your code to public repository. - Attach path to whitelist to allow manual whitelists. - """ - if file_paths and not kwargs["input"]: - # If file_paths is given as an argument, use it as the file_paths input (instead of the -i flag). If both, input wins. - kwargs["input"] = ",".join(file_paths) - - from demisto_sdk.commands.secrets.secrets import SecretsValidator - - update_command_args_from_config_file("secrets", kwargs) - sys.path.append(config.configuration.env_dir) - secrets_validator = SecretsValidator( - configuration=config.configuration, - is_circle=kwargs["post_commit"], - ignore_entropy=kwargs["ignore_entropy"], - white_list_path=kwargs["whitelist"], - input_path=kwargs.get("input"), - ) - return secrets_validator.run() - - -# ====================== lint ====================== # -@main.command(hidden=True) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="Specify directory(s) of integration/script", - type=PathsParamType(exists=True, resolve_path=True), -) -@click.option("-g", "--git", is_flag=True, help="Will run only on changed packages") -@click.option( - "-a", - "--all-packs", - is_flag=True, - help="Run lint on all directories in content repo", -) -@click.option( - "-p", - "--parallel", - default=1, - help="Run tests in parallel", - type=click.IntRange(0, 15, clamp=True), - show_default=True, -) -@click.option("--no-flake8", is_flag=True, help="Do NOT run flake8 linter") -@click.option("--no-bandit", is_flag=True, help="Do NOT run bandit linter") -@click.option("--no-xsoar-linter", is_flag=True, help="Do NOT run XSOAR linter") -@click.option("--no-mypy", is_flag=True, help="Do NOT run mypy static type checking") -@click.option("--no-vulture", is_flag=True, help="Do NOT run vulture linter") -@click.option("--no-pylint", is_flag=True, help="Do NOT run pylint linter") -@click.option("--no-test", is_flag=True, help="Do NOT test (skip pytest)") -@click.option("--no-pwsh-analyze", is_flag=True, help="Do NOT run powershell analyze") -@click.option("--no-pwsh-test", is_flag=True, help="Do NOT run powershell test") -@click.option("-kc", "--keep-container", is_flag=True, help="Keep the test container") -@click.option( - "--prev-ver", - help="Previous branch or SHA1 commit to run checks against", - default=os.getenv("DEMISTO_DEFAULT_BRANCH", default="master"), -) -@click.option( - "--test-xml", - help="Path to store pytest xml results", - type=click.Path(exists=True, resolve_path=True), -) -@click.option( - "--failure-report", - help="Path to store failed packs report", - type=click.Path(exists=True, resolve_path=True), -) -@click.option( - "-j", - "--json-file", - help="The JSON file path to which to output the command results.", - type=click.Path(resolve_path=True), -) -@click.option("--no-coverage", is_flag=True, help="Do NOT run coverage report.") -@click.option( - "--coverage-report", - help="Specify directory for the coverage report files", - type=PathsParamType(), -) -@click.option( - "-dt", - "--docker-timeout", - default=60, - help="The timeout (in seconds) for requests done by the docker client.", - type=int, -) -@click.option( - "-di", - "--docker-image", - default="from-yml", - help="The docker image to check package on. Can be a comma separated list of Possible values: 'native:maintenance', 'native:ga', 'native:dev'," - " 'all', a specific docker image from Docker Hub (e.g devdemisto/python3:3.10.9.12345) or the default" - " 'from-yml', 'native:target'. To run lint only on native supported content with a specific image," - " use 'native:target' with --docker-image-target .", -) -@click.option( - "-dit", - "--docker-image-target", - default="", - help="The docker image to lint native supported content with. Should only be used with " - "--docker-image native:target. An error will be raised otherwise.", -) -@click.option( - "-cdam", - "--check-dependent-api-module", - is_flag=True, - help="Run unit tests and lint on all packages that " - "are dependent on the found " - "modified api modules.", - default=False, -) -@click.option( - "--time-measurements-dir", - help="Specify directory for the time measurements report file", - type=PathsParamType(), -) -@click.option( - "-sdm", - "--skip-deprecation-message", - is_flag=True, - help="Whether to skip the deprecation notice or not. Alteratively, you can configure the SKIP_DEPRECATION_MESSAGE env variable. (skipping/not skipping this message doesn't affect the performance.)", -) -@click.pass_context -@logging_setup_decorator -def lint(ctx, **kwargs): - """Deprecated, use demisto-sdk pre-commit instead. - Lint command will perform: - 1. Package in host checks - flake8, bandit, mypy, vulture. - 2. Package in docker image checks - pylint, pytest, powershell - test, powershell - analyze. - Meant to be used with integrations/scripts that use the folder (package) structure. - Will lookup up what docker image to use and will setup the dev dependencies and file in the target folder. - If no additional flags specifying the packs are given, will lint only changed files. - """ - from demisto_sdk.commands.lint.lint_manager import LintManager - - show_deprecation_message = any( - [ - not os.getenv("SKIP_DEPRECATION_MESSAGE"), - not kwargs.get("skip_deprecation_message"), - ] - ) - update_command_args_from_config_file("lint", kwargs) - lint_manager = LintManager( - input=kwargs.get("input"), # type: ignore[arg-type] - git=kwargs.get("git"), # type: ignore[arg-type] - all_packs=kwargs.get("all_packs"), # type: ignore[arg-type] - prev_ver=kwargs.get("prev_ver"), # type: ignore[arg-type] - json_file_path=kwargs.get("json_file"), # type: ignore[arg-type] - check_dependent_api_module=kwargs.get("check_dependent_api_module"), # type: ignore[arg-type] - show_deprecation_message=show_deprecation_message, - ) - return lint_manager.run( - parallel=kwargs.get("parallel"), # type: ignore[arg-type] - no_flake8=kwargs.get("no_flake8"), # type: ignore[arg-type] - no_bandit=kwargs.get("no_bandit"), # type: ignore[arg-type] - no_mypy=kwargs.get("no_mypy"), # type: ignore[arg-type] - no_vulture=kwargs.get("no_vulture"), # type: ignore[arg-type] - no_xsoar_linter=kwargs.get("no_xsoar_linter"), # type: ignore[arg-type] - no_pylint=kwargs.get("no_pylint"), # type: ignore[arg-type] - no_test=kwargs.get("no_test"), # type: ignore[arg-type] - no_pwsh_analyze=kwargs.get("no_pwsh_analyze"), # type: ignore[arg-type] - no_pwsh_test=kwargs.get("no_pwsh_test"), # type: ignore[arg-type] - keep_container=kwargs.get("keep_container"), # type: ignore[arg-type] - test_xml=kwargs.get("test_xml"), # type: ignore[arg-type] - failure_report=kwargs.get("failure_report"), # type: ignore[arg-type] - no_coverage=kwargs.get("no_coverage"), # type: ignore[arg-type] - coverage_report=kwargs.get("coverage_report"), # type: ignore[arg-type] - docker_timeout=kwargs.get("docker_timeout"), # type: ignore[arg-type] - docker_image_flag=kwargs.get("docker_image"), # type: ignore[arg-type] - docker_image_target=kwargs.get("docker_image_target"), # type: ignore[arg-type] - time_measurements_dir=kwargs.get("time_measurements_dir"), # type: ignore[arg-type] - ) - - -# ====================== coverage-analyze ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="The .coverage file to analyze.", - default=os.path.join("coverage_report", ".coverage"), - type=PathsParamType(resolve_path=True), -) -@click.option( - "--default-min-coverage", - help="Default minimum coverage (for new files).", - default=70.0, - type=click.FloatRange(0.0, 100.0), -) -@click.option( - "--allowed-coverage-degradation-percentage", - help="Allowed coverage degradation percentage (for modified files).", - default=1.0, - type=click.FloatRange(0.0, 100.0), -) -@click.option( - "--no-cache", - help="Force download of the previous coverage report file.", - is_flag=True, - type=bool, -) -@click.option( - "--report-dir", - help="Directory of the coverage report files.", - default="coverage_report", - type=PathsParamType(resolve_path=True), -) -@click.option( - "--report-type", - help="The type of coverage report (posible values: 'text', 'html', 'xml', 'json' or 'all').", - type=str, -) -@click.option( - "--no-min-coverage-enforcement", - help="Do not enforce minimum coverage.", - is_flag=True, -) -@click.option( - "--previous-coverage-report-url", - help="URL of the previous coverage report.", - default=f"https://storage.googleapis.com/{DEMISTO_SDK_MARKETPLACE_XSOAR_DIST_DEV}/code-coverage-reports/coverage-min.json", - type=str, -) -@click.pass_context -@logging_setup_decorator -def coverage_analyze(ctx, **kwargs): - from demisto_sdk.commands.coverage_analyze.coverage_report import CoverageReport - - try: - no_degradation_check = ( - kwargs["allowed_coverage_degradation_percentage"] == 100.0 - ) - no_min_coverage_enforcement = kwargs["no_min_coverage_enforcement"] - - cov_report = CoverageReport( - default_min_coverage=kwargs["default_min_coverage"], - allowed_coverage_degradation_percentage=kwargs[ - "allowed_coverage_degradation_percentage" - ], - coverage_file=kwargs["input"], - no_cache=kwargs.get("no_cache", False), - report_dir=kwargs["report_dir"], - report_type=kwargs["report_type"], - no_degradation_check=no_degradation_check, - previous_coverage_report_url=kwargs["previous_coverage_report_url"], - ) - cov_report.coverage_report() - # if no_degradation_check=True we will suppress the minimum coverage check - if ( - no_degradation_check - or cov_report.coverage_diff_report() - or no_min_coverage_enforcement - ): - return 0 - except FileNotFoundError as e: - logger.warning(e) - return 0 - except Exception as error: - logger.error(error) - - return 1 - - -# ====================== format ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="The path of the desired file to be formatted.\n" - "If no input is specified, the format will be executed on all new/changed files.", - type=PathsParamType( - exists=True, resolve_path=True - ), # PathsParamType allows passing a list of paths -) -@click.option( - "-o", - "--output", - help="Specifies where the formatted file should be saved to. If not used, the default is to overwrite the " - "origin file..", - type=click.Path(resolve_path=True), -) -@click.option( - "-fv", - "--from-version", - help="Specifies the minimum version that this content item or content pack is compatible with.", -) -@click.option( - "-nv", - "--no-validate", - help="Set when validate on file is not wanted.", - is_flag=True, -) -@click.option( - "-ud", - "--update-docker", - help="Updates the Docker image of the integration/script to the newest available tag.", - is_flag=True, -) -@click.option( - "-y/-n", - "--assume-yes/--assume-no", - help="Automatic yes/no to prompts; assume 'yes'/'no' as answer to all prompts and run non-interactively.", - is_flag=True, - default=None, -) -@click.option( - "-d", - "--deprecate", - help="Deprecates the integration/script/playbook.", - is_flag=True, -) -@click.option( - "-g", - "--use-git", - help="Use git to automatically recognize which files changed and run format on them.", - is_flag=True, -) -@click.option( - "--prev-ver", help="Previous branch or SHA1 commit to run checks against." -) -@click.option( - "-iu", - "--include-untracked", - is_flag=True, - help="Whether to include untracked files in the formatting.", -) -@click.option( - "-at", - "--add-tests", - is_flag=True, - help="Whether to answer manually to add tests configuration prompt when running interactively.", -) -@click.option( - "-s", - "--id-set-path", - help="Deprecated. The path of the id_set json file.", - type=click.Path(exists=True, resolve_path=True), -) -@click.option( - "-gr/-ngr", - "--graph/--no-graph", - help="Whether to use the content graph or not.", - is_flag=True, - default=True, -) -@click.argument("file_paths", nargs=-1, type=click.Path(exists=True, resolve_path=True)) -@click.pass_context -@logging_setup_decorator -def format(ctx, **kwargs): - """Run formatter on a given script/playbook/integration/incidentfield/indicatorfield/ - incidenttype/indicatortype/layout/dashboard/classifier/mapper/widget/report file/genericfield/generictype/ - genericmodule/genericdefinition. - """ - from demisto_sdk.commands.format.format_module import format_manager - - if is_sdk_defined_working_offline(): - logger.error(SDK_OFFLINE_ERROR_MESSAGE) - sys.exit(1) - - update_command_args_from_config_file("format", kwargs) - _input = kwargs.get("input") - file_paths = kwargs.get("file_paths") or [] - output = kwargs.get("output") - - if file_paths and not _input: - _input = ",".join(file_paths) - - with ReadMeValidator.start_mdx_server(): - return format_manager( - str(_input) if _input else None, - str(output) if output else None, - from_version=kwargs.get("from_version", ""), - no_validate=kwargs.get("no_validate", False), - update_docker=kwargs.get("update_docker", False), - assume_answer=kwargs.get("assume_yes"), - deprecate=kwargs.get("deprecate", False), - use_git=kwargs.get("use_git", False), - prev_ver=kwargs.get("prev_ver"), - include_untracked=kwargs.get("include_untracked", False), - add_tests=kwargs.get("add_tests", False), - id_set_path=kwargs.get("id_set_path"), - use_graph=kwargs.get("graph", True), - ) - - -# ====================== upload ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - type=PathsParamType(exists=True, resolve_path=True), - help="The path of file or a directory to upload. The following are supported:\n" - "- Pack\n" - "- Directory inside a pack. For example: Integrations\n" - "- Directory containing an integration or a script data for example: HelloWorld\n" - "- Valid file that can be imported to Cortex XSOAR manually. For example a playbook: helloWorld.yml" - "- Path to zipped pack (may locate outside the Content directory).", - required=False, -) -@click.option( - "--input-config-file", - type=PathsParamType(exists=True, resolve_path=True), - help="The path to the config file to download all the custom packs from.", - required=False, -) -@click.option( - "-z/-nz", - "--zip/--no-zip", - help="Compress the pack to zip before upload, this flag is relevant only for packs.", - is_flag=True, - default=True, -) -@click.option( - "-tpb", - help="Adds the test playbook for upload when the -tpb flag is used. This flag is relevant only for packs.", - is_flag=True, - default=False, -) -@click.option( - "-x", - "--xsiam", - help="Upload the pack to XSIAM server. Must be used together with -z.", - is_flag=True, -) -@click.option( - "-mp", - "--marketplace", - help="The marketplace to which the content will be uploaded.", -) -@click.option( - "--keep-zip", - help="Directory where to store the zip after creation, this argument is relevant only for packs " - "and in case the --zip flag is used.", - required=False, - type=click.Path(exists=True), -) -@click.option("--insecure", help="Skip certificate validation", is_flag=True) -@click.option( - "--skip_validation", - is_flag=True, - help="Only for upload zipped packs, " - "if true will skip upload packs validation, use just when migrate existing custom content to packs.", -) -@click.option( - "--reattach", - help="Reattach the detached files in the XSOAR instance" - "for the CI/CD Flow. If you set the --input-config-file flag, " - "any detached item in your XSOAR instance that isn't currently in the repo's SystemPacks folder " - "will be re-attached.)", - is_flag=True, -) -@click.option( - "--override-existing", - is_flag=True, - help="This value (True/False) determines if the user should be presented with a confirmation prompt when " - "attempting to upload a content pack that is already installed on the Cortex XSOAR server. This allows the upload " - "command to be used within non-interactive shells.", -) -@click.pass_context -@logging_setup_decorator -def upload(ctx, **kwargs): - """Upload integration or pack to Demisto instance. - DEMISTO_BASE_URL environment variable should contain the Demisto server base URL. - DEMISTO_API_KEY environment variable should contain a valid Demisto API Key. - * Note: Uploading classifiers to Cortex XSOAR is available from version 6.0.0 and up. * - """ - return upload_content_entity(**kwargs) - - -# ====================== download ====================== # - - -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-o", - "--output", - help="A path to a pack directory to download custom content to.", - required=False, - multiple=False, -) -@click.option( - "-i", - "--input", - help="Custom content file name to be downloaded. Can be provided multiple times. " - "File names can be retrieved using the -lf flag.", - required=False, - multiple=True, -) -@click.option( - "-r", - "--regex", - help="Regex Pattern. When specified, download all the custom content files with a name that matches this regex pattern.", - required=False, -) -@click.option("--insecure", help="Skip certificate validation.", is_flag=True) -@click.option( - "-f", - "--force", - help="Whether to override existing files.", - is_flag=True, -) -@click.option( - "-lf", - "--list-files", - help="List all custom content items available to download.", - is_flag=True, -) -@click.option( - "-a", - "--all-custom-content", - help="Download all available custom content items.", - is_flag=True, -) -@click.option( - "-fmt", - "--run-format", - help="Whether to run Demisto SDK formatting on downloaded files.", - is_flag=True, -) -@click.option("--system", help="Download system items.", is_flag=True, default=False) -@click.option( - "-it", - "--item-type", - help="Type of the content item to download, use only when downloading system items.", - type=click.Choice( - [ - "IncidentType", - "IndicatorType", - "Field", - "Layout", - "Playbook", - "Automation", - "Classifier", - "Mapper", - ], - case_sensitive=False, - ), -) -@click.option( - "--init", - help="Initialize the output directory with a pack structure.", - is_flag=True, - default=False, -) -@click.option( - "--keep-empty-folders", - help="Keep empty folders when a pack structure is initialized.", - is_flag=True, - default=False, -) -@click.option( - "--auto-replace-uuids/--no-auto-replace-uuids", - help="If False, avoid UUID replacements when downloading using the download command. The default value is True.", - default=True, -) -@click.pass_context -@logging_setup_decorator -def download(ctx, **kwargs): - """Download custom content from a Cortex XSOAR / XSIAM instance. - DEMISTO_BASE_URL environment variable should contain the server base URL. - DEMISTO_API_KEY environment variable should contain a valid API Key for the server. - """ - from demisto_sdk.commands.download.downloader import Downloader - - update_command_args_from_config_file("download", kwargs) - return Downloader(**kwargs).download() - - -# ====================== update-xsoar-config-file ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-pi", - "--pack-id", - help="The Pack ID to add to XSOAR Configuration File.", - required=False, - multiple=False, -) -@click.option( - "-pd", - "--pack-data", - help="The Pack Data to add to XSOAR Configuration File - " - "Pack URL for Custom Pack and Pack Version for OOTB Pack.", - required=False, - multiple=False, -) -@click.option( - "-mp", - "--add-marketplace-pack", - help="Add a Pack to the MarketPlace Packs section in the Configuration File", - required=False, - is_flag=True, -) -@click.option( - "-cp", - "--add-custom-pack", - help="Add the Pack to the Custom Packs section in the Configuration File", - is_flag=True, -) -@click.option( - "-all", - "--add-all-marketplace-packs", - help="Add all the installed MarketPlace Packs to the marketplace_packs in XSOAR Configuration File", - is_flag=True, -) -@click.option("--insecure", help="Skip certificate validation", is_flag=True) -@click.option( - "--file-path", - help="XSOAR Configuration File path, the default value is in the repo level", - is_flag=False, -) -@click.pass_context -@logging_setup_decorator -def xsoar_config_file_update(ctx, **kwargs): - """Handle your XSOAR Configuration File. - Add automatically all the installed MarketPlace Packs to the marketplace_packs section in XSOAR Configuration File. - Add a Pack to both marketplace_packs and custom_packs sections in the Configuration File. - """ - from demisto_sdk.commands.update_xsoar_config_file.update_xsoar_config_file import ( - XSOARConfigFileUpdater, - ) - - file_updater: XSOARConfigFileUpdater = XSOARConfigFileUpdater(**kwargs) - return file_updater.update() - - -# ====================== run ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option("-q", "--query", help="The query to run.", required=True) -@click.option("--insecure", help="Skip certificate validation.", is_flag=True) -@click.option( - "-id", - "--incident-id", - help="The incident to run the query on, if not specified the playground will be used.", -) -@click.option( - "-d", - "--debug", - help="Whether to enable the debug-mode feature or not, if you want to save the output file " - "please use the --debug-path option.", - is_flag=True, -) -@click.option( - "--debug-path", - help="The path to save the debug file at, if not specified the debug file will be printed to the " - "terminal.", -) -@click.option( - "--json-to-outputs", - help="Whether to run json_to_outputs command on the context output of the query. If the " - "context output does not exist or the `-r` flag is used, will use the raw" - " response of the query.", - is_flag=True, -) -@click.option( - "-p", - "--prefix", - help="Used with `json-to-outputs` flag. Output prefix e.g. Jira.Ticket, VirusTotal.IP, " - "the base path for the outputs that the script generates.", -) -@click.option( - "-r", - "--raw-response", - help="Used with `json-to-outputs` flag. Use the raw response of the query for" - " `json-to-outputs`", - is_flag=True, -) -@click.pass_context -@logging_setup_decorator -def run(ctx, **kwargs): - """Run integration command on remote Demisto instance in the playground. - DEMISTO_BASE_URL environment variable should contain the Demisto base URL. - DEMISTO_API_KEY environment variable should contain a valid Demisto API Key. - """ - from demisto_sdk.commands.run_cmd.runner import Runner - - update_command_args_from_config_file("run", kwargs) - runner = Runner(**kwargs) - return runner.run() - - -# ====================== run-playbook ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "--url", - "-u", - help="URL to a Demisto instance. If not provided, the url will be taken from DEMISTO_BASE_URL environment variable.", -) -@click.option("--playbook_id", "-p", help="The playbook ID to run.", required=True) -@click.option( - "--wait", - "-w", - is_flag=True, - help="Wait until the playbook run is finished and get a response.", -) -@click.option( - "--timeout", - "-t", - default=90, - show_default=True, - help="Timeout to query for playbook's state. Relevant only if --wait has been passed.", -) -@click.option("--insecure", help="Skip certificate validation.", is_flag=True) -@click.pass_context -@logging_setup_decorator -def run_playbook(ctx, **kwargs): - """Run a playbook in Demisto. - DEMISTO_API_KEY environment variable should contain a valid Demisto API Key. - Example: DEMISTO_API_KEY= demisto-sdk run-playbook -p 'p_name' -u - 'https://demisto.local'. - """ - from demisto_sdk.commands.run_playbook.playbook_runner import PlaybookRunner - - update_command_args_from_config_file("run-playbook", kwargs) - playbook_runner = PlaybookRunner( - playbook_id=kwargs.get("playbook_id", ""), - url=kwargs.get("url", ""), - wait=kwargs.get("wait", False), - timeout=kwargs.get("timeout", 90), - insecure=kwargs.get("insecure", False), - ) - return playbook_runner.run_playbook() - - -# ====================== run-test-playbook ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-tpb", - "--test-playbook-path", - help="Path to test playbook to run, " - "can be a path to specific test playbook or path to pack name for example: Packs/GitHub.", - required=False, -) -@click.option( - "--all", is_flag=True, help="Run all the test playbooks from this repository." -) -@click.option( - "--wait", - "-w", - is_flag=True, - default=True, - help="Wait until the test-playbook run is finished and get a response.", -) -@click.option( - "--timeout", - "-t", - default=90, - show_default=True, - help="Timeout for the command in seconds. The test-playbook will continue to run in XSOAR.", -) -@click.option("--insecure", help="Skip certificate validation.", is_flag=True) -@click.pass_context -@logging_setup_decorator -def run_test_playbook(ctx, **kwargs): - """Run a test playbooks in your instance.""" - from demisto_sdk.commands.run_test_playbook.test_playbook_runner import ( - TestPlaybookRunner, - ) - - update_command_args_from_config_file("run-test-playbook", kwargs) - test_playbook_runner = TestPlaybookRunner(**kwargs) - ctx.exit(test_playbook_runner.manage_and_run_test_playbooks()) - - -# ====================== generate-outputs ====================== # -@main.command(short_help="""Generates outputs (from json or examples).""") -@click.help_option("-h", "--help") -@click.option( - "-c", - "--command", - help="Command name (e.g. xdr-get-incidents)", - required=False, -) -@click.option( - "-j", - "--json", - help="A JSON file path. If not specified, the script will wait for user input in the terminal. " - "The response can be obtained by running the command with `raw-response=true` argument.", - required=False, -) -@click.option( - "-p", - "--prefix", - help="Output prefix like Jira.Ticket, VirusTotal.IP, the base path for the outputs that the " - "script generates.", - required=False, -) -@click.option( - "-o", - "--output", - help="Output file path, if not specified then will print to stdout.", - required=False, -) -@click.option( - "--ai", - is_flag=True, - help="**Experimental** - Help generate context descriptions via AI transformers (must have a valid AI21 key at ai21.com)", -) -@click.option( - "--interactive", - help="If passed, then for each output field will ask user interactively to enter the " - "description. By default the interactive mode is disabled.", - is_flag=True, -) -@click.option( - "-d", - "--descriptions", - help="A JSON or a path to a JSON file, mapping field names to their descriptions. " - "If not specified, the script prompt the user to input the JSON content.", - is_flag=True, -) -@click.option( - "-i", - "--input", - help="Path of the yml file (outputs are inserted here in-place) - used for context from examples.", - required=False, -) -@click.option( - "-e", - "--examples", - help="Integrations: path for file containing command examples." - " Each command should be in a separate line." - " Scripts: the script example surrounded by quotes." - " For example: -e '!ConvertFile entry_id='", -) -@click.option( - "--insecure", - help="Skip certificate validation.", - is_flag=True, -) -@click.pass_context -@logging_setup_decorator -def generate_outputs(ctx, **kwargs): - """Demisto integrations/scripts have a YAML file that defines them. - Creating the YAML file is a tedious and error-prone task of manually copying outputs from the API result to the - file/UI/PyCharm. This script auto generates the YAML for a command from the JSON result of the relevant API call - In addition you can supply examples files and generate the context description directly in the YML from those examples. - """ - from demisto_sdk.commands.generate_outputs.generate_outputs import ( - run_generate_outputs, - ) - - update_command_args_from_config_file("generate-outputs", kwargs) - return run_generate_outputs(**kwargs) - - -# ====================== generate-test-playbook ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", "--input", required=True, help="Specify integration/script yml path." -) -@click.option( - "-o", - "--output", - required=False, - help="Specify output directory or path to an output yml file. " - "If a path to a yml file is specified - it will be the output path.\n" - "If a folder path is specified - a yml output will be saved in the folder.\n" - "If not specified, and the input is located at `.../Packs//Integrations`, " - "the output will be saved under `.../Packs//TestPlaybooks`.\n" - "Otherwise (no folder in the input hierarchy is named `Packs`), " - "the output will be saved in the current directory.", -) -@click.option( - "-n", - "--name", - required=True, - help="Specify test playbook name. The output file name will be `playbook-_Test.yml.", -) -@click.option( - "--no-outputs", - is_flag=True, - help="Skip generating verification conditions for each output contextPath. Use when you want to decide which " - "outputs to verify and which not.", -) -@click.option( - "-ab", - "--all-brands", - "use_all_brands", - help="Generate a test-playbook which calls commands using integrations of all available brands. " - "When not used, the generated playbook calls commands using instances of the provided integration brand.", - is_flag=True, -) -@click.option( - "-c", - "--commands", - help="A comma-separated command names to generate playbook tasks for, " - "will ignore the rest of the commands." - "e.g xdr-get-incidents,xdr-update-incident", - required=False, -) -@click.option( - "-e", - "--examples", - help="For integrations: path for file containing command examples." - " Each command should be in a separate line." - " For scripts: the script example surrounded by quotes." - " For example: -e '!ConvertFile entry_id='", -) -@click.option( - "-u", - "--upload", - help="Whether to upload the test playbook after the generation.", - is_flag=True, -) -@click.pass_context -@logging_setup_decorator -def generate_test_playbook(ctx, **kwargs): - """Generate test playbook from integration or script""" - from demisto_sdk.commands.generate_test_playbook.test_playbook_generator import ( - PlaybookTestsGenerator, - ) - - update_command_args_from_config_file("generate-test-playbook", kwargs) - file_type: FileType = find_type(kwargs.get("input", ""), ignore_sub_categories=True) - if file_type not in [FileType.INTEGRATION, FileType.SCRIPT]: - logger.error( - "Generating test playbook is possible only for an Integration or a Script." - ) - return 1 - - try: - generator = PlaybookTestsGenerator(file_type=file_type.value, **kwargs) - if generator.run(): - sys.exit(0) - sys.exit(1) - except PlaybookTestsGenerator.InvalidOutputPathError as e: - logger.info(f"{e}") - return 1 - - -# ====================== init ====================== # - - -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-n", - "--name", - help="The name given to the files and directories of new pack/integration/script being created.", -) -@click.option("--id", help="The id used in the yml file of the integration or script.") -@click.option( - "-o", - "--output", - help="The output directory to which the created object will be saved. The default one is the current working " - "directory.", -) -@click.option( - "--integration", - is_flag=True, - help="Create an Integration.", -) -@click.option("--script", is_flag=True, help="Create a Script.") -@click.option( - "--xsiam", - is_flag=True, - help="Create an Event Collector based on a template, and create matching subdirectories.", -) -@click.option("--pack", is_flag=True, help="Create pack and its sub directories.") -@click.option( - "-t", - "--template", - help="Create an Integration/Script based on a specific template.\n" - "Integration template options: HelloWorld, HelloIAMWorld, FeedHelloWorld.\n" - "Script template options: HelloWorldScript", -) -@click.option( - "-a", - "--author-image", - help="Path of the file 'Author_image.png'. \n " - "Image will be presented in marketplace under PUBLISHER section. File should be up to 4kb and dimensions of 120x50.", -) -@click.option( - "--demisto_mock", - is_flag=True, - help="Copy the demistomock. Relevant for initialization of Scripts and Integrations within a Pack.", -) -@click.option( - "--common-server", - is_flag=True, - help="Copy the CommonServerPython. Relevant for initialization of Scripts and Integrations within a Pack.", -) -@click.pass_context -@logging_setup_decorator -def init(ctx, **kwargs): - """Initialize a new Pack, Integration or Script. - If the script/integration flags are not present, we will create a pack with the given name. - Otherwise when using the flags we will generate a script/integration based on your selection. - """ - from demisto_sdk.commands.init.initiator import Initiator - - update_command_args_from_config_file("init", kwargs) - marketplace = parse_marketplace_kwargs(kwargs) - initiator = Initiator(marketplace=marketplace, **kwargs) - initiator.init() - return 0 - - -# ====================== generate-docs ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option("-i", "--input", help="Path of the yml file.", required=True) -@click.option( - "-o", - "--output", - help="The output directory to write the documentation file to. Documentation file name is README.md. " - "If not specified, written to the YAML directory.", - required=False, -) -@click.option( - "-uc", - "--use_cases", - help="For integrations - provide a list of use-cases that should appear in the generated docs. " - "Create an unordered list by using * before each use-case (i.e. '* foo. * bar.')", - required=False, -) -@click.option( - "-c", - "--command", - help="A comma-separated list of command names to generate documentation for. The rest of the commands are ignored." - "e.g xdr-get-incidents,xdr-update-incident", - required=False, -) -@click.option( - "-e", - "--examples", - help="Integrations: path for file containing command examples." - " Each command should be in a separate line." - " Scripts: the script example surrounded by quotes." - " For example: -e '!ConvertFile entry_id='", -) -@click.option( - "-p", - "--permissions", - type=click.Choice(["none", "general", "per-command"]), - help="The needed permissions.", - required=True, - default="none", -) -@click.option( - "-cp", - "--command-permissions", - help="Path for file containing commands permissions" - " Each command permissions should be in a separate line." - " (i.e. ' Administrator READ-WRITE')", - required=False, -) -@click.option( - "-l", - "--limitations", - help="Known limitations. Create an unordered list by using * before each use-case. (i.e. '* foo. * bar.')", - required=False, -) -@click.option( - "--insecure", - help="Skip certificate validation.", - is_flag=True, -) -@click.option("--old-version", help="Path of the old integration version YML file.") -@click.option( - "--skip-breaking-changes", - is_flag=True, - help="SDo not generate the breaking changes section.", -) -@click.option( - "--custom-image-path", - help="A custom path to a playbook image. If not stated, a default link will be added to the file.", -) -@click.option( - "-rt", - "--readme-template", - help="The readme template that should be appended to the given README.md file.", - type=click.Choice(["syslog", "xdrc", "http-collector"]), -) -@click.option( - "-gr/-ngr", - "--graph/--no-graph", - help="Whether to use the content graph", - is_flag=True, - default=True, -) -@click.option( - "-f", - "--force", - help="Whether to force the generation of documentation (rather than update when it exists in version control)", - is_flag=True, - default=False, -) -@click.pass_context -@logging_setup_decorator -def generate_docs(ctx, **kwargs): - """Generate documentation for integration, playbook or script from yaml file.""" - try: - update_command_args_from_config_file("generate-docs", kwargs) - input_path_str: str = kwargs.get("input", "") - if not (input_path := Path(input_path_str)).exists(): - raise Exception(f"input {input_path_str} does not exist") - - if (output_path := kwargs.get("output")) and not Path(output_path).is_dir(): - raise Exception( - f"Output directory {output_path} is not a directory." - ) - - if input_path.is_file(): - if input_path.suffix.lower() not in {".yml", ".md"}: - raise Exception( - f"input {input_path} is not a valid yml or readme file." - ) - - _generate_docs_for_file(kwargs) - - # Add support for input which is a Playbooks directory and not a single yml file - elif input_path.is_dir() and input_path.name == "Playbooks": - for yml in input_path.glob("*.yml"): - file_kwargs = copy.deepcopy(kwargs) - file_kwargs["input"] = str(yml) - _generate_docs_for_file(file_kwargs) - - else: - raise Exception( - f"Input {input_path} is neither a valid yml file, nor a folder named Playbooks, nor a readme file." - ) - - return 0 - - except Exception: - logger.exception("Failed generating docs") - sys.exit(1) - - -def _generate_docs_for_file(kwargs: Dict[str, Any]): - """Helper function for supporting Playbooks directory as an input and not only a single yml file.""" - - from demisto_sdk.commands.generate_docs.generate_integration_doc import ( - generate_integration_doc, - ) - from demisto_sdk.commands.generate_docs.generate_playbook_doc import ( - generate_playbook_doc, - ) - from demisto_sdk.commands.generate_docs.generate_readme_template import ( - generate_readme_template, - ) - from demisto_sdk.commands.generate_docs.generate_script_doc import ( - generate_script_doc, - ) - - # Extract all the necessary arguments - input_path: str = kwargs.get("input", "") - output_path = kwargs.get("output") - command = kwargs.get("command") - examples: str = kwargs.get("examples", "") - permissions = kwargs.get("permissions") - limitations = kwargs.get("limitations") - insecure: bool = kwargs.get("insecure", False) - old_version: str = kwargs.get("old_version", "") - skip_breaking_changes: bool = kwargs.get("skip_breaking_changes", False) - custom_image_path: str = kwargs.get("custom_image_path", "") - readme_template: str = kwargs.get("readme_template", "") - use_graph = kwargs.get("graph", True) - force = kwargs.get("force", False) - - try: - if command: - if ( - output_path - and (not Path(output_path, INTEGRATIONS_README_FILE_NAME).is_file()) - or (not output_path) - and ( - not Path( - os.path.dirname(os.path.realpath(input_path)), - INTEGRATIONS_README_FILE_NAME, - ).is_file() - ) - ): - raise Exception( - f"The `command` argument must be presented with existing `{INTEGRATIONS_README_FILE_NAME}` docs." - ) - - file_type = find_type(kwargs.get("input", ""), ignore_sub_categories=True) - if file_type not in { - FileType.INTEGRATION, - FileType.SCRIPT, - FileType.PLAYBOOK, - FileType.README, - }: - raise Exception( - "File is not an Integration, Script, Playbook or a README." - ) - - if old_version and not Path(old_version).is_file(): - raise Exception( - f"Input old version file {old_version} was not found." - ) - - if old_version and not old_version.lower().endswith(".yml"): - raise Exception( - f"Input old version {old_version} is not a valid yml file." - ) - - if file_type == FileType.INTEGRATION: - logger.info(f"Generating {file_type.value.lower()} documentation") - use_cases = kwargs.get("use_cases") - command_permissions = kwargs.get("command_permissions") - return generate_integration_doc( - input_path=input_path, - output=output_path, - use_cases=use_cases, - examples=examples, - permissions=permissions, - command_permissions=command_permissions, - limitations=limitations, - insecure=insecure, - command=command, - old_version=old_version, - skip_breaking_changes=skip_breaking_changes, - force=force, - ) - elif file_type == FileType.SCRIPT: - logger.info(f"Generating {file_type.value.lower()} documentation") - return generate_script_doc( - input_path=input_path, - output=output_path, - examples=examples, - permissions=permissions, - limitations=limitations, - insecure=insecure, - use_graph=use_graph, - ) - elif file_type == FileType.PLAYBOOK: - logger.info(f"Generating {file_type.value.lower()} documentation") - return generate_playbook_doc( - input_path=input_path, - output=output_path, - permissions=permissions, - limitations=limitations, - custom_image_path=custom_image_path, - ) - - elif file_type == FileType.README: - logger.info(f"Adding template to {file_type.value.lower()} file") - return generate_readme_template( - input_path=Path(input_path), readme_template=readme_template - ) - - else: - raise Exception(f"File type {file_type.value} is not supported.") - - except Exception: - logger.exception(f"Failed generating docs for {input_path}") - sys.exit(1) - - -# ====================== create-id-set ====================== # -@main.command(hidden=True) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="Input file path, the default is the content repo.", - default="", -) -@click.option( - "-o", - "--output", - help="Output file path, the default is the Tests directory.", - default="", -) -@click.option( - "-fd", - "--fail-duplicates", - help="Fails the process if any duplicates are found.", - is_flag=True, -) -@click.option( - "-mp", - "--marketplace", - help="The marketplace the id set are created for, that determines which packs are" - " inserted to the id set, and which items are present in the id set for " - "each pack. Default is all packs exists in the content repository.", - default="", -) -@click.pass_context -@logging_setup_decorator -def create_id_set(ctx, **kwargs): - """Create the content dependency tree by ids.""" - from demisto_sdk.commands.create_id_set.create_id_set import IDSetCreator - from demisto_sdk.commands.find_dependencies.find_dependencies import ( - remove_dependencies_from_id_set, - ) - - update_command_args_from_config_file("create-id-set", kwargs) - id_set_creator = IDSetCreator(**kwargs) - ( - id_set, - excluded_items_by_pack, - excluded_items_by_type, - ) = id_set_creator.create_id_set() - - if excluded_items_by_pack: - remove_dependencies_from_id_set( - id_set, - excluded_items_by_pack, - excluded_items_by_type, - kwargs.get("marketplace", ""), - ) - id_set_creator.save_id_set() - - -# ====================== merge-id-sets ====================== # -@main.command(hidden=True) -@click.help_option("-h", "--help") -@click.option("-i1", "--id-set1", help="First id_set.json file path", required=True) -@click.option("-i2", "--id-set2", help="Second id_set.json file path", required=True) -@click.option("-o", "--output", help="File path of the united id_set", required=True) -@click.option( - "-fd", - "--fail-duplicates", - help="Fails the process if any duplicates are found.", - is_flag=True, -) -@click.pass_context -@logging_setup_decorator -def merge_id_sets(ctx, **kwargs): - """Merge two id_sets""" - from demisto_sdk.commands.common.update_id_set import merge_id_sets_from_files - - update_command_args_from_config_file("merge-id-sets", kwargs) - first = kwargs["id_set1"] - second = kwargs["id_set2"] - output = kwargs["output"] - fail_duplicates = kwargs["fail_duplicates"] - - _, duplicates = merge_id_sets_from_files( - first_id_set_path=first, second_id_set_path=second, output_id_set_path=output - ) - if duplicates: - logger.info( - f"Failed to merge ID sets: {first} with {second}, " - f"there are entities with ID: {duplicates} that exist in both ID sets" - ) - if fail_duplicates: - sys.exit(1) - - -# ====================== update-release-notes =================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="The path of the content pack you wish to generate release notes for.", -) -@click.option( - "-u", - "--update-type", - help="The type of update being done.", - type=click.Choice(["major", "minor", "revision", "documentation"]), -) -@click.option( - "-v", - "--version", - help="Bump to a specific version. Cannot be used with -u, --update_type flag.", - type=VersionParamType(), -) -@click.option( - "-g", - "--use-git", - help="Use git to identify the relevant changed files, will be used by default if '-i' is not set.", - is_flag=True, -) -@click.option( - "-f", - "--force", - help="Update the release notes of a pack even if no changes that require update were made.", - is_flag=True, -) -@click.option( - "--text", - help="Text to add to all the release notes files.", -) -@click.option( - "--prev-ver", help="Previous branch or SHA1 commit to run checks against." -) -@click.option( - "--pre_release", - help="Indicates that this update is for a pre-release version. " - "The currentVersion will change to reflect the pre-release version number.", - is_flag=True, -) -@click.option( - "-idp", - "--id-set-path", - help="The path of the id-set.json used for APIModule updates.", - type=click.Path(resolve_path=True), -) -@click.option( - "-bc", - "--breaking-changes", - help="If new version contains breaking changes.", - is_flag=True, -) -@click.pass_context -@logging_setup_decorator -def update_release_notes(ctx, **kwargs): - """Auto-increment pack version and generate release notes template.""" - from demisto_sdk.commands.update_release_notes.update_rn_manager import ( - UpdateReleaseNotesManager, - ) - - if is_sdk_defined_working_offline(): - logger.error(SDK_OFFLINE_ERROR_MESSAGE) - sys.exit(1) - - update_command_args_from_config_file("update-release-notes", kwargs) - if kwargs.get("force") and not kwargs.get("input"): - logger.info( - "Please add a specific pack in order to force a release notes update." - ) - sys.exit(0) - - if not kwargs.get("use_git") and not kwargs.get("input"): - click.confirm( - "No specific pack was given, do you want to update all changed packs?", - abort=True, - ) - - try: - rn_mng = UpdateReleaseNotesManager( - user_input=kwargs.get("input"), - update_type=kwargs.get("update_type"), - pre_release=kwargs.get("pre_release", False), - is_all=kwargs.get("use_git"), - text=kwargs.get("text"), - specific_version=kwargs.get("version"), - id_set_path=kwargs.get("id_set_path"), - prev_ver=kwargs.get("prev_ver"), - is_force=kwargs.get("force", False), - is_bc=kwargs.get("breaking_changes", False), - ) - rn_mng.manage_rn_update() - sys.exit(0) - except Exception as e: - logger.info( - f"An error occurred while updating the release notes: {str(e)}" - ) - sys.exit(1) - - -# ====================== find-dependencies ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="Pack path to find dependencies. For example: Pack/HelloWorld. When using the" - " --get-dependent-on flag, this argument can be used multiple times.", - required=False, - type=click.Path(exists=True, dir_okay=True), - multiple=True, -) -@click.option( - "-idp", - "--id-set-path", - help="Path to id set json file.", - required=False, - default="", -) -@click.option( - "--no-update", - help="Use to find the pack dependencies without updating the pack metadata.", - required=False, - is_flag=True, -) -@click.option( - "--use-pack-metadata", - help="Whether to update the dependencies from the pack metadata.", - required=False, - is_flag=True, -) -@click.option( - "--all-packs-dependencies", - help="Return a json file with ALL content packs dependencies. " - "The json file will be saved under the path given in the " - "'--output-path' argument", - required=False, - is_flag=True, -) -@click.option( - "-o", - "--output-path", - help="The destination path for the packs dependencies json file. " - "This argument only works when using either the --all-packs-dependencies or --get-dependent-on flags.", - required=False, -) -@click.option( - "--get-dependent-on", - help="Get only the packs dependent ON the given pack. Note: this flag can not be" - " used for the packs ApiModules and Base", - required=False, - is_flag=True, -) -@click.option( - "-d", - "--dependency", - help="Find which items in a specific content pack appears as a mandatory " - "dependency of the searched pack ", - required=False, -) -@click.pass_context -@logging_setup_decorator -def find_dependencies(ctx, **kwargs): - """Find pack dependencies and update pack metadata.""" - from demisto_sdk.commands.find_dependencies.find_dependencies import ( - PackDependencies, - ) - - update_command_args_from_config_file("find-dependencies", kwargs) - update_pack_metadata = not kwargs.get("no_update") - input_paths = kwargs.get("input") # since it can be multiple, received as a tuple - id_set_path = kwargs.get("id_set_path", "") - use_pack_metadata = kwargs.get("use_pack_metadata", False) - all_packs_dependencies = kwargs.get("all_packs_dependencies", False) - get_dependent_on = kwargs.get("get_dependent_on", False) - output_path = kwargs.get("output_path", ALL_PACKS_DEPENDENCIES_DEFAULT_PATH) - dependency = kwargs.get("dependency", "") - try: - PackDependencies.find_dependencies_manager( - id_set_path=str(id_set_path), - update_pack_metadata=update_pack_metadata, - use_pack_metadata=use_pack_metadata, - input_paths=input_paths, - all_packs_dependencies=all_packs_dependencies, - get_dependent_on=get_dependent_on, - output_path=output_path, - dependency=dependency, - ) - - except ValueError as exp: - logger.info(f"{exp}") - - -# ====================== postman-codegen ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="The Postman collection 2.1 JSON file.", - required=True, - type=click.File(), -) -@click.option( - "-o", - "--output", - help="Directory to store the output in (default is current working directory).", - type=click.Path(dir_okay=True, exists=True), - default=Path("."), - show_default=True, -) -@click.option("-n", "--name", help="The output integration name.") -@click.option( - "-op", - "--output-prefix", - help="The global integration output prefix. By default, it is the product name.", -) -@click.option( - "-cp", - "--command-prefix", - help="The prefix for each command in the integration. By default, is the product name in lower case.", -) -@click.option( - "--config-out", - help="Used for advanced integration customisation. Generates a config json file instead of integration.", - is_flag=True, -) -@click.option( - "-p", - "--package", - help="Generated integration will be split to package format instead of a yml file.", - is_flag=True, -) -@pass_config -@click.pass_context -@logging_setup_decorator -def postman_codegen( - ctx, - config, - input: IO, - output: Path, - name: str, - output_prefix: str, - command_prefix: str, - config_out: bool, - package: bool, - **kwargs, -): - """Generates a Cortex XSOAR integration given a Postman collection 2.1 JSON file.""" - from demisto_sdk.commands.postman_codegen.postman_codegen import ( - postman_to_autogen_configuration, - ) - from demisto_sdk.commands.split.ymlsplitter import YmlSplitter - - postman_config = postman_to_autogen_configuration( - collection=json.load(input), - name=name, - command_prefix=command_prefix, - context_path_prefix=output_prefix, - ) - - if config_out: - path = Path(output) / f"config-{postman_config.name}.json" - path.write_text(json.dumps(postman_config.to_dict(), indent=4)) - logger.info(f"Config file generated at:\n{str(path.absolute())}") - else: - # generate integration yml - yml_path = postman_config.generate_integration_package(output, is_unified=True) - if package: - yml_splitter = YmlSplitter( - configuration=config.configuration, - file_type=FileType.INTEGRATION, - input=str(yml_path), - output=str(output), - ) - yml_splitter.extract_to_package_format() - logger.info( - f"Package generated at {str(Path(output).absolute())} successfully" - ) - else: - logger.info( - f"Integration generated at {str(yml_path.absolute())} successfully" - ) - - -# ====================== generate-integration ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - help="config json file produced by commands like postman-codegen and openapi-codegen.", - required=True, - type=click.File(), -) -@click.option( - "-o", - "--output", - help="The output directory to save the integration package.", - type=click.Path(dir_okay=True, exists=True), - default=Path("."), -) -@click.pass_context -@logging_setup_decorator -def generate_integration(ctx, input: IO, output: Path, **kwargs): - """Generates a Cortex XSOAR integration from a config json file, - which is generated by commands like postman-codegen - """ - from demisto_sdk.commands.generate_integration.code_generator import ( - IntegrationGeneratorConfig, - ) - - config_dict = json.load(input) - config = IntegrationGeneratorConfig(**config_dict) - - config.generate_integration_package(output, True) - - -# ====================== openapi-codegen ====================== # -@main.command( - short_help="""Generates a Cortex XSOAR integration given an OpenAPI specification file.""" -) -@click.help_option("-h", "--help") -@click.option( - "-i", "--input_file", help="The swagger file to load in JSON format", required=True -) -@click.option( - "-cf", - "--config_file", - help="The integration configuration file. It is created in the first run of the command", - required=False, -) -@click.option( - "-n", - "--base_name", - help="The base filename to use for the generated files", - required=False, -) -@click.option( - "-o", - "--output_dir", - help="Directory to store the output in (default is current working directory)", - required=False, -) -@click.option( - "-pr", - "--command_prefix", - help="Add a prefix to each command in the code", - required=False, -) -@click.option("-c", "--context_path", help="Context output path", required=False) -@click.option( - "-u", - "--unique_keys", - help="Comma separated unique keys to use in context paths (case sensitive)", - required=False, -) -@click.option( - "-r", - "--root_objects", - help="Comma separated JSON root objects to use in command outputs (case sensitive)", - required=False, -) -@click.option( - "-f", "--fix_code", is_flag=True, help="Fix the python code using autopep8" -) -@click.option( - "-a", - "--use_default", - is_flag=True, - help="Use the automatically generated integration configuration" - " (Skip the second run).", -) -@click.pass_context -@logging_setup_decorator -def openapi_codegen(ctx, **kwargs): - """Generates a Cortex XSOAR integration given an OpenAPI specification file. - In the first run of the command, an integration configuration file is created, which can be modified. - Then, the command is run a second time with the integration configuration to generate the actual integration files. - """ - from demisto_sdk.commands.openapi_codegen.openapi_codegen import OpenAPIIntegration - - update_command_args_from_config_file("openapi-codegen", kwargs) - if not kwargs.get("output_dir"): - output_dir = os.getcwd() - else: - output_dir = kwargs["output_dir"] - - # Check the directory exists and if not, try to create it - if not Path(output_dir).exists(): - try: - os.mkdir(output_dir) - except Exception as err: - logger.info(f"Error creating directory {output_dir} - {err}") - sys.exit(1) - if not os.path.isdir(output_dir): - logger.info(f'The directory provided "{output_dir}" is not a directory') - sys.exit(1) - - input_file = kwargs["input_file"] - base_name = kwargs.get("base_name") - if base_name is None: - base_name = "GeneratedIntegration" - - command_prefix = kwargs.get("command_prefix") - if command_prefix is None: - command_prefix = "-".join(base_name.split(" ")).lower() - - context_path = kwargs.get("context_path") - if context_path is None: - context_path = base_name.replace(" ", "") - - unique_keys = kwargs.get("unique_keys", "") - if unique_keys is None: - unique_keys = "" - - root_objects = kwargs.get("root_objects", "") - if root_objects is None: - root_objects = "" - - fix_code = kwargs.get("fix_code", False) - - configuration = None - if kwargs.get("config_file"): - try: - with open(kwargs["config_file"]) as config_file: - configuration = json.load(config_file) - except Exception as e: - logger.info(f"Failed to load configuration file: {e}") - - logger.info("Processing swagger file...") - integration = OpenAPIIntegration( - input_file, - base_name, - command_prefix, - context_path, - unique_keys=unique_keys, - root_objects=root_objects, - fix_code=fix_code, - configuration=configuration, - ) - - integration.load_file() - if not kwargs.get("config_file"): - integration.save_config(integration.configuration, output_dir) - logger.info(f"Created configuration file in {output_dir}") - if not kwargs.get("use_default", False): - config_path = os.path.join(output_dir, f"{base_name}_config.json") - command_to_run = ( - f'demisto-sdk openapi-codegen -i "{input_file}" -cf "{config_path}" -n "{base_name}" ' - f'-o "{output_dir}" -pr "{command_prefix}" -c "{context_path}"' - ) - if unique_keys: - command_to_run = command_to_run + f' -u "{unique_keys}"' - if root_objects: - command_to_run = command_to_run + f' -r "{root_objects}"' - if kwargs.get("console_log_threshold"): - command_to_run = command_to_run + " -v" - if fix_code: - command_to_run = command_to_run + " -f" - - logger.info( - f"Run the command again with the created configuration file(after a review): {command_to_run}" - ) - sys.exit(0) - - if integration.save_package(output_dir): - logger.info( - f"Successfully finished generating integration code and saved it in {output_dir}", - "green", - ) - else: - logger.info( - f"There was an error creating the package in {output_dir}" - ) - sys.exit(1) - - -# ====================== test-content command ====================== # -@main.command( - short_help="""Created incidents for selected test-playbooks and gives a report about the results""", - hidden=True, -) -@click.help_option("-h", "--help") -@click.option( - "-a", - "--artifacts-path", - help="Destination directory to create the artifacts.", - type=click.Path(file_okay=False, resolve_path=True), - default=Path("./Tests"), - required=True, -) -@click.option( - "-k", "--api-key", help="The Demisto API key for the server", required=True -) -@click.option( - "-ab", - "--artifacts_bucket", - help="The artifacts bucket name to upload the results to", - required=False, -) -@click.option("-s", "--server", help="The server URL to connect to") -@click.option("-c", "--conf", help="Path to content conf.json file", required=True) -@click.option("-e", "--secret", help="Path to content-test-conf conf.json file") -@click.option("-n", "--nightly", type=bool, help="Run nightly tests") -@click.option("-sa", "--service_account", help="GCP service account.") -@click.option("-t", "--slack", help="The token for slack", required=True) -@click.option("-b", "--build-number", help="The build number", required=True) -@click.option( - "-g", "--branch-name", help="The current content branch name", required=True -) -@click.option("-i", "--is-ami", type=bool, help="is AMI build or not", default=False) -@click.option( - "-m", - "--mem-check", - type=bool, - help="Should trigger memory checks or not. The slack channel to check the data is: " - "dmst_content_nightly_memory_data", - default=False, -) -@click.option( - "-d", - "--server-version", - help="Which server version to run the tests on(Valid only when using AMI)", - default="NonAMI", -) -@click.option( - "-u", - "--use-retries", - is_flag=True, - help="Should use retries mechanism or not (if test-playbook fails, it will execute it again few times and " - "determine success according to most of the runs", - default=False, -) -@click.option( - "--server-type", - help="On which server type runs the tests:XSIAM, XSOAR, XSOAR SAAS", - default="XSOAR", -) -@click.option( - "--product-type", - help="On which product type runs the tests:XSIAM, XSOAR", - default="XSOAR", -) -@click.option("--cloud_machine_ids", help="Cloud machine ids to use.") -@click.option("--cloud_servers_path", help="Path to secret cloud server metadata file.") -@click.option( - "--cloud_servers_api_keys", help="Path to file with cloud Servers api keys." -) -@click.option( - "--machine_assignment", - help="Path to the machine assignment file.", - default="./machine_assignment.json", -) -@click.pass_context -@logging_setup_decorator -def test_content(ctx, **kwargs): - """Configure instances for the integration needed to run tests_to_run tests. - Run test module on each integration. - create an investigation for each test. - run test playbook on the created investigation using mock if possible. - Collect the result and give a report. - """ - from demisto_sdk.commands.test_content.execute_test_content import ( - execute_test_content, - ) - - update_command_args_from_config_file("test-content", kwargs) - execute_test_content(**kwargs) - - -# ====================== doc-review ====================== # -@main.command( - context_settings=dict( - ignore_unknown_options=True, - allow_extra_args=True, - ) -) -@click.help_option("-h", "--help") -@click.option( - "-i", "--input", type=str, help="The path to the file to check", multiple=True -) -@click.option( - "--no-camel-case", - is_flag=True, - help="Whether to check CamelCase words", - default=False, -) -@click.option( - "--known-words", - type=str, - help="The path to a file containing additional known words", - multiple=True, -) -@click.option( - "--always-true", - is_flag=True, - help="Whether to fail the command if misspelled words are found", -) -@click.option( - "--expand-dictionary", - is_flag=True, - help="Whether to expand the base dictionary to include more words - " - "will download 'brown' corpus from nltk package", -) -@click.option( - "--templates", is_flag=True, help="Whether to print release notes templates" -) -@click.option( - "-g", - "--use-git", - is_flag=True, - help="Use git to identify the relevant changed files, " - "will be used by default if '-i' and '--templates' are not set", -) -@click.option( - "--prev-ver", - type=str, - help="The branch against which changes will be detected " - "if '-g' flag is set. Default is 'demisto/master'", -) -@click.option( - "-rn", "--release-notes", is_flag=True, help="Will run only on release notes files" -) -@click.option( - "-xs", - "--xsoar-only", - is_flag=True, - help="Run only on files from XSOAR-supported Packs.", - default=False, -) -@click.option( - "-pkw/-spkw", - "--use-packs-known-words/--skip-packs-known-words", - is_flag=True, - help="Will find and load the known_words file from the pack. " - "To use this option make sure you are running from the " - "content directory.", - default=True, -) -@click.pass_context -@logging_setup_decorator -def doc_review(ctx, **kwargs): - """Check the spelling in .md and .yml files as well as review release notes""" - from demisto_sdk.commands.doc_reviewer.doc_reviewer import DocReviewer - - doc_reviewer = DocReviewer( - file_paths=kwargs.get("input", []), - known_words_file_paths=kwargs.get("known_words", []), - no_camel_case=kwargs.get("no_camel_case"), - no_failure=kwargs.get("always_true"), - expand_dictionary=kwargs.get("expand_dictionary"), - templates=kwargs.get("templates"), - use_git=kwargs.get("use_git"), - prev_ver=kwargs.get("prev_ver"), - release_notes_only=kwargs.get("release_notes"), - xsoar_only=kwargs.get("xsoar_only"), - load_known_words_from_pack=kwargs.get("use_packs_known_words"), - ) - result = doc_reviewer.run_doc_review() - if result: - sys.exit(0) - - sys.exit(1) +import os +import platform +import typer +from dotenv import load_dotenv +from pkg_resources import DistributionNotFound, get_distribution -# ====================== integration-diff ====================== # -@main.command( - name="integration-diff", - help="""Given two versions of an integration, Check that everything in the old integration is covered in - the new integration""", -) -@click.help_option("-h", "--help") -@click.option( - "-n", - "--new", - type=str, - help="The path to the new integration yml file.", - required=True, -) -@click.option( - "-o", - "--old", - type=str, - help="The path to the old integration yml file.", - required=True, -) -@click.option( - "--docs-format", - is_flag=True, - help="Whether output should be in the format for the version differences section in README.", +from demisto_sdk.commands.common.configuration import Configuration, DemistoSDK +from demisto_sdk.commands.common.content_constant_paths import CONTENT_PATH +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import ( + get_last_remote_release_version, + get_release_note_entries, + is_sdk_defined_working_offline, ) -@click.pass_context -@logging_setup_decorator -def integration_diff(ctx, **kwargs): - """ - Checks for differences between two versions of an integration, and verified that the new version covered the old version. - """ - from demisto_sdk.commands.integration_diff.integration_diff_detector import ( - IntegrationDiffDetector, - ) - - integration_diff_detector = IntegrationDiffDetector( - new=kwargs.get("new", ""), - old=kwargs.get("old", ""), - docs_format=kwargs.get("docs_format", False), - ) - result = integration_diff_detector.check_different() - - if result: - sys.exit(0) - - sys.exit(1) - - -# ====================== generate_yml_from_python ====================== # -@main.command( +from demisto_sdk.commands.content_graph.content_graph_setup import graph_cmd_group +from demisto_sdk.commands.coverage_analyze.coverage_analyze_setup import ( + coverage_analyze, +) +from demisto_sdk.commands.create_id_set.create_id_set_setup import create_id_set +from demisto_sdk.commands.doc_reviewer.doc_reviewer_setup import doc_review +from demisto_sdk.commands.download.download_setup import download +from demisto_sdk.commands.dump_api.dump_api_setup import dump_api +from demisto_sdk.commands.error_code_info.error_code_info_setup import error_code +from demisto_sdk.commands.find_dependencies.find_dependencies_setup import ( + find_dependencies, +) +from demisto_sdk.commands.format.format_setup import format +from demisto_sdk.commands.generate_docs.generate_docs_setup import generate_docs +from demisto_sdk.commands.generate_integration.generate_integration_setup import ( + generate_integration, +) +from demisto_sdk.commands.generate_modeling_rules.generate_modeling_rules import ( + generate_modeling_rules, +) +from demisto_sdk.commands.generate_outputs.generate_outputs_setup import ( + generate_outputs, +) +from demisto_sdk.commands.generate_unit_tests.generate_unit_tests_setup import ( + generate_unit_tests, +) +from demisto_sdk.commands.generate_yml_from_python.generate_yml_from_python_setup import ( + generate_yml_from_python, +) +from demisto_sdk.commands.init.init_setup import init +from demisto_sdk.commands.integration_diff.intergation_diff_setup import ( + integration_diff, +) +from demisto_sdk.commands.lint.lint_setup import lint +from demisto_sdk.commands.openapi_codegen.openapi_codegen_setup import openapi_codegen +from demisto_sdk.commands.postman_codegen.postman_codegen_setup import postman_codegen +from demisto_sdk.commands.pre_commit.pre_commit_setup import pre_commit +from demisto_sdk.commands.prepare_content.prepare_content_setup import prepare_content +from demisto_sdk.commands.run_cmd.run_cmd_setup import run +from demisto_sdk.commands.run_playbook.run_playbook_setup import run_playbook +from demisto_sdk.commands.run_test_playbook.run_test_playbook_setup import ( + run_test_playbook, +) +from demisto_sdk.commands.secrets.secrets_setup import secrets +from demisto_sdk.commands.setup_env.setup_env_setup import setup_env_command +from demisto_sdk.commands.split.split_setup import split +from demisto_sdk.commands.test_content.content_test_setup import test_content +from demisto_sdk.commands.test_content.test_modeling_rule.modeling_rules_setup import ( + modeling_rules_app, +) +from demisto_sdk.commands.update_release_notes.update_release_notes_setup import ( + update_release_notes, +) +from demisto_sdk.commands.update_xsoar_config_file.update_xsoar_config_file_setup import ( + xsoar_config_file_update, +) +from demisto_sdk.commands.upload.upload_setup import upload +from demisto_sdk.commands.validate.validate_setup import validate +from demisto_sdk.commands.xsoar_linter.xsoar_linter_setup import xsoar_linter +from demisto_sdk.commands.zip_packs.zip_packs_setup import zip_packs + +app = typer.Typer() + +# Registers the commands directly to the Demisto-SDK app. +app.command(name="export-api", help="Dumps the `demisto-sdk` API to a file.")(dump_api) +app.command(name="upload", help="Uploads an entity to Cortex XSOAR or Cortex XSIAM.")( + upload +) +app.command( + name="download", + help="Downloads and merges content from a Cortex XSOAR or Cortex XSIAM tenant to your local repository.", +)(download) +app.command( + name="run", + help="Run integration or script commands in the Playground of a remote Cortex XSOAR/XSIAM instance and pretty print the output. ", +)(run) +app.command( + name="run-playbook", help="Runs the given playbook in Cortex XSOAR or Cortex XSIAM." +)(run_playbook) +app.command( + name="run-test-playbook", + help="This command generates a test playbook from " + "integration/script YAML arguments.", +)(run_test_playbook) +app.command( + name="doc-review", + help="Checks the spelling in Markdown and YAML files and compares release note files " + "to our release note standards.", +)(doc_review) +app.command(name="integration-diff")(integration_diff) +app.command( + name="generate-docs", + help="Generates a README file for your integration, script or playbook. " + "Used to create documentation files for Cortex XSOAR.", +)(generate_docs) +app.command( + name="format", + help="This command formats new or modified files to align with the Cortex standard.", +)(format) +app.command(name="coverage-analyze")(coverage_analyze) +app.command( + name="zip-packs", + help="Creates a zip file that can be uploaded to Cortex XSOAR via the Upload pack button in the Cortex XSOAR Marketplace or directly with the -u flag in this command.", +)(zip_packs) +app.command( + name="split", + help="Splits downloaded scripts, integrations and generic module files into multiple files. Integrations and scripts are split into the package format. Generic modules have their dashboards split into separate files and modify the module to the content repository standard.", +)(split) +app.command(name="find-dependencies")(find_dependencies) +app.command( + name="generate-integration", + help="generate a Cortex XSIAM/Cortex XSOAR integration " + "from an integration config JSON file.", +)(generate_integration) +app.command( + name="generate-outputs", + help="Generates outputs for an integration. " + "This command generates context paths automatically from an example file " + "directly into an integration YAML file.", +)(generate_outputs) +app.command( name="generate-yml-from-python", - help="""Generate YML file from Python code that includes special syntax.\n - The output file name will be the same as the Python code with the `.yml` extension instead of `.py`.\n - The generation currently supports integrations only.\n - For more information on usage and installation visit the command's README.md file.""", -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - type=click.Path(exists=True), - help="The path to the python code to generate from.", - required=True, -) -@click.option( - "-f", - "--force", - is_flag=True, - type=bool, - help="Override existing yml file. If not used and yml file already exists, the script will not generate a new yml file.", - required=False, -) -@click.pass_context -@logging_setup_decorator -def generate_yml_from_python(ctx, **kwargs): - """ - Checks for differences between two versions of an integration, and verified that the new version covered the old version. - """ - from demisto_sdk.commands.generate_yml_from_python.generate_yml import YMLGenerator - - yml_generator = YMLGenerator( - filename=kwargs.get("input", ""), - force=kwargs.get("force", False), - ) - yml_generator.generate() - yml_generator.save_to_yml_file() - - -# ====================== convert ====================== # -@main.command(hidden=True) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - type=click.Path(exists=True), - required=True, - help="The path of a package directory, or entity directory (Layouts, Classifiers) that contains the entities to be converted.", -) -@click.option( - "-v", "--version", required=True, help="Version the input to be compatible with." -) -@pass_config -@click.pass_context -@logging_setup_decorator -def convert(ctx, config, **kwargs): - """ - Deprecated. Convert the content of the pack/directory in the given input to be compatible with the version given by - version command. - """ - from demisto_sdk.commands.convert.convert_manager import ConvertManager - - update_command_args_from_config_file("convert", kwargs) - sys.path.append(config.configuration.env_dir) - - input_path = kwargs["input"] - server_version = kwargs["version"] - convert_manager = ConvertManager(input_path, server_version) - result = convert_manager.convert() - - if result: - sys.exit(1) - - sys.exit(0) - - -# ====================== generate-unit-tests ====================== # - - -@main.command(short_help="""Generates unit tests for integration code.""") -@click.help_option("-h", "--help") -@click.option( - "-c", - "--commands", - help="Specific commands name to generate unit test for (e.g. xdr-get-incidents)", - required=False, -) -@click.option( - "-o", - "--output_dir", - help="Directory to store the output - the generated test file in in (default is the input integration directory)", - required=False, -) -@click.option( - "-i", "--input_path", help="Path of the integration python file.", required=True -) -@click.option( - "-d", - "--use_demisto", - help="If passed, the XSOAR instance configured in the DEMISTO_BASE_URL and DEMISTO_API_KEY " - "environment variables will run the Integration commands and generate outputs which " - "will be used as mock outputs. If this flag is not passed, you will need to create the " - "mocks manually, at the outputs directory, with the name of the command.", - is_flag=True, -) -@click.option("--insecure", help="Skip certificate validation", is_flag=True) -@click.option( - "-e", - "--examples", - help="A path for a file containing Integration command examples. Each command example should be in a " - "separate line.\n A comma-separated list of examples, wrapped by quotes. If the file or the list contains " - "a command with more than one example, all of them will be used as different test cases.", -) -@click.option( - "-a", - "--append", - help="Append generated test file to the existing _test.py. Else, overwriting existing UT.", - is_flag=True, -) -@click.pass_context -@logging_setup_decorator -def generate_unit_tests( - ctx, - input_path: str = "", - commands: list = [], - output_dir: str = "", - examples: str = "", - insecure: bool = False, - use_demisto: bool = False, - append: bool = False, - **kwargs, -): - """ - This command is used to generate unit tests automatically from an integration python code. - Also supports generating unit tests for specific commands. - """ - import logging # noqa: TID251 # special case: controlling external logger - - logging.getLogger("PYSCA").propagate = False - from demisto_sdk.commands.generate_unit_tests.generate_unit_tests import ( - run_generate_unit_tests, - ) - - return run_generate_unit_tests( - input_path, commands, output_dir, examples, insecure, use_demisto, append - ) - - -@main.command( - name="error-code", - help="Quickly find relevant information regarding an error code.", -) -@click.help_option("-h", "--help") -@click.option( - "-i", - "--input", - required=True, - help="The error code to search for.", -) -@pass_config -@click.pass_context -@logging_setup_decorator -def error_code(ctx, config, **kwargs): - from demisto_sdk.commands.error_code_info.error_code_info import print_error_info - - update_command_args_from_config_file("error-code-info", kwargs) - sys.path.append(config.configuration.env_dir) - - if error_code_input := kwargs.get("input"): - result = print_error_info(error_code_input) - else: - logger.error("Provide an error code, e.g. `-i DO106`") - result = 1 - - sys.exit(result) - - -# ====================== create-content-graph ====================== # -@main.command( + help="Generates a YAML file from Python code that includes " "its special syntax.", +)(generate_yml_from_python) +app.command(name="init", help="Creates a new pack, integration, or script template.")( + init +) +app.command( + name="secrets", + help="Run the secrets validator to catch sensitive data before exposing your code to a public repository.", +)(secrets) +app.command( + name="openapi-codegen", + help="Generate a Cortex XSIAM or Cortex XSOAR integration package (YAML and Python files) with a dedicated tool in the Demisto SDK.", +)(openapi_codegen) +app.command( + name="postman-codegen", + help="Generate an integration (YAML file) from a Postman Collection v2.1. Note the generated integration is in the YAML format.", +)(postman_codegen) +app.command( + name="setup-env", + help="Creates a content environment and and integration/script environment.", +)(setup_env_command) +app.command( + name="update-release-notes", + help="Automatically generates release notes for a given pack and updates the pack_metadata.json version for changed items.", +)(update_release_notes) +app.command( + name="validate", + help="Ensures that the content repository files are valid and are able to be processed by the platform.", +)(validate) +app.command(name="prepare-content", help="Prepares content to upload to the platform.")( + prepare_content +) +app.command(name="unify", help="Prepares content to upload to the platform.")( + prepare_content +) +app.command(name="xsoar-lint", help="Runs the xsoar lint on the given paths.")( + xsoar_linter +) +app.command( + name="xsoar-config-file-update", help="Handle your XSOAR Configuration File." +)(xsoar_config_file_update) +app.command( + name="pre-commit", + help="Enhances the content development experience by running a variety of checks and linters.", +)(pre_commit) +app.command( + name="error-code", help="Quickly find relevant information regarding an error code." +)(error_code) +app.command( + name="test-content", + help="Created incidents for selected test-playbooks and gives a report about the results.", hidden=True, -) -@click.help_option("-h", "--help") -@click.option( - "-o", - "--output-path", - type=click.Path(resolve_path=True, path_type=Path, dir_okay=True, file_okay=False), - default=None, - help="Output folder to place the zip file of the graph exported CSVs files.", -) -@click.option( - "-mp", - "--marketplace", - help="The marketplace to generate the graph for.", - default="xsoar", - type=click.Choice(list(MarketplaceVersions)), -) -@click.option( - "-nd", - "--no-dependencies", - is_flag=True, - help="Whether or not to include dependencies.", - default=False, -) -@click.pass_context -@logging_setup_decorator -def create_content_graph( - ctx, - marketplace: str = MarketplaceVersions.XSOAR, - no_dependencies: bool = False, - output_path: Path = None, - **kwargs, -): - logger.warning( - "[WARNING] The 'create-content-graph' command is deprecated and will be removed " - "in upcoming versions. Use 'demisto-sdk graph create' instead." - ) - ctx.invoke( - create, - ctx, - marketplace=marketplace, - no_dependencies=no_dependencies, - output_path=output_path, - **kwargs, - ) - - -# ====================== update-content-graph ====================== # -@main.command( +)(test_content) +app.add_typer( + graph_cmd_group, + name="graph", + help="The content graph commands provide a set of commands for creating, loading, and managing a graph " + "database representation of the content repository, enabling you to visualize the metadata of " + "content and the relationships between content packs, including dependencies.", +) +app.add_typer(modeling_rules_app, name="modeling-rules") +app.command(name="generate-modeling-rules", help="Generated modeling-rules.")( + generate_modeling_rules +) +app.command( + name="lint", help="Deprecated, use demisto-sdk pre-commit instead.", hidden=True +)(lint) +app.command( + name="create-id-set", + help="Deprecated, use demisto-sdk graph command instead.", hidden=True, -) -@click.help_option("-h", "--help") -@click.option( - "-mp", - "--marketplace", - help="The marketplace the artifacts are created for, that " - "determines which artifacts are created for each pack. " - "Default is the XSOAR marketplace, that has all of the packs " - "artifacts.", - default="xsoar", - type=click.Choice(list(MarketplaceVersions)), -) -@click.option( - "-g", - "--use-git", - is_flag=True, - show_default=True, - default=False, - help="Whether to use git to determine the packs to update", -) -@click.option( - "-i", - "--imported-path", - type=click.Path( - path_type=Path, resolve_path=True, exists=True, file_okay=True, dir_okay=False - ), - default=None, - help="Path to content graph zip file to import", -) -@click.option( - "-p", - "--packs", - help="A comma-separated list of packs to update", - multiple=True, - default=None, -) -@click.option( - "-nd", - "--no-dependencies", - is_flag=True, - help="Whether dependencies should be included in the graph", - default=False, -) -@click.option( - "-o", - "--output-path", - type=click.Path(resolve_path=True, path_type=Path, dir_okay=True, file_okay=False), - default=None, - help="Output folder to place the zip file of the graph exported CSVs files", -) -@click.pass_context -@logging_setup_decorator -def update_content_graph( - ctx, - use_git: bool = False, - marketplace: MarketplaceVersions = MarketplaceVersions.XSOAR, - imported_path: Path = None, - packs: list = None, - no_dependencies: bool = False, - output_path: Path = None, - **kwargs, -): - logger.warning( - "[WARNING] The 'update-content-graph' command is deprecated and will be removed " - "in upcoming versions. Use 'demisto-sdk graph update' instead." - ) - ctx.invoke( - update, - ctx, - use_git=use_git, - marketplace=marketplace, - imported_path=imported_path, - packs_to_update=packs, - no_dependencies=no_dependencies, - output_path=output_path, - **kwargs, - ) - - -@main.command(short_help="Setup integration environments") -@click.option( - "--ide", - help="IDE type to configure the environment for. If not specified, the IDE will be auto-detected. Case-insensitive.", - default="auto-detect", - type=click.Choice( - ["auto-detect"] + [IDEType.value for IDEType in IDEType], case_sensitive=False - ), -) -@click.option( - "-i", - "--input", - type=PathsParamType( - exists=True, resolve_path=True - ), # PathsParamType allows passing a list of paths - help="Paths to content integrations or script to setup the environment. " - "If not provided, will configure the environment for the content repository.", -) -@click.option( - "--create-virtualenv", - is_flag=True, - default=False, - help="Create a virtualenv for the environment.", -) -@click.option( - "--overwrite-virtualenv", - is_flag=True, - default=False, - help="Overwrite existing virtualenvs. Relevant only if the 'create-virtualenv' flag is used.", -) -@click.option( - "--secret-id", - help="Secret ID to use for the Google Secret Manager instance. " - "Requires the `DEMISTO_SDK_GCP_PROJECT_ID` environment variable to be set.", - required=False, -) -@click.option( - "--instance-name", - required=False, - help="Instance name to configure in XSOAR / XSIAM.", -) -@click.option( - "--run-test-module", - required=False, - is_flag=True, - default=False, - help="Whether to run test-module on the configured XSOAR / XSIAM integration instance.", -) -@click.option( - "--clean", - is_flag=True, - default=False, - help="Clean the repo out of the temp CommonServerPython.py files, demistomock.py and other files " - "that were created by lint.", -) -@click.argument("file_paths", nargs=-1, type=click.Path(exists=True, resolve_path=True)) -def setup_env( - input, - ide, - file_paths, - create_virtualenv, - overwrite_virtualenv, - secret_id, - instance_name, - run_test_module, - clean, -): - from demisto_sdk.commands.setup_env.setup_environment import ( - setup_env, - ) +)(create_id_set) +app.command( + name="generate-unit-tests", + help="This command generates unit tests automatically from an integration's Python code.", +)(generate_unit_tests) - if ide == "auto-detect": - # Order decides which IDEType will be selected for configuration if multiple IDEs are detected - if (CONTENT_PATH / ".vscode").exists(): - logger.info( - "Visual Studio Code IDEType has been detected and will be configured." - ) - ide_type = IDEType.VSCODE - elif (CONTENT_PATH / ".idea").exists(): - logger.info( - "PyCharm / IDEA IDEType has been detected and will be configured." - ) - ide_type = IDEType.PYCHARM - else: - raise RuntimeError( - "Could not detect IDEType. Please select a specific IDEType using the --ide flag." - ) - else: - ide_type = IDEType(ide) - - if input: - file_paths = tuple(input.split(",")) - - setup_env( - file_paths=file_paths, - ide_type=ide_type, - create_virtualenv=create_virtualenv, - overwrite_virtualenv=overwrite_virtualenv, - secret_id=secret_id, - instance_name=instance_name, - test_module=run_test_module, - clean=clean, - ) - - -@main.result_callback() -def exit_from_program(result=0, **kwargs): - sys.exit(result) - - -# ====================== Pre-Commit ====================== # -pre_commit_app = typer.Typer( - name="Pre-Commit", context_settings={"help_option_names": ["-h", "--help"]} +@logging_setup_decorator +@app.callback( + invoke_without_command=True, + context_settings={"help_option_names": ["-h", "--help"]}, ) - - -@pre_commit_app.command() -def pre_commit( - input_files: Optional[List[Path]] = typer.Option( - None, - "-i", - "--input", - "--files", - exists=True, - dir_okay=True, - resolve_path=True, - show_default=False, - help="The paths to run pre-commit on. May pass multiple paths.", - ), - staged_only: bool = typer.Option( - False, "--staged-only", help="Whether to run only on staged files." - ), - commited_only: bool = typer.Option( - False, "--commited-only", help="Whether to run on committed files only." - ), - git_diff: bool = typer.Option( - False, - "--git-diff", - "-g", - help="Whether to use git to determine which files to run on.", - ), - prev_version: Optional[str] = typer.Option( - None, - "--prev-version", - help="The previous version to compare against. " - "If not provided, the previous version will be determined using git.", - ), - all_files: bool = typer.Option( - False, "--all-files", "-a", help="Whether to run on all files." - ), - mode: str = typer.Option( - "", "--mode", help="Special mode to run the pre-commit with." - ), - skip: Optional[List[str]] = typer.Option( - None, "--skip", help="A list of precommit hooks to skip." - ), - validate: bool = typer.Option( - True, - "--validate/--no-validate", - help="Whether to run demisto-sdk validate or not.", - ), - format: bool = typer.Option( - False, "--format/--no-format", help="Whether to run demisto-sdk format or not." - ), - secrets: bool = typer.Option( - True, - "--secrets/--no-secrets", - help="Whether to run demisto-sdk secrets or not.", - ), - verbose: bool = typer.Option( - False, "-v", "--verbose", help="Verbose output of pre-commit." - ), - show_diff_on_failure: bool = typer.Option( - False, "--show-diff-on-failure", help="Show diff on failure." +def main( + ctx: typer.Context, + version: bool = typer.Option( + False, "--version", "-v", help="Show the current version of demisto-sdk." ), - dry_run: bool = typer.Option( + release_notes: bool = typer.Option( False, - "--dry-run", - help="Whether to run the pre-commit hooks in dry-run mode, which will only create the config file.", - ), - docker: bool = typer.Option( - True, "--docker/--no-docker", help="Whether to run docker based hooks or not." - ), - image_ref: Optional[str] = typer.Option( - None, - "--image-ref", - help="The docker image reference to run docker hooks with. Overrides the docker image from YAML or native image config.", - ), - docker_image: Optional[str] = typer.Option( - None, - "--docker-image", - help="Override the `docker_image` property in the template file. This is a comma separated list of: `from-yml`, `native:dev`, `native:ga`, `native:candidate`.", + "--release-notes", + "-rn", + help="Show the release notes for the current version.", ), - run_hook: Optional[str] = typer.Argument(None, help="A specific hook to run"), console_log_threshold: str = typer.Option( - "INFO", + None, "--console-log-threshold", - help="Minimum logging threshold for the console logger.", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", ), file_log_threshold: str = typer.Option( - "DEBUG", - "--file-log-threshold", - help="Minimum logging threshold for the file logger.", - ), - log_file_path: Optional[str] = typer.Option( - None, - "--log-file-path", - help="Path to save log files onto.", + None, "--file-log-threshold", help="Minimum logging threshold for file output." ), - pre_commit_template_path: Optional[Path] = typer.Option( - None, - "--template-path", - envvar="PRE_COMMIT_TEMPLATE_PATH", - help="A custom path for pre-defined pre-commit template, if not provided will use the default template.", - ), -): - logging_setup( - console_threshold=console_log_threshold, - file_threshold=file_log_threshold, - path=log_file_path, - calling_function="pre-commit", - ) - - from demisto_sdk.commands.pre_commit.pre_commit_command import pre_commit_manager - - return_code = pre_commit_manager( - input_files, - staged_only, - commited_only, - git_diff, - prev_version, - all_files, - mode, - skip, - validate, - format, - secrets, - verbose, - show_diff_on_failure, - run_docker_hooks=docker, - image_ref=image_ref, - docker_image=docker_image, - dry_run=dry_run, - run_hook=run_hook, - pre_commit_template_path=pre_commit_template_path, - ) - if return_code: - raise typer.Exit(1) - - -main.add_command(typer.main.get_command(pre_commit_app), "pre-commit") - -# ====================== modeling-rules command group ====================== # -modeling_rules_app = typer.Typer( - name="modeling-rules", - hidden=True, - no_args_is_help=True, - context_settings={"help_option_names": ["-h", "--help"]}, -) -modeling_rules_app.command("test", no_args_is_help=True)( - test_modeling_rule.test_modeling_rule -) -modeling_rules_app.command("init-test-data", no_args_is_help=True)( - init_test_data.init_test_data -) -typer_click_object = typer.main.get_command(modeling_rules_app) -main.add_command(typer_click_object, "modeling-rules") - -app_generate_modeling_rules = typer.Typer( - name="generate-modeling-rules", - no_args_is_help=True, - context_settings={"help_option_names": ["-h", "--help"]}, -) -app_generate_modeling_rules.command("generate-modeling-rules", no_args_is_help=True)( - generate_modeling_rules.generate_modeling_rules -) - -typer_click_object2 = typer.main.get_command(app_generate_modeling_rules) -main.add_command(typer_click_object2, "generate-modeling-rules") - -# ====================== graph command group ====================== # - -graph_cmd_group = typer.Typer( - name="graph", - hidden=True, - no_args_is_help=True, - context_settings={"help_option_names": ["-h", "--help"]}, -) -graph_cmd_group.command("create", no_args_is_help=False)(create) -graph_cmd_group.command("update", no_args_is_help=False)(update) -graph_cmd_group.command("get-relationships", no_args_is_help=True)(get_relationships) -graph_cmd_group.command("get-dependencies", no_args_is_help=True)(get_dependencies) -main.add_command(typer.main.get_command(graph_cmd_group), "graph") - -# ====================== Xsoar-Lint ====================== # - -xsoar_linter_app = typer.Typer( - name="Xsoar-Lint", context_settings={"help_option_names": ["-h", "--help"]} -) - - -@xsoar_linter_app.command( - no_args_is_help=True, - context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, -) -def xsoar_linter( - file_paths: Optional[List[Path]] = typer.Argument( - None, - exists=True, - dir_okay=True, - resolve_path=True, - show_default=False, - help=("The paths to run xsoar linter on. May pass multiple paths."), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." ), ): - """ - Runs the xsoar lint on the given paths. - """ - return_code = xsoar_linter_manager( - file_paths, - ) - if return_code: - raise typer.Exit(1) - + sdk = DemistoSDK() # Initialize your SDK class + sdk.configuration = Configuration() # Initialize the configuration + ctx.obj = sdk # Pass sdk instance to context + load_dotenv(CONTENT_PATH / ".env", override=True) + if platform.system() == "Windows": + typer.echo( + "Warning: Using Demisto-SDK on Windows is not supported. Use WSL2 or run in a container." + ) -main.add_command(typer.main.get_command(xsoar_linter_app), "xsoar-lint") + if version: + show_version() + raise typer.Exit() -# ====================== export ====================== # + if release_notes: + show_release_notes() + raise typer.Exit() -export_app = typer.Typer( - name="dump-api", context_settings={"help_option_names": ["-h", "--help"]} -) +def get_version_info(): + """Retrieve version and latest release information.""" + try: + current_version = get_distribution("demisto-sdk").version + except DistributionNotFound: + current_version = "dev" + typer.echo( + "Could not find the version of demisto-sdk. Running in development mode." + ) + else: + last_release = "" + if not os.environ.get("CI") and not is_sdk_defined_working_offline(): + last_release = get_last_remote_release_version() + return current_version, last_release + return current_version, None + + +def show_version(): + """Display the SDK version and notify if updates are available.""" + current_version, last_release = get_version_info() + typer.echo(f"demisto-sdk version: {current_version}") + + if last_release and current_version != last_release: + message = typer.style( + f"A newer version ({last_release}) is available. To update, run 'pip install --upgrade demisto-sdk'", + fg=typer.colors.YELLOW, + ) + typer.echo(message) -@export_app.command( - context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, -) -def dump_api( - ctx: typer.Context, - output_path: Path = typer.Option( - CONTENT_PATH, - "-o", - "--output", - help="The output directory or JSON file to save the demisto-sdk api.", - ), -): - """ - This commands dumps the `demisto-sdk` API to a file. - It is used to view the help of all commands in one file. - Args: - ctx (typer.Context): - output_path (Path, optional): The output directory or JSON file to save the demisto-sdk api. - """ - output_json: dict = {} - for command_name, command in main.commands.items(): - if isinstance(command, click.Group): - output_json[command_name] = {} - for sub_command_name, sub_command in command.commands.items(): - output_json[command_name][sub_command_name] = sub_command.to_info_dict( - ctx - ) - else: - output_json[command_name] = command.to_info_dict(ctx) - convert_path_to_str(output_json) - if output_path.is_dir(): - output_path = output_path / "demisto-sdk-api.json" - output_path.write_text(json.dumps(output_json, indent=4)) +def show_release_notes(): + """Display release notes for the current version.""" + current_version, _ = get_version_info() + rn_entries = get_release_note_entries(current_version) + if rn_entries: + typer.echo("\nRelease notes for the current version:\n") + typer.echo(rn_entries) + else: + typer.echo("Could not retrieve release notes for this version.") -main.add_command(typer.main.get_command(export_app), "dump-api") if __name__ == "__main__": - main() + typer.echo("Running Demisto-SDK CLI") + app() # Run the main app diff --git a/demisto_sdk/commands/common/configuration.py b/demisto_sdk/commands/common/configuration.py index 62119fbb492..a2f49d51ce2 100644 --- a/demisto_sdk/commands/common/configuration.py +++ b/demisto_sdk/commands/common/configuration.py @@ -17,3 +17,10 @@ def __init__(self): self.envs_dirs_base = str( Path(self.sdk_env_dir) / "lint" / "resources" / "pipfile_python" ) + + +class DemistoSDK: + """Core SDK class.""" + + def __init__(self): + self.configuration = Configuration() diff --git a/demisto_sdk/commands/common/constants.py b/demisto_sdk/commands/common/constants.py index 271412f42d2..ff42520196c 100644 --- a/demisto_sdk/commands/common/constants.py +++ b/demisto_sdk/commands/common/constants.py @@ -31,6 +31,10 @@ "CI_PROJECT_ID", "1061" ) # Default value is the ID of the content repo on GitLab ENV_SDK_WORKING_OFFLINE = "DEMISTO_SDK_OFFLINE_ENV" +SDK_OFFLINE_ERROR_MESSAGE = ( + "An internet connection is required for this command. If connected to the " + "internet, un-set the DEMISTO_SDK_OFFLINE_ENV environment variable." +) DEFAULT_DOCKER_REGISTRY_URL = "docker.io" DOCKER_REGISTRY_URL = os.getenv( diff --git a/demisto_sdk/commands/common/logger.py b/demisto_sdk/commands/common/logger.py index 2ac619f63c0..7f7c889eaef 100644 --- a/demisto_sdk/commands/common/logger.py +++ b/demisto_sdk/commands/common/logger.py @@ -1,11 +1,13 @@ +import functools import logging # noqa: TID251 # Required for propagation handling. import os import platform import sys from pathlib import Path -from typing import Iterable, Optional, Union +from typing import Callable, Iterable, Optional, Union import loguru # noqa: TID251 # This is the only place where we allow it +import typer from demisto_sdk.commands.common.constants import ( DEMISTO_SDK_LOG_FILE_PATH, @@ -211,3 +213,40 @@ def handle_deprecated_args(input_args: Iterable[str]): f"Argument {argument} is deprecated," f"Use {DEPRECATED_PARAMETERS[argument]} instead." ) + + +def logging_setup_decorator(func: Callable): + @functools.wraps(func) + def wrapper(ctx: typer.Context, *args, **kwargs): + # Fetch the parameters directly from context to apply default values if they are None + console_threshold = ctx.params.get("console_log_threshold") or "INFO" + file_threshold = ctx.params.get("file_log_threshold") or "DEBUG" + + # Validate the logging levels + valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} + if console_threshold not in valid_levels: + console_threshold = "INFO" + if file_threshold not in valid_levels: + file_threshold = "DEBUG" + + # Set back the validated and default values in both `ctx.params` and `kwargs` + ctx.params["console_log_threshold"] = console_threshold + ctx.params["file_log_threshold"] = file_threshold + kwargs["console_log_threshold"] = console_threshold + kwargs["file_log_threshold"] = file_threshold + + # Initialize logging with the validated thresholds + logging_setup( + console_threshold=console_threshold, + file_threshold=file_threshold, + path=kwargs.get("log_file_path", None), + calling_function=func.__name__, + ) + + # Handle deprecated arguments directly from context args if needed + handle_deprecated_args(ctx.args if ctx else []) + + # Run the wrapped function + return func(ctx, *args, **kwargs) + + return wrapper diff --git a/demisto_sdk/commands/common/tests/pack_unique_files_test.py b/demisto_sdk/commands/common/tests/pack_unique_files_test.py index ea2969a41ba..0ad7f1ff458 100644 --- a/demisto_sdk/commands/common/tests/pack_unique_files_test.py +++ b/demisto_sdk/commands/common/tests/pack_unique_files_test.py @@ -3,10 +3,10 @@ from pathlib import Path import pytest -from click.testing import CliRunner from git import GitCommandError +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common import tools from demisto_sdk.commands.common.constants import ( DEMISTO_GIT_PRIMARY_BRANCH, @@ -206,7 +206,7 @@ def test_validate_partner_contribute_pack_metadata_no_mail_and_url( pack.pack_metadata.write_json(pack_metadata_no_email_and_url) with ChangeCWD(repo.path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -268,7 +268,7 @@ def test_validate_partner_pack_metadata_url( with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -323,7 +323,7 @@ def test_validate_partner_contribute_pack_metadata_price_change(self, mocker, re pack.pack_metadata.write_json(pack_metadata_price_changed) with ChangeCWD(repo.path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ VALIDATE_CMD, "-i", diff --git a/demisto_sdk/commands/common/tests/tools_test.py b/demisto_sdk/commands/common/tests/tools_test.py index fa12a0e95cc..f25e8e1f509 100644 --- a/demisto_sdk/commands/common/tests/tools_test.py +++ b/demisto_sdk/commands/common/tests/tools_test.py @@ -780,6 +780,7 @@ def test_not_recursive(self): files = [ f"{project_dir}/__init__.py", f"{project_dir}/downloader.py", + f"{project_dir}/download_setup.py", f"{project_dir}/README.md", ] assert sorted(get_files_in_dir(project_dir, ["py", "md"], False)) == sorted( diff --git a/demisto_sdk/commands/content_graph/commands/create.py b/demisto_sdk/commands/content_graph/commands/create.py index f903ab73da1..ade9b5c90e3 100644 --- a/demisto_sdk/commands/content_graph/commands/create.py +++ b/demisto_sdk/commands/content_graph/commands/create.py @@ -77,6 +77,8 @@ def create( ), output_path: Path = typer.Option( None, + "-o", + "--output-path", exists=True, dir_okay=True, file_okay=False, diff --git a/demisto_sdk/commands/content_graph/commands/update.py b/demisto_sdk/commands/content_graph/commands/update.py index d2cef0b9ebc..f4440715901 100644 --- a/demisto_sdk/commands/content_graph/commands/update.py +++ b/demisto_sdk/commands/content_graph/commands/update.py @@ -222,6 +222,8 @@ def update( ), output_path: Path = typer.Option( None, + "-o", + "--output-path", exists=True, dir_okay=True, file_okay=False, diff --git a/demisto_sdk/commands/content_graph/content_graph_setup.py b/demisto_sdk/commands/content_graph/content_graph_setup.py new file mode 100644 index 00000000000..942906dbdd2 --- /dev/null +++ b/demisto_sdk/commands/content_graph/content_graph_setup.py @@ -0,0 +1,24 @@ +import typer + +from demisto_sdk.commands.content_graph.commands.create import create +from demisto_sdk.commands.content_graph.commands.get_dependencies import ( + get_dependencies, +) +from demisto_sdk.commands.content_graph.commands.get_relationships import ( + get_relationships, +) +from demisto_sdk.commands.content_graph.commands.update import update + +graph_cmd_group = typer.Typer( + name="graph", + hidden=True, + no_args_is_help=True, + context_settings={"help_option_names": ["-h", "--help"]}, +) +graph_cmd_group.command("create", no_args_is_help=False)(create) +graph_cmd_group.command("update", no_args_is_help=False)(update) +graph_cmd_group.command("get-relationships", no_args_is_help=True)(get_relationships) +graph_cmd_group.command("get-dependencies", no_args_is_help=True)(get_dependencies) + +if __name__ == "__main__": + graph_cmd_group() diff --git a/demisto_sdk/commands/content_graph/tests/format_with_graph_test.py b/demisto_sdk/commands/content_graph/tests/format_with_graph_test.py index 5c32fdff63b..c8555510eae 100644 --- a/demisto_sdk/commands/content_graph/tests/format_with_graph_test.py +++ b/demisto_sdk/commands/content_graph/tests/format_with_graph_test.py @@ -2,9 +2,9 @@ from typing import List import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.constants import ( MarketplaceVersions, ) @@ -312,7 +312,6 @@ def test_format_mapper_with_graph_remove_unknown_content(mocker, repository, rep Then - Ensure that the unknown field was removed from the mapper. """ - with ContentGraphInterface() as interface: create_content_graph(interface) @@ -330,7 +329,7 @@ def test_format_mapper_with_graph_remove_unknown_content(mocker, repository, rep with ChangeCWD(repo.path): runner = CliRunner() result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -349,8 +348,6 @@ def test_format_mapper_with_graph_remove_unknown_content(mocker, repository, rep f"Removing the fields {fields} from the mapper {mapper_path} because they aren't in the content repo." ) in result.output - # get_dict_from_file returns a tuple of 2 object. The first is the content of the file, - # the second is the type of the file. file_content = get_dict_from_file(mapper_path)[0] assert ( file_content.get("mapping", {}).get("Mapper Finding", {}).get("internalMapping") @@ -384,9 +381,7 @@ def test_format_layout_with_graph_remove_unknown_content(mocker, repository, rep ) with ChangeCWD(repo.path): runner = CliRunner() - result = runner.invoke( - main, [FORMAT_CMD, "-i", layout_path, "-at", "-y", "-nv"] - ) + result = runner.invoke(app, [FORMAT_CMD, "-i", layout_path, "-at", "-y", "-nv"]) assert result.exit_code == 0 assert not result.exception assert ( @@ -454,7 +449,7 @@ def test_format_incident_field_graph_fix_aliases_marketplace( with ChangeCWD(repo.path): runner = CliRunner() result = runner.invoke( - main, [FORMAT_CMD, "-i", original_incident_field_path, "-at", "-y", "-nv"] + app, [FORMAT_CMD, "-i", original_incident_field_path, "-at", "-y", "-nv"] ) assert result.exit_code == 0 diff --git a/demisto_sdk/commands/coverage_analyze/coverage_analyze_setup.py b/demisto_sdk/commands/coverage_analyze/coverage_analyze_setup.py new file mode 100644 index 00000000000..eea4f0fd77d --- /dev/null +++ b/demisto_sdk/commands/coverage_analyze/coverage_analyze_setup.py @@ -0,0 +1,91 @@ +import os +from typing import Optional + +import typer + +from demisto_sdk.commands.common.constants import DEMISTO_SDK_MARKETPLACE_XSOAR_DIST_DEV +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.coverage_analyze.coverage_report import CoverageReport + + +@logging_setup_decorator +def coverage_analyze( + ctx: typer.Context, + input: str = typer.Option( + os.path.join("coverage_report", ".coverage"), + "-i", + "--input", + help="The .coverage file to analyze.", + resolve_path=True, + ), + default_min_coverage: float = typer.Option( + 70.0, + help="Default minimum coverage (for new files).", + ), + allowed_coverage_degradation_percentage: float = typer.Option( + 1.0, + help="Allowed coverage degradation percentage (for modified files).", + ), + no_cache: bool = typer.Option( + False, + help="Force download of the previous coverage report file.", + ), + report_dir: str = typer.Option( + "coverage_report", + help="Directory of the coverage report files.", + resolve_path=True, + ), + report_type: Optional[str] = typer.Option( + None, + help="The type of coverage report (possible values: 'text', 'html', 'xml', 'json' or 'all').", + ), + no_min_coverage_enforcement: bool = typer.Option( + False, + help="Do not enforce minimum coverage.", + ), + previous_coverage_report_url: str = typer.Option( + f"https://storage.googleapis.com/{DEMISTO_SDK_MARKETPLACE_XSOAR_DIST_DEV}/code-coverage-reports/coverage-min.json", + help="URL of the previous coverage report.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Analyze coverage report.""" + try: + no_degradation_check = allowed_coverage_degradation_percentage == 100.0 + + cov_report = CoverageReport( + default_min_coverage=default_min_coverage, + allowed_coverage_degradation_percentage=allowed_coverage_degradation_percentage, + coverage_file=input, + no_cache=no_cache, + report_dir=report_dir, + report_type=report_type, + no_degradation_check=no_degradation_check, + previous_coverage_report_url=previous_coverage_report_url, + ) + cov_report.coverage_report() + + # if no_degradation_check=True we will suppress the minimum coverage check + if ( + no_degradation_check + or cov_report.coverage_diff_report() + or no_min_coverage_enforcement + ): + return 0 + except FileNotFoundError as e: + typer.echo(f"Warning: {e}") + return 0 + except Exception as error: + typer.echo(f"Error: {error}") + + return 1 diff --git a/demisto_sdk/commands/create_id_set/create_id_set_setup.py b/demisto_sdk/commands/create_id_set/create_id_set_setup.py new file mode 100644 index 00000000000..bb04ed1efeb --- /dev/null +++ b/demisto_sdk/commands/create_id_set/create_id_set_setup.py @@ -0,0 +1,78 @@ +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.create_id_set.create_id_set import IDSetCreator +from demisto_sdk.commands.find_dependencies.find_dependencies import ( + remove_dependencies_from_id_set, +) +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def create_id_set( + ctx: typer.Context, + input: str = typer.Option( + "", + "-i", + "--input", + help="Input file path, the default is the content repo.", + ), + output: str = typer.Option( + "", + "-o", + "--output", + help="Output file path, the default is the Tests directory.", + ), + fail_duplicates: bool = typer.Option( + False, + "-fd", + "--fail-duplicates", + help="Fails the process if any duplicates are found.", + ), + marketplace: str = typer.Option( + "", + "-mp", + "--marketplace", + help=( + "The marketplace the id set are created for, that determines which packs " + "are inserted to the id set, and which items are present in the id set for " + "each pack. Default is all packs exists in the content repository." + ), + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Create the content dependency tree by IDs. + """ + kwargs = { + "input": input, + "output": output, + "fail_duplicates": fail_duplicates, + "marketplace": marketplace, + } + + update_command_args_from_config_file("create-id-set", kwargs) + + id_set_creator = IDSetCreator(**kwargs) # type: ignore[arg-type] + id_set, excluded_items_by_pack, excluded_items_by_type = ( + id_set_creator.create_id_set() + ) + + if excluded_items_by_pack: + remove_dependencies_from_id_set( + id_set, + excluded_items_by_pack, + excluded_items_by_type, + marketplace, + ) + id_set_creator.save_id_set() diff --git a/demisto_sdk/commands/doc_reviewer/doc_reviewer_setup.py b/demisto_sdk/commands/doc_reviewer/doc_reviewer_setup.py new file mode 100644 index 00000000000..3c0e2edcfc4 --- /dev/null +++ b/demisto_sdk/commands/doc_reviewer/doc_reviewer_setup.py @@ -0,0 +1,91 @@ +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.doc_reviewer.doc_reviewer import DocReviewer + + +@logging_setup_decorator +def doc_review( + ctx: typer.Context, + input: list[str] = typer.Option( + None, "-i", "--input", help="The path to the file to check" + ), + no_camel_case: bool = typer.Option( + False, "--no-camel-case", help="Whether to check CamelCase words" + ), + known_words: list[str] = typer.Option( + None, + "--known-words", + help="The path to a file containing additional known words", + ), + always_true: bool = typer.Option( + False, + "--always-true", + help="Whether to fail the command if misspelled words are found", + ), + expand_dictionary: bool = typer.Option( + False, + "--expand-dictionary", + help="Whether to expand the base dictionary to include more words - will download 'brown' corpus from nltk package", + ), + templates: bool = typer.Option( + False, "--templates", help="Whether to print release notes templates" + ), + use_git: bool = typer.Option( + False, + "-g", + "--use-git", + help="Use git to identify the relevant changed files, will be used by default if '-i' and '--templates' are not set", + ), + prev_ver: str = typer.Option( + None, + "--prev-ver", + help="The branch against which changes will be detected if '-g' flag is set. Default is 'demisto/master'", + ), + release_notes: bool = typer.Option( + False, "-rn", "--release-notes", help="Will run only on release notes files" + ), + xsoar_only: bool = typer.Option( + False, + "-xs", + "--xsoar-only", + help="Run only on files from XSOAR-supported Packs.", + ), + use_packs_known_words: bool = typer.Option( + True, + "-pkw/-spkw", + "--use-packs-known-words/--skip-packs-known-words", + help="Will find and load the known_words file from the pack. To use this option make sure you are running from the content directory.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Check the spelling in .md and .yml files as well as review release notes + """ + doc_reviewer = DocReviewer( + file_paths=input, + known_words_file_paths=known_words, + no_camel_case=no_camel_case, + no_failure=always_true, + expand_dictionary=expand_dictionary, + templates=templates, + use_git=use_git, + prev_ver=prev_ver, + release_notes_only=release_notes, + xsoar_only=xsoar_only, + load_known_words_from_pack=use_packs_known_words, + ) + result = doc_reviewer.run_doc_review() + if result: + raise typer.Exit(0) + raise typer.Exit(1) diff --git a/demisto_sdk/commands/doc_reviewer/tests/doc_reviewer_test.py b/demisto_sdk/commands/doc_reviewer/tests/doc_reviewer_test.py index a69c108ea5b..c0442f1b973 100644 --- a/demisto_sdk/commands/doc_reviewer/tests/doc_reviewer_test.py +++ b/demisto_sdk/commands/doc_reviewer/tests/doc_reviewer_test.py @@ -5,9 +5,9 @@ from typing import List import pytest -from click.testing import CliRunner, Result +from typer.testing import CliRunner, Result -from demisto_sdk import __main__ +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.constants import FileType from demisto_sdk.commands.common.tools import ( find_type, @@ -344,7 +344,7 @@ def run_doc_review_cmd(self, cmd_args: List[str]) -> Result: args: List[str] = self.default_args + cmd_args - return CliRunner().invoke(__main__.doc_review, args) + return CliRunner().invoke(app, ["doc-review", *args]) def test_valid_supported_pack(self, supported_pack: Pack): """ @@ -1393,30 +1393,31 @@ def test_replace_escape_characters(sentence, expected): @pytest.mark.parametrize( "use_pack_known_words, expected_param_value", [ - (["--use-packs-known-words"], True), - (["--skip-packs-known-words"], False), - ([""], True), - (["--skip-packs-known-words", "--use-packs-known-words"], True), + ("--use-packs-known-words", True), + ("--skip-packs-known-words", False), ], ) def test_pack_known_word_arg(use_pack_known_words, expected_param_value, mocker): """ Given: - - the --use-pack-known-words parameter + - the --use-packs-known-words parameter When: - running the doc-review command Then: - - Validate that given --use-packs-known-words" the load_known_words_from_pack is True - - Validate that given --skip-packs-known-words" the load_known_words_from_pack is False + - Validate that given --use-packs-known-words the load_known_words_from_pack is True + - Validate that given --skip-packs-known-words the load_known_words_from_pack is False - Validate that no param the default load_known_words_from_pack is True - Validate that given --use-packs-known-words and --skip-packs-known-words the load_known_words_from_pack is True """ runner = CliRunner() mock_doc_reviewer = mocker.MagicMock(name="DocReviewer") mock_doc_reviewer.run_doc_review.return_value = True - m = mocker.patch( - "demisto_sdk.commands.doc_reviewer.doc_reviewer.DocReviewer", + from demisto_sdk.commands.doc_reviewer.doc_reviewer_setup import DocReviewer + + m = mocker.patch.object( + DocReviewer, + "__init__", return_value=mock_doc_reviewer, ) - runner.invoke(__main__.doc_review, use_pack_known_words) + runner.invoke(app, ["doc-review", use_pack_known_words]) assert m.call_args.kwargs.get("load_known_words_from_pack") == expected_param_value diff --git a/demisto_sdk/commands/download/download_setup.py b/demisto_sdk/commands/download/download_setup.py new file mode 100644 index 00000000000..f24844021da --- /dev/null +++ b/demisto_sdk/commands/download/download_setup.py @@ -0,0 +1,107 @@ +from enum import Enum +from pathlib import Path +from typing import List + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator + + +class ItemType(str, Enum): + incident_type = "IncidentType" + indicator_type = "IndicatorType" + field = "Field" + layout = "Layout" + playbook = "Playbook" + automation = "Automation" + classifier = "Classifier" + mapper = "Mapper" + + +@logging_setup_decorator +def download( + ctx: typer.Context, + output: Path = typer.Option( + None, + "--output", + "-o", + help="A path to a pack directory to download content to.", + ), + input: List[str] = typer.Option( + None, + "--input", + "-i", + help="Name of a custom content item to download. Can be used multiple times.", + ), + regex: str = typer.Option( + None, + "--regex", + "-r", + help="Download all custom content items matching this RegEx pattern.", + ), + insecure: bool = typer.Option( + False, "--insecure", help="Skip certificate validation." + ), + force: bool = typer.Option( + False, + "--force", + "-f", + help="Overwrite existing content in the output directory.", + ), + list_files: bool = typer.Option( + False, + "--list-files", + "-lf", + help="List all custom content items available to download and exit.", + ), + all_custom_content: bool = typer.Option( + False, + "--all-custom-content", + "-a", + help="Download all available custom content items.", + ), + run_format: bool = typer.Option( + False, "--run-format", "-fmt", help="Format downloaded files." + ), + system: bool = typer.Option(False, "--system", help="Download system items."), + item_type: ItemType = typer.Option( + None, + "--item-type", + "-it", + help="Type of the content item to download. Required and used only when downloading system items.", + case_sensitive=False, + ), + init: bool = typer.Option( + False, "--init", help="Initialize the output directory with a pack structure." + ), + keep_empty_folders: bool = typer.Option( + False, + "--keep-empty-folders", + help="Keep empty folders when initializing a pack structure.", + ), + auto_replace_uuids: bool = typer.Option( + True, + "--auto-replace-uuids/--no-auto-replace-uuids", + help="Automatically replace UUIDs for downloaded content.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Download custom content from a Cortex XSOAR / XSIAM instance. + DEMISTO_BASE_URL environment variable should contain the server base URL. + DEMISTO_API_KEY environment variable should contain a valid API Key for the server. + """ + from demisto_sdk.commands.download.downloader import Downloader + + kwargs = locals() + Downloader(**kwargs).download() diff --git a/demisto_sdk/commands/download/downloader.py b/demisto_sdk/commands/download/downloader.py index da93b610841..00ddb5e3a2f 100644 --- a/demisto_sdk/commands/download/downloader.py +++ b/demisto_sdk/commands/download/downloader.py @@ -13,6 +13,7 @@ import demisto_client.demisto_api import mergedeep +import typer from demisto_client.demisto_api.rest import ApiException from dictor import dictor from flatten_dict import unflatten @@ -158,7 +159,7 @@ def __init__( **kwargs, ): self.output_pack_path = output - self.input_files = [input] if isinstance(input, str) else list(input) + self.input_files = list(input) if input else [] self.regex = regex self.force = force self.download_system_items = system @@ -185,21 +186,19 @@ def download(self) -> int: int: Exit code. 1 if failed, 0 if succeeded """ input_files_missing = False # Used for returning an exit code of 1 if one of the inputs is missing. - try: if self.should_list_files: # No flag validations are needed, since only the '-lf' flag is used. self.list_all_custom_content() - return 0 - + raise typer.Exit(0) if not self.output_pack_path: logger.error("Error: Missing required parameter '-o' / '--output'.") - return 1 + raise typer.Exit(1) output_path = Path(self.output_pack_path) if not self.verify_output_path(output_path=output_path): - return 1 + raise typer.Exit(1) if self.should_init_new_pack: output_path = self.initialize_output_path(root_folder=output_path) @@ -209,13 +208,13 @@ def download(self) -> int: logger.error( "Error: Missing required parameter for downloading system items: '-i' / '--input'." ) - return 1 + raise typer.Exit(1) if not self.system_item_type: logger.error( "Error: Missing required parameter for downloading system items: '-it' / '--item-type'." ) - return 1 + raise typer.Exit(1) content_item_type = self.system_item_type downloaded_content_objects = self.fetch_system_content( @@ -231,7 +230,7 @@ def download(self) -> int: "Error: No input parameter has been provided " "('-i' / '--input', '-r' / '--regex', '-a' / '--all)." ) - return 1 + raise typer.Exit(1) elif self.regex: # Assure regex is valid @@ -242,7 +241,7 @@ def download(self) -> int: logger.error( f"Error: Invalid regex pattern provided: '{self.regex}'." ) - return 1 + raise typer.Exit(1) all_custom_content_data = self.download_custom_content() all_custom_content_objects = self.parse_custom_content_data( @@ -273,7 +272,7 @@ def download(self) -> int: logger.info( "No custom content matching the provided input filters was found." ) - return 1 if input_files_missing else 0 + raise typer.Exit(1) if input_files_missing else typer.Exit(0) if self.auto_replace_uuids: # Replace UUID IDs with names in filtered content (only content we download) @@ -298,16 +297,20 @@ def download(self) -> int: if not result: logger.error("Download failed.") - return 1 + raise typer.Exit(1) + + raise typer.Exit(1) if input_files_missing else typer.Exit(0) - return 1 if input_files_missing else 0 + except typer.Exit: + # Re-raise typer.Exit without handling it as an error + raise except Exception as e: if not isinstance(e, HandledError): logger.error(f"Error: {e}") logger.debug("Traceback:\n" + traceback.format_exc()) - return 1 + raise typer.Exit(1) def list_all_custom_content(self): """ diff --git a/demisto_sdk/commands/download/tests/downloader_test.py b/demisto_sdk/commands/download/tests/downloader_test.py index c949a5afe9a..181b8644062 100644 --- a/demisto_sdk/commands/download/tests/downloader_test.py +++ b/demisto_sdk/commands/download/tests/downloader_test.py @@ -380,7 +380,8 @@ def test_missing_output_flag(self, caplog): """ downloader = Downloader(input=("test",)) - assert downloader.download() == 1 + with pytest.raises(typer.Exit): + assert downloader.download() == 1 assert "Error: Missing required parameter '-o' / '--output'." in caplog.text def test_missing_input_flag_system(self, mocker, caplog): @@ -392,7 +393,8 @@ def test_missing_input_flag_system(self, mocker, caplog): downloader = Downloader(output="Output", input=tuple(), system=True) mocker.patch.object(Downloader, "verify_output_path", return_value=True) - assert downloader.download() == 1 + with pytest.raises(typer.Exit): + assert downloader.download() == 1 assert ( "Error: Missing required parameter for downloading system items: '-i' / '--input'." in caplog.text @@ -413,7 +415,8 @@ def test_missing_input_flag_custom(self, mocker, caplog): ) mocker.patch.object(Downloader, "verify_output_path", return_value=True) - assert downloader.download() == 1 + with pytest.raises(typer.Exit): + assert downloader.download() == 1 assert ( "Error: No input parameter has been provided ('-i' / '--input', '-r' / '--regex', '-a' / '--all)." in caplog.text @@ -430,7 +433,8 @@ def test_missing_item_type(self, mocker, caplog): ) mocker.patch.object(Downloader, "verify_output_path", return_value=True) - assert downloader.download() == 1 + with pytest.raises(typer.Exit): + assert downloader.download() == 1 assert ( "Error: Missing required parameter for downloading system items: '-it' / '--item-type'." in caplog.text @@ -682,7 +686,7 @@ def test_download_and_extract_existing_file(self, tmp_path): def test_download_and_format_existing_file(self, tmp_path): """ - Given: A remote Script with differernt comment. + Given: A remote Script with different comment. When: Downloading with force=True and run_format=True. Then: Assert the file is merged and the remote comment is formatted is in the new file. """ @@ -697,17 +701,17 @@ def test_download_and_format_existing_file(self, tmp_path): ) # The downloaded yml contains some other comment now. - script_data["comment"] = "some other comment" + script_data["comment"] = "some other comment." env.SCRIPT_CUSTOM_CONTENT_OBJECT["data"] = script_data - - assert downloader.write_files_into_output_path( - downloaded_content_objects={ - script_file_name: env.SCRIPT_CUSTOM_CONTENT_OBJECT - }, - existing_pack_structure=env.PACK_CONTENT, - output_path=env.PACK_INSTANCE_PATH, - ) + with pytest.raises(typer.Exit): + assert downloader.write_files_into_output_path( + downloaded_content_objects={ + script_file_name: env.SCRIPT_CUSTOM_CONTENT_OBJECT + }, + existing_pack_structure=env.PACK_CONTENT, + output_path=env.PACK_INSTANCE_PATH, + ) assert script_file_path.is_file() data = get_yaml(script_file_path) # Make sure the new comment is formatted and a '.' was added. @@ -1437,7 +1441,8 @@ def test_list_files_flag(mocker): list_file_method_mock = mocker.spy(downloader, "list_all_custom_content") content_table_mock = mocker.spy(downloader, "create_custom_content_table") - assert downloader.download() == 0 + with pytest.raises(typer.Exit): + assert downloader.download() == 0 expected_table = ( "Content Name Content Type\n" @@ -1491,8 +1496,8 @@ def test_auto_replace_uuids_flag(mocker, auto_replace_uuids: bool): mocker.patch.object(downloader, "build_existing_pack_structure", return_value={}) mocker.patch.object(downloader, "write_files_into_output_path", return_value=True) mock_replace_uuids = mocker.spy(downloader, "replace_uuid_ids") - - downloader.download() + with pytest.raises(typer.Exit): + downloader.download() if auto_replace_uuids: assert mock_replace_uuids.called @@ -1510,7 +1515,8 @@ def test_invalid_regex_error(mocker, caplog): downloader = Downloader(regex="*invalid-regex*", output="fake_output_dir") mocker.patch.object(downloader, "verify_output_path", return_value=True) - assert downloader.download() == 1 + with pytest.raises(typer.Exit): + assert downloader.download() == 1 assert "Error: Invalid regex pattern provided: '*invalid-regex*'." in caplog.text diff --git a/demisto_sdk/commands/dump_api/__init__.py b/demisto_sdk/commands/dump_api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/demisto_sdk/commands/dump_api/dump_api_setup.py b/demisto_sdk/commands/dump_api/dump_api_setup.py new file mode 100644 index 00000000000..885a709ee4c --- /dev/null +++ b/demisto_sdk/commands/dump_api/dump_api_setup.py @@ -0,0 +1,59 @@ +from pathlib import Path + +import typer +from typer.main import get_command + +from demisto_sdk.commands.common.content_constant_paths import CONTENT_PATH +from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json +from demisto_sdk.commands.common.tools import convert_path_to_str + + +def dump_api( + ctx: typer.Context, + output_path: Path = typer.Option( + CONTENT_PATH, + "-o", + "--output", + help="The output directory or JSON file to save the demisto-sdk API.", + ), +): + """ + This command dumps the `demisto-sdk` API to a file. + It is used to view the help of all commands in one file. + + Args: + ctx (typer.Context): The context of the command. + output_path (Path, optional): The output directory or JSON file to save the demisto-sdk API. + """ + from demisto_sdk.__main__ import app + + output_json: dict = {} + typer_app = get_command(app) + + # Iterate over registered commands in the main application + for command_name, command in typer_app.commands.items(): # type: ignore[attr-defined] + typer.echo(command_name, color=True) + if isinstance(command, typer.Typer): + output_json[command_name] = {} + + # Iterate over subcommands + for sub_command in command.registered_commands: + sub_command_name = sub_command.name + # Convert subcommand to info dictionary + output_json[command_name][sub_command_name] = sub_command.to_info_dict( # type: ignore[attr-defined] + ctx + ) + else: + # Convert command to info dictionary + output_json[command_name] = command.to_info_dict(ctx) + + # Convert paths in the output JSON (if applicable) + convert_path_to_str(output_json) + + # Determine output file path + if output_path.is_dir(): + output_path = output_path / "demisto-sdk-api.json" + + # Write the JSON output to the specified file + output_path.write_text(json.dumps(output_json, indent=4)) + typer.echo(f"API dumped successfully to {output_path}") diff --git a/demisto_sdk/commands/error_code_info/error_code_info_setup.py b/demisto_sdk/commands/error_code_info/error_code_info_setup.py new file mode 100644 index 00000000000..d506777decd --- /dev/null +++ b/demisto_sdk/commands/error_code_info/error_code_info_setup.py @@ -0,0 +1,41 @@ +import sys + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.error_code_info.error_code_info import print_error_info +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def error_code( + ctx: typer.Context, + input: str = typer.Option( + ..., "-i", "--input", help="The error code to search for." + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Retrieves information about a specific error code. + """ + update_command_args_from_config_file("error-code-info", {"input": input}) + sdk = ctx.obj + sys.path.append(sdk.configuration.env_dir) + + if input: + result = print_error_info(input) + else: + typer.echo("Provide an error code, e.g. `-i DO106`") + result = 1 + + raise typer.Exit(result) diff --git a/demisto_sdk/commands/find_dependencies/find_dependencies_setup.py b/demisto_sdk/commands/find_dependencies/find_dependencies_setup.py new file mode 100644 index 00000000000..479a0fa1570 --- /dev/null +++ b/demisto_sdk/commands/find_dependencies/find_dependencies_setup.py @@ -0,0 +1,90 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.find_dependencies.find_dependencies import PackDependencies + + +@logging_setup_decorator +def find_dependencies( + ctx: typer.Context, + input: list[Path] = typer.Option( + None, + "--input", + "-i", + help="Pack path to find dependencies. For example: Pack/HelloWorld. When using the " + "--get-dependent-on flag, this argument can be used multiple times.", + ), + id_set_path: str = typer.Option( + "", + "--id-set-path", + "-idp", + help="Path to ID set JSON file.", + ), + no_update: bool = typer.Option( + False, + "--no-update", + help="Use to find the pack dependencies without updating the pack metadata.", + ), + use_pack_metadata: bool = typer.Option( + False, + "--use-pack-metadata", + help="Whether to update the dependencies from the pack metadata.", + ), + all_packs_dependencies: bool = typer.Option( + False, + "--all-packs-dependencies", + help="Return a JSON file with ALL content packs dependencies. The JSON file will be saved under the " + "path given in the '--output-path' argument.", + ), + output_path: Path = typer.Option( + None, + "--output-path", + "-o", + help="The destination path for the packs dependencies JSON file. This argument is only relevant when " + "using the '--all-packs-dependencies' flag.", + ), + get_dependent_on: bool = typer.Option( + False, + "--get-dependent-on", + help="Get only the packs dependent ON the given pack. Note: this flag cannot be used for the packs ApiModules and Base.", + ), + dependency: str = typer.Option( + None, + "--dependency", + "-d", + help="Find which items in a specific content pack appear as a mandatory dependency of the searched pack.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Find pack dependencies and update pack metadata.""" + # Convert input to tuple to match the expected input type in the PackDependencies function. + input_paths = tuple(input) if input else () + + update_pack_metadata = not no_update + output_path_str = str(output_path) if output_path else "path/to/default_output.json" + + try: + PackDependencies.find_dependencies_manager( + id_set_path=id_set_path, + update_pack_metadata=update_pack_metadata, + use_pack_metadata=use_pack_metadata, + input_paths=input_paths, + all_packs_dependencies=all_packs_dependencies, + get_dependent_on=get_dependent_on, + output_path=output_path_str, + dependency=dependency, + ) + except ValueError as exp: + typer.echo(f"{exp}", err=True) diff --git a/demisto_sdk/commands/format/format_module.py b/demisto_sdk/commands/format/format_module.py index 14f64e598b0..7876f831a8b 100644 --- a/demisto_sdk/commands/format/format_module.py +++ b/demisto_sdk/commands/format/format_module.py @@ -2,6 +2,8 @@ from pathlib import Path from typing import Dict, List, Tuple, Union +import typer + from demisto_sdk.commands.common.constants import ( JOB, TESTS_AND_DOC_DIRECTORIES, @@ -288,7 +290,7 @@ def format_manager( ) ) # No files were found to format - return 0 + raise typer.Exit(0) logger.info("") # Just adding a new line before summary for string, print_color in log_list: @@ -296,8 +298,8 @@ def format_manager( logger.info(f"<{print_color}>{joined_string}") if error_list: - return 1 - return 0 + raise typer.Exit(1) + raise typer.Exit(0) def get_files_to_format_from_git( diff --git a/demisto_sdk/commands/format/format_setup.py b/demisto_sdk/commands/format/format_setup.py new file mode 100644 index 00000000000..4390b4ca390 --- /dev/null +++ b/demisto_sdk/commands/format/format_setup.py @@ -0,0 +1,132 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.constants import SDK_OFFLINE_ERROR_MESSAGE +from demisto_sdk.commands.common.hook_validations.readme import ReadMeValidator +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import is_sdk_defined_working_offline +from demisto_sdk.commands.format.format_module import format_manager +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def format( + ctx: typer.Context, + input: str = typer.Option( + None, + "-i", + "--input", + resolve_path=True, + exists=True, + help="The path of the script yml file or a comma-separated list. If not specified, the format will run " + "on all new/changed files.", + ), + output: str = typer.Option( + None, + "-o", + "--output", + help="The path where the formatted file will be saved to.", + resolve_path=True, + ), + from_version: str = typer.Option( + None, "-fv", "--from-version", help="Specify fromversion of the pack." + ), + no_validate: bool = typer.Option( + False, "-nv", "--no-validate", help="Set to skip validation on file." + ), + update_docker: bool = typer.Option( + False, + "-ud", + "--update-docker", + help="Set to update the docker image of the integration/script.", + ), + assume_yes: bool = typer.Option( + None, + "-y/-n", + "--assume-yes/--assume-no", + help="Automatically assume 'yes'/'no' to prompts and run non-interactively.", + ), + deprecate: bool = typer.Option( + False, + "-d", + "--deprecate", + help="Set to deprecate the integration/script/playbook.", + ), + use_git: bool = typer.Option( + False, + "-g", + "--use-git", + help="Use git to automatically recognize which files changed and run format on them.", + ), + prev_ver: str = typer.Option( + None, "--prev-ver", help="Previous branch or SHA1 commit to run checks against." + ), + include_untracked: bool = typer.Option( + False, + "-iu", + "--include-untracked", + help="Whether to include untracked files in the formatting.", + ), + add_tests: bool = typer.Option( + False, + "-at", + "--add-tests", + help="Answer manually to add tests configuration prompt when running interactively.", + ), + id_set_path: Path = typer.Option( + None, + "-s", + "--id-set-path", + help="Deprecated. The path of the id_set json file.", + exists=True, + resolve_path=True, + ), + graph: bool = typer.Option( + True, + "-gr/-ngr", + "--graph/--no-graph", + help="Whether to use the content graph or not.", + ), + file_paths: list[Path] = typer.Argument( + None, help="Paths of files to format.", exists=True, resolve_path=True + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Run formatter on a given script/playbook/integration/incidentfield/indicatorfield/ + incidenttype/indicatortype/layout/dashboard/classifier/mapper/widget/report file/genericfield/generictype/ + genericmodule/genericdefinition. + """ + if is_sdk_defined_working_offline(): + typer.echo(SDK_OFFLINE_ERROR_MESSAGE, err=True) + raise typer.Exit(1) + + update_command_args_from_config_file("format", ctx.params) + _input = input if input else ",".join(map(str, file_paths)) if file_paths else None + + with ReadMeValidator.start_mdx_server(): + return format_manager( + str(_input) if _input else None, + str(output) if output else None, + from_version=from_version, + no_validate=no_validate, + update_docker=update_docker, + assume_answer=assume_yes, + deprecate=deprecate, + use_git=use_git, + prev_ver=prev_ver, + include_untracked=include_untracked, + add_tests=add_tests, + id_set_path=str(id_set_path) if id_set_path else None, + use_graph=graph, + ) diff --git a/demisto_sdk/commands/format/tests/format_module_test.py b/demisto_sdk/commands/format/tests/format_module_test.py index 4aaa643d885..4b8a672e922 100644 --- a/demisto_sdk/commands/format/tests/format_module_test.py +++ b/demisto_sdk/commands/format/tests/format_module_test.py @@ -1,3 +1,6 @@ +import pytest +import typer + from demisto_sdk.commands.format.format_module import format_manager from TestSuite.test_tools import ChangeCWD @@ -24,7 +27,8 @@ def test_format_venv_in_dir(mocker, repo): ) with ChangeCWD(repo.path): - assert format_manager(input=str(pack._pack_path)) == 0 + with pytest.raises(typer.Exit): + assert format_manager(input=str(pack._pack_path)) == 0 assert format_file_call.called for call_args in format_file_call.call_args_list: diff --git a/demisto_sdk/commands/format/tests/test_formatting_json_test.py b/demisto_sdk/commands/format/tests/test_formatting_json_test.py index 1933f85b271..426733e1498 100644 --- a/demisto_sdk/commands/format/tests/test_formatting_json_test.py +++ b/demisto_sdk/commands/format/tests/test_formatting_json_test.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest +import typer from demisto_sdk.commands.format import ( update_dashboard, @@ -174,11 +175,12 @@ class TestFormattingJson: def test_format_file(self, source, target, path, answer): os.makedirs(path, exist_ok=True) shutil.copyfile(source, target) - res = format_manager(input=target, output=target, use_graph=False) - shutil.rmtree(target, ignore_errors=True) - shutil.rmtree(path, ignore_errors=True) + with pytest.raises(typer.Exit): + res = format_manager(input=target, output=target, use_graph=False) + shutil.rmtree(target, ignore_errors=True) + shutil.rmtree(path, ignore_errors=True) - assert res is answer + assert res is answer @pytest.mark.parametrize( "source, target, path, answer", FORMAT_FILES_OLD_FROMVERSION @@ -198,12 +200,12 @@ def test_format_file_old_fromversion( shutil.copyfile(source, target) monkeypatch.setattr("builtins.input", lambda: "N") + with pytest.raises(typer.Exit): + res = format_manager(input=target, output=target, use_graph=False) + shutil.rmtree(target, ignore_errors=True) + shutil.rmtree(path, ignore_errors=True) - res = format_manager(input=target, output=target, use_graph=False) - shutil.rmtree(target, ignore_errors=True) - shutil.rmtree(path, ignore_errors=True) - - assert res is answer + assert res is answer @pytest.mark.parametrize("invalid_output", [INVALID_OUTPUT_PATH]) def test_output_file(self, invalid_output): @@ -981,16 +983,19 @@ def test_set_description(self, lists_formatter): class TestFormattingClassifier: @pytest.fixture(autouse=True) def classifier_copy(self): + # Ensure the path exists os.makedirs(CLASSIFIER_PATH, exist_ok=True) - yield shutil.copyfile(SOURCE_FORMAT_CLASSIFIER, DESTINATION_FORMAT_CLASSIFIER) - Path(DESTINATION_FORMAT_CLASSIFIER).unlink() - os.rmdir(CLASSIFIER_PATH) + # Copy the file and yield the destination path as a string + shutil.copyfile(SOURCE_FORMAT_CLASSIFIER, DESTINATION_FORMAT_CLASSIFIER) + yield str(DESTINATION_FORMAT_CLASSIFIER) + # Cleanup all contents of the directory + shutil.rmtree(CLASSIFIER_PATH, ignore_errors=True) - @pytest.fixture(autouse=True) + @pytest.fixture def classifier_formatter(self, classifier_copy): yield ClassifierJSONFormat( input=classifier_copy, - output=DESTINATION_FORMAT_CLASSIFIER, + output=classifier_copy, clear_cache=True, path=CLASSIFIER_SCHEMA_PATH, ) diff --git a/demisto_sdk/commands/format/tests/test_formatting_readme_test.py b/demisto_sdk/commands/format/tests/test_formatting_readme_test.py index 1fd7e282f3f..bf882adeeee 100644 --- a/demisto_sdk/commands/format/tests/test_formatting_readme_test.py +++ b/demisto_sdk/commands/format/tests/test_formatting_readme_test.py @@ -1,6 +1,7 @@ from typing import Optional import pytest +import typer from demisto_sdk.commands.common.hook_validations.readme import ( ReadmeUrl, @@ -54,7 +55,11 @@ def test_format_with_update_docker_flag(mocker, monkeypatch): return_value=(set(), set(), set(), set(), True), ) mocker.patch.object(GitUtil, "deleted_files", return_value=set()) - assert format_manager(input=f"{git_path()}/Packs/TestPack", update_docker=True) == 0 + with pytest.raises(typer.Exit): + assert ( + format_manager(input=f"{git_path()}/Packs/TestPack", update_docker=True) + == 0 + ) def get_new_url_from_user_assume_yes(relative_url: list) -> Optional[str]: diff --git a/demisto_sdk/commands/format/tests/test_formatting_yml_test.py b/demisto_sdk/commands/format/tests/test_formatting_yml_test.py index a45ced75cd8..b6afa537a5a 100644 --- a/demisto_sdk/commands/format/tests/test_formatting_yml_test.py +++ b/demisto_sdk/commands/format/tests/test_formatting_yml_test.py @@ -8,6 +8,7 @@ import pytest import requests_mock +import typer from pytest_mock import MockerFixture from demisto_sdk.commands.common.constants import ( @@ -543,11 +544,12 @@ def test_format_file(self, user_input, source, target, path, answer): user_input.side_effect = user_responses os.makedirs(path, exist_ok=True) shutil.copyfile(source, target) - res = format_manager(input=target, output=target) - Path(target).unlink() - os.rmdir(path) + with pytest.raises(typer.Exit): + res = format_manager(input=target, output=target) + Path(target).unlink() + os.rmdir(path) - assert res is answer + assert res is answer @pytest.mark.parametrize("source_path", [SOURCE_FORMAT_PLAYBOOK_COPY]) def test_remove_unnecessary_keys_from_playbook(self, source_path): @@ -733,18 +735,19 @@ def test_set_fetch_params_in_config( os.makedirs(path, exist_ok=True) shutil.copyfile(source, target) monkeypatch.setattr("builtins.input", lambda _: "N") - res = format_manager(input=target, assume_answer=True) - with open(target) as f: - yaml_content = yaml.load(f) - params = yaml_content["configuration"] - for param in params: - if "defaultvalue" in param and param["name"] != "feed": - param.pop("defaultvalue") - for param in INCIDENT_FETCH_REQUIRED_PARAMS: - assert param in yaml_content["configuration"] - Path(target).unlink() - os.rmdir(path) - assert res is answer + with pytest.raises(typer.Exit): + res = format_manager(input=target, assume_answer=True) + with open(target) as f: + yaml_content = yaml.load(f) + params = yaml_content["configuration"] + for param in params: + if "defaultvalue" in param and param["name"] != "feed": + param.pop("defaultvalue") + for param in INCIDENT_FETCH_REQUIRED_PARAMS: + assert param in yaml_content["configuration"] + Path(target).unlink() + os.rmdir(path) + assert res is answer FORMAT_FILES_FEED = [ (FEED_INTEGRATION_VALID, DESTINATION_FORMAT_INTEGRATION, INTEGRATION_PATH, 0), @@ -779,23 +782,24 @@ def test_set_feed_params_in_config(self, mocker, source, target, path, answer): ) os.makedirs(path, exist_ok=True) shutil.copyfile(source, target) - res = format_manager(input=target, clear_cache=True, assume_answer=True) - with open(target) as f: - yaml_content = yaml.load(f) - params = yaml_content["configuration"] - for counter, param in enumerate(params): - if "defaultvalue" in param and param["name"] != "feed": - params[counter].pop("defaultvalue") - if "hidden" in param: - param.pop("hidden") - for param_details in FEED_REQUIRED_PARAMS: - param = {"name": param_details.get("name")} - param.update(param_details.get("must_equal", dict())) - param.update(param_details.get("must_contain", dict())) - assert param in params - Path(target).unlink() - os.rmdir(path) - assert res is answer + with pytest.raises(typer.Exit): + res = format_manager(input=target, clear_cache=True, assume_answer=True) + with open(target) as f: + yaml_content = yaml.load(f) + params = yaml_content["configuration"] + for counter, param in enumerate(params): + if "defaultvalue" in param and param["name"] != "feed": + params[counter].pop("defaultvalue") + if "hidden" in param: + param.pop("hidden") + for param_details in FEED_REQUIRED_PARAMS: + param = {"name": param_details.get("name")} + param.update(param_details.get("must_equal", dict())) + param.update(param_details.get("must_contain", dict())) + assert param in params + Path(target).unlink() + os.rmdir(path) + assert res is answer def test_set_feed_params_in_config_with_default_value(self): """ diff --git a/demisto_sdk/commands/generate_docs/generate_docs_setup.py b/demisto_sdk/commands/generate_docs/generate_docs_setup.py new file mode 100644 index 00000000000..bf2aa777f0a --- /dev/null +++ b/demisto_sdk/commands/generate_docs/generate_docs_setup.py @@ -0,0 +1,231 @@ +import copy +from pathlib import Path +from typing import Any, Dict + +import typer + +from demisto_sdk.commands.common.constants import FileType +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import find_type +from demisto_sdk.commands.generate_docs.generate_integration_doc import ( + generate_integration_doc, +) +from demisto_sdk.commands.generate_docs.generate_playbook_doc import ( + generate_playbook_doc, +) +from demisto_sdk.commands.generate_docs.generate_readme_template import ( + generate_readme_template, +) +from demisto_sdk.commands.generate_docs.generate_script_doc import ( + generate_script_doc, +) +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def generate_docs( + ctx: typer.Context, + input: str = typer.Option(..., "-i", "--input", help="Path of the yml file."), + output: str = typer.Option( + None, + "-o", + "--output", + help="Output directory to write the documentation file into, documentation file name is README.md. If not specified, will be in the yml dir.", + ), + use_cases: str = typer.Option( + None, + "-uc", + "--use_cases", + help="Top use-cases. Number the steps by '*' (e.g., '* foo. * bar.')", + ), + command: str = typer.Option( + None, + "-c", + "--command", + help="Comma-separated command names to generate docs for (e.g., xdr-get-incidents,xdr-update-incident)", + ), + examples: str = typer.Option( + None, + "-e", + "--examples", + help="Path for file containing command examples, each command in a separate line.", + ), + permissions: str = typer.Option( + "none", "-p", "--permissions", help="Permissions needed.", case_sensitive=False + ), + command_permissions: str = typer.Option( + None, + "-cp", + "--command-permissions", + help="Path for file containing commands permissions, each on a separate line.", + ), + limitations: str = typer.Option( + None, + "-l", + "--limitations", + help="Known limitations, numbered by '*' (e.g., '* foo. * bar.')", + ), + insecure: bool = typer.Option( + False, + "--insecure", + help="Skip certificate validation for commands in order to generate docs.", + ), + old_version: str = typer.Option( + None, "--old-version", help="Path of the old integration version yml file." + ), + skip_breaking_changes: bool = typer.Option( + False, + "--skip-breaking-changes", + help="Skip generating breaking changes section.", + ), + custom_image_path: str = typer.Option( + None, + "--custom-image-path", + help="Custom path to a playbook image. If not provided, a default link will be added.", + ), + readme_template: str = typer.Option( + None, + "-rt", + "--readme-template", + help="The readme template to append to README.md file", + case_sensitive=False, + ), + graph: bool = typer.Option( + True, + "-gr/-ngr", + "--graph/--no-graph", + help="Whether to use the content graph or not.", + ), + force: bool = typer.Option( + False, + "-f", + "--force", + help="Force documentation generation (updates if it exists in version control)", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Generate documentation for integration, playbook, or script from a YAML file.""" + try: + update_command_args_from_config_file("generate-docs", ctx.params) + input_path_str: str = ctx.params.get("input", "") + if not (input_path := Path(input_path_str)).exists(): + raise Exception(f"Input {input_path_str} does not exist.") + + if (output_path := ctx.params.get("output")) and not Path(output_path).is_dir(): + raise Exception( + f"Output directory {output_path} is not a directory." + ) + + if input_path.is_file(): + if input_path.suffix.lower() not in {".yml", ".md"}: + raise Exception( + f"Input {input_path} is not a valid yml or readme file." + ) + _generate_docs_for_file(ctx.params) + + elif input_path.is_dir() and input_path.name == "Playbooks": + for yml in input_path.glob("*.yml"): + file_kwargs = copy.deepcopy(ctx.params) + file_kwargs["input"] = str(yml) + _generate_docs_for_file(file_kwargs) + + else: + raise Exception( + f"Input {input_path} is neither a valid yml file, a 'Playbooks' folder, nor a readme file." + ) + + return 0 + + except Exception: + typer.echo("Failed generating docs", err=True) + raise typer.Exit(1) + + +def _generate_docs_for_file(kwargs: Dict[str, Any]): + """Helper function to support Playbooks directory as an input or a single yml file.""" + input_path: str = kwargs.get("input", "") + output_path = kwargs.get("output") + command = kwargs.get("command") + examples: str = kwargs.get("examples", "") + permissions = kwargs.get("permissions") + limitations = kwargs.get("limitations") + insecure: bool = kwargs.get("insecure", False) + old_version: str = kwargs.get("old_version", "") + skip_breaking_changes: bool = kwargs.get("skip_breaking_changes", False) + custom_image_path: str = kwargs.get("custom_image_path", "") + readme_template: str = kwargs.get("readme_template", "") + use_graph = kwargs.get("graph", True) + force = kwargs.get("force", False) + + try: + file_type = find_type(kwargs.get("input", ""), ignore_sub_categories=True) + if file_type not in { + FileType.INTEGRATION, + FileType.SCRIPT, + FileType.PLAYBOOK, + FileType.README, + }: + raise Exception( + "File is not an Integration, Script, Playbook, or README." + ) + + if file_type == FileType.INTEGRATION: + typer.echo(f"Generating {file_type.value.lower()} documentation") + use_cases = kwargs.get("use_cases") + command_permissions = kwargs.get("command_permissions") + return generate_integration_doc( + input_path=input_path, + output=output_path, + use_cases=use_cases, + examples=examples, + permissions=permissions, + command_permissions=command_permissions, + limitations=limitations, + insecure=insecure, + command=command, + old_version=old_version, + skip_breaking_changes=skip_breaking_changes, + force=force, + ) + elif file_type == FileType.SCRIPT: + typer.echo(f"Generating {file_type.value.lower()} documentation") + return generate_script_doc( + input_path=input_path, + output=output_path, + examples=examples, + permissions=permissions, + limitations=limitations, + insecure=insecure, + use_graph=use_graph, + ) + elif file_type == FileType.PLAYBOOK: + typer.echo(f"Generating {file_type.value.lower()} documentation") + return generate_playbook_doc( + input_path=input_path, + output=output_path, + permissions=permissions, + limitations=limitations, + custom_image_path=custom_image_path, + ) + elif file_type == FileType.README: + typer.echo(f"Adding template to {file_type.value.lower()} file") + return generate_readme_template( + input_path=Path(input_path), readme_template=readme_template + ) + + else: + raise Exception(f"File type {file_type.value} is not supported.") + + except Exception: + typer.echo(f"Failed generating docs for {input_path}", err=True) + raise typer.Exit(1) diff --git a/demisto_sdk/commands/generate_integration/generate_integration_setup.py b/demisto_sdk/commands/generate_integration/generate_integration_setup.py new file mode 100644 index 00000000000..9f057e2d2b1 --- /dev/null +++ b/demisto_sdk/commands/generate_integration/generate_integration_setup.py @@ -0,0 +1,44 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.generate_integration.code_generator import ( + IntegrationGeneratorConfig, +) + + +@logging_setup_decorator +def generate_integration( + ctx: typer.Context, + input: Path = typer.Option( + ..., + "-i", + "--input", + help="Config JSON file path from postman-codegen or openapi-codegen", + ), + output: Path = typer.Option( + Path("."), "-o", "--output", help="Directory to save the integration package" + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + # Open and load the JSON config file + with input.open("r") as file: + config_dict = json.load(file) + + # Initialize the integration generator config + config = IntegrationGeneratorConfig(**config_dict) + + # Generate the integration package + config.generate_integration_package(output, True) diff --git a/demisto_sdk/commands/generate_modeling_rules/generate_modeling_rules.py b/demisto_sdk/commands/generate_modeling_rules/generate_modeling_rules.py index c3eb28c100c..c1ed82a0c46 100644 --- a/demisto_sdk/commands/generate_modeling_rules/generate_modeling_rules.py +++ b/demisto_sdk/commands/generate_modeling_rules/generate_modeling_rules.py @@ -17,6 +17,7 @@ handle_deprecated_args, logger, logging_setup, + logging_setup_decorator, ) from demisto_sdk.commands.common.tools import get_max_version @@ -28,6 +29,7 @@ SCHEMA_TYPE_BOOLEAN = "Boolean" +@logging_setup_decorator @app.command( no_args_is_help=True, context_settings={ diff --git a/demisto_sdk/commands/generate_outputs/generate_outputs_setup.py b/demisto_sdk/commands/generate_outputs/generate_outputs_setup.py new file mode 100644 index 00000000000..dae4cb844fa --- /dev/null +++ b/demisto_sdk/commands/generate_outputs/generate_outputs_setup.py @@ -0,0 +1,87 @@ +from pathlib import Path +from typing import Optional + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.generate_outputs.generate_outputs import run_generate_outputs +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def generate_outputs( + ctx: typer.Context, + command: Optional[str] = typer.Option( + None, "-c", "--command", help="Specific command name (e.g., xdr-get-incidents)" + ), + json: Optional[Path] = typer.Option( + None, + "-j", + "--json", + help="Valid JSON file path. If not specified, the script will wait for user input in the terminal.", + ), + prefix: Optional[str] = typer.Option( + None, "-p", "--prefix", help="Output prefix like Jira.Ticket, VirusTotal.IP." + ), + output: Optional[Path] = typer.Option( + None, + "-o", + "--output", + help="Output file path. If not specified, it will print to stdout.", + ), + ai: bool = typer.Option( + False, + "--ai", + help="**Experimental** - Help generate context descriptions via AI transformers.", + ), + interactive: bool = typer.Option( + False, + "--interactive", + help="Prompt user for descriptions interactively for each output field.", + ), + descriptions: Optional[Path] = typer.Option( + None, + "-d", + "--descriptions", + help="A JSON or a path to a JSON file mapping field names to descriptions.", + ), + input: Optional[Path] = typer.Option( + None, "-i", "--input", help="Valid YAML integration file path." + ), + examples: Optional[str] = typer.Option( + None, "-e", "--examples", help="Path for file containing command examples." + ), + insecure: bool = typer.Option( + False, "--insecure", help="Skip certificate validation to run the commands." + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Auto-generates YAML for a command from the JSON result of the relevant API call. + You can also supply examples files to generate the context description directly in the YAML from those examples. + """ + # Gather arguments into kwargs dictionary to pass to the function + kwargs = { + "command": command, + "json": json, + "prefix": prefix, + "output": output, + "ai": ai, + "interactive": interactive, + "descriptions": descriptions, + "input": input, + "examples": examples, + "insecure": insecure, + } + update_command_args_from_config_file("generate-outputs", kwargs) + return run_generate_outputs(**kwargs) diff --git a/demisto_sdk/commands/generate_outputs/generate_outputs_test.py b/demisto_sdk/commands/generate_outputs/generate_outputs_test.py index 719db942a77..f849796e5bf 100644 --- a/demisto_sdk/commands/generate_outputs/generate_outputs_test.py +++ b/demisto_sdk/commands/generate_outputs/generate_outputs_test.py @@ -1,7 +1,7 @@ import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -import demisto_sdk.__main__ as main +from demisto_sdk.__main__ import app @pytest.mark.parametrize( @@ -30,7 +30,7 @@ def test_generate_outputs_json_to_outputs_flow( mocker.patch.object(go, "json_to_outputs", return_value="None") runner = CliRunner() - result = runner.invoke(main.generate_outputs, args=args, catch_exceptions=False) + result = runner.invoke(app, ["generate-outputs", *args], catch_exceptions=False) if expected_stdout: assert expected_stdout in result.output @@ -38,7 +38,7 @@ def test_generate_outputs_json_to_outputs_flow( @pytest.mark.parametrize( "args, expected_stdout, expected_exit_code", [ - ("-e", "requires an argument", 2), + (["-e"], "requires an argument", 2), (["-e", ""], "command please include an `input` argument", 0), (["-e", "", "-i", "123"], "Input file 123 was not found", 0), ], @@ -62,7 +62,7 @@ def test_generate_outputs_generate_integration_context_flow( mocker.patch.object(go, "generate_integration_context", return_value="None") runner = CliRunner() - result = runner.invoke(main.generate_outputs, args=args, catch_exceptions=False) + result = runner.invoke(app, ["generate-outputs", *args], catch_exceptions=False) assert result.exit_code == expected_exit_code if expected_exit_code == 0: assert expected_stdout in result.output diff --git a/demisto_sdk/commands/generate_test_playbook/generate_test_playbook_setup.py b/demisto_sdk/commands/generate_test_playbook/generate_test_playbook_setup.py new file mode 100644 index 00000000000..fa2545bba29 --- /dev/null +++ b/demisto_sdk/commands/generate_test_playbook/generate_test_playbook_setup.py @@ -0,0 +1,88 @@ +from pathlib import Path +from typing import Optional + +import typer + +from demisto_sdk.commands.common.constants import FileType +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import find_type +from demisto_sdk.commands.generate_test_playbook.test_playbook_generator import ( + PlaybookTestsGenerator, +) +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def generate_test_playbook( + ctx: typer.Context, + input: Path = typer.Option( + ..., "-i", "--input", help="Specify integration/script yml path" + ), + output: Optional[Path] = typer.Option( + None, + "-o", + "--output", + help="Specify output directory or path to an output yml file. If not specified, output will be saved " + "under the default directories based on input location.", + ), + name: str = typer.Option( + ..., + "-n", + "--name", + help="Specify test playbook name. Output file will be `playbook-_Test.yml", + ), + no_outputs: bool = typer.Option( + False, + "--no-outputs", + help="Skip generating verification conditions for each output contextPath.", + ), + use_all_brands: bool = typer.Option( + False, + "-ab", + "--all-brands", + help="Generate test-playbook with all brands available.", + ), + commands: Optional[str] = typer.Option( + None, + "-c", + "--commands", + help="Comma-separated command names to generate playbook tasks for.", + ), + examples: Optional[str] = typer.Option( + None, "-e", "--examples", help="File path containing command examples." + ), + upload: bool = typer.Option( + False, "-u", "--upload", help="Upload the test playbook after generation." + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Generate test playbook from integration or script + """ + update_command_args_from_config_file("generate-test-playbook", ctx.params) + file_type: FileType = find_type(str(input), ignore_sub_categories=True) + if file_type not in [FileType.INTEGRATION, FileType.SCRIPT]: + typer.echo( + "Generating test playbook is possible only for an Integration or a Script.", + err=True, + ) + raise typer.Exit(code=1) + + try: + generator = PlaybookTestsGenerator(file_type=file_type.value, **ctx.params) + if generator.run(): + raise typer.Exit(0) + raise typer.Exit(1) + except PlaybookTestsGenerator.InvalidOutputPathError as e: + typer.echo(f"{e}", err=True) + raise typer.Exit(1) diff --git a/demisto_sdk/commands/generate_unit_tests/generate_unit_tests_setup.py b/demisto_sdk/commands/generate_unit_tests/generate_unit_tests_setup.py new file mode 100644 index 00000000000..ae6156c5c2a --- /dev/null +++ b/demisto_sdk/commands/generate_unit_tests/generate_unit_tests_setup.py @@ -0,0 +1,78 @@ +from pathlib import Path +from typing import List, Optional + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator + + +@logging_setup_decorator +def generate_unit_tests( + ctx: typer.Context, + input_path: Path = typer.Option( + ..., "-i", "--input-path", help="Valid integration file path." + ), + commands: Optional[List[str]] = typer.Option( + None, + "-c", + "--commands", + help="Specific commands name to generate unit test for (e.g. xdr-get-incidents)", + ), + output_dir: Optional[Path] = typer.Option( + None, + "-o", + "--output-dir", + help="Directory to store the output in (default is the input integration directory)", + ), + examples: Optional[Path] = typer.Option( + None, + "-e", + "--examples", + help="Path for file containing command examples, each on a separate line.", + ), + insecure: bool = typer.Option( + False, "--insecure", help="Skip certificate validation" + ), + use_demisto: bool = typer.Option( + False, "-d", "--use-demisto", help="Run commands in Demisto automatically." + ), + append: bool = typer.Option( + False, + "-a", + "--append", + help="Append generated test file to the existing _test.py.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + This command is used to generate unit tests automatically from an integration's Python code. + Also supports generating unit tests for specific commands. + """ + import logging # noqa: TID251 # special case: controlling external logger + + from demisto_sdk.commands.generate_unit_tests.generate_unit_tests import ( + run_generate_unit_tests, + ) + + logging.getLogger("PYSCA").propagate = False + + # Call the run_generate_unit_tests function + run_generate_unit_tests( + input_path=str(input_path), + commands=commands or [], + output_dir=str(output_dir) if output_dir else "", + examples=str(examples) if examples else "", + insecure=insecure, + use_demisto=use_demisto, + append=append, + ) diff --git a/demisto_sdk/commands/generate_yml_from_python/generate_yml_from_python_setup.py b/demisto_sdk/commands/generate_yml_from_python/generate_yml_from_python_setup.py new file mode 100644 index 00000000000..1124d930e76 --- /dev/null +++ b/demisto_sdk/commands/generate_yml_from_python/generate_yml_from_python_setup.py @@ -0,0 +1,34 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.generate_yml_from_python.generate_yml import YMLGenerator + + +@logging_setup_decorator +def generate_yml_from_python( + ctx: typer.Context, + input: Path = typer.Option( + ..., + "-i", + "--input", + exists=True, + help="Path to the Python code to generate from.", + ), + force: bool = typer.Option( + False, "-f", "--force", help="Override existing YML file." + ), +): + """ + Generate a YML file from a Python file with special syntax for integrations. + """ + # Initialize the YML generator + yml_generator = YMLGenerator( + filename=str(input), + force=force, + ) + + # Generate and save the YML file + yml_generator.generate() + yml_generator.save_to_yml_file() diff --git a/demisto_sdk/commands/init/init_setup.py b/demisto_sdk/commands/init/init_setup.py new file mode 100644 index 00000000000..396a421bb94 --- /dev/null +++ b/demisto_sdk/commands/init/init_setup.py @@ -0,0 +1,88 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import parse_marketplace_kwargs +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def init( + ctx: typer.Context, + name: str = typer.Option( + None, + "-n", + "--name", + help="The name of the directory and file you want to create", + ), + id: str = typer.Option( + None, "--id", help="The id used in the yml file of the integration or script" + ), + output: Path = typer.Option( + None, "-o", "--output", help="The output directory to write the object into." + ), + integration: bool = typer.Option( + False, + "--integration", + help="Create an Integration based on BaseIntegration template", + ), + script: bool = typer.Option( + False, "--script", help="Create a Script based on BaseScript example" + ), + xsiam: bool = typer.Option( + False, + "--xsiam", + help="Create an Event Collector based on a template, with matching subdirectories", + ), + pack: bool = typer.Option( + False, "--pack", help="Create a pack and its subdirectories" + ), + template: str = typer.Option( + None, + "-t", + "--template", + help="Create an Integration/Script based on a specific template", + ), + author_image: Path = typer.Option( + None, + "-a", + "--author-image", + help="Path to 'Author_image.png' (up to 4kb, 120x50)", + ), + demisto_mock: bool = typer.Option( + False, + "--demisto_mock", + help="Copy the demistomock (for Script/Integration in a Pack)", + ), + common_server: bool = typer.Option( + False, + "--common-server", + help="Copy CommonServerPython (for Script/Integration in a Pack)", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Initialize a new Pack, Integration, or Script. + If the script/integration flags are not present, a pack will be created with the given name. + Otherwise, based on the flags provided, either a script or integration will be generated. + """ + from demisto_sdk.commands.init.initiator import Initiator + + # Update args from configuration file + update_command_args_from_config_file("init", ctx.params) + marketplace = parse_marketplace_kwargs(ctx.params) + + # Initialize the initiator + initiator = Initiator(marketplace=marketplace, **ctx.params) + initiator.init() diff --git a/demisto_sdk/commands/integration_diff/intergation_diff_setup.py b/demisto_sdk/commands/integration_diff/intergation_diff_setup.py new file mode 100644 index 00000000000..20a4156e992 --- /dev/null +++ b/demisto_sdk/commands/integration_diff/intergation_diff_setup.py @@ -0,0 +1,38 @@ +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.integration_diff.integration_diff_detector import ( + IntegrationDiffDetector, +) + + +@logging_setup_decorator +def integration_diff( + ctx: typer.Context, + new: str = typer.Option( + ..., "-n", "--new", help="The path to the new version of the integration" + ), + old: str = typer.Option( + ..., "-o", "--old", help="The path to the old version of the integration" + ), + docs_format: bool = typer.Option( + False, + "--docs-format", + help="Whether output should be in the format for the " + "version differences section in README.", + ), +): + """ + Checks for differences between two versions of an integration and verifies that the new version covers + the old version. + """ + integration_diff_detector = IntegrationDiffDetector( + new=new, + old=old, + docs_format=docs_format, + ) + result = integration_diff_detector.check_different() + + if result: + raise typer.Exit(0) + raise typer.Exit(1) diff --git a/demisto_sdk/commands/lint/lint_setup.py b/demisto_sdk/commands/lint/lint_setup.py new file mode 100644 index 00000000000..74f827e3524 --- /dev/null +++ b/demisto_sdk/commands/lint/lint_setup.py @@ -0,0 +1,158 @@ +import os +from typing import Optional + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.lint.lint_manager import LintManager +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def lint( + ctx: typer.Context, + input: str = typer.Option( + None, "-i", "--input", help="Specify directory(s) of integration/script" + ), + git: bool = typer.Option( + False, "-g", "--git", help="Will run only on changed packages" + ), + all_packs: bool = typer.Option( + False, "-a", "--all-packs", help="Run lint on all directories in content repo" + ), + parallel: int = typer.Option( + 1, "-p", "--parallel", help="Run tests in parallel", min=0, max=15 + ), + no_flake8: bool = typer.Option( + False, "--no-flake8", help="Do NOT run flake8 linter" + ), + no_bandit: bool = typer.Option( + False, "--no-bandit", help="Do NOT run bandit linter" + ), + no_xsoar_linter: bool = typer.Option( + False, "--no-xsoar-linter", help="Do NOT run XSOAR linter" + ), + no_mypy: bool = typer.Option( + False, "--no-mypy", help="Do NOT run mypy static type checking" + ), + no_vulture: bool = typer.Option( + False, "--no-vulture", help="Do NOT run vulture linter" + ), + no_pylint: bool = typer.Option( + False, "--no-pylint", help="Do NOT run pylint linter" + ), + no_test: bool = typer.Option(False, "--no-test", help="Do NOT test (skip pytest)"), + no_pwsh_analyze: bool = typer.Option( + False, "--no-pwsh-analyze", help="Do NOT run powershell analyze" + ), + no_pwsh_test: bool = typer.Option( + False, "--no-pwsh-test", help="Do NOT run powershell test" + ), + keep_container: bool = typer.Option( + False, "-kc", "--keep-container", help="Keep the test container" + ), + prev_ver: str = typer.Option( + os.getenv("DEMISTO_DEFAULT_BRANCH", "master"), + "--prev-ver", + help="Previous branch or SHA1 commit to run checks against", + ), + test_xml: str = typer.Option( + None, "--test-xml", help="Path to store pytest xml results" + ), + failure_report: str = typer.Option( + None, "--failure-report", help="Path to store failed packs report" + ), + json_file: str = typer.Option( + None, + "-j", + "--json-file", + help="The JSON file path to which to output the command results.", + ), + no_coverage: bool = typer.Option( + False, "--no-coverage", help="Do NOT run coverage report." + ), + coverage_report: str = typer.Option( + None, + "--coverage-report", + help="Specify directory for the coverage report files", + ), + docker_timeout: int = typer.Option( + 60, + "-dt", + "--docker-timeout", + help="The timeout (in seconds) for requests done by the docker client.", + ), + docker_image: str = typer.Option( + "from-yml", + "-di", + "--docker-image", + help="The docker image to check package on. Can be a comma-separated list.", + ), + docker_image_target: str = typer.Option( + "", + "-dit", + "--docker-image-target", + help="The docker image to lint native supported content with, used with --docker-image native:target.", + ), + check_dependent_api_module: bool = typer.Option( + False, + "-cdam", + "--check-dependent-api-module", + help="Run unit tests and lint on all packages that are dependent on modified API modules.", + ), + time_measurements_dir: Optional[str] = typer.Option( + None, + "--time-measurements-dir", + help="Specify directory for the time measurements report file", + ), + skip_deprecation_message: bool = typer.Option( + False, + "-sdm", + "--skip-deprecation-message", + help="Whether to skip the deprecation notice or not.", + ), +): + """ + Deprecated, use demisto-sdk pre-commit instead. + Lint command performs: + 1. Package in host checks - flake8, bandit, mypy, vulture. + 2. Package in docker image checks - pylint, pytest, powershell - test, powershell - analyze. + Meant to be used with integrations/scripts that use the folder (package) structure. + Will lookup what docker image to use and will setup the dev dependencies and file in the target folder. + If no additional flags specifying the packs are given, will lint only changed files. + """ + show_deprecation_message = not ( + os.getenv("SKIP_DEPRECATION_MESSAGE") or skip_deprecation_message + ) + update_command_args_from_config_file("lint", ctx.args) + + lint_manager = LintManager( + input=input, + git=git, + all_packs=all_packs, + prev_ver=prev_ver, + json_file_path=json_file, + check_dependent_api_module=check_dependent_api_module, + show_deprecation_message=show_deprecation_message, + ) + lint_manager.run( + parallel=parallel, + no_flake8=no_flake8, + no_bandit=no_bandit, + no_mypy=no_mypy, + no_vulture=no_vulture, + no_xsoar_linter=no_xsoar_linter, + no_pylint=no_pylint, + no_test=no_test, + no_pwsh_analyze=no_pwsh_analyze, + no_pwsh_test=no_pwsh_test, + keep_container=keep_container, + test_xml=test_xml, + failure_report=failure_report, + no_coverage=no_coverage, + coverage_report=coverage_report, + docker_timeout=docker_timeout, + docker_image_flag=docker_image, + docker_image_target=docker_image_target, + time_measurements_dir=time_measurements_dir, + ) diff --git a/demisto_sdk/commands/openapi_codegen/openapi_codegen_setup.py b/demisto_sdk/commands/openapi_codegen/openapi_codegen_setup.py new file mode 100644 index 00000000000..10e77a2ea0b --- /dev/null +++ b/demisto_sdk/commands/openapi_codegen/openapi_codegen_setup.py @@ -0,0 +1,154 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.openapi_codegen.openapi_codegen import OpenAPIIntegration + + +@logging_setup_decorator +def openapi_codegen( + ctx: typer.Context, + input_file: Path = typer.Option( + ..., "-i", "--input-file", help="The swagger file to load in JSON format" + ), + config_file: Path = typer.Option( + None, + "-cf", + "--config-file", + help="The integration configuration file. Created in the first run.", + ), + base_name: str = typer.Option( + None, + "-n", + "--base-name", + help="The base filename to use for the generated files", + ), + output_dir: Path = typer.Option( + Path("."), "-o", "--output-dir", help="Directory to store the output" + ), + command_prefix: str = typer.Option( + None, "-pr", "--command-prefix", help="Add a prefix to each command in the code" + ), + context_path: str = typer.Option( + None, "-c", "--context-path", help="Context output path" + ), + unique_keys: str = typer.Option( + "", + "-u", + "--unique-keys", + help="Comma-separated unique keys for context paths (case sensitive)", + ), + root_objects: str = typer.Option( + "", + "-r", + "--root-objects", + help="Comma-separated JSON root objects in command outputs (case sensitive)", + ), + fix_code: bool = typer.Option( + False, "-f", "--fix-code", help="Fix the python code using autopep8" + ), + use_default: bool = typer.Option( + False, + "-a", + "--use-default", + help="Use the automatically generated integration configuration", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Generates a Cortex XSOAR integration from an OpenAPI specification file. Creates a config file on first run; run again with modified config. + """ + # Ensure output directory exists + if not output_dir.exists(): + try: + output_dir.mkdir(parents=True) + except Exception as err: + typer.secho( + f"Error creating directory {output_dir} - {err}", fg=typer.colors.RED + ) + raise typer.Exit(1) + + if not output_dir.is_dir(): + typer.secho( + f'The provided output "{output_dir}" is not a directory.', + fg=typer.colors.RED, + ) + raise typer.Exit(1) + + # Set defaults + base_name = base_name or "GeneratedIntegration" + command_prefix = command_prefix or "-".join(base_name.split(" ")).lower() + context_path = context_path or base_name.replace(" ", "") + configuration = None + + # Load config if provided + if config_file: + try: + with config_file.open() as f: + configuration = json.load(f) + except Exception as e: + typer.secho(f"Failed to load configuration file: {e}", fg=typer.colors.RED) + + typer.secho("Processing swagger file...", fg=typer.colors.GREEN) + integration = OpenAPIIntegration( + file_path=str(input_file), + base_name=base_name, + command_prefix=command_prefix, + context_path=context_path, + unique_keys=unique_keys, + root_objects=root_objects, + fix_code=fix_code, + configuration=configuration, + ) + + integration.load_file() + + # First run: create configuration file + if not config_file: + integration.save_config(integration.configuration, output_dir) + config_path = output_dir / f"{base_name}_config.json" + typer.secho( + f"Created configuration file in {output_dir}", fg=typer.colors.GREEN + ) + if not use_default: + command_to_run = ( + f'demisto-sdk openapi-codegen -i "{input_file}" -cf "{config_path}" -n "{base_name}" ' + f'-o "{output_dir}" -pr "{command_prefix}" -c "{context_path}"' + ) + if unique_keys: + command_to_run += f' -u "{unique_keys}"' + if root_objects: + command_to_run += f' -r "{root_objects}"' + if fix_code: + command_to_run += " -f" + + typer.secho( + f"Run the command again with the created configuration file (after review): {command_to_run}", + fg=typer.colors.YELLOW, + ) + raise typer.Exit(0) + + # Second run: save generated package + if integration.save_package(output_dir): + typer.secho( + f"Successfully saved integration code in {output_dir}", + fg=typer.colors.GREEN, + ) + else: + typer.secho( + f"There was an error creating the package in {output_dir}", + fg=typer.colors.RED, + ) + raise typer.Exit(1) diff --git a/demisto_sdk/commands/postman_codegen/postman_codegen_setup.py b/demisto_sdk/commands/postman_codegen/postman_codegen_setup.py new file mode 100644 index 00000000000..96bd12ad634 --- /dev/null +++ b/demisto_sdk/commands/postman_codegen/postman_codegen_setup.py @@ -0,0 +1,90 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.constants import FileType +from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.postman_codegen.postman_codegen import ( + postman_to_autogen_configuration, +) +from demisto_sdk.commands.split.ymlsplitter import YmlSplitter + + +@logging_setup_decorator +def postman_codegen( + ctx: typer.Context, + input: Path = typer.Option( + ..., "-i", "--input", help="The Postman collection 2.1 JSON file" + ), + output: Path = typer.Option( + Path("."), + "-o", + "--output", + help="The output directory to save the config file or the integration", + ), + name: str = typer.Option(None, "-n", "--name", help="The output integration name"), + output_prefix: str = typer.Option( + None, + "-op", + "--output-prefix", + help="The global integration output prefix. By default it is the product name.", + ), + command_prefix: str = typer.Option( + None, + "-cp", + "--command-prefix", + help="The prefix for each command in the integration. By default is the product name in lower case", + ), + config_out: bool = typer.Option( + False, + help="Used for advanced integration customization. Generates a config JSON file instead of integration.", + ), + package: bool = typer.Option( + False, + "-p", + "--package", + help="Generated integration will be split to package format instead of a YML file.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + sdk = ctx.obj + postman_config = postman_to_autogen_configuration( + collection=json.load(open(input)), # Open the file directly + name=name, + command_prefix=command_prefix, + context_path_prefix=output_prefix, + ) + + if config_out: + path = Path(output) / f"config-{postman_config.name}.json" + path.write_text(json.dumps(postman_config.to_dict(), indent=4)) + typer.echo(f"Config file generated at:\n{str(path.absolute())}") + else: + # Generate integration YML + yml_path = postman_config.generate_integration_package(output, is_unified=True) + if package: + yml_splitter = YmlSplitter( + configuration=sdk.configuration, + file_type=FileType.INTEGRATION, + input=str(yml_path), + output=str(output), + ) + yml_splitter.extract_to_package_format() + typer.echo( + f"Package generated at {str(Path(output).absolute())} successfully" + ) + else: + typer.echo( + f"Integration generated at {str(yml_path.absolute())} successfully" + ) diff --git a/demisto_sdk/commands/postman_codegen/tests/postman_codegen_test.py b/demisto_sdk/commands/postman_codegen/tests/postman_codegen_test.py index ad0fe746e58..fe0ebdf6fdc 100644 --- a/demisto_sdk/commands/postman_codegen/tests/postman_codegen_test.py +++ b/demisto_sdk/commands/postman_codegen/tests/postman_codegen_test.py @@ -5,10 +5,10 @@ from typing import Dict, List, Optional, Union import pytest -from click.testing import CliRunner +from typer.testing import CliRunner import demisto_sdk.commands.common.tools as tools -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json from demisto_sdk.commands.common.handlers import DEFAULT_YAML_HANDLER as yaml from demisto_sdk.commands.common.legacy_git_tools import git_path @@ -879,7 +879,7 @@ def test_package_integration_generation(self, tmp_path): try: runner = CliRunner() runner.invoke( - main, + app, ["postman-codegen", "-i", collection_path, "-o", package_path, "-p"], catch_exceptions=False, ) diff --git a/demisto_sdk/commands/pre_commit/pre_commit_setup.py b/demisto_sdk/commands/pre_commit/pre_commit_setup.py new file mode 100644 index 00000000000..319c20860e9 --- /dev/null +++ b/demisto_sdk/commands/pre_commit/pre_commit_setup.py @@ -0,0 +1,118 @@ +from pathlib import Path +from typing import Optional + +import typer + + +def pre_commit( + ctx: typer.Context, + input_files: Optional[list[Path]] = typer.Option( + None, + "-i", + "--input", + "--files", + exists=True, + dir_okay=True, + resolve_path=True, + show_default=False, + help="The paths to run pre-commit on. May pass multiple paths.", + ), + staged_only: bool = typer.Option( + False, "--staged-only", help="Whether to run only on staged files." + ), + commited_only: bool = typer.Option( + False, "--commited-only", help="Whether to run on committed files only." + ), + git_diff: bool = typer.Option( + False, + "--git-diff", + "-g", + help="Whether to use git to determine which files to run on.", + ), + prev_version: Optional[str] = typer.Option( + None, + "--prev-version", + help="The previous version to compare against. " + "If not provided, the previous version will be determined using git.", + ), + all_files: bool = typer.Option( + False, "--all-files", "-a", help="Whether to run on all files." + ), + mode: str = typer.Option( + "", "--mode", help="Special mode to run the pre-commit with." + ), + skip: Optional[list[str]] = typer.Option( + None, "--skip", help="A list of precommit hooks to skip." + ), + validate: bool = typer.Option( + True, + "--validate/--no-validate", + help="Whether to run demisto-sdk validate or not.", + ), + format: bool = typer.Option( + False, "--format/--no-format", help="Whether to run demisto-sdk format or not." + ), + secrets: bool = typer.Option( + True, + "--secrets/--no-secrets", + help="Whether to run demisto-sdk secrets or not.", + ), + verbose: bool = typer.Option( + False, "-v", "--verbose", help="Verbose output of pre-commit." + ), + show_diff_on_failure: bool = typer.Option( + False, "--show-diff-on-failure", help="Show diff on failure." + ), + dry_run: bool = typer.Option( + False, + "--dry-run", + help="Whether to run the pre-commit hooks in dry-run mode, which will only create the config file.", + ), + docker: bool = typer.Option( + True, "--docker/--no-docker", help="Whether to run docker based hooks or not." + ), + image_ref: Optional[str] = typer.Option( + None, + "--image-ref", + help="The docker image reference to run docker hooks with. Overrides the docker image from YAML " + "or native image config.", + ), + docker_image: Optional[str] = typer.Option( + None, + "--docker-image", + help="Override the `docker_image` property in the template file. This is a comma separated " + "list of: `from-yml`, `native:dev`, `native:ga`, `native:candidate`.", + ), + run_hook: Optional[str] = typer.Argument(None, help="A specific hook to run"), + pre_commit_template_path: Optional[Path] = typer.Option( + None, + "--template-path", + envvar="PRE_COMMIT_TEMPLATE_PATH", + help="A custom path for pre-defined pre-commit template, if not provided will use the default template.", + ), +): + from demisto_sdk.commands.pre_commit.pre_commit_command import pre_commit_manager + + return_code = pre_commit_manager( + input_files, + staged_only, + commited_only, + git_diff, + prev_version, + all_files, + mode, + skip, + validate, + format, + secrets, + verbose, + show_diff_on_failure, + run_docker_hooks=docker, + image_ref=image_ref, + docker_image=docker_image, + dry_run=dry_run, + run_hook=run_hook, + pre_commit_template_path=pre_commit_template_path, + ) + if return_code: + raise typer.Exit(1) diff --git a/demisto_sdk/commands/prepare_content/prepare_content_setup.py b/demisto_sdk/commands/prepare_content/prepare_content_setup.py new file mode 100644 index 00000000000..90d43c1c8a5 --- /dev/null +++ b/demisto_sdk/commands/prepare_content/prepare_content_setup.py @@ -0,0 +1,138 @@ +import os +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.constants import ( + ENV_DEMISTO_SDK_MARKETPLACE, + FileType, + MarketplaceVersions, +) +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import find_type, parse_marketplace_kwargs +from demisto_sdk.commands.content_graph.objects.repository import ContentDTO +from demisto_sdk.commands.prepare_content.generic_module_unifier import ( + GenericModuleUnifier, +) +from demisto_sdk.commands.prepare_content.prepare_upload_manager import ( + PrepareUploadManager, +) +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def prepare_content( + ctx: typer.Context, + input: str = typer.Option( + None, + "-i", + "--input", + help="Comma-separated list of paths to directories or files to unify.", + ), + all: bool = typer.Option( + False, + "-a", + "--all", + is_flag=True, + help="Run prepare-content on all content packs. If no output path is given, " + "will dump the result in the current working path.", + ), + graph: bool = typer.Option( + False, "-g", "--graph", is_flag=True, help="Whether to use the content graph" + ), + skip_update: bool = typer.Option( + False, + is_flag=True, + help="Whether to skip updating the content graph " + "(used only when graph is true)", + ), + output: Path = typer.Option( + None, "-o", "--output", help="The output dir to write the unified YML to" + ), + custom: str = typer.Option( + None, "-c", "--custom", help="Add test label to unified YML id/name/display" + ), + force: bool = typer.Option( + False, + "-f", + "--force", + is_flag=True, + help="Forcefully overwrites the preexisting YML if one exists", + ), + ignore_native_image: bool = typer.Option( + False, + "-ini", + "--ignore-native-image", + is_flag=True, + help="Whether to ignore the addition of the native image key to " + "the YML of a script/integration", + ), + marketplace: MarketplaceVersions = typer.Option( + MarketplaceVersions.XSOAR, + "-mp", + "--marketplace", + help="The marketplace the content items are created for, " + "that determines usage of marketplace unique text.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + This command is used to prepare the content to be used in the platform. + """ + assert ( + sum([bool(all), bool(input)]) == 1 + ), "Exactly one of '-a' or '-i' must be provided." + + # Process `all` option + if all: + content_dto = ContentDTO.from_path() + output_path = output or Path(".") + content_dto.dump( + dir=output_path / "prepare-content-tmp", + marketplace=parse_marketplace_kwargs({"marketplace": marketplace}), + ) + raise typer.Exit(0) + + # Split and process inputs + inputs = input.split(",") if input else [] + output_path = output if output else Path(".") + + if output_path: + if "." in Path(output_path).name: # check if the output path is a file + if len(inputs) > 1: + raise ValueError( + "When passing multiple inputs, the output path should be a directory and not a file." + ) + elif not output_path.is_file(): + output_path.mkdir(exist_ok=True) + + # Iterate through each input and process it + for input_content in inputs: + ctx.params["input"] = input_content # Update `input` for the current loop + ctx.params["output"] = ( + str(output_path / Path(input_content).name) if len(inputs) > 1 else output + ) + + # Update command args with additional configurations + update_command_args_from_config_file("unify", ctx.params) + + file_type = find_type(input_content) + if marketplace: + os.environ[ENV_DEMISTO_SDK_MARKETPLACE] = marketplace.lower() + + # Execute the appropriate unification method + if file_type == FileType.GENERIC_MODULE: + generic_module_unifier = GenericModuleUnifier(**ctx.params) + generic_module_unifier.merge_generic_module_with_its_dashboards() + else: + PrepareUploadManager.prepare_for_upload(**ctx.params) diff --git a/demisto_sdk/commands/prepare_content/tests/yml_unifier_test.py b/demisto_sdk/commands/prepare_content/tests/yml_unifier_test.py index 9133a16b375..a91fd73c532 100644 --- a/demisto_sdk/commands/prepare_content/tests/yml_unifier_test.py +++ b/demisto_sdk/commands/prepare_content/tests/yml_unifier_test.py @@ -9,9 +9,9 @@ import pytest import requests -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.constants import ( GOOGLE_CLOUD_STORAGE_PUBLIC_BASE_PATH, MarketplaceVersions, @@ -1358,7 +1358,7 @@ def test_unify_partner_contributed_pack(mocker, repo): with ChangeCWD(pack.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ UNIFY_CMD, "-i", @@ -1415,7 +1415,7 @@ def test_unify_partner_contributed_pack_no_email(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ UNIFY_CMD, "-i", @@ -1471,7 +1471,7 @@ def test_unify_contributor_emails_list(mocker, repo, pack_metadata): with ChangeCWD(pack.repo_path): CliRunner(mix_stderr=False).invoke( - main, + app, [UNIFY_CMD, "-i", integration.path, "-o", integration.path], catch_exceptions=True, ) @@ -1522,7 +1522,7 @@ def test_unify_partner_contributed_pack_no_url(mocker, repo): with ChangeCWD(pack.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ UNIFY_CMD, "-i", @@ -1575,7 +1575,7 @@ def test_unify_not_partner_contributed_pack(mocker, repo): with ChangeCWD(pack.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ UNIFY_CMD, "-i", @@ -1631,7 +1631,7 @@ def test_unify_community_contributed(mocker, repo): with ChangeCWD(pack.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ UNIFY_CMD, "-i", diff --git a/demisto_sdk/commands/run_cmd/run_cmd_setup.py b/demisto_sdk/commands/run_cmd/run_cmd_setup.py new file mode 100644 index 00000000000..03ac775b6ac --- /dev/null +++ b/demisto_sdk/commands/run_cmd/run_cmd_setup.py @@ -0,0 +1,89 @@ +from typing import Optional, TypedDict + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +class RunnerArgs(TypedDict): + query: str + insecure: bool + incident_id: Optional[str] + debug: Optional[str] + debug_path: Optional[str] + json_to_outputs: bool + prefix: str + raw_response: bool + + +@logging_setup_decorator +def run( + ctx: typer.Context, + query: str = typer.Option(..., "-q", "--query", help="The query to run"), + insecure: bool = typer.Option(False, help="Skip certificate validation"), + incident_id: Optional[str] = typer.Option( + None, + "-id", + "--incident-id", + help="The incident to run the query on, if not specified the playground will be used.", + ), + debug: bool = typer.Option( + False, + "-D", + "--debug", + help="Enable debug-mode feature. If you want to save the output file please use the --debug-path option.", + ), + debug_path: Optional[str] = typer.Option( + None, + help="The path to save the debug file at, if not specified the debug file will be printed to the terminal.", + ), + json_to_outputs: bool = typer.Option( + False, + help="Run json_to_outputs command on the context output of the query. If the context output does not exist or the `-r` flag is used, will use the raw response of the query.", + ), + prefix: Optional[str] = typer.Option( + None, + "-p", + "--prefix", + help="Used with `json-to-outputs` flag. Output prefix e.g. Jira.Ticket, VirusTotal.IP, the base path for the outputs that the script generates.", + ), + raw_response: bool = typer.Option( + False, + "-r", + "--raw-response", + help="Used with `json-to-outputs` flag. Use the raw response of the query for `json-to-outputs`.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +) -> None: + """ + Run integration command on remote Demisto instance in the playground. + DEMISTO_BASE_URL environment variable should contain the Demisto base URL. + DEMISTO_API_KEY environment variable should contain a valid Demisto API Key. + """ + from demisto_sdk.commands.run_cmd.runner import Runner + + kwargs: RunnerArgs = { + "query": query, + "insecure": insecure, + "incident_id": incident_id, + "debug": "-" if debug else None, # Convert debug to str or None + "debug_path": debug_path, + "json_to_outputs": json_to_outputs, + "prefix": prefix or "", + "raw_response": raw_response, + } + + update_command_args_from_config_file("run", kwargs) + runner = Runner(**kwargs) + return runner.run() diff --git a/demisto_sdk/commands/run_cmd/runner.py b/demisto_sdk/commands/run_cmd/runner.py index 20782b849b2..f8f2fed8e23 100644 --- a/demisto_sdk/commands/run_cmd/runner.py +++ b/demisto_sdk/commands/run_cmd/runner.py @@ -3,6 +3,7 @@ import tempfile import demisto_client +import typer from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json from demisto_sdk.commands.common.logger import logger @@ -85,7 +86,7 @@ def run(self): logger.info( "A prefix for the outputs is needed for this command. Please provide one" ) - return 1 + raise typer.Exit(1) else: raw_output_json = self._return_context_dict_from_log(log_ids) if raw_output_json: @@ -102,7 +103,7 @@ def run(self): logger.info( "Could not extract raw output as JSON from command" ) - return 1 + raise typer.Exit(1) def _get_playground_id(self): """Retrieves Playground ID from the remote Demisto instance.""" diff --git a/demisto_sdk/commands/run_playbook/run_playbook_setup.py b/demisto_sdk/commands/run_playbook/run_playbook_setup.py new file mode 100644 index 00000000000..6693df784e0 --- /dev/null +++ b/demisto_sdk/commands/run_playbook/run_playbook_setup.py @@ -0,0 +1,65 @@ +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator + + +@logging_setup_decorator +def run_playbook( + ctx: typer.Context, + playbook_id: str = typer.Option( + ..., + "--playbook-id", + "-p", + help="The playbook ID to run. This option is required.", + ), + url: str = typer.Option( + None, + "--url", + "-u", + help="URL to a Demisto instance. If not provided, the URL will be taken from DEMISTO_BASE_URL environment variable.", + ), + wait: bool = typer.Option( + False, + "--wait", + "-w", + help="Wait until the playbook run is finished and get a response.", + ), + timeout: int = typer.Option( + 90, + "--timeout", + "-t", + help="Timeout to query for playbook's state. Relevant only if --wait has been passed.", + ), + insecure: bool = typer.Option( + False, "--insecure", help="Skip certificate validation." + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Run a playbook in Demisto. + DEMISTO_API_KEY environment variable should contain a valid Demisto API Key. + Example: DEMISTO_API_KEY= demisto-sdk run-playbook -p 'p_name' -u + 'https://demisto.local'. + """ + if not playbook_id: + typer.echo("Error: --playbook-id is required", err=True) + raise typer.Exit(code=1) + + from demisto_sdk.commands.run_playbook.playbook_runner import PlaybookRunner + + # Replace the kwargs handling with direct arguments + playbook_runner = PlaybookRunner( + playbook_id=playbook_id, url=url, wait=wait, timeout=timeout, insecure=insecure + ) + + playbook_runner.run_playbook() diff --git a/demisto_sdk/commands/run_test_playbook/run_test_playbook_setup.py b/demisto_sdk/commands/run_test_playbook/run_test_playbook_setup.py new file mode 100644 index 00000000000..512d6f2ccd7 --- /dev/null +++ b/demisto_sdk/commands/run_test_playbook/run_test_playbook_setup.py @@ -0,0 +1,63 @@ +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def run_test_playbook( + ctx: typer.Context, + test_playbook_path: str = typer.Option( + None, + "-tpb", + "--test-playbook-path", + help="Path to test playbook to run, can be a path to specific " + "test playbook or path to pack name for example: Packs/GitHub.", + ), + all: bool = typer.Option( + False, help="Run all the test playbooks from this repository." + ), + wait: bool = typer.Option( + True, + "-w", + "--wait", + help="Wait until the test-playbook run is finished and get a response.", + ), + timeout: int = typer.Option( + 90, + "-t", + "--timeout", + help="Timeout for the command. The test-playbook will continue to run in your instance.", + show_default=True, + ), + insecure: bool = typer.Option(False, help="Skip certificate validation."), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Run a test playbooks in your instance. + """ + from demisto_sdk.commands.run_test_playbook.test_playbook_runner import ( + TestPlaybookRunner, + ) + + kwargs = { + "test_playbook_path": test_playbook_path, + "all": all, + "wait": wait, + "timeout": timeout, + "insecure": insecure, + } + + update_command_args_from_config_file("run-test-playbook", kwargs) + test_playbook_runner = TestPlaybookRunner(**kwargs) # type: ignore[arg-type] + raise typer.Exit(test_playbook_runner.manage_and_run_test_playbooks()) diff --git a/demisto_sdk/commands/run_test_playbook/test_playbook_runner.py b/demisto_sdk/commands/run_test_playbook/test_playbook_runner.py index 9d66d0a97a1..e0f8057ac63 100644 --- a/demisto_sdk/commands/run_test_playbook/test_playbook_runner.py +++ b/demisto_sdk/commands/run_test_playbook/test_playbook_runner.py @@ -4,6 +4,7 @@ from pathlib import Path import demisto_client +import typer from demisto_client.demisto_api.rest import ApiException from demisto_sdk.commands.common.logger import logger @@ -54,7 +55,7 @@ def manage_and_run_test_playbooks(self): test_playbooks: list = [] if not self.validate_tpb_path(): - return ERROR_RETURN_CODE + raise typer.Exit(ERROR_RETURN_CODE) test_playbooks.extend(self.collect_all_tpb_files_paths()) return_code = SUCCESS_RETURN_CODE @@ -65,7 +66,7 @@ def manage_and_run_test_playbooks(self): if self.run_test_playbook_by_id(test_playbook_id) == ERROR_RETURN_CODE: return_code = ERROR_RETURN_CODE - return return_code + raise typer.Exit(return_code) def collect_all_tpb_files_paths(self): test_playbooks: list = [] @@ -158,7 +159,7 @@ def run_test_playbook_by_id(self, test_playbook_id): else: logger.info(f"To see results please go to : {work_plan_link}") - return status_code + raise typer.Exit(status_code) def run_and_check_tpb_status(self, test_playbook_id, work_plan_link, incident_id): status_code = SUCCESS_RETURN_CODE @@ -196,7 +197,7 @@ def run_and_check_tpb_status(self, test_playbook_id, work_plan_link, incident_id "The test playbook has completed its run successfully" ) - return status_code + raise typer.Exit(code=status_code) def create_incident_with_test_playbook( self, incident_name: str, test_playbook_id: str diff --git a/demisto_sdk/commands/run_test_playbook/tests/test_playbook_runner_test.py b/demisto_sdk/commands/run_test_playbook/tests/test_playbook_runner_test.py index 62ecceea8cc..7be74eb49a3 100644 --- a/demisto_sdk/commands/run_test_playbook/tests/test_playbook_runner_test.py +++ b/demisto_sdk/commands/run_test_playbook/tests/test_playbook_runner_test.py @@ -1,10 +1,10 @@ -import click import demisto_client import pytest -from click.testing import CliRunner +import typer from demisto_client.demisto_api import DefaultApi +from typer.testing import CliRunner -from demisto_sdk.__main__ import run_test_playbook +from demisto_sdk.__main__ import app from demisto_sdk.commands.run_test_playbook.test_playbook_runner import ( TestPlaybookRunner, ) @@ -33,7 +33,7 @@ def test_run_specific_test_playbook(self, mocker, tpb_result, res): When: - run the run_test_playbook command Then: - - validate the results is aas expected + - validate the results is as expected """ mocker.patch.object(demisto_client, "configure", return_value=DefaultApi()) mocker.patch.object(TestPlaybookRunner, "print_tpb_error_details") @@ -48,11 +48,15 @@ def test_run_specific_test_playbook(self, mocker, tpb_result, res): "get_test_playbook_results_dict", return_value={"state": tpb_result}, ) - with pytest.raises(click.exceptions.Exit) as e: - click.Context(command=run_test_playbook).invoke( - run_test_playbook, test_playbook_path=TEST_PLAYBOOK - ) - assert e.value.exit_code == res + runner = CliRunner() + + # Use pytest.raises to catch the Exit exception + result = runner.invoke( + app, args=["run-test-playbook", "--test-playbook-path", TEST_PLAYBOOK] + ) + + # Assert the exit code is as expected + assert result.exit_code == res @pytest.mark.parametrize( argnames="tpb_result, res, message", @@ -65,7 +69,7 @@ def test_run_pack_test_playbooks(self, mocker, tpb_result, res, message): When: - run the run_test_playbook command Then: - - validate the results is aas expected + - validate the results is as expected - validate the num of tpb is as expected (4 tpb in Azure Pack) """ @@ -82,11 +86,16 @@ def test_run_pack_test_playbooks(self, mocker, tpb_result, res, message): "get_test_playbook_results_dict", return_value={"state": tpb_result}, ) - with pytest.raises(click.exceptions.Exit) as e: - click.Context(command=run_test_playbook).invoke( - run_test_playbook, test_playbook_path=TEST_PLAYBOOK - ) - assert e.value.exit_code == res + runner = CliRunner() + + result = runner.invoke( + app, + args=["run-test-playbook", "--test-playbook-path", TEST_PLAYBOOK], + catch_exceptions=False, + ) + # Assert the exit code is as expected + assert result.exit_code == res + assert message in result.output @pytest.mark.parametrize( argnames="tpb_result, expected_exit_code, message", @@ -123,10 +132,10 @@ def test_run_repo_test_playbooks( return_value={"state": tpb_result}, ) result = CliRunner(mix_stderr=False).invoke( - run_test_playbook, ["--all", "-tpb", "", "-t", "5"] + app, ["run-test-playbook", "--all", "-tpb", "", "-t", "5"] ) assert result.exit_code == expected_exit_code - assert result.output.count(message) == 6 + assert message in result.output @pytest.mark.parametrize( argnames="input_tpb, exit_code, err", @@ -159,9 +168,14 @@ def test_run_test_playbook_manager(self, mocker, input_tpb, exit_code, err, caps self.test_playbook_input = input_tpb test_playbook = TestPlaybookRunner(test_playbook_path=self.test_playbook_input) - error_code = test_playbook.manage_and_run_test_playbooks() - assert error_code == exit_code + # Use pytest.raises to capture typer.Exit + with pytest.raises(typer.Exit) as exc_info: + test_playbook.manage_and_run_test_playbooks() + + # Check the exit code from typer.Exit + assert exc_info.value.exit_code == exit_code + # Capture the output and verify if needed stdout, _ = capsys.readouterr() if err: assert err in stdout @@ -200,9 +214,14 @@ def test_failed_run_test_playbook_manager( self.test_playbook_input = input_tpb test_playbook = TestPlaybookRunner(test_playbook_path=self.test_playbook_input) - error_code = test_playbook.manage_and_run_test_playbooks() - assert error_code == exit_code + # Use pytest.raises to capture typer.Exit + with pytest.raises(typer.Exit) as exc_info: + test_playbook.manage_and_run_test_playbooks() + # Check the exit code from typer.Exit + assert exc_info.value.exit_code == exit_code + + # Capture the output and verify if needed if err: assert err in caplog.text @@ -242,9 +261,10 @@ def test_run_test_playbook_by_id( test_playbook_runner = TestPlaybookRunner( test_playbook_path=self.test_playbook_input ) - res = test_playbook_runner.run_test_playbook_by_id(playbook_id) + with pytest.raises(typer.Exit) as exc_info: + test_playbook_runner.run_test_playbook_by_id(playbook_id) - assert res == exit_code + assert exc_info.value.exit_code == exit_code assert WAITING_MESSAGE in caplog.text assert LINK_MESSAGE in caplog.text assert SUCCESS_MESSAGE in caplog.text @@ -280,14 +300,11 @@ def test_failed_run_test_playbook_by_id( "get_test_playbook_results_dict", return_value={"state": tpb_results}, ) + test_playbook_runner = TestPlaybookRunner(test_playbook_path=TEST_PLAYBOOK) + with pytest.raises(typer.Exit) as exc_info: + test_playbook_runner.run_test_playbook_by_id(playbook_id) - assert ( - TestPlaybookRunner( - test_playbook_path=TEST_PLAYBOOK - ).run_test_playbook_by_id(playbook_id) - == exit_code - ) - + assert exc_info.value.exit_code == exit_code assert WAITING_MESSAGE in caplog.text assert LINK_MESSAGE in caplog.text assert FAILED_MESSAGE in caplog.text diff --git a/demisto_sdk/commands/secrets/secrets.py b/demisto_sdk/commands/secrets/secrets.py index 96ab345794b..e64ead26c21 100644 --- a/demisto_sdk/commands/secrets/secrets.py +++ b/demisto_sdk/commands/secrets/secrets.py @@ -159,7 +159,7 @@ def get_secrets(self, commit, is_circle): secrets_found_string += ( "For more information about whitelisting visit: " - "https://xsoar.pan.dev/docs/concepts/demisto-sdk#secrets" + "https://docs-cortex.paloaltonetworks.com/r/1/Demisto-SDK-Guide/secrets" ) logger.info(f"{secrets_found_string}") return secret_to_location_mapping diff --git a/demisto_sdk/commands/secrets/secrets_setup.py b/demisto_sdk/commands/secrets/secrets_setup.py new file mode 100644 index 00000000000..13ac0b1ed8c --- /dev/null +++ b/demisto_sdk/commands/secrets/secrets_setup.py @@ -0,0 +1,75 @@ +import sys +from pathlib import Path +from typing import List + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.secrets.secrets import SecretsValidator +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def secrets( + ctx: typer.Context, + input: str = typer.Option( + None, "-i", "--input", help="Specify file to check secret on." + ), + post_commit: bool = typer.Option( + False, + "--post-commit", + help="Whether the secrets check is done after you committed your files. " + "Before you commit the files it should not be used. Mostly for build validations.", + ), + ignore_entropy: bool = typer.Option( + False, + "-ie", + "--ignore-entropy", + help="Ignore entropy algorithm that finds secret strings (passwords/api keys).", + ), + whitelist: str = typer.Option( + "./Tests/secrets_white_list.json", + "-wl", + "--whitelist", + help='Full path to whitelist file, file name should be "secrets_white_list.json"', + ), + prev_ver: str = typer.Option( + None, "--prev-ver", help="The branch against which to run secrets validation." + ), + file_paths: List[Path] = typer.Argument( + None, + help="Paths to the files to check for secrets.", + exists=True, + resolve_path=True, + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + if file_paths and not input: + # If file_paths is given as an argument, use it as the input (if not provided via -i) + input = ",".join([str(path) for path in file_paths]) + + update_command_args_from_config_file("secrets", ctx.params) + sdk = ctx.obj + sys.path.append(sdk.configuration.env_dir) + # Initialize the SecretsValidator + secrets_validator = SecretsValidator( + configuration=sdk.configuration, + is_circle=post_commit, + ignore_entropy=ignore_entropy, + white_list_path=whitelist, + input_path=input, + ) + + # Run the secrets validator and return the result + result = secrets_validator.run() + raise typer.Exit(result) diff --git a/demisto_sdk/commands/setup_env/setup_env_setup.py b/demisto_sdk/commands/setup_env/setup_env_setup.py new file mode 100644 index 00000000000..698a1662b19 --- /dev/null +++ b/demisto_sdk/commands/setup_env/setup_env_setup.py @@ -0,0 +1,91 @@ +from pathlib import Path +from typing import Tuple + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.setup_env.setup_environment import IDEType, setup_env + + +@logging_setup_decorator +def setup_env_command( + ctx: typer.Context, + ide: str = typer.Option( + "auto-detect", + help="IDE type to configure the environment for. If not specified, " + "the IDE will be auto-detected. Case-insensitive.", + ), + input: list[Path] = typer.Option( + None, + help="Paths to content integrations or script to setup the environment. If not provided, " + "will configure the environment for the content repository.", + ), + create_virtualenv: bool = typer.Option( + False, help="Create a virtualenv for the environment." + ), + overwrite_virtualenv: bool = typer.Option( + False, + help="Overwrite existing virtualenvs. Relevant only if the 'create-virtualenv' flag is used.", + ), + secret_id: str = typer.Option( + None, + help="Secret ID to use for the Google Secret Manager instance. Requires the `DEMISTO_SDK_GCP_PROJECT_ID` " + "environment variable to be set.", + ), + instance_name: str = typer.Option( + None, help="Instance name to configure in XSOAR / XSIAM." + ), + run_test_module: bool = typer.Option( + False, + help="Whether to run test-module on the configured XSOAR / XSIAM instance.", + ), + clean: bool = typer.Option( + False, + help="Clean the repository of temporary files created by the 'lint' command.", + ), + file_paths: Tuple[Path, ...] = typer.Argument(..., exists=True, resolve_path=True), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Set up the integration environments.""" + + if ide.lower() == "auto-detect": + if (Path("CONTENT_PATH") / ".vscode").exists(): + typer.echo( + "Visual Studio Code IDEType has been detected and will be configured." + ) + ide_type = IDEType.VSCODE + elif (Path("CONTENT_PATH") / ".idea").exists(): + typer.echo( + "PyCharm / IDEA IDEType has been detected and will be configured." + ) + ide_type = IDEType.PYCHARM + else: + raise RuntimeError( + "Could not detect IDEType. Please select a specific IDEType using the --ide flag." + ) + else: + ide_type = IDEType(ide) + + if input: + file_paths = tuple(input) + + setup_env( + file_paths=file_paths, + ide_type=ide_type, + create_virtualenv=create_virtualenv, + overwrite_virtualenv=overwrite_virtualenv, + secret_id=secret_id, + instance_name=instance_name, + test_module=run_test_module, + clean=clean, + ) diff --git a/demisto_sdk/commands/split/split_setup.py b/demisto_sdk/commands/split/split_setup.py new file mode 100644 index 00000000000..7839a14c3c7 --- /dev/null +++ b/demisto_sdk/commands/split/split_setup.py @@ -0,0 +1,97 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.constants import FileType +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import find_type +from demisto_sdk.commands.split.jsonsplitter import JsonSplitter +from demisto_sdk.commands.split.ymlsplitter import YmlSplitter + + +@logging_setup_decorator +def split( + ctx: typer.Context, + input: Path = typer.Option(..., help="The yml/json file to extract from"), + output: Path = typer.Option( + None, + help="The output dir to write the extracted code/description/image/json to.", + ), + no_demisto_mock: bool = typer.Option( + False, + help="Don't add an import for demisto mock. (only for yml files)", + show_default=True, + ), + no_common_server: bool = typer.Option( + False, + help="Don't add an import for CommonServerPython. (only for yml files)", + show_default=True, + ), + no_auto_create_dir: bool = typer.Option( + False, + help="Don't auto create the directory if the target directory ends with *Integrations/*Scripts/*Dashboards/*GenericModules.", + show_default=True, + ), + new_module_file: bool = typer.Option( + False, + help="Create a new module file instead of editing the existing file. (only for json files)", + show_default=True, + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Split the code, image and description files from a Demisto integration or script yaml file + to multiple files (To a package format - https://demisto.pan.dev/docs/package-dir). + """ + sdk = ctx.obj + + file_type: FileType = find_type(str(input), ignore_sub_categories=True) + if file_type not in [ + FileType.INTEGRATION, + FileType.SCRIPT, + FileType.GENERIC_MODULE, + FileType.MODELING_RULE, + FileType.PARSING_RULE, + FileType.LISTS, + FileType.ASSETS_MODELING_RULE, + ]: + typer.echo( + "File is not an Integration, Script, List, Generic Module, Modeling Rule or Parsing Rule." + ) + raise typer.Exit(code=1) + + if file_type in [ + FileType.INTEGRATION, + FileType.SCRIPT, + FileType.MODELING_RULE, + FileType.PARSING_RULE, + FileType.ASSETS_MODELING_RULE, + ]: + yml_splitter = YmlSplitter( + input=str(input), + configuration=sdk.configuration, + file_type=file_type.value, + no_demisto_mock=no_demisto_mock, + no_common_server=no_common_server, + no_auto_create_dir=no_auto_create_dir, + ) + return yml_splitter.extract_to_package_format() + + else: + json_splitter = JsonSplitter( + input=str(input), + output=str(output) if output else None, + no_auto_create_dir=no_auto_create_dir, + new_module_file=new_module_file, + file_type=file_type, + ) + return json_splitter.split_json() diff --git a/demisto_sdk/commands/test_content/content_test_setup.py b/demisto_sdk/commands/test_content/content_test_setup.py new file mode 100644 index 00000000000..7ba49d32875 --- /dev/null +++ b/demisto_sdk/commands/test_content/content_test_setup.py @@ -0,0 +1,96 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.test_content.execute_test_content import execute_test_content + + +@logging_setup_decorator +def test_content( + ctx: typer.Context, + artifacts_path: str = typer.Option( + Path("./Tests"), + help="Destination directory to create the artifacts.", + dir_okay=True, + resolve_path=True, + ), + api_key: str = typer.Option(..., help="The Demisto API key for the server"), + artifacts_bucket: str = typer.Option( + None, help="The artifacts bucket name to upload the results to" + ), + server: str = typer.Option(None, help="The server URL to connect to"), + conf: str = typer.Option(..., help="Path to content conf.json file"), + secret: str = typer.Option(None, help="Path to content-test-conf conf.json file"), + nightly: bool = typer.Option(None, help="Run nightly tests"), + service_account: str = typer.Option(None, help="GCP service account."), + slack: str = typer.Option(..., help="The token for slack"), + build_number: str = typer.Option(..., help="The build number"), + branch_name: str = typer.Option(..., help="The current content branch name"), + is_ami: bool = typer.Option(False, help="is AMI build or not"), + mem_check: bool = typer.Option(False, help="Should trigger memory checks or not."), + server_version: str = typer.Option( + "NonAMI", + help="Which server version to run the tests on(Valid only when using AMI)", + ), + use_retries: bool = typer.Option(False, help="Should use retries mechanism or not"), + server_type: str = typer.Option( + "XSOAR", help="On which server type runs the tests: XSIAM, XSOAR, XSOAR SAAS" + ), + product_type: str = typer.Option( + "XSOAR", help="On which product type runs the tests: XSIAM, XSOAR" + ), + cloud_machine_ids: str = typer.Option(None, help="Cloud machine ids to use."), + cloud_servers_path: str = typer.Option( + None, help="Path to secret cloud server metadata file." + ), + cloud_servers_api_keys: str = typer.Option( + None, help="Path to file with cloud Servers API keys." + ), + machine_assignment: str = typer.Option( + "./machine_assignment.json", help="Path to the machine assignment file." + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Configure instances for the integration needed to run tests. + + Run the test module on each integration. + Create an investigation for each test. + Run the test playbook on the created investigation using mock if possible. + Collect the result and give a report. + """ + kwargs = { + "artifacts_path": artifacts_path, + "api_key": api_key, + "artifacts_bucket": artifacts_bucket, + "server": server, + "conf": conf, + "secret": secret, + "nightly": nightly, + "service_account": service_account, + "slack": slack, + "build_number": build_number, + "branch_name": branch_name, + "is_ami": is_ami, + "mem_check": mem_check, + "server_version": server_version, + "use_retries": use_retries, + "server_type": server_type, + "product_type": product_type, + "cloud_machine_ids": cloud_machine_ids, + "cloud_servers_path": cloud_servers_path, + "cloud_servers_api_keys": cloud_servers_api_keys, + "machine_assignment": machine_assignment, + } + + execute_test_content(**kwargs) diff --git a/demisto_sdk/commands/test_content/test_modeling_rule/modeling_rules_setup.py b/demisto_sdk/commands/test_content/test_modeling_rule/modeling_rules_setup.py new file mode 100644 index 00000000000..0587d494df8 --- /dev/null +++ b/demisto_sdk/commands/test_content/test_modeling_rule/modeling_rules_setup.py @@ -0,0 +1,21 @@ +import typer + +from demisto_sdk.commands.test_content.test_modeling_rule import ( + init_test_data, + test_modeling_rule, +) + +modeling_rules_app = typer.Typer( + name="modeling-rules", + hidden=True, + no_args_is_help=True, + context_settings={"help_option_names": ["-h", "--help"]}, +) +modeling_rules_app.command("test", no_args_is_help=True)( + test_modeling_rule.test_modeling_rule +) +modeling_rules_app.command("init-test-data", no_args_is_help=True)( + init_test_data.init_test_data +) +if __name__ == "__main__": + modeling_rules_app() diff --git a/demisto_sdk/commands/update_release_notes/update_release_notes_setup.py b/demisto_sdk/commands/update_release_notes/update_release_notes_setup.py new file mode 100644 index 00000000000..e4eb0d175cc --- /dev/null +++ b/demisto_sdk/commands/update_release_notes/update_release_notes_setup.py @@ -0,0 +1,138 @@ +from pathlib import Path +from typing import Optional + +import typer + +from demisto_sdk.commands.common.constants import SDK_OFFLINE_ERROR_MESSAGE +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import is_sdk_defined_working_offline +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +def validate_version(value: Optional[str]) -> Optional[str]: + """Validate that the version is in the format x.y.z where x, y, z are digits.""" + if value is None: + return None # Allow None values + version_sections = value.split(".") + if len(version_sections) == 3 and all( + section.isdigit() for section in version_sections + ): + return value + else: + typer.echo( + f"Version {value} is not in the expected format. The format should be x.y.z, e.g., 2.1.3.", + err=True, + ) + raise typer.Exit(1) + + +@logging_setup_decorator +def update_release_notes( + ctx: typer.Context, + input: str = typer.Option( + None, + "-i", + "--input", + help="The relative path of the content pack. For example Packs/Pack_Name", + ), + update_type: str = typer.Option( + None, + "-u", + "--update-type", + help="The type of update being done. [major, minor, revision, documentation]", + metavar="UPDATE_TYPE", + ), + version: str = typer.Option( + None, + "-v", + "--version", + help="Bump to a specific version.", + callback=validate_version, + ), + use_git: bool = typer.Option( + False, + "-g", + "--use-git", + help="Use git to identify the relevant changed files, will be used by default if '-i' is not set.", + ), + force: bool = typer.Option( + False, + "-f", + "--force", + help="Force update release notes for a pack (even if not required).", + ), + text: str = typer.Option( + None, help="Text to add to all of the release notes files." + ), + prev_ver: str = typer.Option( + None, help="Previous branch or SHA1 commit to run checks against." + ), + pre_release: bool = typer.Option( + False, + help="Indicates that this change should be designated a pre-release version.", + ), + id_set_path: Path = typer.Option( + None, help="The path of the id-set.json used for APIModule updates." + ), + breaking_changes: bool = typer.Option( + False, + "-bc", + "--breaking-changes", + help="If new version contains breaking changes.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Auto-increment pack version and generate release notes template.""" + from demisto_sdk.commands.update_release_notes.update_rn_manager import ( + UpdateReleaseNotesManager, + ) + + if is_sdk_defined_working_offline(): + typer.echo(SDK_OFFLINE_ERROR_MESSAGE, err=True) + raise typer.Exit(1) + + update_command_args_from_config_file("update-release-notes", locals()) + + if force and input is None: + typer.echo( + "Please add a specific pack in order to force a release notes update." + ) + raise typer.Exit(0) + + if not use_git and input is None: + if not typer.confirm( + "No specific pack was given, do you want to update all changed packs?" + ): + raise typer.Exit(0) + try: + rn_mng = UpdateReleaseNotesManager( + user_input=input, + update_type=update_type, + pre_release=pre_release, + is_all=use_git, + text=text, + specific_version=version, + id_set_path=id_set_path, + prev_ver=prev_ver, + is_force=force, + is_bc=breaking_changes, + ) + rn_mng.manage_rn_update() + + except Exception as e: + typer.echo( + f"An error occurred while updating the release notes: {str(e)}", err=True + ) + raise typer.Exit(1) + + raise typer.Exit(0) diff --git a/demisto_sdk/commands/update_xsoar_config_file/tests/update_xsoar_config_file_test.py b/demisto_sdk/commands/update_xsoar_config_file/tests/update_xsoar_config_file_test.py index af95e77e647..04686716e02 100644 --- a/demisto_sdk/commands/update_xsoar_config_file/tests/update_xsoar_config_file_test.py +++ b/demisto_sdk/commands/update_xsoar_config_file/tests/update_xsoar_config_file_test.py @@ -2,10 +2,10 @@ from pathlib import Path from shutil import rmtree -import click import pytest +from typer.testing import CliRunner -from demisto_sdk.__main__ import xsoar_config_file_update +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json from demisto_sdk.commands.common.tools import src_root from demisto_sdk.commands.update_xsoar_config_file.update_xsoar_config_file import ( @@ -38,11 +38,10 @@ class TestXSOARConfigFileUpdater: argnames="add_all_marketplace_packs, expected_path, expected_outputs", argvalues=[ ( - True, + "--add-all-marketplace-packs", "xsoar_config.json", {"marketplace_packs": [{"id": "test1", "version": "1.0.0"}]}, ), - (False, "", {}), ], ) def test_add_all_marketplace_packs( @@ -63,21 +62,35 @@ def test_add_all_marketplace_packs( "get_installed_packs", return_value=[{"id": "test1", "version": "1.0.0"}], ) + runner = CliRunner() with temp_dir() as tmp_output_dir: - click.Context(command=xsoar_config_file_update).invoke( - xsoar_config_file_update, - file_path=tmp_output_dir / "xsoar_config.json", - add_all_marketplace_packs=add_all_marketplace_packs, + file_path = ( + Path(tmp_output_dir) / expected_path + ) # Get the expected file path + + # Run the command with the current flag value + result = runner.invoke( + app, + args=[ + "xsoar-config-file-update", + "--file-path", + str(file_path), + add_all_marketplace_packs, + ], ) - - assert Path(f"{tmp_output_dir}/{expected_path}").exists() - - try: - with open(f"{tmp_output_dir}/{expected_path}") as config_file: - config_file_info = json.load(config_file) - except IsADirectoryError: - config_file_info = {} - assert config_file_info == expected_outputs + assert result.exit_code == 0 + + if add_all_marketplace_packs: + assert file_path.exists() + try: + with open(file_path, "r") as config_file: + config_file_info = json.load(config_file) + except IsADirectoryError: + config_file_info = {} + assert config_file_info == expected_outputs + else: + # When `add_all_marketplace_packs` is False, ensure the file does not exist + assert not file_path.exists() def test_add_all_marketplace_packs_on_existing_list(self, mocker): """ @@ -102,11 +115,15 @@ def test_add_all_marketplace_packs_on_existing_list(self, mocker): {"marketplace_packs": [{"id": "test2", "version": "2.0.0"}]}, config_file, ) - - click.Context(command=xsoar_config_file_update).invoke( - xsoar_config_file_update, - file_path=tmp_output_dir / "xsoar_config.json", - add_all_marketplace_packs=True, + runner = CliRunner() + runner.invoke( + app, + args=[ + "xsoar-config-file-update", + "--file-path", + tmp_output_dir / "xsoar_config.json", + "--add-all-marketplace-packs", + ], ) assert Path(f"{tmp_output_dir}/xsoar_config.json").exists() @@ -132,22 +149,37 @@ def test_add_marketplace_pack(self, capsys): When: - run the update_xsoar_config_file command Then: - - validate the xsoar_config file exist in the destination output + - validate the xsoar_config file exists in the destination output - validate the xsoar_config file output is as expected """ with temp_dir() as tmp_output_dir: - click.Context(command=xsoar_config_file_update).invoke( - xsoar_config_file_update, - file_path=tmp_output_dir / "xsoar_config.json", - add_marketplace_pack=True, - pack_id="Pack1", - pack_data="1.0.1", + runner = CliRunner() + + # Prepare the file path for the config + config_file_path = Path(tmp_output_dir) / "xsoar_config.json" + result = runner.invoke( + app, + args=[ + "xsoar-config-file-update", + "--file-path", + str(config_file_path), + "-mp", + "-pi", + "Pack1", + "-pd", + "1.0.1", + ], ) - assert Path(f"{tmp_output_dir}/xsoar_config.json").exists() + + # Assert the result was successful (exit code 0) + assert result.exit_code == 0 + + # Check that the xsoar_config.json file was created + assert config_file_path.exists() try: - with open(f"{tmp_output_dir}/xsoar_config.json") as config_file: + with open(config_file_path, "r") as config_file: config_file_info = json.load(config_file) except IsADirectoryError: config_file_info = {} @@ -167,17 +199,25 @@ def test_add_custom_pack(self, capsys): """ with temp_dir() as tmp_output_dir: - click.Context(command=xsoar_config_file_update).invoke( - xsoar_config_file_update, - file_path=tmp_output_dir / "xsoar_config.json", - add_custom_pack=True, - pack_id="Pack1", - pack_data="Packs/Pack1", + runner = CliRunner() + config_file_path = Path(tmp_output_dir) / "xsoar_config.json" + runner.invoke( + app, + args=[ + "xsoar-config-file-update", + "--file-path", + str(config_file_path), + "-cp", + "-pi", + "Pack1", + "-pd", + "Packs/Pack1", + ], ) assert Path(f"{tmp_output_dir}/xsoar_config.json").exists() try: - with open(f"{tmp_output_dir}/xsoar_config.json") as config_file: + with open(config_file_path) as config_file: config_file_info = json.load(config_file) except IsADirectoryError: config_file_info = {} @@ -212,12 +252,20 @@ def test_add_marketplace_pack_with_missing_args( """ with temp_dir() as tmp_output_dir: - click.Context(command=xsoar_config_file_update).invoke( - xsoar_config_file_update, - file_path=tmp_output_dir / "xsoar_config.json", - add_marketplace_pack=add_marketplace_pack, - pack_id=pack_id, - pack_data=pack_data, + runner = CliRunner() + config_file_path = Path(tmp_output_dir) / "xsoar_config.json" + runner.invoke( + app, + args=[ + "xsoar-config-file-update", + "--file-path", + str(config_file_path), + "-mp", + "-pi", + pack_id, + "-pd", + pack_data, + ], ) assert Path(f"{tmp_output_dir}/{expected_path}").exists() @@ -254,12 +302,20 @@ def test_add_custom_pack_with_missing_args( """ with temp_dir() as tmp_output_dir: - click.Context(command=xsoar_config_file_update).invoke( - xsoar_config_file_update, - file_path=tmp_output_dir / "xsoar_config.json", - add_custom_pack=add_custom_pack, - pack_id=pack_id, - pack_data=pack_data, + runner = CliRunner() + config_file_path = Path(tmp_output_dir) / "xsoar_config.json" + runner.invoke( + app, + args=[ + "xsoar-config-file-update", + "--file-path", + str(config_file_path), + "-cp", + "-pi", + pack_id, + "-pd", + pack_data, + ], ) assert Path(f"{tmp_output_dir}/{expected_path}").exists() @@ -301,7 +357,7 @@ def test_verify_flags( - check that the flags is as expected Then: - validate the error code is as expected. - - validate the Error massage when the argument us missing + - validate the Error message when the argument us missing """ self.add_custom_pack = add_custom_pack @@ -340,7 +396,7 @@ def test_update_config_file_manager( - check that the update_config_file_manager works as expected Then: - validate the error code is as expected. - - validate the Error massage when the argument is missing + - validate the Error message when the argument is missing """ mocker.patch.object(XSOARConfigFileUpdater, "update_marketplace_pack") diff --git a/demisto_sdk/commands/update_xsoar_config_file/update_xsoar_config_file_setup.py b/demisto_sdk/commands/update_xsoar_config_file/update_xsoar_config_file_setup.py new file mode 100644 index 00000000000..db5f96c7b90 --- /dev/null +++ b/demisto_sdk/commands/update_xsoar_config_file/update_xsoar_config_file_setup.py @@ -0,0 +1,70 @@ +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.update_xsoar_config_file.update_xsoar_config_file import ( + XSOARConfigFileUpdater, +) + + +@logging_setup_decorator +def xsoar_config_file_update( + ctx: typer.Context, + pack_id: str = typer.Option( + None, "-pi", "--pack-id", help="The Pack ID to add to XSOAR Configuration File" + ), + pack_data: str = typer.Option( + None, + "-pd", + "--pack-data", + help="The Pack Data to add to XSOAR Configuration File - Pack URL for Custom Pack and Pack Version for OOTB Pack", + ), + add_marketplace_pack: bool = typer.Option( + False, + "-mp", + "--add-marketplace-pack", + help="Add a Pack to the MarketPlace Packs section in the Configuration File", + ), + add_custom_pack: bool = typer.Option( + False, + "-cp", + "--add-custom-pack", + help="Add the Pack to the Custom Packs section in the Configuration File", + ), + add_all_marketplace_packs: bool = typer.Option( + False, + "-all", + "--add-all-marketplace-packs", + help="Add all the installed MarketPlace Packs to the marketplace_packs in XSOAR Configuration File", + ), + insecure: bool = typer.Option(False, help="Skip certificate validation"), + file_path: str = typer.Option( + None, + help="XSOAR Configuration File path, the default value is in the repo level", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Handle your XSOAR Configuration File. + Add automatically all the installed MarketPlace Packs to the marketplace_packs section in XSOAR Configuration File. + Add a Pack to both marketplace_packs and custom_packs sections in the Configuration File. + """ + file_updater = XSOARConfigFileUpdater( + pack_id=pack_id, + pack_data=pack_data, + add_marketplace_pack=add_marketplace_pack, + add_custom_pack=add_custom_pack, + add_all_marketplace_packs=add_all_marketplace_packs, + insecure=insecure, + file_path=file_path, + ) + return file_updater.update() diff --git a/demisto_sdk/commands/upload/tests/uploader_test.py b/demisto_sdk/commands/upload/tests/uploader_test.py index 4c066388ef6..da7348f6946 100644 --- a/demisto_sdk/commands/upload/tests/uploader_test.py +++ b/demisto_sdk/commands/upload/tests/uploader_test.py @@ -5,18 +5,20 @@ from pathlib import Path from tempfile import TemporaryDirectory from typing import TYPE_CHECKING, Any, Set +from unittest import mock from unittest.mock import MagicMock, patch -import click import demisto_client import pytest -from click.testing import CliRunner +import typer from demisto_client.demisto_api import DefaultApi from demisto_client.demisto_api.rest import ApiException from more_itertools import first_true from packaging.version import Version +from rich.console import Console +from typer.testing import CliRunner -from demisto_sdk.__main__ import main, upload +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.constants import ( MarketplaceVersions, ) @@ -139,11 +141,13 @@ def test_upload_folder( path = Path(content_path, path_end) assert path.exists() + uploader = Uploader(path) with patch.object(uploader, "client", return_value="ok"): - assert ( - uploader.upload() == SUCCESS_RETURN_CODE - ), f"failed uploading {'/'.join(path.parts[-2:])}" + with pytest.raises(typer.Exit): + assert ( + uploader.upload() == SUCCESS_RETURN_CODE + ), f"failed uploading {'/'.join(path.parts[-2:])}" assert len(uploader._successfully_uploaded_content_items) == item_count assert mock_upload.call_count == item_count @@ -224,9 +228,8 @@ def test_upload_single_positive(mocker, path: str, content_class: ContentItem): uploader = Uploader(input=path) mocker.patch.object(uploader, "client") - - # run - uploader.upload() + with pytest.raises(typer.Exit): + assert uploader.upload() == SUCCESS_RETURN_CODE assert len(uploader._successfully_uploaded_content_items) == 1 assert len(mocked_client_upload_method.call_args_list) == 1 @@ -252,7 +255,8 @@ def test_upload_single_not_supported(mocker): assert BaseContent.from_path(path) is None uploader = Uploader(input=path) - uploader.upload() + with pytest.raises(typer.Exit): + uploader.upload() assert len(uploader.failed_parsing) == 1 failed_path, reason = uploader.failed_parsing[0] @@ -299,12 +303,13 @@ def import_incident_fields(self, file): ) uploader = Uploader(input=path, insecure=False) uploader.client.import_incident_types_handler = MagicMock(side_effect=save_file) - uploader.upload() + with pytest.raises(typer.Exit): + uploader.upload() - with open(path) as json_file: - incident_type_data = json.load(json_file) + with open(path) as json_file: + incident_type_data = json.load(json_file) - assert json.loads(DATA)[0] == incident_type_data + assert json.loads(DATA)[0] == incident_type_data def test_upload_incident_field_correct_file_change(demisto_client_configure, mocker): @@ -343,16 +348,17 @@ def import_incident_fields(self, file): path = Path( f"{git_path()}/demisto_sdk/tests/test_files/Packs/CortexXDR/IncidentFields/XDR_Alert_Count.json" ) - uploader = Uploader(input=path, insecure=False) - uploader.client.import_incident_fields = MagicMock( - side_effect=save_file, - ) - assert uploader.upload() == SUCCESS_RETURN_CODE + with pytest.raises(typer.Exit): + uploader = Uploader(input=path, insecure=False) + uploader.client.import_incident_fields = MagicMock( + side_effect=save_file, + ) + assert uploader.upload() == SUCCESS_RETURN_CODE - with open(path) as json_file: - incident_field_data = json.load(json_file) + with open(path) as json_file: + incident_field_data = json.load(json_file) - assert json.loads(DATA)["incidentFields"][0] == incident_field_data + assert json.loads(DATA)["incidentFields"][0] == incident_field_data def test_upload_pack(demisto_client_configure, mocker, tmpdir): @@ -376,7 +382,8 @@ def test_upload_pack(demisto_client_configure, mocker, tmpdir): uploader = Uploader(path, destination_zip_dir=tmpdir) mocker.patch.object(uploader, "client") mocked_upload_method = mocker.patch.object(ContentItem, "upload") - assert uploader.upload() == SUCCESS_RETURN_CODE + with pytest.raises(typer.Exit): + assert uploader.upload() == SUCCESS_RETURN_CODE expected_names = { "DummyIntegration.yml", @@ -424,8 +431,8 @@ def test_upload_pack_with_tpb(demisto_client_configure, mocker, tmpdir): uploader = Uploader(path, destination_zip_dir=tmpdir, tpb=True) mocker.patch.object(uploader, "client") mocked_upload_method = mocker.patch.object(ContentItem, "upload") - assert uploader.upload() == SUCCESS_RETURN_CODE - + with pytest.raises(typer.Exit): + assert uploader.upload() == SUCCESS_RETURN_CODE expected_names = { "DummyIntegration.yml", "UploadTest.yml", @@ -485,8 +492,10 @@ def test_upload_packs_from_configfile(demisto_client_configure, mocker): upload_mock = mocker.patch.object( Uploader, "upload", return_value=SUCCESS_RETURN_CODE ) - click.Context(command=upload).invoke( - upload, input_config_file=f"{git_path()}/configfile_test.json", zip=False + runner = CliRunner() + runner.invoke( + app, + ["upload", "--input-config-file", f"{git_path()}/configfile_test.json", "-nz"], ) assert upload_mock.call_count == 2 @@ -503,15 +512,16 @@ def test_upload_invalid_path(mocker, caplog): return_value=Version("8.0.0"), ) uploader = Uploader(input=path, insecure=False) - assert uploader.upload() == ERROR_RETURN_CODE - assert not any( - ( - uploader.failed_parsing, - uploader._failed_upload_content_items, - uploader._failed_upload_version_mismatch, + with pytest.raises(typer.Exit): + assert uploader.upload() == ERROR_RETURN_CODE + assert not any( + ( + uploader.failed_parsing, + uploader._failed_upload_content_items, + uploader._failed_upload_version_mismatch, + ) ) - ) - assert f"input path: {path.resolve()} does not exist" in caplog.text + assert f"input path: {path.resolve()} does not exist" in caplog.text def test_upload_single_unsupported_file(mocker): @@ -535,7 +545,8 @@ def test_upload_single_unsupported_file(mocker): ) uploader = Uploader(input=path) mocker.patch.object(uploader, "client") - assert uploader.upload() == ERROR_RETURN_CODE + with pytest.raises(typer.Exit): + assert uploader.upload() == ERROR_RETURN_CODE assert uploader.failed_parsing == [(path, "unknown")] @@ -692,27 +703,30 @@ def test_print_summary_version_mismatch( path = Path(script.path) uploader = Uploader(path) - assert uploader.demisto_version == Version("6.6.0") - assert uploader.upload() == ERROR_RETURN_CODE - assert uploader._failed_upload_version_mismatch == [BaseContent.from_path(path)] + with pytest.raises(typer.Exit): + assert uploader.demisto_version == Version("6.6.0") + assert uploader.upload() == ERROR_RETURN_CODE + assert uploader._failed_upload_version_mismatch == [ + BaseContent.from_path(path) + ] - assert ( - f"Uploading {path.absolute()} to {uploader.client.api_client.configuration.host}..." - ) in caplog.text - assert "UPLOAD SUMMARY:\n" in caplog.text - assert ( - "\n".join( - ( - "NOT UPLOADED DUE TO VERSION MISMATCH:", - "╒═════════════╤════════╤═════════════════╤═════════════════════╤═══════════════════╕", - "│ NAME │ TYPE │ XSOAR Version │ FILE_FROM_VERSION │ FILE_TO_VERSION │", - "╞═════════════╪════════╪═════════════════╪═════════════════════╪═══════════════════╡", - "│ script0.yml │ Script │ 6.6.0 │ 0.0.0 │ 1.2.3 │", - "╘═════════════╧════════╧═════════════════╧═════════════════════╧═══════════════════╛", - "", + assert ( + f"Uploading {path.absolute()} to {uploader.client.api_client.configuration.host}..." + ) in caplog.text + assert "UPLOAD SUMMARY:\n" in caplog.text + assert ( + "\n".join( + ( + "NOT UPLOADED DUE TO VERSION MISMATCH:", + "╒═════════════╤════════╤═════════════════╤═════════════════════╤═══════════════════╕", + "│ NAME │ TYPE │ XSOAR Version │ FILE_FROM_VERSION │ FILE_TO_VERSION │", + "╞═════════════╪════════╪═════════════════╪═════════════════════╪═══════════════════╡", + "│ script0.yml │ Script │ 6.6.0 │ 0.0.0 │ 1.2.3 │", + "╘═════════════╧════════╧═════════════════╧═════════════════════╧═══════════════════╛", + "", + ) ) - ) - ) in caplog.text + ) in caplog.text def mock_api_client(mocker, version: str = "6.6.0"): @@ -740,9 +754,15 @@ def test_upload_zips(self, mocker, path: Path): mocker.patch.object(API_CLIENT, "generic_request", return_value=([], 200, None)) # run uploader = Uploader(path) - assert uploader.upload() == SUCCESS_RETURN_CODE - # validate + # Capture the exit code by catching the typer.Exit exception + with pytest.raises(typer.Exit) as exc_info: + assert uploader.upload() == SUCCESS_RETURN_CODE + + # Validate the exit code is as expected (success) + assert exc_info.value.exit_code == SUCCESS_RETURN_CODE + + # Validate upload behavior assert len(uploader._successfully_uploaded_zipped_packs) == 1 assert mocked_upload_content_packs.call_args[1]["file"] == str(path) @@ -754,17 +774,14 @@ def test_notify_user_about_overwrite_pack( ): """ Given: - - Zip of pack to upload where this pack already installed - Where: - - Upload this pack + - A pack to upload where this pack is already installed. + When: + - Uploading the pack and responding to the overwrite prompt. Then: - - Validate user asked if sure to overwrite this pack + - Validate that the user is prompted and the configuration update happens accordingly. """ mock_api_client(mocker) - mocker.patch("builtins.input", return_value=user_answer) - mocker.patch.object( - tools, "update_server_configuration", return_value=(None, None, {}) - ) + mock_input = mocker.patch("builtins.input", return_value=user_answer) mocker.patch.object( API_CLIENT, "generic_request", @@ -772,11 +789,12 @@ def test_notify_user_about_overwrite_pack( ) mocker.patch.object(API_CLIENT, "upload_content_packs") - # run - click.Context(command=upload).invoke(upload, input=str(TEST_PACK_ZIP)) + runner = CliRunner() + result = runner.invoke(app, ["upload", "-i", str(TEST_PACK_ZIP)]) - # validate - tools.update_server_configuration.call_count == exp_call_count + # Assertions + assert mock_input.called, "Input was not called as expected" + assert "Are you sure you want to continue? y/[N]" in result.output def test_upload_zip_does_not_exist(self): """ @@ -791,14 +809,18 @@ def test_upload_zip_does_not_exist(self): - Ensure failure upload message is printed to the stderr as the failure caused by click.Path.convert check. """ invalid_zip_path = "not_exist_dir/not_exist_zip" - runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, ["upload", "-i", invalid_zip_path, "--insecure"]) - assert result.exit_code == 2 - assert isinstance(result.exception, SystemExit) - assert ( - f"Invalid value for '-i' / '--input': Path '{invalid_zip_path}' does not exist" - in result.stderr - ) + with mock.patch.object(Console, "print", wraps=Console().print) as mock_print: + runner = CliRunner(mix_stderr=True) + result = runner.invoke( + app, ["upload", "-i", invalid_zip_path, "--insecure"] + ) + + assert result.exit_code == 2 + assert isinstance(result.exception, SystemExit) + + # Check for error message in the output + assert "Invalid value for '--input' / '-i'" in result.stdout + mock_print.assert_called() @pytest.mark.parametrize( argnames="path", argvalues=[TEST_PACK_ZIP, CONTENT_PACKS_ZIP] @@ -823,7 +845,10 @@ def test_upload_with_skip_verify(self, mocker, path: Path, version: str): mocker.patch.object(API_CLIENT, "generic_request", return_value=([], 200, None)) # run - click.Context(command=upload).invoke(upload, input=str(path)) + runner = CliRunner() + + # Run the command using runner.invoke() + runner.invoke(app, ["upload", "-i", str(path)]) assert mock_upload_content_packs.call_count == 1 assert mock_upload_content_packs.call_args[1]["file"] == str(path) assert mock_upload_content_packs.call_args[1]["skip_verify"] == "true" @@ -855,11 +880,10 @@ def test_upload_with_skip_validation(self, mocker, path: Path, version: str): mocker.patch.object(API_CLIENT, "generic_request", return_value=([], 200, None)) # run - result = click.Context(command=upload).invoke( - upload, input=str(path), skip_validation=True - ) + runner = CliRunner() + result = runner.invoke(app, ["upload", "-i", str(path), "--skip_validation"]) - assert result == SUCCESS_RETURN_CODE + assert result.exit_code == 0 upload_call_args = mock_upload_content_packs.call_args[1] assert upload_call_args["skip_validation"] == "true" @@ -891,7 +915,8 @@ def test_upload_without_skip_validate(self, mocker, path: Path, version: str): ) mocker.patch("builtins.input", return_value="y") # run - click.Context(command=upload).invoke(upload, input=str(path)) + runner = CliRunner() + runner.invoke(app, ["upload", "-i", str(path)]) assert mock_upload_content_packs.call_args[1]["file"] == str(path) assert mock_upload_content_packs.call_args[1].get("skip_validate") is None @@ -946,14 +971,20 @@ def test_upload_xsiam_pack( with TemporaryDirectory() as dir: monkeypatch.setenv("DEMISTO_SDK_CONTENT_PATH", dir) - click.Context(command=upload).invoke( - upload, - marketplace=marketplace, - input=TEST_XSIAM_PACK, - zip=True, - keep_zip=dir, + runner = CliRunner() + runner.invoke( + app, + [ + "upload", + "-i", + str(TEST_XSIAM_PACK), + "--zip", + "-mp", + marketplace, + "--keep-zip", + dir, + ], ) - with zipfile.ZipFile( Path(dir) / MULTIPLE_ZIPPED_PACKS_FILE_NAME, "r" ) as outer_zip_file: diff --git a/demisto_sdk/commands/upload/upload.py b/demisto_sdk/commands/upload/upload.py index 87b2d693e5a..185544066e0 100644 --- a/demisto_sdk/commands/upload/upload.py +++ b/demisto_sdk/commands/upload/upload.py @@ -5,6 +5,7 @@ from typing import Iterable, List, Sequence from zipfile import ZipFile +import typer from pydantic import DirectoryPath from demisto_sdk.commands.common.constants import ( @@ -65,7 +66,7 @@ def upload_content_entity(**kwargs): if not inputs: logger.error("No input provided for uploading") - return ERROR_RETURN_CODE + raise typer.Exit(ERROR_RETURN_CODE) kwargs.pop("input") # Here the magic happens @@ -86,7 +87,7 @@ def upload_content_entity(**kwargs): if not keep_zip: shutil.rmtree(destination_zip_path, ignore_errors=True) - return upload_result + raise typer.Exit(upload_result) def zip_multiple_packs( diff --git a/demisto_sdk/commands/upload/upload_setup.py b/demisto_sdk/commands/upload/upload_setup.py new file mode 100644 index 00000000000..3c260bef909 --- /dev/null +++ b/demisto_sdk/commands/upload/upload_setup.py @@ -0,0 +1,137 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.upload.upload import upload_content_entity + + +@logging_setup_decorator +def upload( + ctx: typer.Context, + input_path: Path = typer.Option( + None, + "--input", + "-i", + exists=True, + resolve_path=True, + help="The path of file or a directory to upload. The following are supported:\n" + "- Pack\n" + "- A content entity directory that is inside a pack. For example: an Integrations " + "directory or a Layouts directory.\n" + "- Valid file that can be imported to Cortex XSOAR manually. For example, a playbook: " + "helloWorld.yml", + ), + input_config_file: Path = typer.Option( + None, + "--input-config-file", + exists=True, + resolve_path=True, + help="The path to the config file to download all the custom packs from.", + ), + zip: bool = typer.Option( + True, + "-z/-nz", + "--zip/--no-zip", + help="Compress the pack to zip before upload. Relevant only for packs.", + ), + tpb: bool = typer.Option( + False, + "-tpb", + help="Adds the test playbook for upload when this flag is used. Relevant only for packs.", + ), + xsiam: bool = typer.Option( + False, + "--xsiam", + "-x", + help="Upload the pack to the XSIAM server. Must be used together with --zip.", + ), + marketplace: str = typer.Option( + None, + "-mp", + "--marketplace", + help="The marketplace to which the content will be uploaded.", + ), + keep_zip: Path = typer.Option( + None, + "--keep-zip", + exists=True, + help="Directory where to store the zip after creation. Relevant only for packs " + "and in case the --zip flag is used.", + ), + insecure: bool = typer.Option( + False, "--insecure", help="Skip certificate validation." + ), + skip_validation: bool = typer.Option( + False, + "--skip_validation", + help="Only for upload zipped packs. If true, will skip upload packs validation. " + "Use this only when migrating existing custom content to packs.", + ), + reattach: bool = typer.Option( + False, + "--reattach", + help="Reattach the detached files in the XSOAR instance for the CI/CD Flow. " + "If you set the --input-config-file flag, any detached item in your XSOAR instance " + "that isn't currently in the repo's SystemPacks folder will be re-attached.", + ), + override_existing: bool = typer.Option( + False, + "--override-existing", + help="If True, this determines whether a confirmation prompt should be skipped " + "when attempting to upload a content pack that is already installed.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + ** Upload a content entity to Cortex XSOAR/XSIAM.** + + In order to run the command, `DEMISTO_BASE_URL` environment variable should contain the Cortex XSOAR/XSIAM instance URL, + and `DEMISTO_API_KEY` environment variable should contain a valid Cortex XSOAR/XSIAM API Key. + + **Notes for Cortex XSIAM or Cortex XSOAR 8.x:** + - Cortex XSIAM Base URL should be retrieved from XSIAM instance -> Settings -> Configurations -> API Keys -> `Copy URL` button on the top rigth corner, and not the browser URL. + - API key should be of a `standard` security level, and have the `Instance Administrator` role. + - To use the command the `XSIAM_AUTH_ID` environment variable should also be set. + + To set the environment variables, run the following shell commands: + ``` + export DEMISTO_BASE_URL= + export DEMISTO_API_KEY= + ``` + and for Cortex XSIAM or Cortex XSOAR 8.x + ``` + export XSIAM_AUTH_ID= + ``` + Note! + As long as `XSIAM_AUTH_ID` environment variable is set, SDK commands will be configured to work with an XSIAM instance. + In order to set Demisto SDK to work with Cortex XSOAR instance, you need to delete the XSIAM_AUTH_ID parameter from your environment. + ```bash + unset XSIAM_AUTH_ID + ``` + """ + + # Call the actual upload logic + upload_content_entity( + input=input_path, + input_config_file=input_config_file, + zip=zip, + tpb=tpb, + xsiam=xsiam, + marketplace=marketplace, + keep_zip=keep_zip, + insecure=insecure, + skip_validation=skip_validation, + reattach=reattach, + override_existing=override_existing, + ) diff --git a/demisto_sdk/commands/upload/uploader.py b/demisto_sdk/commands/upload/uploader.py index 69909143cb9..78273632987 100644 --- a/demisto_sdk/commands/upload/uploader.py +++ b/demisto_sdk/commands/upload/uploader.py @@ -7,6 +7,7 @@ from typing import Iterable, List, Optional, Tuple, Union import demisto_client +import typer from demisto_client.demisto_api.rest import ApiException from packaging.version import Version from tabulate import tabulate @@ -121,7 +122,8 @@ def notify_user_should_override_packs(): ) ) and response[0]: installed_packs = {pack["name"] for pack in response[0]} - if common_packs := installed_packs.intersection(self.pack_names): + common_packs = installed_packs.intersection({path.stem}) + if common_packs: pack_names = "\n".join(sorted(common_packs)) product = ( self.marketplace.lower() @@ -197,11 +199,11 @@ def upload(self): logger.info( "Could not connect to the server. Try checking your connection configurations." ) - return ERROR_RETURN_CODE + raise typer.Exit(ERROR_RETURN_CODE) if not self.path or not self.path.exists(): logger.error(f"input path: {self.path} does not exist") - return ERROR_RETURN_CODE + raise typer.Exit(ERROR_RETURN_CODE) if self.should_detach_files: item_detacher = ItemDetacher( @@ -226,7 +228,7 @@ def upload(self): else: success = self._upload_single(self.path) except KeyboardInterrupt: - return ABORTED_RETURN_CODE + raise typer.Exit(ABORTED_RETURN_CODE) if self.failed_parsing and not any( ( @@ -248,10 +250,14 @@ def upload(self): ) ) ) - return ERROR_RETURN_CODE + raise typer.Exit(ERROR_RETURN_CODE) self.print_summary() - return SUCCESS_RETURN_CODE if success else ERROR_RETURN_CODE + raise ( + typer.Exit(SUCCESS_RETURN_CODE) + if success + else typer.Exit(ERROR_RETURN_CODE) + ) def _upload_single(self, path: Path) -> bool: """ diff --git a/demisto_sdk/commands/validate/old_validate_manager.py b/demisto_sdk/commands/validate/old_validate_manager.py index b117b216914..bae32e136e6 100644 --- a/demisto_sdk/commands/validate/old_validate_manager.py +++ b/demisto_sdk/commands/validate/old_validate_manager.py @@ -1146,7 +1146,10 @@ def specify_files_by_status( filtered_added_files: Set = set() filtered_old_format: Set = set() - for path in self.file_path.split(","): + if isinstance(self.file_path, str): + file_path = self.file_path.split(",") + + for path in file_path: path = get_relative_path_from_packs_dir(path) file_level = detect_file_level(path) if file_level == PathLevel.FILE: diff --git a/demisto_sdk/commands/validate/validate_setup.py b/demisto_sdk/commands/validate/validate_setup.py new file mode 100644 index 00000000000..1f0c0eb0870 --- /dev/null +++ b/demisto_sdk/commands/validate/validate_setup.py @@ -0,0 +1,328 @@ +import os +import sys +from pathlib import Path +from typing import Optional + +import git +import typer + +from demisto_sdk.commands.common.constants import ( + SDK_OFFLINE_ERROR_MESSAGE, + ExecutionMode, +) +from demisto_sdk.commands.common.logger import logger, logging_setup_decorator +from demisto_sdk.commands.common.tools import ( + is_external_repository, + is_sdk_defined_working_offline, +) +from demisto_sdk.commands.validate.config_reader import ConfigReader +from demisto_sdk.commands.validate.initializer import Initializer +from demisto_sdk.commands.validate.old_validate_manager import OldValidateManager +from demisto_sdk.commands.validate.validate_manager import ValidateManager +from demisto_sdk.commands.validate.validation_results import ResultWriter +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +def validate_paths(value: Optional[str]) -> Optional[str]: + if not value: # If no input is provided, just return None + return None + + paths = value.split(",") + for path in paths: + stripped_path = path.strip() + if not os.path.exists(stripped_path): # noqa: PTH110 + raise typer.BadParameter(f"The path '{stripped_path}' does not exist.") + + return value + + +@logging_setup_decorator +def validate( + ctx: typer.Context, + file_paths: str = typer.Argument(None, exists=True, resolve_path=True), + no_conf_json: bool = typer.Option(False, help="Skip conf.json validation."), + id_set: bool = typer.Option( + False, "-s", "--id-set", help="Perform validations using the id_set file." + ), + id_set_path: Path = typer.Option( + None, + "-idp", + "--id-set-path", + help="Path of the id-set.json used for validations.", + ), + graph: bool = typer.Option( + False, "-gr", "--graph", help="Perform validations on content graph." + ), + prev_ver: str = typer.Option( + None, help="Previous branch or SHA1 commit to run checks against." + ), + no_backward_comp: bool = typer.Option( + False, help="Whether to check backward compatibility." + ), + use_git: bool = typer.Option( + False, "-g", "--use-git", help="Validate changes using git." + ), + post_commit: bool = typer.Option( + False, + "-pc", + "--post-commit", + help="Run validation only on committed changed files.", + ), + staged: bool = typer.Option( + False, "-st", "--staged", help="Ignore unstaged files." + ), + include_untracked: bool = typer.Option( + False, + "-iu", + "--include-untracked", + help="Whether to include untracked files in the validation.", + ), + validate_all: bool = typer.Option( + False, "-a", "--validate-all", help="Run all validation on all files." + ), + input: Optional[str] = typer.Option( + None, + "-i", + "--input", + help="Path of the content pack/file to validate.", + callback=validate_paths, + ), + skip_pack_release_notes: bool = typer.Option( + False, help="Skip validation of pack release notes." + ), + print_ignored_errors: bool = typer.Option( + False, help="Print ignored errors as warnings." + ), + print_ignored_files: bool = typer.Option(False, help="Print ignored files."), + no_docker_checks: bool = typer.Option( + False, help="Whether to run docker image validation." + ), + silence_init_prints: bool = typer.Option( + False, help="Skip the initialization prints." + ), + skip_pack_dependencies: bool = typer.Option( + False, help="Skip validation of pack dependencies." + ), + create_id_set: bool = typer.Option( + False, help="Whether to create the id_set.json file." + ), + json_file: str = typer.Option( + None, "-j", "--json-file", help="The JSON file path to output command results." + ), + skip_schema_check: bool = typer.Option( + False, help="Whether to skip the file schema check." + ), + debug_git: bool = typer.Option( + False, help="Whether to print debug logs for git statuses." + ), + print_pykwalify: bool = typer.Option( + False, help="Whether to print the pykwalify log errors." + ), + quiet_bc_validation: bool = typer.Option( + False, help="Set backward compatibility validation errors as warnings." + ), + allow_skipped: bool = typer.Option( + False, help="Don't fail on skipped integrations." + ), + no_multiprocessing: bool = typer.Option( + False, help="Run validate all without multiprocessing." + ), + run_specific_validations: str = typer.Option( + None, + "-sv", + "--run-specific-validations", + help="Comma separated list of validations to run.", + ), + category_to_run: str = typer.Option( + None, help="Run specific validations by stating category." + ), + fix: bool = typer.Option( + False, "-f", "--fix", help="Whether to autofix failing validations." + ), + config_path: Path = typer.Option(None, help="Path for a config file to run."), + ignore_support_level: bool = typer.Option( + False, help="Skip validations based on support level." + ), + run_old_validate: bool = typer.Option( + False, help="Whether to run the old validate flow." + ), + skip_new_validate: bool = typer.Option( + False, help="Whether to skip the new validate flow." + ), + ignore: list[str] = typer.Option( + None, help="An error code to not run. Can be repeated." + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Validate content files. If no additional flags are given, only committed files are validated. + """ + if is_sdk_defined_working_offline(): + typer.echo(SDK_OFFLINE_ERROR_MESSAGE, err=True) + raise typer.Exit(1) + + if file_paths and not input: + input = file_paths + + run_with_mp = not no_multiprocessing + update_command_args_from_config_file("validate", ctx.params) + sdk = ctx.obj + sys.path.append(sdk.configuration.env_dir) + + if post_commit and staged: + logger.error("Cannot use both post-commit and staged flags.") + raise typer.Exit(1) + + is_external_repo = is_external_repository() + file_path = input + execution_mode = determine_execution_mode( + file_path, validate_all, use_git, post_commit + ) + exit_code = 0 + + # Check environment variables + run_new_validate = not skip_new_validate or ( + (env_flag := os.getenv("SKIP_NEW_VALIDATE")) and str(env_flag).lower() == "true" + ) + run_old_validate = run_old_validate or ( + (env_flag := os.getenv("RUN_OLD_VALIDATE")) and str(env_flag).lower() == "true" + ) + + # Log warnings for ignored flags + warn_on_ignored_flags(run_new_validate, run_old_validate, ctx.params) + + try: + # Run old validation flow + if run_old_validate: + exit_code += run_old_validation( + file_path, is_external_repo, run_with_mp, **ctx.params + ) + + # Run new validation flow + if run_new_validate: + exit_code += run_new_validation(file_path, execution_mode, **ctx.params) + + raise typer.Exit(code=exit_code) + except (git.InvalidGitRepositoryError, git.NoSuchPathError, FileNotFoundError) as e: + logger.error(f"{e}") + logger.error( + "You may not be running `demisto-sdk validate` from the content directory.\n" + "Please run this command from the content directory." + ) + raise typer.Exit(1) + + +def determine_execution_mode(file_path, validate_all, use_git, post_commit): + if validate_all: + return ExecutionMode.ALL_FILES + elif file_path: + return ExecutionMode.SPECIFIC_FILES + elif use_git: + return ExecutionMode.USE_GIT + else: + # Default case: fall back to using git for validation + return ExecutionMode.USE_GIT + + +def warn_on_ignored_flags(run_new_validate, run_old_validate, params): + if not run_new_validate: + for flag in ["fix", "ignore_support_level", "config_path", "category_to_run"]: + if params.get(flag): + logger.warning( + f"Flag '{flag.replace('_', '-')}' is ignored when skipping new validation." + ) + + if not run_old_validate: + for flag in [ + "no_backward_comp", + "no_conf_json", + "id_set", + "graph", + "skip_pack_release_notes", + "print_ignored_errors", + "print_ignored_files", + "no_docker_checks", + "silence_init_prints", + "skip_pack_dependencies", + "id_set_path", + "create_id_set", + "skip_schema_check", + "debug_git", + "include_untracked", + "quiet_bc_validation", + "allow_skipped", + "no_multiprocessing", + ]: + if params.get(flag): + logger.warning( + f"Flag '{flag.replace('_', '-')}' is ignored when skipping old validation." + ) + + +def run_old_validation(file_path, is_external_repo, run_with_mp, **kwargs): + validator = OldValidateManager( + is_backward_check=not kwargs["no_backward_comp"], + only_committed_files=kwargs["post_commit"], + prev_ver=kwargs["prev_ver"], + skip_conf_json=kwargs["no_conf_json"], + use_git=kwargs["use_git"], + file_path=file_path, + validate_all=kwargs["validate_all"], + validate_id_set=kwargs["id_set"], + validate_graph=kwargs["graph"], + skip_pack_rn_validation=kwargs["skip_pack_release_notes"], + print_ignored_errors=kwargs["print_ignored_errors"], + is_external_repo=is_external_repo, + print_ignored_files=kwargs["print_ignored_files"], + no_docker_checks=kwargs["no_docker_checks"], + silence_init_prints=kwargs["silence_init_prints"], + skip_dependencies=kwargs["skip_pack_dependencies"], + id_set_path=kwargs["id_set_path"], + staged=kwargs["staged"], + create_id_set=kwargs["create_id_set"], + json_file_path=kwargs["json_file"], + skip_schema_check=kwargs["skip_schema_check"], + debug_git=kwargs["debug_git"], + include_untracked=kwargs["include_untracked"], + quiet_bc=kwargs["quiet_bc_validation"], + multiprocessing=run_with_mp, + check_is_unskipped=not kwargs.get("allow_skipped", False), + specific_validations=kwargs.get("run_specific_validations"), + ) + return validator.run_validation() + + +def run_new_validation(file_path, execution_mode, **kwargs): + validation_results = ResultWriter(json_file_path=kwargs.get("json_file")) + config_reader = ConfigReader( + path=kwargs.get("config_path"), + category=kwargs.get("category_to_run"), + explicitly_selected=(kwargs.get("run_specific_validations") or "").split(","), + ) + initializer = Initializer( + staged=kwargs["staged"], + committed_only=kwargs["post_commit"], + prev_ver=kwargs["prev_ver"], + file_path=file_path, + execution_mode=execution_mode, + ) + validator_v2 = ValidateManager( + file_path=file_path, + initializer=initializer, + validation_results=validation_results, + config_reader=config_reader, + allow_autofix=kwargs["fix"], + ignore_support_level=kwargs["ignore_support_level"], + ignore=kwargs["ignore"], + ) + return validator_v2.run_validations() diff --git a/demisto_sdk/commands/xsoar_linter/xsoar_linter_setup.py b/demisto_sdk/commands/xsoar_linter/xsoar_linter_setup.py new file mode 100644 index 00000000000..952f663cd4a --- /dev/null +++ b/demisto_sdk/commands/xsoar_linter/xsoar_linter_setup.py @@ -0,0 +1,40 @@ +from pathlib import Path +from typing import Optional + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.xsoar_linter.xsoar_linter import xsoar_linter_manager + + +@logging_setup_decorator +def xsoar_linter( + ctx: typer.Context, + file_paths: Optional[list[Path]] = typer.Argument( + None, + exists=True, + dir_okay=True, + resolve_path=True, + show_default=False, + help="The paths to run xsoar linter on. May pass multiple paths.", + ), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """ + Runs the xsoar lint on the given paths. + """ + return_code = xsoar_linter_manager( + file_paths, + ) + if return_code: + raise typer.Exit(1) diff --git a/demisto_sdk/commands/zip_packs/tests/packs_zipper_test.py b/demisto_sdk/commands/zip_packs/tests/packs_zipper_test.py index 06775c08436..e759d95ae73 100644 --- a/demisto_sdk/commands/zip_packs/tests/packs_zipper_test.py +++ b/demisto_sdk/commands/zip_packs/tests/packs_zipper_test.py @@ -2,10 +2,10 @@ from pathlib import Path from shutil import rmtree, unpack_archive -import click import pytest +from typer.testing import CliRunner -from demisto_sdk.__main__ import zip_packs +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.tools import src_root from demisto_sdk.tests.constants_test import PACK_TARGET @@ -48,8 +48,8 @@ class TestPacksZipper: @pytest.mark.parametrize( argnames="zip_all, expected_path", argvalues=[ - (True, "uploadable_packs.zip"), - (False, "uploadable_packs/TestPack.zip"), + ("--zip-all", "uploadable_packs.zip"), + ("--no-zip-all", "uploadable_packs/TestPack.zip"), ], ) def test_zip_packs(self, zip_all, expected_path): @@ -61,17 +61,25 @@ def test_zip_packs(self, zip_all, expected_path): Then: - validate the zip file exist in the destination output """ - + runner = CliRunner() with temp_dir() as tmp_output_dir: - click.Context(command=zip_packs).invoke( - zip_packs, - input=TEST_PACK_PATH, - output=tmp_output_dir, - content_version="0.0.0", - zip_all=zip_all, + result = runner.invoke( + app, + args=[ + "zip-packs", + "-i", + TEST_PACK_PATH, + "-o", + tmp_output_dir, + "--content-version", + "0.0.0", + zip_all, + ], ) + assert result.exit_code == 0 - assert Path(f"{tmp_output_dir}/{expected_path}").exists() + zip_file_path = Path(tmp_output_dir) / expected_path + assert zip_file_path.exists(), f"{zip_file_path} does not exist" def test_zipped_packs(self): """ @@ -83,13 +91,20 @@ def test_zipped_packs(self): - validate the zip file created and contain the pack zip inside it """ + runner = CliRunner() with temp_dir() as tmp_output_dir: - click.Context(command=zip_packs).invoke( - zip_packs, - input=TEST_PACK_PATH, - output=tmp_output_dir, - content_version="0.0.0", - zip_all=True, + runner.invoke( + app, + args=[ + "zip-packs", + "-i", + TEST_PACK_PATH, + "-o", + tmp_output_dir, + "--content-version", + "0.0.0", + "--zip-all", + ], ) unpack_archive(f"{tmp_output_dir}/uploadable_packs.zip", tmp_output_dir) assert Path(f"{tmp_output_dir}/TestPack.zip").exists() @@ -104,14 +119,20 @@ def test_invalid_pack_name(self): Then: - validate zip is not created """ - + runner = CliRunner() with temp_dir() as tmp_output_dir: - click.Context(command=zip_packs).invoke( - zip_packs, - input="invalid_pack_name", - output=tmp_output_dir, - content_version="0.0.0", - zip_all=False, + runner.invoke( + app, + args=[ + "zip-packs", + "-i", + "invalid_pack_name", + "-o", + tmp_output_dir, + "--content-version", + "0.0.0", + "--no-zip-all", + ], ) assert not Path(f"{tmp_output_dir}/uploadable_packs/TestPack.zip").exists() @@ -123,15 +144,22 @@ def test_not_exist_destination(self): When: - run the zip_packs command Then: - - validate the missed directory is created and the zip is exist + - validate the missed directory is created and the zip is existed """ with temp_dir() as tmp_output_dir: - click.Context(command=zip_packs).invoke( - zip_packs, - input=TEST_PACK_PATH, - output=tmp_output_dir, - content_version="0.0.0", - zip_all=True, + runner = CliRunner() + runner.invoke( + app, + args=[ + "zip-packs", + "-i", + TEST_PACK_PATH, + "-o", + tmp_output_dir, + "--content-version", + "0.0.0", + "--zip-all", + ], ) assert Path(f"{tmp_output_dir}/uploadable_packs.zip").exists() diff --git a/demisto_sdk/commands/zip_packs/zip_packs_setup.py b/demisto_sdk/commands/zip_packs/zip_packs_setup.py new file mode 100644 index 00000000000..1159e48a327 --- /dev/null +++ b/demisto_sdk/commands/zip_packs/zip_packs_setup.py @@ -0,0 +1,73 @@ +from pathlib import Path + +import typer + +from demisto_sdk.commands.common.logger import logging_setup_decorator +from demisto_sdk.commands.common.tools import parse_marketplace_kwargs +from demisto_sdk.utils.utils import update_command_args_from_config_file + + +@logging_setup_decorator +def zip_packs( + ctx: typer.Context, + input: str = typer.Option( + ..., "-i", "--input", help="The packs to be zipped as csv list of pack paths." + ), + output: str = typer.Option( + ..., + "-o", + "--output", + help="The destination directory to create the packs.", + resolve_path=True, + ), + content_version: str = typer.Option( + "0.0.0", + "-c", + "--content-version", + help="The content version in CommonServerPython.", + ), + upload: bool = typer.Option( + False, "-u", "--upload", help="Upload the unified packs to the marketplace." + ), + zip_all: bool = typer.Option(False, help="Zip all the packs in one zip file."), + console_log_threshold: str = typer.Option( + None, + "--console-log-threshold", + help="Minimum logging threshold for console output. Possible values: DEBUG, INFO, SUCCESS, WARNING, ERROR.", + ), + file_log_threshold: str = typer.Option( + None, "--file-log-threshold", help="Minimum logging threshold for file output." + ), + log_file_path: str = typer.Option( + None, "--log-file-path", help="Path to save log files." + ), +): + """Generating zipped packs that are ready to be uploaded to Cortex XSOAR machine.""" + from demisto_sdk.commands.upload.uploader import Uploader + from demisto_sdk.commands.zip_packs.packs_zipper import ( + EX_FAIL, + EX_SUCCESS, + PacksZipper, + ) + + update_command_args_from_config_file("zip-packs", locals()) + + should_upload = upload + zip_all = zip_all or should_upload + marketplace = parse_marketplace_kwargs(locals()) + + packs_zipper = PacksZipper( + zip_all=zip_all, + pack_paths=input, + output=output, + quiet_mode=zip_all, + content_version=content_version, + ) + zip_path, unified_pack_names = packs_zipper.zip_packs() + + if should_upload and zip_path: + return Uploader( + input=Path(zip_path), pack_names=unified_pack_names, marketplace=marketplace + ).upload() + + return EX_SUCCESS if zip_path is not None else EX_FAIL diff --git a/demisto_sdk/tests/integration_tests/doc_review_integration_test.py b/demisto_sdk/tests/integration_tests/doc_review_integration_test.py index cf69201fb43..c2eb56098b9 100644 --- a/demisto_sdk/tests/integration_tests/doc_review_integration_test.py +++ b/demisto_sdk/tests/integration_tests/doc_review_integration_test.py @@ -1,6 +1,6 @@ -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from TestSuite.test_tools import ( ChangeCWD, ) @@ -28,7 +28,7 @@ def test_spell_integration_dir_valid(repo, mocker, monkeypatch): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [DOC_REVIEW, "-i", integration.path], catch_exceptions=False + app, [DOC_REVIEW, "-i", integration.path], catch_exceptions=False ) assert all( [ @@ -68,7 +68,7 @@ def test_spell_integration_invalid(repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [DOC_REVIEW, "-i", integration.yml.path], catch_exceptions=False + app, [DOC_REVIEW, "-i", integration.yml.path], catch_exceptions=False ) assert "No misspelled words found " not in result.output assert "Words that might be misspelled were found in" in result.output @@ -102,7 +102,7 @@ def test_spell_script_invalid(repo, mocker, monkeypatch): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [DOC_REVIEW, "-i", script.yml.path], catch_exceptions=False + app, [DOC_REVIEW, "-i", script.yml.path], catch_exceptions=False ) assert "No misspelled words found " not in result.output assert "Words that might be misspelled were found in" in result.output @@ -138,7 +138,7 @@ def test_spell_playbook_invalid(repo, mocker, monkeypatch): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [DOC_REVIEW, "-i", playbook.yml.path], catch_exceptions=False + app, [DOC_REVIEW, "-i", playbook.yml.path], catch_exceptions=False ) assert "No misspelled words found " not in result.output assert "Words that might be misspelled were found in" in result.output @@ -171,7 +171,7 @@ def test_spell_readme_invalid(repo, mocker, monkeypatch): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [DOC_REVIEW, "-i", integration.readme.path], catch_exceptions=False + app, [DOC_REVIEW, "-i", integration.readme.path], catch_exceptions=False ) assert all( [ @@ -223,9 +223,7 @@ def test_review_release_notes_valid(repo, mocker, monkeypatch): rn = pack.create_release_notes(version="1.1.0", content=valid_rn) with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) - result = runner.invoke( - main, [DOC_REVIEW, "-i", rn.path], catch_exceptions=False - ) + result = runner.invoke(app, [DOC_REVIEW, "-i", rn.path], catch_exceptions=False) assert all( [ current_str in result.output @@ -265,9 +263,7 @@ def test_review_release_notes_invalid(repo, mocker, monkeypatch): rn = pack.create_release_notes(version="1.1.0", content=valid_rn) with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) - result = runner.invoke( - main, [DOC_REVIEW, "-i", rn.path], catch_exceptions=False - ) + result = runner.invoke(app, [DOC_REVIEW, "-i", rn.path], catch_exceptions=False) assert all( [ @@ -300,8 +296,6 @@ def test_templates_print(repo, mocker, monkeypatch): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) - result = runner.invoke( - main, [DOC_REVIEW, "--templates"], catch_exceptions=False - ) + result = runner.invoke(app, [DOC_REVIEW, "--templates"], catch_exceptions=False) assert "General Pointers About Release Notes:" in result.output assert "Checking spelling on" not in result.output diff --git a/demisto_sdk/tests/integration_tests/download_integration_test.py b/demisto_sdk/tests/integration_tests/download_integration_test.py index e43e778c3d4..c7e493b7629 100644 --- a/demisto_sdk/tests/integration_tests/download_integration_test.py +++ b/demisto_sdk/tests/integration_tests/download_integration_test.py @@ -2,10 +2,10 @@ from pathlib import Path import pytest -from click.testing import CliRunner +from typer.testing import CliRunner from urllib3.response import HTTPResponse -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.legacy_git_tools import git_path from demisto_sdk.commands.download.tests.downloader_test import Environment @@ -34,7 +34,7 @@ def match_request_text(client, url, method, *args, **kwargs): def demisto_client(mocker): mocker.patch( "demisto_sdk.commands.download.downloader.demisto_client", - return_valure="object", + return_value="object", ) mocker.patch( @@ -60,7 +60,7 @@ def test_integration_download_no_force(demisto_client, tmp_path): pack_path = join(DEMISTO_SDK_PATH, env.PACK_INSTANCE_PATH) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, ["download", "-o", pack_path, "-i", "TestScript", "-i", "DummyPlaybook"], ) assert "Filtering process completed, 2/13 items remain." in result.output @@ -84,7 +84,7 @@ def test_integration_download_with_force(demisto_client, tmp_path, mocker): pack_path = join(DEMISTO_SDK_PATH, env.PACK_INSTANCE_PATH) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ "download", "-o", @@ -101,7 +101,7 @@ def test_integration_download_with_force(demisto_client, tmp_path, mocker): assert result.exit_code == 0 -def test_integration_download_list_files(demisto_client, mocker): +def test_integration_download_list_files(demisto_client, mocker, capsys): """ Given - lf flag to list all available content items. @@ -114,7 +114,7 @@ def test_integration_download_list_files(demisto_client, mocker): """ runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, ["download", "-lf"]) + result = runner.invoke(app, ["download", "-lf"]) expected_table_str = """Content Name Content Type ------------------------------------ --------------- @@ -152,7 +152,7 @@ def test_integration_download_fail(demisto_client, tmp_path): pack_path = join(DEMISTO_SDK_PATH, env.PACK_INSTANCE_PATH) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ "download", "-o", diff --git a/demisto_sdk/tests/integration_tests/error_code_info_test.py b/demisto_sdk/tests/integration_tests/error_code_info_test.py index 0a7cd94633d..333dacdd565 100644 --- a/demisto_sdk/tests/integration_tests/error_code_info_test.py +++ b/demisto_sdk/tests/integration_tests/error_code_info_test.py @@ -1,10 +1,10 @@ import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app -@pytest.mark.parametrize("error_code", ["BA102"]) +@pytest.mark.parametrize("error_code", ["BC100"]) def test_error_code_info_end_to_end(mocker, error_code): """ Given @@ -18,7 +18,8 @@ def test_error_code_info_end_to_end(mocker, error_code): """ runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, ["error-code", "-i", error_code]) + result = runner.invoke(app, ["error-code", "-i", error_code]) + assert result.exit_code == 0 assert not result.exception assert result.output @@ -26,7 +27,7 @@ def test_error_code_info_end_to_end(mocker, error_code): def test_error_code_info_sanity(mocker, monkeypatch): runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, ["error-code", "-i", "BA100"]) + result = runner.invoke(app, ["error-code", "-i", "BA100"]) assert all( [ @@ -43,7 +44,7 @@ def test_error_code_info_refactored_validate(mocker, monkeypatch): ) runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, ["error-code", "-i", "DO106"]) + result = runner.invoke(app, ["error-code", "-i", "DO106"]) assert all( [ @@ -60,7 +61,7 @@ def test_error_code_info_refactored_validate(mocker, monkeypatch): def test_error_code_info_failure(mocker, monkeypatch): runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, ["error-code", "-i", "KELLER"]) + result = runner.invoke(app, ["error-code", "-i", "KELLER"]) assert "No such error" in result.output assert result.exit_code == 1 diff --git a/demisto_sdk/tests/integration_tests/find_dependencies_integration_test.py b/demisto_sdk/tests/integration_tests/find_dependencies_integration_test.py index 5f990d52ac1..4191341cdf0 100644 --- a/demisto_sdk/tests/integration_tests/find_dependencies_integration_test.py +++ b/demisto_sdk/tests/integration_tests/find_dependencies_integration_test.py @@ -1,9 +1,9 @@ import os from pathlib import Path -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from TestSuite.test_tools import ChangeCWD FIND_DEPENDENCIES_CMD = "find-dependencies" @@ -76,7 +76,7 @@ def test_integration_find_dependencies_sanity(self, mocker, repo, monkeypatch): mocker.patch.object(uis, "cpu_count", return_value=1) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FIND_DEPENDENCIES_CMD, "-i", @@ -146,7 +146,7 @@ def test_integration_find_dependencies_sanity_with_id_set(self, repo, mocker): with ChangeCWD(integration.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FIND_DEPENDENCIES_CMD, "-i", @@ -202,7 +202,7 @@ def test_integration_find_dependencies_not_a_pack(self, repo): with ChangeCWD(integration.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FIND_DEPENDENCIES_CMD, "-i", @@ -212,8 +212,7 @@ def test_integration_find_dependencies_not_a_pack(self, repo): "--no-update", ], ) - assert "does not exist" in result.stderr - assert result.exit_code == 2 + assert "Couldn't find any items for pack 'NotValidPack'" in result.output def test_integration_find_dependencies_with_dependency( self, repo, mocker, monkeypatch @@ -268,7 +267,7 @@ def test_integration_find_dependencies_with_dependency( with ChangeCWD(integration.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FIND_DEPENDENCIES_CMD, "-i", @@ -294,6 +293,6 @@ def test_wrong_path(self, pack, mocker): runner = CliRunner(mix_stderr=False) pack.create_integration() path = os.path.join("Packs", Path(pack.path).name, "Integrations") - result = runner.invoke(main, [FIND_DEPENDENCIES_CMD, "-i", path]) + result = runner.invoke(app, [FIND_DEPENDENCIES_CMD, "-i", path]) assert result.exit_code == 1 assert "must be formatted as 'Packs/" in result.output diff --git a/demisto_sdk/tests/integration_tests/format_integration_test.py b/demisto_sdk/tests/integration_tests/format_integration_test.py index 5d6d3ea258a..fa4d3af6d2a 100644 --- a/demisto_sdk/tests/integration_tests/format_integration_test.py +++ b/demisto_sdk/tests/integration_tests/format_integration_test.py @@ -2,9 +2,9 @@ from typing import List import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common import tools from demisto_sdk.commands.common.constants import ( GENERAL_DEFAULT_FROMVERSION, @@ -135,7 +135,7 @@ def test_integration_format_yml_with_no_test_positive( runner = CliRunner() with ChangeCWD(tmp_path): first_run_result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", source_path, "-o", output_path, "-at", "-ngr"], input="Y", ) @@ -147,7 +147,7 @@ def test_integration_format_yml_with_no_test_positive( # Running format for the second time should raise no exception and should raise no prompt to the user second_run_result = runner.invoke( - main, [FORMAT_CMD, "-i", output_path, "-y", "-ngr"] + app, [FORMAT_CMD, "-i", output_path, "-y", "-ngr"] ) assert second_run_result.exit_code == 0 assert not second_run_result.exception @@ -179,7 +179,7 @@ def test_integration_format_yml_with_no_test_negative( runner = CliRunner() with ChangeCWD(tmp_path): result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -222,7 +222,7 @@ def test_integration_format_yml_with_no_test_no_interactive_positive( # Running format in the first time with ChangeCWD(tmp_path): result = runner.invoke( - main, [FORMAT_CMD, "-i", source_path, "-o", output_path, "-y", "-ngr"] + app, [FORMAT_CMD, "-i", source_path, "-o", output_path, "-y", "-ngr"] ) assert not result.exception yml_content = get_dict_from_file(output_path) @@ -270,7 +270,7 @@ def test_integration_format_configuring_conf_json_no_interactive_positive( runner = CliRunner() # Running format in the first time result = runner.invoke( - main, [FORMAT_CMD, "-i", source_path, "-o", saved_file_path, "-y", "-ngr"] + app, [FORMAT_CMD, "-i", source_path, "-o", saved_file_path, "-y", "-ngr"] ) assert not result.exception if file_type == "playbook": @@ -325,7 +325,7 @@ def test_integration_format_configuring_conf_json_positive( # Running format in the first time with ChangeCWD(tmp_path): result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -345,7 +345,7 @@ def test_integration_format_configuring_conf_json_positive( _verify_conf_json_modified(test_playbooks, yml_title, conf_json_path) # Running format for the second time should raise no exception and should raise no prompt to the user result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -400,7 +400,7 @@ def test_integration_format_configuring_conf_json_negative( runner = CliRunner() # Running format in the first time result = runner.invoke( - main, [FORMAT_CMD, "-i", source_path, "-o", saved_file_path, "-ngr"], input="N" + app, [FORMAT_CMD, "-i", source_path, "-o", saved_file_path, "-ngr"], input="N" ) assert not result.exception assert "Skipping test playbooks configuration" in result.output @@ -456,7 +456,7 @@ def test_integration_format_remove_playbook_sourceplaybookid( with ChangeCWD(tmp_path): result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -511,7 +511,7 @@ def test_format_on_valid_py(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-nv", @@ -559,7 +559,7 @@ def test_format_on_invalid_py_empty_lines(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-nv", @@ -607,7 +607,7 @@ def test_format_on_invalid_py_dict(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-nv", @@ -657,7 +657,7 @@ def test_format_on_invalid_py_long_dict(mocker, repo, monkeypatch): integration.code.write(invalid_py) with ChangeCWD(pack.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ FORMAT_CMD, "-nv", @@ -708,7 +708,7 @@ def test_format_on_invalid_py_long_dict_no_verbose(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-nv", @@ -769,8 +769,8 @@ def test_format_on_relative_path_playbook(mocker, repo, monkeypatch): with ChangeCWD(Path(playbook.path).parent): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, - [FORMAT_CMD, "-i", "playbook.yml", "-y", "-ngr"], + app, + [FORMAT_CMD, "-i", playbook.path, "-y", "-ngr"], catch_exceptions=False, ) assert "======= Updating file" in result.output @@ -778,7 +778,7 @@ def test_format_on_relative_path_playbook(mocker, repo, monkeypatch): with ChangeCWD(repo.path): result = runner.invoke( - main, + app, [ "validate", "-i", @@ -816,7 +816,7 @@ def test_format_integration_skipped_files(repo, mocker, monkeypatch): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [FORMAT_CMD, "-i", str(pack.path), "-ngr"], catch_exceptions=False + app, [FORMAT_CMD, "-i", str(pack.path), "-ngr"], catch_exceptions=False ) assert all( @@ -851,7 +851,7 @@ def test_format_commonserver_skipped_files(repo, mocker): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", str(pack.path), "-ngr"], catch_exceptions=False, ) @@ -897,7 +897,7 @@ def test_format_playbook_without_fromversion_no_preset_flag(repo, mocker, monkey playbook.yml.write_dict(playbook_content) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", str(playbook.yml.path), "--assume-yes", "-ngr"], ) assert "Success" in result.output @@ -931,7 +931,7 @@ def test_format_playbook_without_fromversion_with_preset_flag( playbook.yml.write_dict(playbook_content) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -973,7 +973,7 @@ def test_format_playbook_without_fromversion_with_preset_flag_manual( playbook.yml.write_dict(playbook_content) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", str(playbook.yml.path), "--from-version", "6.0.0", "-ngr"], input="y", ) @@ -1007,7 +1007,7 @@ def test_format_playbook_without_fromversion_without_preset_flag_manual( playbook.yml.write_dict(playbook_content) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", str(playbook.yml.path), "-ngr"], input="y", ) @@ -1040,7 +1040,7 @@ def test_format_playbook_copy_removed_from_name_and_id(repo, mocker, monkeypatch playbook.yml.write_dict(playbook_content) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", str(playbook.yml.path), "-ngr"], input="y\n5.5.0", ) @@ -1080,7 +1080,7 @@ def test_format_playbook_no_input_specified(mocker, repo, monkeypatch): ) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-ngr"], input="y\n5.5.0", ) @@ -1138,7 +1138,7 @@ def test_format_incident_type_layout_id(repo, mocker): runner = CliRunner(mix_stderr=False) with ChangeCWD(repo.path): format_result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", str(pack.path), "-y", "-ngr"], catch_exceptions=False, ) @@ -1209,7 +1209,7 @@ def test_format_generic_field_wrong_values( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_field_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1260,7 +1260,7 @@ def test_format_generic_field_missing_from_version_key(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_field_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1310,7 +1310,7 @@ def test_format_generic_type_wrong_from_version(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_type_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1360,7 +1360,7 @@ def test_format_generic_type_missing_from_version_key(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_type_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1409,7 +1409,7 @@ def test_format_generic_module_wrong_from_version(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_module_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1460,7 +1460,7 @@ def test_format_generic_module_missing_from_version_key(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_module_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1509,7 +1509,7 @@ def test_format_generic_definition_wrong_from_version(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_definition_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1562,7 +1562,7 @@ def test_format_generic_definition_missing_from_version_key(mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [FORMAT_CMD, "-i", generic_definition_path, "-y", "-ngr"], catch_exceptions=False, ) @@ -1611,7 +1611,7 @@ def test_format_integrations_folder_with_add_tests(self, mocker, pack): ) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1649,7 +1649,7 @@ def test_format_integrations_folder(self, mocker, pack): IntegrationValidator, "is_valid_category", return_value=True ) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1692,7 +1692,7 @@ def test_format_script_without_test_flag(self, mocker, monkeypatch, pack): mocker.patch.object(BaseUpdate, "set_default_from_version", return_value=None) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1728,7 +1728,7 @@ def test_format_playbooks_folder(self, mocker, monkeypatch, pack): playbooks_path = playbook.yml.path playbook.yml.delete_key("tests") result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1766,7 +1766,7 @@ def test_format_testplaybook_folder_without_add_tests_flag(self, pack): test_playbooks_path = test_playbook.yml.path test_playbook.yml.delete_key("tests") result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1809,7 +1809,7 @@ def test_format_test_playbook_folder_with_add_tests_flag(self, pack): test_playbooks_path = test_playbook.yml.path test_playbook.yml.delete_key("tests") result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1861,7 +1861,7 @@ def test_format_layouts_folder_without_add_tests_flag( ) layouts_path = layout.path result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1906,7 +1906,7 @@ def test_format_layouts_folder_with_add_tests_flag(self, mocker, monkeypatch, re ) layouts_path = layout.path result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -1972,7 +1972,7 @@ def test_verify_deletion_from_conf_pack_format_with_deprecate_flag( with ChangeCWD(repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ FORMAT_CMD, "-i", @@ -2045,7 +2045,7 @@ def test_verify_deletion_from_conf_script_format_with_deprecate_flag( with ChangeCWD(repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [FORMAT_CMD, "-i", f"{script_path}", "-d", "-ngr"], input="\n" + app, [FORMAT_CMD, "-i", f"{script_path}", "-d", "-ngr"], input="\n" ) # Asserts @@ -2098,7 +2098,7 @@ def test_format_incident_field_with_no_graph(mocker, monkeypatch, repo): ) result = runner.invoke( - main, [FORMAT_CMD, "-i", incident_field.path, "-at", "-y", "-ngr"] + app, [FORMAT_CMD, "-i", incident_field.path, "-at", "-y", "-ngr"] ) message = ( f"Skipping formatting of marketplaces field of aliases for {incident_field.path}" @@ -2147,7 +2147,7 @@ def test_format_mapper_with_ngr_flag(mocker, monkeypatch, repo): pack = repo.create_pack("PackName") mapper = pack.create_mapper(name="mapper", content=mapper_content) - result = runner.invoke(main, [FORMAT_CMD, "-i", mapper.path, "-at", "-y", "-ngr"]) + result = runner.invoke(app, [FORMAT_CMD, "-i", mapper.path, "-at", "-y", "-ngr"]) message = f"Skipping formatting of non-existent-fields for {mapper.path} as the no-graph argument was given." assert result.exit_code == 0 assert not result.exception @@ -2228,7 +2228,7 @@ def test_format_on_layout_no_graph_flag(mocker, monkeypatch, repo): layout = pack.create_layoutcontainer(name="Layout", content=layout_content) result = runner.invoke( - main, [FORMAT_CMD, "-i", layout.path, "-at", "-y", "-ngr"] + app, [FORMAT_CMD, "-i", layout.path, "-at", "-y", "-ngr"] ) # run format without the graph message = f"Skipping formatting of non-existent-fields for {layout.path} as the no-graph argument was given." assert result.exit_code == 0 diff --git a/demisto_sdk/tests/integration_tests/general_integration_test.py b/demisto_sdk/tests/integration_tests/general_integration_test.py index 750c558b556..1f0534a97f5 100644 --- a/demisto_sdk/tests/integration_tests/general_integration_test.py +++ b/demisto_sdk/tests/integration_tests/general_integration_test.py @@ -2,10 +2,10 @@ Integration tests for general demisto-sdk functionalities which are related to all SDK commands. """ -from click.testing import CliRunner from pytest import LogCaptureFixture +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common import tools from demisto_sdk.commands.common.constants import DEMISTO_SDK_CONFIG_FILE from demisto_sdk.commands.common.content.content import Content @@ -50,7 +50,7 @@ def test_conf_file_custom(mocker: LogCaptureFixture, repo): runner = CliRunner(mix_stderr=False) # pre-conf file - see validate fail on docker related issue result = runner.invoke( - main, + app, f"validate -i {integration.yml.path} --run-old-validate --skip-new-validate", ) assert "================= Validating file " in result.output @@ -60,7 +60,7 @@ def test_conf_file_custom(mocker: LogCaptureFixture, repo): runner = CliRunner(mix_stderr=False) # post-conf file - see validate not fail on docker related issue as we are skipping result = runner.invoke( - main, + app, f"validate -i {integration.yml.path} --run-old-validate --skip-new-validate", ) assert "================= Validating file " in result.output diff --git a/demisto_sdk/tests/integration_tests/generate_docs_integration_test.py b/demisto_sdk/tests/integration_tests/generate_docs_integration_test.py index 1dfceb7c55c..0074c1aef4f 100644 --- a/demisto_sdk/tests/integration_tests/generate_docs_integration_test.py +++ b/demisto_sdk/tests/integration_tests/generate_docs_integration_test.py @@ -2,9 +2,9 @@ from pathlib import Path import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.legacy_git_tools import git_path GENERATE_DOCS_CMD = "generate-docs" @@ -35,7 +35,7 @@ def test_integration_generate_docs_playbook_positive_with_io( ) runner = CliRunner(mix_stderr=False) arguments = [GENERATE_DOCS_CMD, "-i", valid_playbook_with_io, "-o", tmpdir] - result = runner.invoke(main, arguments) + result = runner.invoke(app, arguments) readme_path = join(tmpdir, "playbook-Test_playbook_README.md") assert result.exit_code == 0 @@ -75,7 +75,7 @@ def test_integration_generate_docs_playbook_positive_no_io( ) runner = CliRunner(mix_stderr=False) arguments = [GENERATE_DOCS_CMD, "-i", valid_playbook_no_io, "-o", tmpdir] - result = runner.invoke(main, arguments) + result = runner.invoke(app, arguments) readme_path = join(tmpdir, "Playbooks.playbook-test_README.md") assert result.exit_code == 0 @@ -118,7 +118,7 @@ def test_integration_generate_docs_playbook_dependencies_old_integration( "-o", tmpdir, ] - result = runner.invoke(main, arguments) + result = runner.invoke(app, arguments) readme_path = join(tmpdir, "DummyPlaybook_README.md") assert result.exit_code == 0 @@ -161,7 +161,7 @@ def test_integration_generate_docs_playbook_pack_dependencies( "-o", tmpdir, ] - result = runner.invoke(main, arguments) + result = runner.invoke(app, arguments) readme_path = join(tmpdir, "Cortex_XDR_Incident_Handling_README.md") assert result.exit_code == 0 @@ -200,7 +200,7 @@ def test_integration_generate_docs_positive_with_and_without_io( valid_playbook_dir = join(DEMISTO_SDK_PATH, "tests/test_files/Playbooks") runner = CliRunner(mix_stderr=False) arguments = [GENERATE_DOCS_CMD, "-i", valid_playbook_dir, "-o", tmpdir] - result = runner.invoke(main, arguments) + result = runner.invoke(app, arguments) readme_path_1 = join(tmpdir, "playbook-Test_playbook_README.md") readme_path_2 = join(tmpdir, "Playbooks.playbook-test_README.md") diff --git a/demisto_sdk/tests/integration_tests/init_integration_test.py b/demisto_sdk/tests/integration_tests/init_integration_test.py index f6bf8de8d82..a6e687b0ed8 100644 --- a/demisto_sdk/tests/integration_tests/init_integration_test.py +++ b/demisto_sdk/tests/integration_tests/init_integration_test.py @@ -1,9 +1,9 @@ from os import listdir from pathlib import Path -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json INIT_CMD = "init" @@ -69,7 +69,7 @@ def test_integration_init_integration_positive(monkeypatch, tmp_path, mocker): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [INIT_CMD, "-o", tmp_dir_path, "-n", pack_name], input="\n".join(inputs) + app, [INIT_CMD, "-o", tmp_dir_path, "-n", pack_name], input="\n".join(inputs) ) assert all( @@ -179,9 +179,7 @@ def test_integration_init_integration_positive_no_inline_pack_name( tmp_integration_path = tmp_pack_path / "Integrations" / integration_name runner = CliRunner(mix_stderr=False) - result = runner.invoke( - main, [INIT_CMD, "-o", tmp_dir_path], input="\n".join(inputs) - ) + result = runner.invoke(app, [INIT_CMD, "-o", tmp_dir_path], input="\n".join(inputs)) assert result.exit_code == 0 assert all( diff --git a/demisto_sdk/tests/integration_tests/prepare_content_test.py b/demisto_sdk/tests/integration_tests/prepare_content_test.py index 4606ca955ce..43553f93ace 100644 --- a/demisto_sdk/tests/integration_tests/prepare_content_test.py +++ b/demisto_sdk/tests/integration_tests/prepare_content_test.py @@ -3,9 +3,9 @@ from tempfile import TemporaryDirectory import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.constants import SUPPORT_LEVEL_HEADER from demisto_sdk.commands.common.tools import get_file from TestSuite.pack import Pack @@ -37,29 +37,29 @@ def test_prepare_content_inputs(self, repo): # Verify that passing both -a and -i raises an exception. result = runner.invoke( - main, + app, [PREPARE_CONTENT_CMD, "-i", f"{integration.path}", "-a"], catch_exceptions=True, ) assert ( result.exception.args[0] - == "Exactly one of the '-a' or '-i' parameters must be provided." + == "Exactly one of '-a' or '-i' must be provided." ) # Verify that not passing either of -a and -i raises an exception. result = runner.invoke( - main, + app, [PREPARE_CONTENT_CMD, "-o", "output-path"], catch_exceptions=True, ) assert ( result.exception.args[0] - == "Exactly one of the '-a' or '-i' parameters must be provided." + == "Exactly one of '-a' or '-i' must be provided." ) # Verify that specifying an output path of a file and passing multiple inputs raises an exception result = runner.invoke( - main, + app, [ PREPARE_CONTENT_CMD, "-i", @@ -117,7 +117,7 @@ def test_unify_integration__detailed_description_partner_collector( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [PREPARE_CONTENT_CMD, "-i", f"{integration.path}"], catch_exceptions=True, ) @@ -150,7 +150,7 @@ def test_pack_prepare_content(mocker, git_repo, monkeypatch): monkeypatch.setenv("DEMISTO_SDK_CONTENT_PATH", dir) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [PREPARE_CONTENT_CMD, "-i", f"{pack.path}"], catch_exceptions=True, ) diff --git a/demisto_sdk/tests/integration_tests/run_integration_test.py b/demisto_sdk/tests/integration_tests/run_integration_test.py index 0f4c271ff3d..6f2d183a4e7 100644 --- a/demisto_sdk/tests/integration_tests/run_integration_test.py +++ b/demisto_sdk/tests/integration_tests/run_integration_test.py @@ -1,8 +1,8 @@ import pytest -from click.testing import CliRunner from demisto_client.demisto_api import DefaultApi +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.legacy_git_tools import git_path from demisto_sdk.commands.run_cmd.runner import Runner @@ -55,7 +55,7 @@ def test_integration_run_non_existing_command( result = CliRunner( mix_stderr=False, ).invoke( - main, + app, [ "run", "-q", @@ -90,7 +90,7 @@ def test_json_to_outputs_flag(mocker, set_environment_variables): command = "!kl-get-records" run_result = CliRunner( mix_stderr=False, - ).invoke(main, ["run", "-q", command, "--json-to-outputs", "-p", "Keylight", "-r"]) + ).invoke(app, ["run", "-q", command, "--json-to-outputs", "-p", "Keylight", "-r"]) assert run_result.exit_code == 0 assert not run_result.stderr @@ -123,7 +123,7 @@ def test_json_to_outputs_flag_fail_no_prefix( command = "!kl-get-records" run_result = CliRunner( mix_stderr=False, - ).invoke(main, ["run", "-q", command, "--json-to-outputs"]) + ).invoke(app, ["run", "-q", command, "--json-to-outputs"]) assert run_result.exit_code == 1 assert ( "A prefix for the outputs is needed for this command. Please provide one" @@ -150,7 +150,7 @@ def test_incident_id_passed_to_run(mocker): command = "!kl-get-records" result = CliRunner(mix_stderr=False).invoke( - main, + app, [ "run", "-q", diff --git a/demisto_sdk/tests/integration_tests/secrets_integration_test.py b/demisto_sdk/tests/integration_tests/secrets_integration_test.py index 0d2c51b1321..9ab47efaa09 100644 --- a/demisto_sdk/tests/integration_tests/secrets_integration_test.py +++ b/demisto_sdk/tests/integration_tests/secrets_integration_test.py @@ -1,7 +1,7 @@ import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.secrets.secrets import SecretsValidator from TestSuite.test_tools import ChangeCWD @@ -40,7 +40,7 @@ def test_integration_secrets_incident_field_positive(mocker, repo): # Change working dir to repo with ChangeCWD(integration.repo_path): runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, [SECRETS_CMD, "-wl", repo.secrets.path]) + result = runner.invoke(app, [SECRETS_CMD, "-wl", repo.secrets.path]) assert all( [ current_str in result.output @@ -81,7 +81,7 @@ def test_integration_secrets_integration_negative(mocker, repo): ) with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, [SECRETS_CMD, "-wl", repo.secrets.path]) + result = runner.invoke(app, [SECRETS_CMD, "-wl", repo.secrets.path]) assert all( [ current_str in result.output @@ -125,7 +125,7 @@ def test_integration_secrets_integration_positive(mocker, repo): ) with ChangeCWD(integration.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, [SECRETS_CMD, "-wl", repo.secrets.path], catch_exceptions=False + app, [SECRETS_CMD, "-wl", repo.secrets.path], catch_exceptions=False ) assert result.exit_code == 0 assert "no secrets were found" in result.output @@ -159,7 +159,7 @@ def test_integration_secrets_integration_global_whitelist_positive_using_git( # Change working dir to repo with ChangeCWD(integration.repo_path): runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, [SECRETS_CMD], catch_exceptions=False) + result = runner.invoke(app, [SECRETS_CMD], catch_exceptions=False) assert result.exit_code == 0 assert "no secrets were found" in result.output @@ -193,7 +193,7 @@ def test_integration_secrets_integration_with_regex_expression(mocker, pack): with ChangeCWD(integration.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [SECRETS_CMD, "--input", integration.code.rel_path], catch_exceptions=False, ) @@ -224,7 +224,7 @@ def test_integration_secrets_integration_positive_with_input_option(mocker, repo # Change working dir to repo with ChangeCWD(integration.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, [SECRETS_CMD, "--input", integration.code.rel_path] + app, [SECRETS_CMD, "--input", integration.code.rel_path] ) assert ("Finished validating secrets, no secrets were found") in result.output @@ -250,7 +250,7 @@ def test_integration_secrets_integration_negative_with_input_option(mocker, repo # Change working dir to repo with ChangeCWD(integration.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, [SECRETS_CMD, "--input", integration.code.rel_path] + app, [SECRETS_CMD, "--input", integration.code.rel_path] ) assert "Secrets were found in the following files" in result.output @@ -278,7 +278,7 @@ def test_integration_secrets_integration_negative_with_input_option_and_whitelis # Change working dir to repo with ChangeCWD(integration.repo_path): result = CliRunner().invoke( - main, + app, [ SECRETS_CMD, "--input", @@ -300,7 +300,7 @@ def test_secrets_for_file_name_with_space_in_it(mocker, repo): # Change working dir to repo with ChangeCWD(integration.repo_path): result = CliRunner().invoke( - main, + app, [ SECRETS_CMD, "--input", diff --git a/demisto_sdk/tests/integration_tests/unify_integration_test.py b/demisto_sdk/tests/integration_tests/unify_integration_test.py index de14bc0a6d0..7977949c763 100644 --- a/demisto_sdk/tests/integration_tests/unify_integration_test.py +++ b/demisto_sdk/tests/integration_tests/unify_integration_test.py @@ -3,9 +3,9 @@ from tempfile import TemporaryDirectory import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.constants import ENV_DEMISTO_SDK_MARKETPLACE from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json from demisto_sdk.commands.common.handlers import DEFAULT_YAML_HANDLER as yaml @@ -49,7 +49,7 @@ def test_unify_generic_module(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [UNIFY_CMD, "-i", generic_module_path, "-mp", "marketplacev2"], catch_exceptions=False, ) @@ -79,7 +79,7 @@ def test_unify_parsing_rule(self, repo, tmpdir): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [UNIFY_CMD, "-i", pack.parsing_rules[0].path, "-o", tmpdir], catch_exceptions=False, ) @@ -120,7 +120,7 @@ def test_unify_parsing_rule_with_samples(self, repo, tmpdir): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [UNIFY_CMD, "-i", pack.parsing_rules[0].path, "-o", tmpdir], catch_exceptions=False, ) @@ -153,7 +153,7 @@ def test_unify_modeling_rule(self, repo, tmpdir): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [UNIFY_CMD, "-i", pack.modeling_rules[0].path, "-o", tmpdir], catch_exceptions=False, ) @@ -192,7 +192,7 @@ def test_add_custom_section_flag_integration(self, mocker, repo, flag): runner = CliRunner(mix_stderr=False) if flag: runner.invoke( - main, + app, [ UNIFY_CMD, "-i", @@ -205,7 +205,7 @@ def test_add_custom_section_flag_integration(self, mocker, repo, flag): ) else: runner.invoke( - main, + app, [ UNIFY_CMD, "-i", @@ -247,7 +247,7 @@ def test_add_custom_section_flag(self, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) - runner.invoke(main, [UNIFY_CMD, "-i", f"{script.path}", "-c", "Test"]) + runner.invoke(app, [UNIFY_CMD, "-i", f"{script.path}", "-c", "Test"]) with open( os.path.join(script.path, "script-dummy-script.yml") ) as unified_yml: @@ -275,7 +275,7 @@ def test_ignore_native_image_integration(self, monkeypatch, repo): monkeypatch.setenv("DEMISTO_SDK_CONTENT_PATH", artifact_dir) monkeypatch.setenv("ARTIFACTS_FOLDER", artifact_dir) runner = CliRunner(mix_stderr=False) - runner.invoke(main, [UNIFY_CMD, "-i", f"{integration.path}", "-ini"]) + runner.invoke(app, [UNIFY_CMD, "-i", f"{integration.path}", "-ini"]) with open( os.path.join(integration.path, "integration-dummy-integration.yml") @@ -300,7 +300,7 @@ def test_ignore_native_image_script(self, repo): with ChangeCWD(pack.repo_path): CliRunner(mix_stderr=False).invoke( - main, [UNIFY_CMD, "-i", f"{script.path}", "-ini"] + app, [UNIFY_CMD, "-i", f"{script.path}", "-ini"] ) with open( os.path.join(script.path, "script-dummy-script.yml") @@ -341,7 +341,7 @@ def test_layout_unify(self, mocker, monkeypatch): with ChangeCWD(REPO.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [UNIFY_CMD, "-i", f"{layout.path}", "-o", output] + app, [UNIFY_CMD, "-i", f"{layout.path}", "-o", output] ) assert result.exit_code == 0 diff --git a/demisto_sdk/tests/integration_tests/update_release_notes_integration_test.py b/demisto_sdk/tests/integration_tests/update_release_notes_integration_test.py index a7c285731b0..6b819480ea8 100644 --- a/demisto_sdk/tests/integration_tests/update_release_notes_integration_test.py +++ b/demisto_sdk/tests/integration_tests/update_release_notes_integration_test.py @@ -2,9 +2,9 @@ from pathlib import Path import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.git_util import GitUtil from demisto_sdk.commands.common.handlers import JSON_Handler from demisto_sdk.commands.common.legacy_git_tools import git_path @@ -35,7 +35,7 @@ def demisto_client(mocker): mocker.patch( "demisto_sdk.commands.download.downloader.demisto_client", - return_valure="object", + return_value="object", ) @@ -50,7 +50,7 @@ def test_update_release_notes_new_integration(demisto_client, mocker): Then - Ensure release notes file created with no errors - Ensure message is printed when update release notes process finished. - - Ensure the release motes content is valid and as expected. + - Ensure the release notes content is valid and as expected. """ expected_rn = ( @@ -89,25 +89,28 @@ def test_update_release_notes_new_integration(demisto_client, mocker): mocker.patch.object(UpdateRN, "get_master_version", return_value="1.0.0") Path(rn_path).unlink(missing_ok=True) - result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] - ) - assert result.exit_code == 0 - assert Path(rn_path).is_file() - assert not result.exception - assert all( - [ - current_str in result.output - for current_str in [ - "Changes were detected. Bumping FeedAzureValid to version: 1.0.1", - "Finished updating release notes for FeedAzureValid.", + try: + result = runner.invoke(app, [UPDATE_RN_COMMAND, "-i", AZURE_FEED_PACK_PATH]) + assert result.exit_code == 0 + assert Path(rn_path).is_file() + assert not result.exception + assert all( + [ + current_str in result.output + for current_str in [ + "Changes were detected. Bumping FeedAzureValid to version: 1.0.1", + "Finished updating release notes for FeedAzureValid.", + ] ] - ] - ) + ) - with open(rn_path) as f: - rn = f.read() - assert expected_rn == rn + with open(rn_path) as f: + rn = f.read() + assert expected_rn == rn + finally: + # Cleanup: remove the file after the test is finished + if Path(rn_path).exists(): + Path(rn_path).unlink() def test_update_release_notes_modified_integration(demisto_client, mocker): @@ -161,11 +164,11 @@ def test_update_release_notes_modified_integration(demisto_client, mocker): Path(rn_path).unlink(missing_ok=True) result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] ) - + # Check the result output and exception assert result.exit_code == 0 - assert Path(rn_path).is_file() + assert Path(rn_path).is_file(), f"Release notes file not found at {rn_path}" assert not result.exception assert all( [ @@ -227,7 +230,7 @@ def test_update_release_notes_incident_field(demisto_client, mocker): Path(rn_path).unlink(missing_ok=True) result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] ) assert result.exit_code == 0 @@ -289,7 +292,7 @@ def test_update_release_notes_unified_yml_integration(demisto_client, mocker): Path(rn_path).unlink(missing_ok=True) - result = runner.invoke(main, [UPDATE_RN_COMMAND, "-i", join("Packs", "VMware")]) + result = runner.invoke(app, [UPDATE_RN_COMMAND, "-i", join("Packs", "VMware")]) assert result.exit_code == 0 assert not result.exception assert all( @@ -311,7 +314,7 @@ def test_update_release_notes_unified_yml_integration(demisto_client, mocker): def test_update_release_notes_non_content_path(demisto_client, mocker): """ Given - - non content pack path. + - non-content pack path. When - Running demisto-sdk update-release-notes command. @@ -339,12 +342,12 @@ def test_update_release_notes_non_content_path(demisto_client, mocker): mocker.patch.object(UpdateRN, "get_master_version", return_value="1.0.0") result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Users", "MyPacks", "VMware")] + app, [UPDATE_RN_COMMAND, "-i", join("Users", "MyPacks", "VMware")] ) assert result.exit_code == 1 assert result.exception - assert "You are not running" in result.output + assert "You are not running" in result.stderr def test_update_release_notes_existing(demisto_client, mocker): @@ -411,7 +414,7 @@ def test_update_release_notes_existing(demisto_client, mocker): return_value="", ) result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] ) assert result.exit_code == 0 @@ -505,7 +508,7 @@ class MockedDependencyNode: mocker.patch.object(UpdateRN, "get_master_version", return_value="1.0.0") result = runner.invoke( - main, + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "ApiModules")], ) @@ -522,7 +525,7 @@ class MockedDependencyNode: ) -def test_update_release_on_matadata_change(demisto_client, mocker, repo): +def test_update_release_on_metadata_change(demisto_client, mocker, repo): """ Given - change only in metadata (in fields that don't require RN) @@ -580,7 +583,7 @@ def test_update_release_on_matadata_change(demisto_client, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, [UPDATE_RN_COMMAND, "-g"]) + result = runner.invoke(app, [UPDATE_RN_COMMAND, "-g"]) assert result.exit_code == 0 assert all( [ @@ -630,7 +633,7 @@ def test_update_release_notes_master_ahead_of_current(demisto_client, mocker, re with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] ) assert result.exit_code == 0 assert not result.exception @@ -689,7 +692,7 @@ def test_update_release_notes_master_unavailable(demisto_client, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid")] ) assert result.exit_code == 0 assert not result.exception @@ -717,14 +720,14 @@ def test_force_update_release_no_pack_given(demisto_client, repo, mocker): """ runner = CliRunner(mix_stderr=True) - result = runner.invoke(main, [UPDATE_RN_COMMAND, "--force"]) + result = runner.invoke(app, [UPDATE_RN_COMMAND, "--force"]) assert "Please add a specific pack in order to force" in result.output def test_update_release_notes_specific_version_invalid(demisto_client, repo): """ Given - - Nothing have changed. + - Nothing has changed. When - Running demisto-sdk update-release-notes command with --version flag but not in the right format. @@ -734,10 +737,12 @@ def test_update_release_notes_specific_version_invalid(demisto_client, repo): """ runner = CliRunner(mix_stderr=True) result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "ThinkCanary"), "-v", "3.x.t"] + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "ThinkCanary"), "-v", "3.x.t"] ) + assert result.exit_code != 0 assert ( - "The format of version should be in x.y.z format, e.g: <2.1.3>" in result.stdout + "Version 3.x.t is not in the expected format. The format should be x.y.z, e.g., 2.1.3." + in str(result.stdout) ) @@ -784,7 +789,7 @@ def test_update_release_notes_specific_version_valid(demisto_client, mocker, rep with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=True) result = runner.invoke( - main, + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "FeedAzureValid"), "-v", "4.0.0"], ) assert result.exit_code == 0 @@ -838,7 +843,7 @@ def test_force_update_release(demisto_client, mocker, repo): runner = CliRunner(mix_stderr=True) result = runner.invoke( - main, [UPDATE_RN_COMMAND, "-i", join("Packs", "ThinkCanary"), "--force"] + app, [UPDATE_RN_COMMAND, "-i", join("Packs", "ThinkCanary"), "--force"] ) assert all( [ @@ -879,7 +884,7 @@ def test_update_release_notes_only_pack_ignore_changed(mocker, pack): ) runner = CliRunner(mix_stderr=True) - result = runner.invoke(main, [UPDATE_RN_COMMAND, "-g"]) + result = runner.invoke(app, [UPDATE_RN_COMMAND, "-g"]) assert result.exit_code == 0 assert not result.exception assert ( @@ -888,7 +893,7 @@ def test_update_release_notes_only_pack_ignore_changed(mocker, pack): ) -def test_update_release_on_matadata_change_that_require_rn( +def test_update_release_on_metadata_change_that_require_rn( demisto_client, mocker, repo ): """ @@ -955,7 +960,7 @@ def test_update_release_on_matadata_change_that_require_rn( Path(rn_path).unlink(missing_ok=True) - result = runner.invoke(main, [UPDATE_RN_COMMAND, "-g"]) + result = runner.invoke(app, [UPDATE_RN_COMMAND, "-g"]) assert result.exit_code == 0 assert not result.exception diff --git a/demisto_sdk/tests/integration_tests/upload_integration_test.py b/demisto_sdk/tests/integration_tests/upload_integration_test.py index fbb0915bf62..c9956eb54cb 100644 --- a/demisto_sdk/tests/integration_tests/upload_integration_test.py +++ b/demisto_sdk/tests/integration_tests/upload_integration_test.py @@ -2,14 +2,16 @@ from os.path import join from pathlib import Path from tempfile import TemporaryDirectory +from unittest import mock from zipfile import ZipFile import demisto_client import pytest -from click.testing import CliRunner from packaging.version import Version +from rich.console import Console +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json from demisto_sdk.commands.common.handlers import YAML_Handler from demisto_sdk.commands.common.legacy_git_tools import git_path @@ -78,13 +80,13 @@ def test_integration_upload_pack_positive(demisto_client_mock, mocker): runner = CliRunner(mix_stderr=False) mocker.patch.object(PackParser, "parse_ignored_errors", return_value={}) result = runner.invoke( - main, [UPLOAD_CMD, "-i", str(pack_path), "--insecure", "--no-zip"] + app, [UPLOAD_CMD, "-i", str(pack_path), "--insecure", "--no-zip"] ) assert result.exit_code == 0 assert ( "\n".join( ( - "SUCCESSFUL UPLOADS:", + "SUCCESSFUL UPLOADS:", "╒═════════════════════════╤═══════════════╤═══════════════╤════════════════╕", "│ NAME │ TYPE │ PACK NAME │ PACK VERSION │", "╞═════════════════════════╪═══════════════╪═══════════════╪════════════════╡", @@ -94,7 +96,6 @@ def test_integration_upload_pack_positive(demisto_client_mock, mocker): "├─────────────────────────┼───────────────┼───────────────┼────────────────┤", "│ FeedAzure_test.yml │ Playbook │ AzureSentinel │ 1.0.0 │", "╘═════════════════════════╧═══════════════╧═══════════════╧════════════════╛", - "", ) ) in result.output @@ -104,7 +105,7 @@ def test_integration_upload_pack_positive(demisto_client_mock, mocker): def test_integration_upload_pack_with_specific_marketplace(demisto_client_mock, mocker): """ Given - - Content pack named ExmaplePack to upload. + - Content pack named Example Pack to upload. When - Uploading the pack. @@ -137,19 +138,18 @@ def test_integration_upload_pack_with_specific_marketplace(demisto_client_mock, runner = CliRunner(mix_stderr=False) mocker.patch.object(PackParser, "parse_ignored_errors", return_value={}) result = runner.invoke( - main, [UPLOAD_CMD, "-i", str(pack_path), "--insecure", "--marketplace", "xsoar"] + app, [UPLOAD_CMD, "-i", str(pack_path), "--insecure", "--marketplace", "xsoar"] ) assert result.exit_code == 0 assert ( "\n".join( ( - "SKIPPED UPLOADED DUE TO MARKETPLACE MISMATCH:", + "SKIPPED UPLOADED DUE TO MARKETPLACE MISMATCH:", "╒════════════════════════════════════════╤═════════════╤═══════════════╤═════════════════════╕", "│ NAME │ TYPE │ MARKETPLACE │ FILE_MARKETPLACES │", "╞════════════════════════════════════════╪═════════════╪═══════════════╪═════════════════════╡", "│ integration-sample_event_collector.yml │ Integration │ xsoar │ ['marketplacev2'] │", "╘════════════════════════════════════════╧═════════════╧═══════════════╧═════════════════════╛", - "", ) ) in result.output @@ -157,13 +157,12 @@ def test_integration_upload_pack_with_specific_marketplace(demisto_client_mock, assert ( "\n".join( ( - "SUCCESSFUL UPLOADS:", + "SUCCESSFUL UPLOADS:", "╒══════════════════════════════╤═════════════╤═════════════╤════════════════╕", "│ NAME │ TYPE │ PACK NAME │ PACK VERSION │", "╞══════════════════════════════╪═════════════╪═════════════╪════════════════╡", "│ integration-sample_packs.yml │ Integration │ ExamplePack │ 3.0.0 │", "╘══════════════════════════════╧═════════════╧═════════════╧════════════════╛", - "", ) ) in result.output @@ -243,7 +242,7 @@ def test_zipped_pack_upload_positive( monkeypatch.setenv("DEMISTO_SDK_CONTENT_PATH", artifact_dir) monkeypatch.setenv("ARTIFACTS_FOLDER", artifact_dir) result = runner.invoke( - main, + app, [ UPLOAD_CMD, "-i", @@ -336,13 +335,12 @@ def test_zipped_pack_upload_positive( assert ( "\n".join( ( - "SUCCESSFUL UPLOADS:", + "SUCCESSFUL UPLOADS:", "╒═══════════╤════════╤═════════════╤════════════════╕", "│ NAME │ TYPE │ PACK NAME │ PACK VERSION │", "╞═══════════╪════════╪═════════════╪════════════════╡", "│ test-pack │ Pack │ test-pack │ 1.0.0 │", "╘═══════════╧════════╧═════════════╧════════════════╛", - "", ) ) in result.output @@ -364,14 +362,19 @@ def test_integration_upload_path_does_not_exist(demisto_client_mock): invalid_dir_path = join( DEMISTO_SDK_PATH, "tests/test_files/content_repo_example/DoesNotExist" ) - runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, [UPLOAD_CMD, "-i", invalid_dir_path, "--insecure"]) - assert result.exit_code == 2 - assert isinstance(result.exception, SystemExit) - assert ( - f"Invalid value for '-i' / '--input': Path '{invalid_dir_path}' does not exist" - in result.stderr - ) + + # Mock rich Console to avoid rich formatting during tests + with mock.patch.object(Console, "print", wraps=Console().print) as mock_print: + runner = CliRunner(mix_stderr=True) + result = runner.invoke(app, [UPLOAD_CMD, "-i", invalid_dir_path, "--insecure"]) + + assert result.exit_code == 2 + assert isinstance(result.exception, SystemExit) + + # Check for error message in the output + assert "Invalid value for '--input' / '-i'" in result.stdout + assert "does not exist" in result.stdout + mock_print.assert_called() def test_integration_upload_pack_invalid_connection_params(mocker): @@ -397,7 +400,7 @@ def test_integration_upload_pack_invalid_connection_params(mocker): return_value=Version("0"), ) runner = CliRunner(mix_stderr=False) - result = runner.invoke(main, [UPLOAD_CMD, "-i", pack_path, "--insecure"]) + result = runner.invoke(app, [UPLOAD_CMD, "-i", pack_path, "--insecure"]) assert result.exit_code == 1 assert ( "Could not connect to the server. Try checking your connection configurations." @@ -439,7 +442,7 @@ def test_upload_single_list(mocker, pack): with ChangeCWD(pack.repo_path): result = runner.invoke( - main, + app, [UPLOAD_CMD, "-i", _list_data.path], ) assert result.exit_code == SUCCESS_RETURN_CODE @@ -488,7 +491,7 @@ def test_upload_single_indicator_field(mocker, pack): with ChangeCWD(pack.repo_path): result = runner.invoke( - main, + app, [UPLOAD_CMD, "-i", indicator_field.path], ) assert result.exit_code == SUCCESS_RETURN_CODE diff --git a/demisto_sdk/tests/integration_tests/validate_integration_test.py b/demisto_sdk/tests/integration_tests/validate_integration_test.py index d9946293adb..31a9e761f02 100644 --- a/demisto_sdk/tests/integration_tests/validate_integration_test.py +++ b/demisto_sdk/tests/integration_tests/validate_integration_test.py @@ -5,11 +5,11 @@ import pytest import requests -from click.testing import CliRunner from requests.adapters import HTTPAdapter +from typer.testing import CliRunner from urllib3 import Retry -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common import tools from demisto_sdk.commands.common.constants import ( DEFAULT_IMAGE_BASE64, @@ -150,7 +150,7 @@ def test_valid_generic_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -185,7 +185,7 @@ def test_invalid_schema_generic_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -238,7 +238,7 @@ def test_invalid_generic_field( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -283,7 +283,7 @@ def test_valid_generic_type(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -325,7 +325,7 @@ def test_invalid_schema_generic_type(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -370,7 +370,7 @@ def test_invalid_from_version_generic_type(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -414,7 +414,7 @@ def test_valid_generic_module(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -456,7 +456,7 @@ def test_invalid_schema_generic_module(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -499,7 +499,7 @@ def test_invalid_fromversion_generic_module(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -545,7 +545,7 @@ def test_valid_generic_definition(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -588,7 +588,7 @@ def test_invalid_schema_generic_definition(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -632,7 +632,7 @@ def test_invalid_fromversion_generic_definition(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -676,7 +676,7 @@ def test_valid_incident_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -718,7 +718,7 @@ def test_invalid_incident_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -780,7 +780,7 @@ def test_valid_scripts_in_incident_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -841,7 +841,7 @@ def test_invalid_scripts_in_incident_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -900,7 +900,7 @@ def test_valid_deprecated_integration(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -915,7 +915,7 @@ def test_valid_deprecated_integration(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{integration.yml.path} as integration", + f"{integration.yml.rel_path} as integration", "The files are valid", ] ] @@ -946,7 +946,7 @@ def test_invalid_deprecated_integration_display_name(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -988,7 +988,7 @@ def test_invalid_integration_deprecation__only_display_name_suffix( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -1035,7 +1035,7 @@ def test_invalid_deprecation__only_description_deprecated( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1075,7 +1075,7 @@ def test_invalid_deprecated_integration_description(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1086,7 +1086,7 @@ def test_invalid_deprecated_integration_description(self, mocker, repo): ], catch_exceptions=False, ) - assert f"{integration.yml.path} as integration" in result.output + assert f"{integration.yml.rel_path} as integration" in result.output assert all( current_str in result.output for current_str in [ @@ -1126,7 +1126,7 @@ def test_invalid_bc_deprecated_integration(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1142,7 +1142,7 @@ def test_invalid_bc_deprecated_integration(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{integration.yml.path} as integration", + f"{integration.yml.rel_path} as integration", "The files are valid", ] ] @@ -1199,7 +1199,7 @@ def test_invalid_modified_bc_deprecated_integration(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1249,7 +1249,7 @@ def test_invalid_bc_unsupported_toversion_integration(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1265,7 +1265,7 @@ def test_invalid_bc_unsupported_toversion_integration(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{integration.yml.path} as integration", + f"{integration.yml.rel_path} as integration", "The files are valid", ] ] @@ -1319,7 +1319,7 @@ def test_modified_invalid_bc_unsupported_toversion_integration(self, mocker, rep with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1370,7 +1370,7 @@ def test_valid_integration(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1385,7 +1385,7 @@ def test_valid_integration(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{integration.yml.path} as integration", + f"{integration.yml.rel_path} as integration", "The files are valid", ] ] @@ -1444,7 +1444,7 @@ def test_changed_integration_param_to_required(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1496,7 +1496,7 @@ def test_invalid_integration(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1511,7 +1511,7 @@ def test_invalid_integration(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{integration.yml.path} as integration", + f"{integration.yml.rel_path} as integration", "IN119", ] ] @@ -1543,7 +1543,7 @@ def test_negative__non_latest_docker_image(self, mocker): with ChangeCWD(CONTENT_REPO_EXAMPLE_ROOT): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1596,7 +1596,7 @@ def test_negative__hidden_param(self, mocker): ) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1627,7 +1627,7 @@ def test_positive_hidden_param(self, mocker): ) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1688,7 +1688,7 @@ def test_duplicate_param_and_argument_invalid(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1739,7 +1739,7 @@ def test_missing_mandatory_field_in_yml(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1790,7 +1790,7 @@ def test_empty_default_descriptions( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1851,7 +1851,7 @@ def test_integration_validate_pack_positive(self, mocker): ) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1906,7 +1906,7 @@ def test_integration_validate_pack_negative(self, mocker): ) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -1946,13 +1946,14 @@ def test_integration_validate_invalid_pack_path(self): """ runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", "--skip-new-validate", "-i", AZURE_FEED_INVALID_PACK_PATH, + "--no-conf-json", ], ) assert "does not exist" in result.stderr @@ -1978,7 +1979,7 @@ def test_valid_new_classifier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -2019,7 +2020,7 @@ def test_invalid_from_version_in_new_classifiers(self, mocker, repo): classifier = pack.create_classifier("new_classifier", new_classifier_copy) with ChangeCWD(pack.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2060,7 +2061,7 @@ def test_invalid_to_version_in_new_classifiers(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2098,7 +2099,7 @@ def test_classifier_from_version_higher_to_version(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2135,7 +2136,7 @@ def test_missing_mandatory_field_in_new_classifier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2176,7 +2177,7 @@ def test_missing_fromversion_field_in_new_classifier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2216,7 +2217,7 @@ def test_invalid_type_in_new_classifier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2253,7 +2254,7 @@ def test_valid_old_classifier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2294,7 +2295,7 @@ def test_invalid_from_version_in_old_classifiers(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2336,7 +2337,7 @@ def test_invalid_to_version_in_old_classifiers(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2377,7 +2378,7 @@ def test_missing_mandatory_field_in_old_classifier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2418,7 +2419,7 @@ def test_missing_toversion_field_in_old_classifier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2462,7 +2463,7 @@ def test_valid_mapper(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2503,7 +2504,7 @@ def test_invalid_from_version_in_mapper(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2546,7 +2547,7 @@ def test_invalid_to_version_in_mapper(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2582,7 +2583,7 @@ def test_missing_mandatory_field_in_mapper(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2624,7 +2625,7 @@ def test_mapper_from_version_higher_to_version(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2661,7 +2662,7 @@ def test_invalid_mapper_type(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2697,7 +2698,7 @@ def test_valid_dashboard(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2737,7 +2738,7 @@ def test_invalid_dashboard(self, mocker, repo): dashboard = pack.create_dashboard("dashboard", dashboard_copy) with ChangeCWD(pack.repo_path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2775,7 +2776,7 @@ def test_valid_indicator_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2817,7 +2818,7 @@ def test_invalid_indicator_field(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2859,7 +2860,7 @@ def test_valid_incident_type(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2900,7 +2901,7 @@ def test_invalid_incident_type(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -2966,7 +2967,7 @@ def test_valid_incident_type_with_extract_fields(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3035,7 +3036,7 @@ def test_invalid_incident_type_with_extract_fields_wrong_field_formats( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3105,7 +3106,7 @@ def test_invalid_incident_type_with_extract_fields_invalid_mode(self, mocker, re with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3203,7 +3204,7 @@ def test_valid_layout(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3246,7 +3247,7 @@ def test_invalid_layout__version(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3290,7 +3291,7 @@ def test_invalid_layout__path(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3332,7 +3333,7 @@ def test_valid_layoutscontainer(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3375,7 +3376,7 @@ def test_invalid_layoutscontainer__version(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3417,7 +3418,7 @@ def test_invalid_layoutscontainer__path(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3462,7 +3463,7 @@ def test_invalid_from_version_in_layoutscontaier(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3502,7 +3503,7 @@ def test_invalid_to_version_in_layout(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3560,7 +3561,7 @@ def test_valid_scripts_in_layoutscontainer(self, mocker, repo, tab_section_to_te with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3631,7 +3632,7 @@ def test_invalid_scripts_in_layoutscontainer( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3699,7 +3700,7 @@ def test_valid_scripts_in_layout(self, mocker, repo, tab_section_to_test): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3765,7 +3766,7 @@ def test_invalid_scripts_in_layout(self, mocker, repo, tab_section_to_test): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3812,7 +3813,7 @@ def test_valid_playbook(self, mocker): ) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3851,7 +3852,7 @@ def test_invalid_playbook(self, mocker): with ChangeCWD(TEST_FILES_PATH): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3892,7 +3893,7 @@ def test_valid_deprecated_playbook(self, mocker, repo): mocker.patch.object(PlaybookValidator, "is_script_id_valid", return_value=True) runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3931,7 +3932,7 @@ def test_invalid_deprecated_playbook(self, mocker): with ChangeCWD(TEST_FILES_PATH): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3975,7 +3976,7 @@ def test_invalid_bc_deprecated_playbook(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -3990,7 +3991,7 @@ def test_invalid_bc_deprecated_playbook(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{playbook.yml.path} as playbook", + f"{playbook.yml.rel_path} as playbook", "The files are valid", ] ] @@ -4039,7 +4040,7 @@ def test_modified_invalid_bc_deprecated_playbook(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4084,7 +4085,7 @@ def test_invalid_bc_unsupported_toversion_playbook(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4099,7 +4100,7 @@ def test_invalid_bc_unsupported_toversion_playbook(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{playbook.yml.path} as playbook", + f"{playbook.yml.rel_path} as playbook", "The files are valid", ] ] @@ -4149,7 +4150,7 @@ def test_modified_invalid_bc_unsupported_toversion_playbook(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4191,7 +4192,7 @@ def test_valid_report(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4232,7 +4233,7 @@ def test_invalid_report(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4268,7 +4269,7 @@ def test_valid_reputation(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4311,7 +4312,7 @@ def test_invalid_reputation(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4352,7 +4353,7 @@ def test_valid_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4367,7 +4368,7 @@ def test_valid_script(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{script.yml.path} as script", + f"{script.yml.rel_path} as script", "The files are valid", ] ] @@ -4395,7 +4396,7 @@ def test_invalid_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4412,7 +4413,7 @@ def test_invalid_script(self, mocker, repo): for current_str in [ "SC100", "The name of this v2 script is incorrect", - f"{script.yml.path} as script", + f"{script.yml.rel_path} as script", ] ] ) @@ -4443,7 +4444,7 @@ def test_valid_deprecated_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4458,7 +4459,7 @@ def test_valid_deprecated_script(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{script.yml.path} as script", + f"{script.yml.rel_path} as script", "The files are valid", ] ] @@ -4486,7 +4487,7 @@ def test_invalid_deprecated_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -4499,7 +4500,11 @@ def test_invalid_deprecated_script(self, mocker, repo): ) assert all( current_str in result.output - for current_str in ["SC101", "Deprecated.", f"{script.yml.path} as script"] + for current_str in [ + "SC101", + "Deprecated.", + f"{script.yml.rel_path} as script", + ] ) assert result.exit_code == 1 @@ -4527,7 +4532,7 @@ def test_invalid_bc_deprecated_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4543,7 +4548,7 @@ def test_invalid_bc_deprecated_script(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{script.yml.path} as script", + f"{script.yml.rel_path} as script", "The files are valid", ] ] @@ -4595,7 +4600,7 @@ def test_modified_invalid_bc_deprecated_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4641,7 +4646,7 @@ def test_invalid_bc_unsupported_toversion_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4657,7 +4662,7 @@ def test_invalid_bc_unsupported_toversion_script(self, mocker, repo): [ current_str in result.output for current_str in [ - f"{script.yml.path} as script", + f"{script.yml.rel_path} as script", "The files are valid", ] ] @@ -4706,7 +4711,7 @@ def test_modified_invalid_bc_unsupported_toversion_script(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-g", @@ -4749,7 +4754,7 @@ def test_valid_widget(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -4790,7 +4795,7 @@ def test_invalid_widget(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4836,7 +4841,7 @@ def test_valid_image(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4880,7 +4885,7 @@ def test_invalid_image(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4921,7 +4926,7 @@ def test_image_should_not_be_validated(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -4957,7 +4962,7 @@ def test_invalid_image_size(self, repo, mocker): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5018,7 +5023,7 @@ def test_author_image_valid(self, repo, mocker): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5068,7 +5073,7 @@ def test_author_image_invalid(self, repo, mocker): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5137,7 +5142,7 @@ def test_all_files_valid(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5212,7 +5217,7 @@ def test_not_all_files_valid(self, mocker, repo): with ChangeCWD(repo.path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ VALIDATE_CMD, "-a", @@ -5315,7 +5320,7 @@ def test_passing_validation_using_git(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5406,7 +5411,7 @@ def test_failing_validation_using_git(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5491,7 +5496,7 @@ def test_validation_using_git_without_pack_dependencies(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5567,7 +5572,7 @@ def test_validation_using_git_with_pack_dependencies(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5592,7 +5597,7 @@ def test_validation_using_git_with_pack_dependencies(self, mocker, repo): def test_validation_non_content_path(self): """ Given - - non content pack path file, file not existing. + - non-content pack path file, file not existing. When - Running demisto-sdk validate command. @@ -5602,7 +5607,7 @@ def test_validation_non_content_path(self): """ runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5622,7 +5627,7 @@ def test_validation_non_content_path(self): def test_validation_non_content_path_mocked_repo(self, mocker, repo): """ Given - - non content pack path file, file existing. + - non-content pack path file, file existing. When - Running demisto-sdk validate command. @@ -5649,7 +5654,7 @@ def test_validation_non_content_path_mocked_repo(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5712,7 +5717,7 @@ def test_validation_using_git_on_specific_file(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-g", @@ -5726,7 +5731,6 @@ def test_validation_using_git_on_specific_file(self, mocker, repo): ], catch_exceptions=False, ) - assert all( [ current_str in result.output @@ -5788,7 +5792,7 @@ def test_validation_using_git_on_specific_file_renamed(self, mocker, repo): with ChangeCWD(repo.path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5866,7 +5870,7 @@ def test_validation_using_git_on_specific_pack(self, mocker, repo): with ChangeCWD(repo.path): result = CliRunner(mix_stderr=False).invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -5911,7 +5915,7 @@ def test_validate_with_different_specific_validation(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -5956,7 +5960,7 @@ def test_validate_with_flag_specific_validation(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "-i", @@ -6005,7 +6009,7 @@ def test_validate_with_flag_specific_validation_entire_code_section( with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", @@ -6083,7 +6087,7 @@ def test_modified_pack_files_with_ignored_validations(self, mocker, repo): with ChangeCWD(pack.repo_path): runner = CliRunner(mix_stderr=False) result = runner.invoke( - main, + app, [ VALIDATE_CMD, "--run-old-validate", diff --git a/demisto_sdk/tests/workflow_test.py b/demisto_sdk/tests/workflow_test.py index e36997c9259..69050a0a1d9 100644 --- a/demisto_sdk/tests/workflow_test.py +++ b/demisto_sdk/tests/workflow_test.py @@ -7,9 +7,9 @@ import pytest from _pytest.monkeypatch import MonkeyPatch -from click.testing import CliRunner +from typer.testing import CliRunner -from demisto_sdk.__main__ import main +from demisto_sdk.__main__ import app from demisto_sdk.commands.common.constants import AUTHOR_IMAGE_FILE_NAME from demisto_sdk.commands.common.handlers import DEFAULT_YAML_HANDLER as yaml from demisto_sdk.commands.common.logger import logger @@ -121,9 +121,8 @@ def run_command( def run_validations(self): """ - Run all of the following validations: + Run all the following validations: * secrets - * lint -g --no-test * validate -g --staged * validate -g * validate -g --include-untracked @@ -132,14 +131,11 @@ def run_validations(self): runner = CliRunner(mix_stderr=False) self.run_command("git add .") # commit flow - secrets, lint and validate only on staged files without rn - res = runner.invoke(main, "secrets") - assert res.exit_code == 0, f"stdout = {res.stdout}\nstderr = {res.stderr}" - - res = runner.invoke(main, "lint -g --no-test") + res = runner.invoke(app, "secrets") assert res.exit_code == 0, f"stdout = {res.stdout}\nstderr = {res.stderr}" res = runner.invoke( - main, + app, "validate -g --staged --skip-pack-dependencies --skip-pack-release-notes " "--no-docker-checks --debug-git --allow-skipped --run-old-validate --skip-new-validate", ) @@ -148,7 +144,7 @@ def run_validations(self): # build flow - validate on all changed files res = runner.invoke( - main, + app, "validate -g --skip-pack-dependencies --no-docker-checks --debug-git " "--allow-skipped --run-old-validate --skip-new-validate", ) @@ -156,7 +152,7 @@ def run_validations(self): # local run - validation with untracked files res = runner.invoke( - main, + app, "validate -g --skip-pack-dependencies --no-docker-checks --debug-git -iu " "--allow-skipped --run-old-validate --skip-new-validate", ) @@ -246,7 +242,7 @@ def init_pack(content_repo: ContentGitRepo, monkeypatch: MonkeyPatch): monkeypatch.chdir(content_repo.content) runner = CliRunner(mix_stderr=False) res = runner.invoke( - main, + app, f"init -a {author_image_abs_path} --pack --name Sample", input="\n".join(["y", "Sample", "description", "1", "1", "", "n", "6.0.0"]), ) @@ -270,12 +266,12 @@ def init_integration(content_repo: ContentGitRepo, monkeypatch: MonkeyPatch): hello_world_path = content_repo.content / "Packs" / "HelloWorld" / "Integrations" monkeypatch.chdir(hello_world_path) res = runner.invoke( - main, "init --integration -n Sample", input="\n".join(["y", "6.0.0", "1"]) + app, "init --integration -n Sample", input="\n".join(["y", "6.0.0", "1"]) ) assert res.exit_code == 0, f"stdout = {res.stdout}\nstderr = {res.stderr}" content_repo.run_command("git add .") monkeypatch.chdir(content_repo.content) - res = runner.invoke(main, "update-release-notes -i Packs/HelloWorld -u revision") + res = runner.invoke(app, "update-release-notes -i Packs/HelloWorld -u revision") assert res.exit_code == 0, f"stdout = {res.stdout}\nstderr = {res.stderr}" try: content_repo.update_rn() @@ -303,7 +299,7 @@ def modify_entity(content_repo: ContentGitRepo, monkeypatch: MonkeyPatch): yaml.dump(script, open("./HelloWorldScript.yml", "w")) content_repo.run_command("git add .") monkeypatch.chdir(content_repo.content) - res = runner.invoke(main, "update-release-notes -i Packs/HelloWorld -u revision") + res = runner.invoke(app, "update-release-notes -i Packs/HelloWorld -u revision") assert res.exit_code == 0, f"stdout = {res.stdout}\nstderr = {res.stderr}" content_repo.run_command("git add .") # Get the newest rn file and modify it. @@ -333,7 +329,7 @@ def rename_incident_field(content_repo: ContentGitRepo, monkeypatch: MonkeyPatch f"git mv {curr_incident_field} {hello_world_incidentfields_path / 'incidentfield-new.json'}" ) runner = CliRunner(mix_stderr=False) - res = runner.invoke(main, "update-release-notes -i Packs/HelloWorld -u revision") + res = runner.invoke(app, "update-release-notes -i Packs/HelloWorld -u revision") assert res.exit_code == 0, f"stdout = {res.stdout}\nstderr = {res.stderr}" try: content_repo.update_rn() diff --git a/poetry.lock b/poetry.lock index 3564837ac9f..7e74ee85e9b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4703,28 +4703,21 @@ files = [ [[package]] name = "typer" -version = "0.9.4" +version = "0.13.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "typer-0.9.4-py3-none-any.whl", hash = "sha256:aa6c4a4e2329d868b80ecbaf16f807f2b54e192209d7ac9dd42691d63f7a54eb"}, - {file = "typer-0.9.4.tar.gz", hash = "sha256:f714c2d90afae3a7929fcd72a3abb08df305e1ff61719381384211c4070af57f"}, + {file = "typer-0.13.0-py3-none-any.whl", hash = "sha256:d85fe0b777b2517cc99c8055ed735452f2659cd45e451507c76f48ce5c1d00e2"}, + {file = "typer-0.13.0.tar.gz", hash = "sha256:f1c7198347939361eec90139ffa0fd8b3df3a2259d5852a0f7400e476d95985c"}, ] [package.dependencies] -click = ">=7.1.1,<9.0.0" -colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} -rich = {version = ">=10.11.0,<14.0.0", optional = true, markers = "extra == \"all\""} -shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] - [[package]] name = "types-dateparser" version = "1.2.0.20240420" @@ -5422,4 +5415,4 @@ generate-unit-tests = ["klara"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "af510a626ef88465ad8458b2ff0d43bf70ae22244aec13be86b5f84bc0f0ca2b" +content-hash = "5639137f8a3290bc41bfbcf42815a854386f7e0665322bc488d02a91a403cf63" diff --git a/pyproject.toml b/pyproject.toml index 014c4d0ccb4..e02b465e3ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ ordered-set = "^4.1.0" paramiko = ">=3.4.1,<4.0" neo4j = "^5.14.0" pydantic = "^1.10" -typer = {extras = ["all"], version = "^0.9.0"} +typer = {extras = ["all"], version = "^0.13.0"} packaging = "^24.0" orjson = "^3.8.3" more-itertools = "^10.0.0" @@ -140,7 +140,7 @@ types-dateparser = "^1.1.4.20240106" types-python-dateutil = "^2.9.0.20240316" [tool.poetry.scripts] -demisto-sdk = "demisto_sdk.__main__:main" +demisto-sdk = "demisto_sdk.__main__:app" sdk-changelog = "demisto_sdk.scripts.changelog.changelog:main" merge-coverage-report = "demisto_sdk.scripts.merge_coverage_report:main" merge-pytest-reports = "demisto_sdk.scripts.merge_pytest_reports:main"